Skip to content

Commit

Permalink
Intergrated ESI, allowing users to allocate ESI Network quotas
Browse files Browse the repository at this point in the history
  • Loading branch information
QuanMPhm committed Jul 2, 2024
1 parent 6a2d53a commit ff61b8d
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ usage: coldfront add_openshift_resource [-h] --name NAME --auth-url AUTH_URL [--
coldfront add_openshift_resource: error: the following arguments are required: --name, --auth-url
```

### Configuring for ESI
(Quan Pham) TODO Add instructions for configuring ESI

### Quotas

The amount of quota to start out a resource allocation after approval, can be
Expand Down
4 changes: 4 additions & 0 deletions ci/run_functional_tests_openstack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_SECRET=$(
export OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_ID=$(
openstack application credential show "$credential_name" -f value -c id)

export ESI_ESI_APPLICATION_CREDENTIAL_SECRET=$OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_SECRET
export ESI_ESI_APPLICATION_CREDENTIAL_ID=$OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_ID

export OPENSTACK_PUBLIC_NETWORK_ID=$(openstack network show public -f value -c id)

if [[ ! "${CI}" == "true" ]]; then
Expand All @@ -28,6 +31,7 @@ export KEYCLOAK_PASS="nomoresecret"
export KEYCLOAK_REALM="master"

coverage run --source="." -m django test coldfront_plugin_cloud.tests.functional.openstack
coverage run --source="." -m django test coldfront_plugin_cloud.tests.functional.esi
coverage report

openstack application credential delete $OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_ID
6 changes: 6 additions & 0 deletions src/coldfront_plugin_cloud/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class CloudAllocationAttribute:
QUOTA_REQUESTS_GPU = 'OpenShift Request on GPU Quota'
QUOTA_PVC = 'OpenShift Persistent Volume Claims Quota'

###########################################################
# ESI Quota Attributes
ESI_FLOATING_IPS = 'ESI Floating IP Quota'
ESI_NETWORKS = 'ESI Network Quota'

ALLOCATION_QUOTA_ATTRIBUTES = [
CloudAllocationAttribute(name=QUOTA_INSTANCES),
Expand All @@ -105,4 +109,6 @@ class CloudAllocationAttribute:
CloudAllocationAttribute(name=QUOTA_REQUESTS_STORAGE),
CloudAllocationAttribute(name=QUOTA_REQUESTS_GPU),
CloudAllocationAttribute(name=QUOTA_PVC),
CloudAllocationAttribute(name=ESI_FLOATING_IPS),
CloudAllocationAttribute(name=ESI_NETWORKS)
]
54 changes: 54 additions & 0 deletions src/coldfront_plugin_cloud/esi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import logging
import functools
import os

from keystoneauth1.identity import v3
from keystoneauth1 import session


from coldfront_plugin_cloud import attributes, utils

from coldfront_plugin_cloud.openstack import OpenStackResourceAllocator


QUOTA_KEY_MAPPING = {
'network': {
'keys': {
attributes.ESI_FLOATING_IPS: 'floatingip',
attributes.ESI_NETWORKS: 'network'
}
}
}


QUOTA_KEY_MAPPING_ALL_KEYS = dict()
for service in QUOTA_KEY_MAPPING.keys():
QUOTA_KEY_MAPPING_ALL_KEYS.update(QUOTA_KEY_MAPPING[service]['keys'])


class ESIResourceAllocator(OpenStackResourceAllocator):

resource_type = 'esi'

def set_quota(self, project_id):
for service_name, service in QUOTA_KEY_MAPPING.items():
payload = dict()
for coldfront_attr, openstack_key in service['keys'].items():
value = self.allocation.get_attribute(coldfront_attr)
if value is not None:
payload[openstack_key] = value

if not payload:
# Skip if service doesn't have any associated attributes
continue

self.network.update_quota(project_id, body={'quota': payload})

def get_quota(self, project_id):
quotas = dict()

network_quota = self.network.show_quota(project_id)['quota']
for k in QUOTA_KEY_MAPPING['network']['keys'].values():
quotas[k] = network_quota.get(k)

return quotas
100 changes: 100 additions & 0 deletions src/coldfront_plugin_cloud/management/commands/add_esi_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from django.core.management.base import BaseCommand
from django.core.management import call_command

from coldfront.core.resource.models import (Resource,
ResourceAttribute,
ResourceAttributeType,
ResourceType)

from coldfront_plugin_cloud import attributes


class Command(BaseCommand):
help = 'Create ESI resource'
# (Quan Pham) TODO Know what other attributes an ESI resource must/could have
# That could be set from cli, or through other means

def add_arguments(self, parser):
parser.add_argument('--name', type=str, required=True,
help='Name of ESI resource')
parser.add_argument('--auth-url', type=str, required=True,
help='URL of the ESI esi Identity Endpoint')
parser.add_argument('--users-domain', type=str, default='default',
help='Domain ID to create users')
parser.add_argument('--projects-domain', type=str, default='default',
help='Domain ID to create projects')
parser.add_argument('--idp', type=str, required=True,
help='Identity provider configured in ESI cluster')
parser.add_argument('--protocol', type=str, default='openid',
help='Federation protocol (default: openid)')
parser.add_argument('--role', type=str, default='member',
help='Role for user when added to project (default: member)')
parser.add_argument('--public-network', type=str, default='',
help='Public network ID for default networks. '
'If not specified, no default network is '
'created for new projects.')
parser.add_argument('--network-cidr', type=str, default='192.168.0.0/24',
help='CIDR for default networks. '
'Ignored if no --public-network.')

def handle(self, *args, **options):
esi, _ = Resource.objects.get_or_create(
resource_type=ResourceType.objects.get(name='ESI'),
parent_resource=None,
name=options['name'],
description='ESI Bare Metal environment',
is_available=True,
is_public=True,
is_allocatable=True
)

ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_AUTH_URL),
resource=esi,
value=options['auth_url']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_PROJECT_DOMAIN),
resource=esi,
value=options['projects_domain']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_USER_DOMAIN),
resource=esi,
value=options['users_domain']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_IDP),
resource=esi,
value=options['idp']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_FEDERATION_PROTOCOL),
resource=esi,
value=options['protocol']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_ROLE),
resource=esi,
value=options['role']
)

if options['public_network']:
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_DEFAULT_PUBLIC_NETWORK),
resource=esi,
value=options['public_network']
)
ResourceAttribute.objects.get_or_create(
resource_attribute_type=ResourceAttributeType.objects.get(
name=attributes.RESOURCE_DEFAULT_NETWORK_CIDR),
resource=esi,
value=options['network_cidr']
)
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ def register_resource_type(self):
resource_models.ResourceType.objects.get_or_create(
name='OpenShift', description='OpenShift Cloud'
)
resource_models.ResourceType.objects.get_or_create(
name='ESI', description='ESI Bare Metal Cloud'
)

def handle(self, *args, **options):
self.register_resource_type()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from coldfront_plugin_cloud import attributes
from coldfront_plugin_cloud import openstack
from coldfront_plugin_cloud import openshift
from coldfront_plugin_cloud import esi
from coldfront_plugin_cloud import utils
from coldfront_plugin_cloud import tasks

Expand Down Expand Up @@ -273,3 +274,76 @@ def handle(self, *args, **options):
except Exception as e:
logger.error(f'setting openshift quota failed: {e}')
continue

esi_resources = Resource.objects.filter(
resource_type=ResourceType.objects.get(
name='ESI'
)
)
esi_allocations = Allocation.objects.filter(
resources__in=esi_resources,
status=AllocationStatusChoice.objects.get(name='Active')
)
for allocation in esi_allocations:
self.check_institution_specific_code(allocation, options["apply"])
allocation_str = f'{allocation.pk} of project "{allocation.project.title}"'
msg = f'Starting resource validation for allocation {allocation_str}.'
logger.debug(msg)

failed_validation = False

allocator = esi.ESIResourceAllocator(
allocation.resources.first(),
allocation
)

project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
if not project_id:
logger.error(f'{allocation_str} is active but has no Project ID set.')
continue

try:
allocator.identity.projects.get(project_id)
except http.NotFound:
logger.error(f'{allocation_str} has Project ID {project_id}. But'
f' no project found in OpenStack.')
continue

quota = allocator.get_quota(project_id)

failed_validation = Command.sync_users(project_id, allocation, allocator, options["apply"])

obj_key = openstack.QUOTA_KEY_MAPPING['object']['keys'][attributes.QUOTA_OBJECT_GB]

for attr in attributes.ALLOCATION_QUOTA_ATTRIBUTES:
if 'ESI' in attr.name:
key = esi.QUOTA_KEY_MAPPING_ALL_KEYS.get(attr.name, None)

expected_value = allocation.get_attribute(attr.name)
current_value = quota.get(key, None)
if expected_value is None and current_value:
msg = (f'Attribute "{attr.name}" expected on allocation {allocation_str} but not set.'
f' Current quota is {current_value}.')
if options['apply']:
utils.set_attribute_on_allocation(
allocation, attr.name, current_value
)
msg = f'{msg} Attribute set to match current quota.'
logger.warning(msg)
elif not current_value == expected_value:
failed_validation = True
msg = (f'Value for quota for {attr.name} = {current_value} does not match expected'
f' value of {expected_value} on allocation {allocation_str}')
logger.warning(msg)

if failed_validation and options['apply']:
try:
allocator.set_quota(
allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
)
except Exception as e:
logger.error(f'setting openstack quota failed: {e}')
continue
logger.warning(f'Quota for allocation {allocation_str} was out of date. Reapplied!')


10 changes: 10 additions & 0 deletions src/coldfront_plugin_cloud/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
base,
openstack,
openshift,
esi,
utils)

logger = logging.getLogger(__name__)
Expand All @@ -35,6 +36,10 @@
attributes.QUOTA_REQUESTS_STORAGE: 20,
attributes.QUOTA_REQUESTS_GPU: 0,
attributes.QUOTA_PVC: 2
},
'esi': {
attributes.ESI_FLOATING_IPS: 0,
attributes.ESI_NETWORKS: 0
}
}

Expand All @@ -48,6 +53,10 @@
},
'openshift': {
attributes.QUOTA_REQUESTS_GPU: 0,
},
'esi': {
attributes.ESI_FLOATING_IPS: 1,
attributes.ESI_NETWORKS: 1
}
}

Expand All @@ -56,6 +65,7 @@ def find_allocator(allocation) -> base.ResourceAllocator:
allocators = {
'openstack': openstack.OpenStackResourceAllocator,
'openshift': openshift.OpenShiftResourceAllocator,
'esi': esi.ESIResourceAllocator,
}
# TODO(knikolla): It doesn't seem to be possible to select multiple resources
# when requesting a new allocation, so why is this multivalued?
Expand Down
20 changes: 19 additions & 1 deletion src/coldfront_plugin_cloud/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,27 @@ def new_user(username=None) -> User:
username = username or f'{uuid.uuid4().hex}@example.com'
User.objects.create(username=username, email=username)
return User.objects.get(username=username)

@staticmethod
def new_esi_resource(name=None, auth_url=None) -> Resource:
resource_name = name or uuid.uuid4().hex

call_command(
'add_esi_resource',
name=resource_name,
auth_url=auth_url or f'https://{resource_name}/identity/v3',
projects_domain='default',
users_domain='default',
idp='sso',
protocol='openid',
role='member',
public_network=os.getenv('OPENSTACK_PUBLIC_NETWORK_ID'),
network_cidr='192.168.0.0/24',
)
return Resource.objects.get(name=resource_name)

@staticmethod
def new_resource(name=None, auth_url=None) -> Resource:
def new_openstack_resource(name=None, auth_url=None) -> Resource:
resource_name = name or uuid.uuid4().hex

call_command(
Expand Down
Empty file.
Loading

0 comments on commit ff61b8d

Please sign in to comment.