From 74ab2ff12bd08b6cd11d7967cd9ec5fccad0f901 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 21 Nov 2025 10:44:06 +0000 Subject: [PATCH 01/13] Added coral credits to quota view --- api/azimuth/provider/openstack/provider.py | 28 +++++++++++++++++++ api/azimuth/settings.py | 5 ++++ .../files/api/settings/13-coral-credits.yaml | 6 ++++ chart/templates/api/settings.yaml | 2 ++ chart/values.yaml | 3 ++ 5 files changed, 44 insertions(+) create mode 100644 chart/files/api/settings/13-coral-credits.yaml diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 5b43bea5..aea473e9 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -4,6 +4,8 @@ import base64 import dataclasses +import datetime +from django.utils.timezone import make_aware import functools import hashlib import logging @@ -14,10 +16,12 @@ import certifi import dateutil.parser import rackit +import requests import yaml from .. import base, dto, errors # noqa: TID252 from . import api +from ...settings import cloud_settings logger = logging.getLogger(__name__) @@ -365,6 +369,30 @@ def quotas(self): len(list(self._connection.network.floatingips.all())), ) ) + # Get coral credits if available + if not cloud_settings.CORAL_CREDITS.CORAL_URI is None: + headers = {"Authorization": "Bearer "+cloud_settings.CORAL_CREDITS.AUTH_TOKEN} + accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() + tenancy_account = next(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts))["account"] + all_allocations = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers).json() + account_allocations = filter(lambda a:a["account"] == tenancy_account,all_allocations) + datetime_format = "%Y-%m-%dT%H:%M:%SZ" + current_time = make_aware(datetime.datetime.now()) + target_tz = current_time.tzinfo + active_allocation = next(filter( + lambda a: datetime.datetime.strptime(a["start"],datetime_format).replace(tzinfo=target_tz) < current_time and + current_time < datetime.datetime.strptime(a["end"],datetime_format).replace(tzinfo=target_tz), + account_allocations))["id"] + for resource in requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation/"+ str(active_allocation) +"/resources", headers=headers).json(): + quotas.append( + dto.Quota( + resource["resource_class"]["name"], + resource["resource_class"]["name"]+" hours", + "resource hours", + resource["allocated_resource_hours"], + resource["allocated_resource_hours"] - resource["resource_hours"] + ) + ) # The volume service is optional # In the case where the service is not enabled, just don't add the quotas try: diff --git a/api/azimuth/settings.py b/api/azimuth/settings.py index 351660b7..9765bceb 100644 --- a/api/azimuth/settings.py +++ b/api/azimuth/settings.py @@ -215,6 +215,9 @@ class SchedulingSettings(SettingsObject): #: Indicates whether advanced scheduling should be enabled ENABLED = Setting(default=False) +class CoralCreditsSetting(SettingsObject): + AUTH_TOKEN = Setting(default=None) + CORAL_URI = Setting(default=None) class AzimuthSettings(SettingsObject): """ @@ -295,6 +298,8 @@ class AzimuthSettings(SettingsObject): #: Configuration for advanced scheduling SCHEDULING = NestedSetting(SchedulingSettings) + CORAL_CREDITS = NestedSetting(CoralCreditsSetting) + #: URL for documentation DOCUMENTATION_URL = Setting( default="https://azimuth-cloud.github.io/azimuth-user-docs/" diff --git a/chart/files/api/settings/13-coral-credits.yaml b/chart/files/api/settings/13-coral-credits.yaml new file mode 100644 index 00000000..c59da4bc --- /dev/null +++ b/chart/files/api/settings/13-coral-credits.yaml @@ -0,0 +1,6 @@ +{{- with .Values.settings.coralCredits }} +AZIMUTH: + CORAL_CREDITS: + AUTH_TOKEN: {{ quote .token }} + CORAL_URI: {{ quote .uri }} +{{- end }} diff --git a/chart/templates/api/settings.yaml b/chart/templates/api/settings.yaml index ebe2a587..2be8b42c 100644 --- a/chart/templates/api/settings.yaml +++ b/chart/templates/api/settings.yaml @@ -35,3 +35,5 @@ data: {{- tpl (.Files.Get "files/api/settings/11-scheduling.yaml") . | b64enc | nindent 4 }} 12-apps-provider.yaml: | {{- tpl (.Files.Get "files/api/settings/12-apps-provider.yaml") . | b64enc | nindent 4 }} + 13-coral-credits.yaml: | + {{- tpl (.Files.Get "files/api/settings/13-coral-credits.yaml") . | b64enc | nindent 4 }} diff --git a/chart/values.yaml b/chart/values.yaml index e1905439..83186880 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -182,6 +182,9 @@ settings: # # and "ephemeral_disk" for the current flavor # description: >- # {{ cpus }} CPUs, {{ ram }} RAM, {{ disk }} disk, {{ ephemeral_disk }} ephemeral disk + coralCredits: + # uri: + # token: # Configuration for authentication authentication: From 256247c8ae6aa1db771586877a5dabb25fded36f Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 21 Nov 2025 11:09:07 +0000 Subject: [PATCH 02/13] move to secret ref --- api/azimuth/provider/openstack/provider.py | 9 ++++++++- api/azimuth/settings.py | 2 +- chart/files/api/settings/13-coral-credits.yaml | 4 +++- chart/values.yaml | 7 +++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index aea473e9..498ba04d 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -290,6 +290,13 @@ def __init__( project_id_safe = self._connection.project_id.replace("-", "") self._project_share_user = prefix + project_id_safe + # Get Coral bearer token if enabled + if not cloud_settings.CORAL_CREDITS.ADMIN_PASSWORD is None: + self._coral_auth_token = requests.post(cloud_settings.CORAL_CREDITS.CORAL_URI+"/api-token-auth/",json={ + "username": "admin", + "password": cloud_settings.CORAL_CREDITS.ADMIN_PASSWORD + }).json()["token"] + def _log(self, message, *args, level=logging.INFO, **kwargs): logger.log( level, @@ -371,7 +378,7 @@ def quotas(self): ) # Get coral credits if available if not cloud_settings.CORAL_CREDITS.CORAL_URI is None: - headers = {"Authorization": "Bearer "+cloud_settings.CORAL_CREDITS.AUTH_TOKEN} + headers = {"Authorization": "Bearer "+self._coral_auth_token} accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() tenancy_account = next(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts))["account"] all_allocations = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers).json() diff --git a/api/azimuth/settings.py b/api/azimuth/settings.py index 9765bceb..598b54a5 100644 --- a/api/azimuth/settings.py +++ b/api/azimuth/settings.py @@ -216,7 +216,7 @@ class SchedulingSettings(SettingsObject): ENABLED = Setting(default=False) class CoralCreditsSetting(SettingsObject): - AUTH_TOKEN = Setting(default=None) + ADMIN_PASSWORD = Setting(default=None) CORAL_URI = Setting(default=None) class AzimuthSettings(SettingsObject): diff --git a/chart/files/api/settings/13-coral-credits.yaml b/chart/files/api/settings/13-coral-credits.yaml index c59da4bc..f47204c2 100644 --- a/chart/files/api/settings/13-coral-credits.yaml +++ b/chart/files/api/settings/13-coral-credits.yaml @@ -1,6 +1,8 @@ {{- with .Values.settings.coralCredits }} AZIMUTH: CORAL_CREDITS: - AUTH_TOKEN: {{ quote .token }} CORAL_URI: {{ quote .uri }} + {{- with .passwordSecretRef }} + ADMIN_PASSWORD: {{ index (lookup "v1" "Secret" .namespace .name).data .key | b64dec }} + {{- end }} {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index 83186880..ca311016 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -183,8 +183,11 @@ settings: # description: >- # {{ cpus }} CPUs, {{ ram }} RAM, {{ disk }} disk, {{ ephemeral_disk }} ephemeral disk coralCredits: - # uri: - # token: + uri: + passwordSecretRef: + name: + namespace: + key: # Configuration for authentication authentication: From 2a15773608a4b494e63b3b3a06c58d9dd1472bc3 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Tue, 25 Nov 2025 16:49:08 +0000 Subject: [PATCH 03/13] Fixed API errors for tenancies without credits --- api/azimuth/provider/openstack/provider.py | 58 ++++++++++++++-------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 498ba04d..e08cb458 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -378,28 +378,7 @@ def quotas(self): ) # Get coral credits if available if not cloud_settings.CORAL_CREDITS.CORAL_URI is None: - headers = {"Authorization": "Bearer "+self._coral_auth_token} - accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() - tenancy_account = next(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts))["account"] - all_allocations = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers).json() - account_allocations = filter(lambda a:a["account"] == tenancy_account,all_allocations) - datetime_format = "%Y-%m-%dT%H:%M:%SZ" - current_time = make_aware(datetime.datetime.now()) - target_tz = current_time.tzinfo - active_allocation = next(filter( - lambda a: datetime.datetime.strptime(a["start"],datetime_format).replace(tzinfo=target_tz) < current_time and - current_time < datetime.datetime.strptime(a["end"],datetime_format).replace(tzinfo=target_tz), - account_allocations))["id"] - for resource in requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation/"+ str(active_allocation) +"/resources", headers=headers).json(): - quotas.append( - dto.Quota( - resource["resource_class"]["name"], - resource["resource_class"]["name"]+" hours", - "resource hours", - resource["allocated_resource_hours"], - resource["allocated_resource_hours"] - resource["resource_hours"] - ) - ) + quotas.extend(self.get_coral_quotas()) # The volume service is optional # In the case where the service is not enabled, just don't add the quotas try: @@ -425,6 +404,41 @@ def quotas(self): except api.ServiceNotSupported: pass return quotas + + def get_coral_quotas(self): + headers = {"Authorization": "Bearer "+self._coral_auth_token} + accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() + + tenancy_account_list = list(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts)) + if len(tenancy_account_list) != 1: + return [] + tenancy_account = tenancy_account_list[0]["account"] + all_allocations = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers).json() + account_allocations = filter(lambda a:a["account"] == tenancy_account,all_allocations) + + datetime_format = "%Y-%m-%dT%H:%M:%SZ" + current_time = make_aware(datetime.datetime.now()) + target_tz = current_time.tzinfo + + active_allocation_list = list(filter( + lambda a: datetime.datetime.strptime(a["start"],datetime_format).replace(tzinfo=target_tz) < current_time and + current_time < datetime.datetime.strptime(a["end"],datetime_format).replace(tzinfo=target_tz), + account_allocations)) + + quotas = [] + if len(active_allocation_list) == 1: + active_allocation_id = active_allocation_list[0]["id"] + for resource in requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation/"+ str(active_allocation_id) +"/resources", headers=headers).json(): + quotas.append( + dto.Quota( + resource["resource_class"]["name"], + resource["resource_class"]["name"]+" hours", + "resource hours", + resource["allocated_resource_hours"], + resource["allocated_resource_hours"] - resource["resource_hours"] + ) + ) + return quotas def _from_api_image(self, api_image): """ From bdc5b78b038bf023d9bdc519316043e572a6a2d3 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Thu, 27 Nov 2025 14:10:04 +0000 Subject: [PATCH 04/13] Add human readable names for quotas --- api/azimuth/provider/openstack/provider.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index e08cb458..0ddf53ff 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -425,14 +425,20 @@ def get_coral_quotas(self): current_time < datetime.datetime.strptime(a["end"],datetime_format).replace(tzinfo=target_tz), account_allocations)) + human_readable_names = { + "MEMORY_MB": "RAM (MB)", + "DISK_GB": "Root disk (GB)" # TODO: is this always the root disk? + } + quotas = [] if len(active_allocation_list) == 1: active_allocation_id = active_allocation_list[0]["id"] for resource in requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation/"+ str(active_allocation_id) +"/resources", headers=headers).json(): + resource_name = resource["resource_class"]["name"] quotas.append( dto.Quota( - resource["resource_class"]["name"], - resource["resource_class"]["name"]+" hours", + resource_name, + human_readable_names.get(resource_name, resource_name)+" hours (credits)", "resource hours", resource["allocated_resource_hours"], resource["allocated_resource_hours"] - resource["resource_hours"] From 2b1252ff9d031bf161ca7502eb144f268f988e83 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 12:11:06 +0000 Subject: [PATCH 05/13] Disable unknown resource in platform creation view --- ui/src/components/pages/tenancy/platforms/scheduling.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/src/components/pages/tenancy/platforms/scheduling.js b/ui/src/components/pages/tenancy/platforms/scheduling.js index 1a6cbf65..009a6fa5 100644 --- a/ui/src/components/pages/tenancy/platforms/scheduling.js +++ b/ui/src/components/pages/tenancy/platforms/scheduling.js @@ -62,7 +62,7 @@ const ProjectedQuotaProgressBar = ({ quota }) => { const ProjectedQuotas = ({ quotas }) => { - const sortedQuotas = sortBy( + let sortedQuotas = sortBy( quotas, q => { // Use a tuple of (index, name) so we can support unknown quotas @@ -70,6 +70,13 @@ const ProjectedQuotas = ({ quotas }) => { return [index >= 0 ? index : quotaOrdering.length, q.resource]; } ); + + // These components don't seem to get optional fields from the UI + // to filter for Coral credits resources with so just showing known + // quotas for now until we have a way to calculate projections for Coral + // or otherwise unknown quotas + sortedQuotas = sortedQuotas.filter((q) => quotaOrdering.includes(q.resource)); + return sortedQuotas.map( quota => Date: Fri, 28 Nov 2025 13:28:22 +0000 Subject: [PATCH 06/13] unset coral parameters by default --- chart/values.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chart/values.yaml b/chart/values.yaml index ca311016..b8c3eba5 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -183,11 +183,11 @@ settings: # description: >- # {{ cpus }} CPUs, {{ ram }} RAM, {{ disk }} disk, {{ ephemeral_disk }} ephemeral disk coralCredits: - uri: - passwordSecretRef: - name: - namespace: - key: + # uri: + # passwordSecretRef: + # name: + # namespace: + # key: # Configuration for authentication authentication: From 94511654f85ef283e3494951fec27095301fb237 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 15:05:21 +0000 Subject: [PATCH 07/13] Now accepts token directly --- api/azimuth/provider/openstack/provider.py | 11 ++--------- api/azimuth/settings.py | 2 +- chart/files/api/settings/13-coral-credits.yaml | 4 ++-- chart/values.yaml | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 0ddf53ff..140b3be2 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -290,13 +290,6 @@ def __init__( project_id_safe = self._connection.project_id.replace("-", "") self._project_share_user = prefix + project_id_safe - # Get Coral bearer token if enabled - if not cloud_settings.CORAL_CREDITS.ADMIN_PASSWORD is None: - self._coral_auth_token = requests.post(cloud_settings.CORAL_CREDITS.CORAL_URI+"/api-token-auth/",json={ - "username": "admin", - "password": cloud_settings.CORAL_CREDITS.ADMIN_PASSWORD - }).json()["token"] - def _log(self, message, *args, level=logging.INFO, **kwargs): logger.log( level, @@ -377,7 +370,7 @@ def quotas(self): ) ) # Get coral credits if available - if not cloud_settings.CORAL_CREDITS.CORAL_URI is None: + if not (cloud_settings.CORAL_CREDITS.CORAL_URI is None or cloud_settings.CORAL_CREDITS.TOKEN is None): quotas.extend(self.get_coral_quotas()) # The volume service is optional # In the case where the service is not enabled, just don't add the quotas @@ -406,7 +399,7 @@ def quotas(self): return quotas def get_coral_quotas(self): - headers = {"Authorization": "Bearer "+self._coral_auth_token} + headers = {"Authorization": "Bearer "+cloud_settings.CORAL_CREDITS.TOKEN} accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() tenancy_account_list = list(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts)) diff --git a/api/azimuth/settings.py b/api/azimuth/settings.py index 598b54a5..ca80b39f 100644 --- a/api/azimuth/settings.py +++ b/api/azimuth/settings.py @@ -216,7 +216,7 @@ class SchedulingSettings(SettingsObject): ENABLED = Setting(default=False) class CoralCreditsSetting(SettingsObject): - ADMIN_PASSWORD = Setting(default=None) + TOKEN = Setting(default=None) CORAL_URI = Setting(default=None) class AzimuthSettings(SettingsObject): diff --git a/chart/files/api/settings/13-coral-credits.yaml b/chart/files/api/settings/13-coral-credits.yaml index f47204c2..b4cfff04 100644 --- a/chart/files/api/settings/13-coral-credits.yaml +++ b/chart/files/api/settings/13-coral-credits.yaml @@ -2,7 +2,7 @@ AZIMUTH: CORAL_CREDITS: CORAL_URI: {{ quote .uri }} - {{- with .passwordSecretRef }} - ADMIN_PASSWORD: {{ index (lookup "v1" "Secret" .namespace .name).data .key | b64dec }} + {{- with .tokenSecretRef }} + TOKEN: {{ index (lookup "v1" "Secret" .namespace .name).data .key | b64dec }} {{- end }} {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index b8c3eba5..e2129b9a 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -184,7 +184,7 @@ settings: # {{ cpus }} CPUs, {{ ram }} RAM, {{ disk }} disk, {{ ephemeral_disk }} ephemeral disk coralCredits: # uri: - # passwordSecretRef: + # tokenSecretRef: # name: # namespace: # key: From e94eac3203c9761096a48ea6c9bda374a1df8b04 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 15:12:50 +0000 Subject: [PATCH 08/13] linting --- api/azimuth/provider/openstack/provider.py | 72 ++++++++++++++++------ api/azimuth/settings.py | 2 + 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 140b3be2..4287ac8c 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -5,7 +5,6 @@ import base64 import dataclasses import datetime -from django.utils.timezone import make_aware import functools import hashlib import logging @@ -18,10 +17,12 @@ import rackit import requests import yaml +from django.utils.timezone import make_aware + +from azimuth.settings import cloud_settings from .. import base, dto, errors # noqa: TID252 from . import api -from ...settings import cloud_settings logger = logging.getLogger(__name__) @@ -370,7 +371,10 @@ def quotas(self): ) ) # Get coral credits if available - if not (cloud_settings.CORAL_CREDITS.CORAL_URI is None or cloud_settings.CORAL_CREDITS.TOKEN is None): + if not ( + cloud_settings.CORAL_CREDITS.CORAL_URI is None + or cloud_settings.CORAL_CREDITS.TOKEN is None + ): quotas.extend(self.get_coral_quotas()) # The volume service is optional # In the case where the service is not enabled, just don't add the quotas @@ -397,44 +401,72 @@ def quotas(self): except api.ServiceNotSupported: pass return quotas - - def get_coral_quotas(self): - headers = {"Authorization": "Bearer "+cloud_settings.CORAL_CREDITS.TOKEN} - accounts = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", headers=headers).json() - tenancy_account_list = list(filter(lambda a: a["project_id"].replace('-', '') == self._tenancy.id,accounts)) + def get_coral_quotas(self): + headers = {"Authorization": "Bearer " + cloud_settings.CORAL_CREDITS.TOKEN} + accounts = requests.get( + cloud_settings.CORAL_CREDITS.CORAL_URI + "/resource_provider_account", + headers=headers, + ).json() + + tenancy_account_list = list( + filter( + lambda a: a["project_id"].replace("-", "") == self._tenancy.id, accounts + ) + ) if len(tenancy_account_list) != 1: return [] tenancy_account = tenancy_account_list[0]["account"] - all_allocations = requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers).json() - account_allocations = filter(lambda a:a["account"] == tenancy_account,all_allocations) + all_allocations = requests.get( + cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation", headers=headers + ).json() + account_allocations = filter( + lambda a: a["account"] == tenancy_account, all_allocations + ) datetime_format = "%Y-%m-%dT%H:%M:%SZ" current_time = make_aware(datetime.datetime.now()) target_tz = current_time.tzinfo - active_allocation_list = list(filter( - lambda a: datetime.datetime.strptime(a["start"],datetime_format).replace(tzinfo=target_tz) < current_time and - current_time < datetime.datetime.strptime(a["end"],datetime_format).replace(tzinfo=target_tz), - account_allocations)) - + active_allocation_list = list( + filter( + lambda a: datetime.datetime.strptime( + a["start"], datetime_format + ).replace(tzinfo=target_tz) + < current_time + and current_time + < datetime.datetime.strptime(a["end"], datetime_format).replace( + tzinfo=target_tz + ), + account_allocations, + ) + ) + human_readable_names = { "MEMORY_MB": "RAM (MB)", - "DISK_GB": "Root disk (GB)" # TODO: is this always the root disk? + "DISK_GB": "Root disk (GB)", # TODO: is this always the root disk? } - + quotas = [] if len(active_allocation_list) == 1: active_allocation_id = active_allocation_list[0]["id"] - for resource in requests.get(cloud_settings.CORAL_CREDITS.CORAL_URI + "/allocation/"+ str(active_allocation_id) +"/resources", headers=headers).json(): + for resource in requests.get( + cloud_settings.CORAL_CREDITS.CORAL_URI + + "/allocation/" + + str(active_allocation_id) + + "/resources", + headers=headers, + ).json(): resource_name = resource["resource_class"]["name"] quotas.append( dto.Quota( resource_name, - human_readable_names.get(resource_name, resource_name)+" hours (credits)", + human_readable_names.get(resource_name, resource_name) + + " hours (credits)", "resource hours", resource["allocated_resource_hours"], - resource["allocated_resource_hours"] - resource["resource_hours"] + resource["allocated_resource_hours"] + - resource["resource_hours"], ) ) return quotas diff --git a/api/azimuth/settings.py b/api/azimuth/settings.py index ca80b39f..9a8c3cad 100644 --- a/api/azimuth/settings.py +++ b/api/azimuth/settings.py @@ -215,10 +215,12 @@ class SchedulingSettings(SettingsObject): #: Indicates whether advanced scheduling should be enabled ENABLED = Setting(default=False) + class CoralCreditsSetting(SettingsObject): TOKEN = Setting(default=None) CORAL_URI = Setting(default=None) + class AzimuthSettings(SettingsObject): """ Settings object for the ``AZIMUTH`` setting. From 65814965f7429045524425b4007b345cad80a7ec Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 15:45:55 +0000 Subject: [PATCH 09/13] Add quota/credit labels to views --- api/azimuth/provider/dto.py | 3 +++ api/azimuth/provider/openstack/provider.py | 5 +++-- ui/src/components/pages/tenancy/quotas.js | 9 ++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/api/azimuth/provider/dto.py b/api/azimuth/provider/dto.py index 130eda3b..4a70df15 100644 --- a/api/azimuth/provider/dto.py +++ b/api/azimuth/provider/dto.py @@ -67,6 +67,9 @@ class Quota: allocated: int #: The amount of the resource that has been used used: int + #: Indicates if this is a quota for Coral credits, as opposed to + #: an Openstack resource + is_coral_quota: bool = False @dataclass(frozen=True) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 4287ac8c..ef3fc0df 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -444,7 +444,7 @@ def get_coral_quotas(self): human_readable_names = { "MEMORY_MB": "RAM (MB)", - "DISK_GB": "Root disk (GB)", # TODO: is this always the root disk? + "DISK_GB": "Root disk (GB)", } quotas = [] @@ -462,11 +462,12 @@ def get_coral_quotas(self): dto.Quota( resource_name, human_readable_names.get(resource_name, resource_name) - + " hours (credits)", + + " hours", "resource hours", resource["allocated_resource_hours"], resource["allocated_resource_hours"] - resource["resource_hours"], + is_coral_quota=True ) ) return quotas diff --git a/ui/src/components/pages/tenancy/quotas.js b/ui/src/components/pages/tenancy/quotas.js index edf83486..68670d34 100644 --- a/ui/src/components/pages/tenancy/quotas.js +++ b/ui/src/components/pages/tenancy/quotas.js @@ -56,7 +56,7 @@ const quotaOrdering = ["machines", "volumes", "external_ips", "cpus", "ram", "st const Quotas = ({ resourceData }) => { - const sortedQuotas = sortBy( + let sortedQuotas = sortBy( Object.values(resourceData), q => { // Use a tuple of (index, name) so we can support unknown quotas @@ -64,6 +64,13 @@ const Quotas = ({ resourceData }) => { return [index >= 0 ? index : quotaOrdering.length, q.resource]; } ); + const hasMixedCoralAndResourceQuotas = new Set(sortedQuotas.map((q) => q.is_coral_quota)).size > 1 + if(hasMixedCoralAndResourceQuotas){ + for(let i = 0;i < sortedQuotas.length;i++){ + const prefix = sortedQuotas[i].is_coral_quota ? "Credits: " : "Quota: "; + sortedQuotas[i].label = prefix + sortedQuotas[i].label; + } + } return ( // The volume service is optional, so quotas might not always be available for it From 58ed50bc8e4035bbf1b4427d07dfeca9b94e17f4 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 15:48:01 +0000 Subject: [PATCH 10/13] Linting + snapshot --- api/azimuth/provider/openstack/provider.py | 2 +- chart/tests/__snapshot__/snapshot_test.yaml.snap | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) mode change 100644 => 100755 chart/tests/__snapshot__/snapshot_test.yaml.snap diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index ef3fc0df..41338a1d 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -467,7 +467,7 @@ def get_coral_quotas(self): resource["allocated_resource_hours"], resource["allocated_resource_hours"] - resource["resource_hours"], - is_coral_quota=True + is_coral_quota=True, ) ) return quotas diff --git a/chart/tests/__snapshot__/snapshot_test.yaml.snap b/chart/tests/__snapshot__/snapshot_test.yaml.snap old mode 100644 new mode 100755 index d5b68090..531e1675 --- a/chart/tests/__snapshot__/snapshot_test.yaml.snap +++ b/chart/tests/__snapshot__/snapshot_test.yaml.snap @@ -158,7 +158,7 @@ templated manifests should match snapshot: template: metadata: annotations: - azimuth.stackhpc.com/settings-checksum: 4456f249ad2b10af8275e59c37bb0435e01dcc236bdb926b6ebc3c19ba8eeea6 + azimuth.stackhpc.com/settings-checksum: ba9764bba470b2cacf65a4646a795dd5a62707a4879baac5749a8884af5dc0cd azimuth.stackhpc.com/theme-checksum: ec0f36322392deee39d80b7f77ecd634df60358857af9dc208077860c4e174ab kubectl.kubernetes.io/default-container: api labels: @@ -273,6 +273,8 @@ templated manifests should match snapshot: QVpJTVVUSDoKICBTQ0hFRFVMSU5HOgogICAgRU5BQkxFRDogZmFsc2UK 12-apps-provider.yaml: | Cg== + 13-coral-credits.yaml: | + Cg== kind: Secret metadata: labels: From 2884f2b27ac292c71d848b35fe0894789728fd53 Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 16:24:21 +0000 Subject: [PATCH 11/13] Add quota for allocation expiry --- api/azimuth/provider/openstack/provider.py | 97 +++++++++++++--------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 41338a1d..df1f35f7 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -401,6 +401,56 @@ def quotas(self): except api.ServiceNotSupported: pass return quotas + + def coral_quotas_from_allocation(self,allocation,headers): + quotas = [] + + human_readable_names = { + "MEMORY_MB": "RAM (MB)", + "DISK_GB": "Root disk (GB)", + } + + # Add quota for time until allocation expiry + current_time = make_aware(datetime.datetime.now()) + target_tz = current_time.tzinfo + start_time = parse_time_and_correct_tz(allocation["start"],target_tz) + end_time = parse_time_and_correct_tz(allocation["end"],target_tz) + + allocated_duration = (end_time - start_time).total_seconds() / 3600 + used_duration = (current_time - start_time).total_seconds() / 3600 + + quotas.append(dto.Quota( + "expiry", + "Allocated time used (hours)", + "hours", + int(allocated_duration), + int(used_duration), + is_coral_quota=True + )) + + # Add quotas for Coral resource quotas + active_allocation_id = allocation["id"] + + for resource in requests.get( + cloud_settings.CORAL_CREDITS.CORAL_URI + + "/allocation/" + + str(active_allocation_id) + + "/resources", + headers=headers, + ).json(): + resource_name = resource["resource_class"]["name"] + quotas.append( + dto.Quota( + resource_name, + human_readable_names.get(resource_name, resource_name) + " hours", + "resource hours", + resource["allocated_resource_hours"], + resource["allocated_resource_hours"] + - resource["resource_hours"], + is_coral_quota=True, + ) + ) + return quotas def get_coral_quotas(self): headers = {"Authorization": "Bearer " + cloud_settings.CORAL_CREDITS.TOKEN} @@ -424,53 +474,23 @@ def get_coral_quotas(self): lambda a: a["account"] == tenancy_account, all_allocations ) - datetime_format = "%Y-%m-%dT%H:%M:%SZ" current_time = make_aware(datetime.datetime.now()) target_tz = current_time.tzinfo active_allocation_list = list( filter( - lambda a: datetime.datetime.strptime( - a["start"], datetime_format - ).replace(tzinfo=target_tz) - < current_time - and current_time - < datetime.datetime.strptime(a["end"], datetime_format).replace( - tzinfo=target_tz - ), + lambda a: + parse_time_and_correct_tz(a["start"],target_tz) < current_time + and + current_time < parse_time_and_correct_tz(a["end"],target_tz), account_allocations, ) ) - human_readable_names = { - "MEMORY_MB": "RAM (MB)", - "DISK_GB": "Root disk (GB)", - } - - quotas = [] if len(active_allocation_list) == 1: - active_allocation_id = active_allocation_list[0]["id"] - for resource in requests.get( - cloud_settings.CORAL_CREDITS.CORAL_URI - + "/allocation/" - + str(active_allocation_id) - + "/resources", - headers=headers, - ).json(): - resource_name = resource["resource_class"]["name"] - quotas.append( - dto.Quota( - resource_name, - human_readable_names.get(resource_name, resource_name) - + " hours", - "resource hours", - resource["allocated_resource_hours"], - resource["allocated_resource_hours"] - - resource["resource_hours"], - is_coral_quota=True, - ) - ) - return quotas + return self.coral_quotas_from_allocation(active_allocation_list[0],headers) + else: + return [] def _from_api_image(self, api_image): """ @@ -1585,3 +1605,6 @@ def close(self): """ # Make sure the underlying api connection is closed self._connection.close() + +def parse_time_and_correct_tz(time_str,tz): + return datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=tz) From 7f874bafa9af928bfe3cfc2ac43714f688bb3bbb Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Fri, 28 Nov 2025 16:25:19 +0000 Subject: [PATCH 12/13] Linting --- api/azimuth/provider/openstack/provider.py | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index df1f35f7..6a7f65d7 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -401,8 +401,8 @@ def quotas(self): except api.ServiceNotSupported: pass return quotas - - def coral_quotas_from_allocation(self,allocation,headers): + + def coral_quotas_from_allocation(self, allocation, headers): quotas = [] human_readable_names = { @@ -413,20 +413,22 @@ def coral_quotas_from_allocation(self,allocation,headers): # Add quota for time until allocation expiry current_time = make_aware(datetime.datetime.now()) target_tz = current_time.tzinfo - start_time = parse_time_and_correct_tz(allocation["start"],target_tz) - end_time = parse_time_and_correct_tz(allocation["end"],target_tz) - + start_time = parse_time_and_correct_tz(allocation["start"], target_tz) + end_time = parse_time_and_correct_tz(allocation["end"], target_tz) + allocated_duration = (end_time - start_time).total_seconds() / 3600 used_duration = (current_time - start_time).total_seconds() / 3600 - quotas.append(dto.Quota( - "expiry", - "Allocated time used (hours)", - "hours", - int(allocated_duration), - int(used_duration), - is_coral_quota=True - )) + quotas.append( + dto.Quota( + "expiry", + "Allocated time used (hours)", + "hours", + int(allocated_duration), + int(used_duration), + is_coral_quota=True, + ) + ) # Add quotas for Coral resource quotas active_allocation_id = allocation["id"] @@ -445,8 +447,7 @@ def coral_quotas_from_allocation(self,allocation,headers): human_readable_names.get(resource_name, resource_name) + " hours", "resource hours", resource["allocated_resource_hours"], - resource["allocated_resource_hours"] - - resource["resource_hours"], + resource["allocated_resource_hours"] - resource["resource_hours"], is_coral_quota=True, ) ) @@ -479,16 +480,15 @@ def get_coral_quotas(self): active_allocation_list = list( filter( - lambda a: - parse_time_and_correct_tz(a["start"],target_tz) < current_time - and - current_time < parse_time_and_correct_tz(a["end"],target_tz), + lambda a: parse_time_and_correct_tz(a["start"], target_tz) + < current_time + and current_time < parse_time_and_correct_tz(a["end"], target_tz), account_allocations, ) ) if len(active_allocation_list) == 1: - return self.coral_quotas_from_allocation(active_allocation_list[0],headers) + return self.coral_quotas_from_allocation(active_allocation_list[0], headers) else: return [] @@ -1606,5 +1606,6 @@ def close(self): # Make sure the underlying api connection is closed self._connection.close() -def parse_time_and_correct_tz(time_str,tz): + +def parse_time_and_correct_tz(time_str, tz): return datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=tz) From 62464318931722d5a3e17212ca289ce8018f361f Mon Sep 17 00:00:00 2001 From: wtripp180901 Date: Mon, 1 Dec 2025 13:14:52 +0000 Subject: [PATCH 13/13] Fix label categories being repeatedlt prefixed --- ui/src/components/pages/tenancy/quotas.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/src/components/pages/tenancy/quotas.js b/ui/src/components/pages/tenancy/quotas.js index 68670d34..20899722 100644 --- a/ui/src/components/pages/tenancy/quotas.js +++ b/ui/src/components/pages/tenancy/quotas.js @@ -16,7 +16,7 @@ import { sortBy, usePageTitle, formatSize } from '../../utils'; import { ResourcePanel } from './resource-utils'; -const QuotaProgress = ({ quota: { label, units, allocated, used } }) => { +const QuotaProgress = ({ quota: { label, units, allocated, used, is_coral_quota }, addPrefix }) => { const percent = allocated > 0 ? (used * 100) / allocated : 0; const formatAmount = amount => ( ["MB", "GB"].includes(units) ? @@ -29,10 +29,17 @@ const QuotaProgress = ({ quota: { label, units, allocated, used } }) => { `${formatAmount(used)} used` ); const colour = (percent <= 60 ? '#5cb85c' : (percent <= 80 ? '#f0ad4e' : '#d9534f')); + + let labelPrefix = "" + if(addPrefix){ + labelPrefix = is_coral_quota ? "Credits: " : "Quota: "; + } + + const displayLabel = labelPrefix + label return ( - {label} + {displayLabel} { return [index >= 0 ? index : quotaOrdering.length, q.resource]; } ); - const hasMixedCoralAndResourceQuotas = new Set(sortedQuotas.map((q) => q.is_coral_quota)).size > 1 - if(hasMixedCoralAndResourceQuotas){ - for(let i = 0;i < sortedQuotas.length;i++){ - const prefix = sortedQuotas[i].is_coral_quota ? "Credits: " : "Quota: "; - sortedQuotas[i].label = prefix + sortedQuotas[i].label; - } - } + const hasMixedCoralAndResourceQuotas = new Set( + sortedQuotas.map((q) => q.is_coral_quota) + ).size > 1 return ( // The volume service is optional, so quotas might not always be available for it {sortedQuotas.map(quota => ( - ) + ) )} );