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 1, 2024
1 parent e196076 commit 43f9bf6
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 13 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
2 changes: 2 additions & 0 deletions ci/devstack_esi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# (Quan Pham) TODO Know we want to install in a test Devstack cluster
# to test ESI
32 changes: 32 additions & 0 deletions ci/run_functional_tests_esi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Creates the appropriate credentials and runs tests
#
# Tests expect the resource to be name ESI
set -xe

source /opt/stack/devstack/openrc admin admin

credential_name=$(openssl rand -base64 12)

export ESI_ESI_APPLICATION_CREDENTIAL_SECRET=$(
openstack application credential create "$credential_name" -f value -c secret)
export ESI_ESI_APPLICATION_CREDENTIAL_ID=$(
openstack application credential show "$credential_name" -f value -c id)

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

if [[ ! "${CI}" == "true" ]]; then
source /tmp/coldfront_venv/bin/activate
fi

export DJANGO_SETTINGS_MODULE="local_settings"
export FUNCTIONAL_TESTS="True"
export OS_AUTH_URL="http://$HOST_IP/identity"
export KEYCLOAK_URL="http://$HOST_IP:8080"
export KEYCLOAK_USER="admin"
export KEYCLOAK_PASS="nomoresecret"
export KEYCLOAK_REALM="master"

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

openstack application credential delete $ESI_ESI_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)
]
72 changes: 72 additions & 0 deletions src/coldfront_plugin_cloud/esi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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

logger = logging.getLogger(__name__)

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

def get_session_for_resource(resource):
auth_url = resource.get_attribute(attributes.RESOURCE_AUTH_URL)
var_name = utils.env_safe_name(resource.name)
auth = v3.ApplicationCredential(
auth_url=auth_url,
application_credential_id=os.environ.get(
f'ESI_{var_name}_APPLICATION_CREDENTIAL_ID'),
application_credential_secret=os.environ.get(
f'ESI_{var_name}_APPLICATION_CREDENTIAL_SECRET')
)
return session.Session(
auth,
verify=os.environ.get('FUNCTIONAL_TESTS', '') != 'True'
)


class ESIResourceAllocator(OpenStackResourceAllocator):

resource_type = 'esi'

@functools.cached_property
def session(self) -> session.Session:
return get_session_for_resource(self.resource)

def set_quota(self, project_id):
for service_name, service in QUOTA_KEY_MAPPING.items():
if service_name not in self.available_service_types:
logger.error(f"Service {service_name} needed for ESI allocation not available!")
else:
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

if service_name == 'network':
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)
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 @@ -273,3 +273,17 @@ def handle(self, *args, **options):
except Exception as e:
logger.error(f'setting openshift quota failed: {e}')
continue


# (Quan Pham) TODO Implement validating ESI resource
# Should these validation code be put in a sepearate file or
# in tasks.py or utils.py as functions for each resource type?
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')
)
9 changes: 0 additions & 9 deletions src/coldfront_plugin_cloud/openstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@

# Map the attribute name in ColdFront, to the client of the respective
# service, the version of the API, and the key in the payload.
# (Quan Pham) TODO: The name of the services should be the same as the
# names or aliases listed in the Openstack Service Type Authority list
# (https://opendev.org/openstack/service-types-authority/)
#
# This is because we query the Openstack cluster to check what services
# are available. It is probably a bad idea to hardcode the service names here
QUOTA_KEY_MAPPING = {
'compute': {
'keys': {
Expand Down Expand Up @@ -149,8 +143,6 @@ def __init__(self, resource, allocation):
service_list = self.identity.services.list()
self.available_service_types = [service.type for service in service_list]



def create_project(self, suggested_project_name) -> base.ResourceAllocator.Project:
project_name = utils.get_unique_project_name(
suggested_project_name,
Expand Down Expand Up @@ -209,7 +201,6 @@ def _set_object_quota(self, project_id, payload):
payload[obj_q_mapping] *= GB_IN_BYTES
if payload[obj_q_mapping] <= 0:
payload[obj_q_mapping] = 1

self.object(project_id).post_account(headers=payload)
except ksa_exceptions.catalog.EndpointNotFound:
logger.debug('No swift available, skipping its quota.')
Expand Down
6 changes: 6 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 Down Expand Up @@ -48,6 +49,10 @@
},
'openshift': {
attributes.QUOTA_REQUESTS_GPU: 0,
},
'esi': {
attributes.ESI_FLOATING_IPS: 1,
attributes.ESI_NETWORKS: 1
}
}

Expand All @@ -56,6 +61,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('ESI_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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
import unittest
import uuid

from coldfront_plugin_cloud import esi
from coldfront_plugin_cloud.tests import base

from django.core.management import call_command
from keystoneclient.v3 import client
from cinderclient import client as cinderclient
from neutronclient.v2_0 import client as neutronclient
from novaclient import client as novaclient

@unittest.skipUnless(os.getenv('FUNCTIONAL_TESTS'), 'Functional tests not enabled.')
class TestAllocation(base.TestBase):

# (Quan Pham) TODO What are the test cases for an ESI resource?
# What do we need to test?

def setUp(self) -> None:
super().setUp()
self.resource = self.new_esi_resource(name='ESI',
auth_url=os.getenv('OS_AUTH_URL'))
self.session = esi.get_session_for_resource(self.resource)
self.identity = client.Client(session=self.session)
self.compute = novaclient.Client(session=self.session, version=2)
self.volume = cinderclient.Client(session=self.session, version=3)
self.networking = neutronclient.Client(session=self.session)
self.role_member = self.identity.roles.find(name='member')

def test_new_ESI_allocation(self):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TestAllocation(base.TestBase):

def setUp(self) -> None:
super().setUp()
self.resource = self.new_resource(name='Devstack',
self.resource = self.new_openstack_resource(name='Devstack',
auth_url=os.getenv('OS_AUTH_URL'))
self.session = openstack.get_session_for_resource(self.resource)
self.identity = client.Client(session=self.session)
Expand Down
Loading

0 comments on commit 43f9bf6

Please sign in to comment.