def test_valid_account_number_and_org_admin_fake_header_defaults(self): """ Test with valid account_number and org_admin returns defaults. Test with an internal account header, valid account_number and True org_admin headers, expect a normal response for the default start_date of "yesterday". """ yesterday = get_today() - datetime.timedelta(days=1) today = get_today() api_helper.calculate_concurrent(yesterday, today, self.user1.id) fake_header = util_helper.get_identity_auth_header( account_number=self.user1.username, is_org_admin=True, ) client = APIClient() client.credentials( HTTP_X_RH_IDENTITY=util_helper.get_internal_identity_auth_header(), HTTP_X_RH_INTERNAL_FAKE_IDENTITY=fake_header, ) response = client.get(self.concurrent_api_url, data={}, format="json") body = response.json() self.assertEqual(body["meta"]["count"], 1) self.assertEqual(len(body["data"]), 1) self.assertEqual(body["data"][0]["date"], str(yesterday))
def enable(self): """ Mark this CloudAccount as enabled and perform operations to make it so. This has the side effect of calling the related content_object (e.g. AwsCloudAccount) to make any cloud-specific changes. If any that cloud-specific function fails, we rollback our state change and re-raise the exception for the caller to handle further. """ logger.info( _("'is_enabled' is %(is_enabled)s before enabling %(cloudaccount)s"), {"is_enabled": self.is_enabled, "cloudaccount": self}, ) if not self.is_enabled: self.is_enabled = True self.enabled_at = get_now() self.save() try: self.content_object.enable() # delete stale ConcurrentUsage when an clount is enabled ConcurrentUsage.objects.filter(user=self.user, date=get_today()).delete() except Exception as e: # All failure notifications should happen during the failure logger.info(e) transaction.set_rollback(True) return False sources.notify_application_availability( self.user.username, self.platform_application_id, "available" )
def get_queryset(self): # noqa: C901 """Get the queryset of dates filtered to the appropriate inputs.""" user = self.request.user errors = {} tomorrow = get_today() + timedelta(days=1) try: start_date = self.request.query_params.get("start_date", None) start_date = parse( start_date).date() if start_date else get_today() # Start date is inclusive, if start date is tomorrow or after, # we do not return anything if start_date >= tomorrow: errors["start_date"] = [ _("start_date cannot be in the future.") ] except ValueError: errors["start_date"] = [ _("start_date must be a date (YYYY-MM-DD).") ] try: end_date = self.request.query_params.get("end_date", None) # End date is noninclusive, set it to tomorrow if one is not provided end_date = parse(end_date).date() if end_date else tomorrow # If end date is after tomorrow, we do not return anything if end_date > tomorrow: errors["end_date"] = [_("end_date cannot be in the future.")] if end_date <= user.date_joined.date(): errors["end_date"] = [ _("end_date must be after user creation date.") ] except ValueError: errors["end_date"] = [_("end_date must be a date (YYYY-MM-DD).")] if errors: raise exceptions.ValidationError(errors) queryset = DailyConcurrentUsageDummyQueryset(start_date, end_date, user.id) return queryset
def days(self): """Get all days.""" if self._days is None: tomorrow = get_today() + timedelta(days=1) days = [] current = self.start_date delta_day = timedelta(days=1) end_date = min(self.end_date, tomorrow) while current < end_date: days.append(current) current += delta_day self._days = days return self._days
def test_start_date_end_date_defaults(self): """ Test with no start_date and no end_date set. Default start_date is "yesterday" and default end_date is "today", and since start_date is inclusive and end_date is exclusive, the resulting output should be data for one day: yesterday. """ yesterday = get_today() - datetime.timedelta(days=1) today = get_today() data = {} api_helper.calculate_concurrent(yesterday, today, self.user1.id) client = APIClient() client.force_authenticate(user=self.user1) response = client.get(self.concurrent_api_url, data=data, format="json") body = response.json() self.assertEqual(body["meta"]["count"], 1) self.assertEqual(len(body["data"]), 1) self.assertEqual(body["data"][0]["date"], str(yesterday))
def test_invalid_account_number(self): """ Test with an invalid account_number fake identity. Test with an internal account header, an invalid account_number and True org_admin fake identity header, expect an authentication error. """ yesterday = get_today() - datetime.timedelta(days=1) today = get_today() fake_header = util_helper.get_identity_auth_header( account_number=str(_faker.pyint()), is_org_admin=True, ) api_helper.calculate_concurrent(yesterday, today, self.user1.id) client = APIClient() client.credentials( HTTP_X_RH_IDENTITY=util_helper.get_internal_identity_auth_header(), HTTP_X_RH_INTERNAL_FAKE_IDENTITY=fake_header, ) response = client.get(self.concurrent_api_url, data={}, format="json") body = response.json() self.assertEqual(response.status_code, 403) self.assertEqual(body["detail"], "Incorrect authentication credentials.")
def test_with_false_is_org_admin_parameter(self): """ Test with a False is_org_admin parameter. Test with an internal account header, a valid account_number and False org_admin headers, expect an authentication error. """ yesterday = get_today() - datetime.timedelta(days=1) today = get_today() api_helper.calculate_concurrent(yesterday, today, self.user1.id) fake_header = util_helper.get_identity_auth_header( account_number=self.user1.username, is_org_admin=False, ) client = APIClient() client.credentials( HTTP_X_RH_IDENTITY=util_helper.get_internal_identity_auth_header(), HTTP_X_RH_INTERNAL_FAKE_IDENTITY=fake_header, ) response = client.get(self.concurrent_api_url, data={}, format="json") body = response.json() self.assertEqual(response.status_code, 403) self.assertEqual(body["detail"], "User must be an org admin.")
def test_future_start_date_returns_400(self): """ Test with far-future start_date, expecting it to return 400. When an start_date is given that is tomorrow or later, we return a 400 response, because we cannot predict the future. """ today = get_today() future = today + datetime.timedelta(days=1) data = {"start_date": str(future)} api_helper.calculate_concurrent(today, future, self.user1.id) client = APIClient() client.force_authenticate(user=self.user1) response = client.get(self.concurrent_api_url, data=data, format="json") self.assertEqual(response.status_code, 400) body = response.json() self.assertEqual(body["start_date"], [_("start_date cannot be in the future.")])
def test_future_start_and_end_date_returns_400(self): """ Test with far-future start_date and end_date, expecting no results. If the request filters result in dates that are only in the future, we must always expect a 400 response because we cannot possibly know anything beyond today. """ today = get_today() future_start = today + datetime.timedelta(days=50) future_end = today + datetime.timedelta(days=100) data = {"start_date": str(future_start), "end_date": str(future_end)} client = APIClient() client.force_authenticate(user=self.user1) response = client.get(self.concurrent_api_url, data=data, format="json") self.assertEqual(response.status_code, 400) body = response.json() self.assertEqual(body["start_date"], [_("start_date cannot be in the future.")]) self.assertEqual(body["end_date"], [_("end_date cannot be in the future.")])
def test_concurrent_usage_deleted_when_clount_enables( self, mock_initial_aws_describe_instances, mock_verify_permissions, mock_notify_sources, ): """Test stale ConcurrentUsage is deleted when clount is enabled.""" user = util_helper.generate_test_user() concurrent_usage = ConcurrentUsage(user=user, date=get_today()) concurrent_usage.save() self.assertEqual(1, ConcurrentUsage.objects.filter(user=user).count()) aws_account_id = util_helper.generate_dummy_aws_account_id() account = api_helper.generate_cloud_account( aws_account_id=aws_account_id, user=user, ) account.enable() self.assertEqual(0, ConcurrentUsage.objects.filter(user=user).count())
def latest_start_date(self): """Return the latest allowed start_date.""" return get_today() - timedelta(days=1)
def latest_end_date(self): """Return the latest allowed end_date.""" return get_today() + timedelta(days=1)
def latest_start_date(self): """Return the latest allowed start_date.""" return get_today()
def calculate_max_concurrent_usage_from_runs(runs): """ Calculate maximum concurrent usage of RHEL instances from given Runs. We try to find the common intersection of the given runs across the dates, users, and cloud accounts referenced in the given Runs. """ date_user_cloud_accounts = set() # This nested set of for loops should find the set of all days, user_ids, # and cloud_account_ids that may be affected by the given Runs. All of # those combinations need to have their max concurrent usage calculated. for run in runs: start_date = run.start_time.date() if run.end_time is not None: end_date = run.end_time.date() else: # No end time on the run? Act like the run ends "today". end_date = get_today() # But really the end is *tomorrow* since the end is exclusive. end_date = end_date + timedelta(days=1) user_id = run.instance.cloud_account.user.id delta_days = (end_date - start_date).days for offset in range(delta_days): date = start_date + timedelta(days=offset) date_user_cloud_account = (date, user_id) date_user_cloud_accounts.add(date_user_cloud_account) date_user_no_cloud_account = (date, user_id) date_user_cloud_accounts.add(date_user_no_cloud_account) for date, user_id in date_user_cloud_accounts: # If we already have a concurrent usage calculation task with the same userid # same date, and status=SCHEDULED, then we do not run this last_calculate_task = get_last_scheduled_concurrent_usage_calculation_task( user_id=user_id, date=date) # If no such task exists, then schedule one if last_calculate_task is None: schedule_concurrent_calculation_task(date, user_id) # If this task is already complete or is in the process of running, # then we schedule a new task. elif last_calculate_task.status != ConcurrentUsageCalculationTask.SCHEDULED: schedule_concurrent_calculation_task(date, user_id) # If the task is scheduled (but has not started running) and it has been # in the queue for too long, then we revoke it and schedule a new task. elif last_calculate_task.created_at < datetime.now(tz=tz.tzutc( )) - timedelta( seconds=settings.SCHEDULE_CONCURRENT_USAGE_CALCULATION_DELAY): logger.info( "Previous scheduled task to calculate concurrent usage for " "user_id: %(user_id)s and date: %(date)s has expired. " "Scheduling new calculation task." % { "user_id": user_id, "date": date }) last_calculate_task.cancel() schedule_concurrent_calculation_task(date, user_id) # If the task already exists and is scheduled and it reasonably recent, # simply log this message for visibility. else: logger.info( "Task to calculate concurrent usage for user_id: %(user_id)s and " "date: %(date)s already exists." % { "user_id": user_id, "date": date })
def get_max_concurrent_usage(date, user_id): """ Get maximum concurrent usage of RHEL instances in given parameters. This may get a pre-calculated version of the results from ConcurrentUsage. We attempt to load stored results from the database; we only perform the underlying calculation (and save results for future requests) if the result does not yet exist. Args: date (datetime.date): the day during which we are measuring usage user_id (int): required filter on user Returns: ConcurrentUsage for the give date and user_id. """ logger.debug( "Getting Concurrent usage for user_id: %(user_id)s, date: %(date)s" % { "user_id": user_id, "date": date }) today = get_today() if date > today: # Early return stub values; we never project future calculations. return ConcurrentUsage(date=date, user_id=user_id) try: logger.info( _("Fetching ConcurrentUsage(date=%(date)s, user_id=%(user_id)s)"), { "date": date, "user_id": user_id }, ) concurrent_usage = ConcurrentUsage.objects.get(date=date, user_id=user_id) return concurrent_usage except ConcurrentUsage.DoesNotExist: logger.info( _("Could not find ConcurrentUsage(date=%(date)s, user_id=%(user_id)s)" ), { "date": date, "user_id": user_id }, ) # If it does not exist, we will create it after this exception handler. pass last_calculation_task = get_last_scheduled_concurrent_usage_calculation_task( user_id, date) logger.info( _("last_calculation_task is %(last_calculation_task)s"), {"last_calculation_task": last_calculation_task}, ) if last_calculation_task is not None: if last_calculation_task.status in ( ConcurrentUsageCalculationTask.SCHEDULED, ConcurrentUsageCalculationTask.RUNNING, ): raise ResultsUnavailable schedule_concurrent_calculation_task(date, user_id) raise ResultsUnavailable
def default_end_date(self): """Return the default end_date.""" return get_today()
def default_start_date(self): """Return the default start_date.""" return get_today() - timedelta(days=1)
def latest_end_date(self): """Return the latest allowed end_date.""" return get_today()