Skip to content
Open
3 changes: 3 additions & 0 deletions api/azimuth/provider/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
105 changes: 105 additions & 0 deletions api/azimuth/provider/openstack/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import base64
import dataclasses
import datetime
import functools
import hashlib
import logging
Expand All @@ -14,7 +15,11 @@
import certifi
import dateutil.parser
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
Expand Down Expand Up @@ -365,6 +370,12 @@ 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
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
try:
Expand All @@ -391,6 +402,96 @@ def quotas(self):
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}
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
)

current_time = make_aware(datetime.datetime.now())
target_tz = current_time.tzinfo

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),
account_allocations,
)
)

if len(active_allocation_list) == 1:
return self.coral_quotas_from_allocation(active_allocation_list[0], headers)
else:
return []

def _from_api_image(self, api_image):
"""
Converts an OpenStack API image object into a :py:class:`.dto.Image`.
Expand Down Expand Up @@ -1504,3 +1605,7 @@ 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)
7 changes: 7 additions & 0 deletions api/azimuth/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ class SchedulingSettings(SettingsObject):
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.
Expand Down Expand Up @@ -295,6 +300,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/"
Expand Down
8 changes: 8 additions & 0 deletions chart/files/api/settings/13-coral-credits.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{- with .Values.settings.coralCredits }}
AZIMUTH:
CORAL_CREDITS:
CORAL_URI: {{ quote .uri }}
{{- with .tokenSecretRef }}
TOKEN: {{ index (lookup "v1" "Secret" .namespace .name).data .key | b64dec }}
{{- end }}
{{- end }}
2 changes: 2 additions & 0 deletions chart/templates/api/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
4 changes: 3 additions & 1 deletion chart/tests/__snapshot__/snapshot_test.yaml.snap
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -273,6 +273,8 @@ templated manifests should match snapshot:
QVpJTVVUSDoKICBTQ0hFRFVMSU5HOgogICAgRU5BQkxFRDogZmFsc2UK
12-apps-provider.yaml: |
Cg==
13-coral-credits.yaml: |
Cg==
kind: Secret
metadata:
labels:
Expand Down
6 changes: 6 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ settings:
# # and "ephemeral_disk" for the current flavor
# description: >-
# {{ cpus }} CPUs, {{ ram }} RAM, {{ disk }} disk, {{ ephemeral_disk }} ephemeral disk
coralCredits:
# uri:
# tokenSecretRef:
# name:
# namespace:
# key:

# Configuration for authentication
authentication:
Expand Down
9 changes: 8 additions & 1 deletion ui/src/components/pages/tenancy/platforms/scheduling.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,21 @@ 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
const index = quotaOrdering.findIndex(el => el === q.resource);
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 => <ProjectedQuotaProgressBar
key={quota.resource}
Expand Down
18 changes: 14 additions & 4 deletions ui/src/components/pages/tenancy/quotas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?
Expand All @@ -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 (
<Col className="quota-card-wrapper">
<Card className="h-100">
<Card.Header><strong>{label}</strong></Card.Header>
<Card.Header><strong>{displayLabel}</strong></Card.Header>
<Card.Body>
<CircularProgressbar
className={allocated < 0 ? "quota-no-limit" : undefined}
Expand All @@ -56,19 +63,22 @@ 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
const index = quotaOrdering.findIndex(el => el === q.resource);
return [index >= 0 ? index : quotaOrdering.length, q.resource];
}
);
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
<Row className="g-3 justify-content-center">
{sortedQuotas.map(quota => (
<QuotaProgress key={quota.resource} quota={quota} />)
<QuotaProgress key={quota.resource} quota={quota} addPrefix={hasMixedCoralAndResourceQuotas}/>)
)}
</Row>
);
Expand Down
Loading