diff --git a/.github/workflows/test-py38-functional-devstack.yaml b/.github/workflows/test-py38-functional-devstack.yaml index 5f7a563..a06533a 100644 --- a/.github/workflows/test-py38-functional-devstack.yaml +++ b/.github/workflows/test-py38-functional-devstack.yaml @@ -23,4 +23,4 @@ jobs: - name: Run functional tests run: | - ./ci/run_functional_tests.sh + ./ci/run_functional_tests_openstack.sh diff --git a/.github/workflows/test-py39-functional-microshift.yaml b/.github/workflows/test-py39-functional-microshift.yaml new file mode 100644 index 0000000..775d709 --- /dev/null +++ b/.github/workflows/test-py39-functional-microshift.yaml @@ -0,0 +1,31 @@ +name: test-py39-functional-microshift + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Install Microshift + run: | + ./ci/microshift.sh + + - name: Install ColdFront and plugin + run: | + ./ci/setup.sh + + - name: Run functional tests + run: | + ./ci/run_functional_tests_openshift.sh diff --git a/.github/workflows/test-py39-functional.yaml b/.github/workflows/test-py39-functional.yaml index 14da222..650b4c8 100644 --- a/.github/workflows/test-py39-functional.yaml +++ b/.github/workflows/test-py39-functional.yaml @@ -45,4 +45,4 @@ jobs: export OPENSTACK_PUBLIC_NETWORK_ID=$(microstack.openstack network show external -f value -c id) export OS_AUTH_URL="https://localhost:5000" - coldfront test coldfront_plugin_openstack.tests.functional + coldfront test coldfront_plugin_openstack.tests.functional.openstack diff --git a/Vagrantfile b/Vagrantfile index 717057c..f6105b1 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,21 +1,42 @@ Vagrant.configure("2") do |config| - config.vm.box = "generic/ubuntu2004" config.vm.synced_folder ".", "/home/vagrant/coldfront-plugin-openstack/" - config.vm.network :private_network - config.vm.provider "vmware_fusion" do |vb| - vb.gui = false - vb.memory = "9000" - vb.cpus = "4" + config.vm.define "openstack" do |openstack| + openstack.vm.box = "generic/ubuntu2004" + + openstack.vm.provider "vmware_fusion" do |vb| + vb.gui = false + vb.memory = "9000" + vb.cpus = "4" + end + + openstack.vm.provision "shell", privileged: false, inline: <<-SHELL + set -xe + + cd ~/coldfront-plugin-openstack + ./ci/devstack.sh + ./ci/setup.sh + ./ci/run_functional_tests.sh + SHELL end - config.vm.provision "shell", privileged: false, inline: <<-SHELL - set -xe + config.vm.define "openshift" do |openshift| + openshift.vm.box = "generic/ubuntu2004" - cd ~/coldfront-plugin-openstack - ./ci/devstack.sh - ./ci/setup.sh - ./ci/run_functional_tests.sh - SHELL + openshift.vm.provider "vmware_fusion" do |vb| + vb.gui = false + vb.memory = "4096" + vb.cpus = "4" + end + + openshift.vm.provision "shell", privileged: false, inline: <<-SHELL + set -xe + + cd ~/coldfront-plugin-openstack + ./ci/microshift.sh + ./ci/setup.sh + ./ci/run_functional_tests_openshift.sh + SHELL + end end diff --git a/ci/microshift.sh b/ci/microshift.sh new file mode 100755 index 0000000..fba3423 --- /dev/null +++ b/ci/microshift.sh @@ -0,0 +1,44 @@ +# +# Installs Microshift on Docker +# +set -xe + +export ACCT_MGT_VERSION="e955158dc9fbd2a7aa68a8818fb7018315141d2b" + +sudo apt-get update && sudo apt-get upgrade -y + +if [[ ! "${CI}" == "true" ]]; then + sudo apt-get install docker.io docker-compose python3-virtualenv -y +fi + +echo '127.0.0.1 onboarding-onboarding.cluster.local' | sudo tee -a /etc/hosts + +sudo docker run -d --rm --name microshift --privileged \ + --network host \ + -v microshift-data:/var/lib \ + quay.io/microshift/microshift-aio:latest + +sudo docker run -d --name registry --network host registry:2 + +curl -O "https://mirror.openshift.com/pub/openshift-v4/$(uname -m)/clients/ocp/stable/openshift-client-linux.tar.gz" +sudo tar -xf openshift-client-linux.tar.gz -C /usr/local/bin oc kubectl + +mkdir ~/.kube +sudo docker cp microshift:/var/lib/microshift/resources/kubeadmin/kubeconfig ~/.kube/config + +while ! oc get all -h; do + echo "Waiting on Microshift" + sleep 5 +done + +# Install OpenShift Account Management +git clone https://github.com/cci-moc/openshift-acct-mgt.git ~/openshift-acct-mgt +cd ~/openshift-acct-mgt +git checkout "$ACCT_MGT_VERSION" +sudo docker build . -t "localhost:5000/cci-moc/openshift-acct-mgt:latest" +sudo docker push "localhost:5000/cci-moc/openshift-acct-mgt:latest" + +oc apply -k k8s/overlays/crc +oc wait -n onboarding --for=condition=available --timeout=800s deployment/onboarding + +sleep 60 diff --git a/ci/run_functional_tests_openshift.sh b/ci/run_functional_tests_openshift.sh new file mode 100755 index 0000000..209e373 --- /dev/null +++ b/ci/run_functional_tests_openshift.sh @@ -0,0 +1,17 @@ +# Creates the appropriate credentials and runs tests +# +# Tests expect the resource to be name Devstack +set -xe + +export OPENSHIFT_MICROSHIFT_USERNAME="admin" +export OPENSHIFT_MICROSHIFT_PASSWORD="pass" + +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="https://onboarding-onboarding.cluster.local" + +coldfront test coldfront_plugin_openstack.tests.functional.openshift diff --git a/ci/run_functional_tests.sh b/ci/run_functional_tests_openstack.sh similarity index 86% rename from ci/run_functional_tests.sh rename to ci/run_functional_tests_openstack.sh index 9ad4787..845216c 100755 --- a/ci/run_functional_tests.sh +++ b/ci/run_functional_tests_openstack.sh @@ -15,7 +15,9 @@ export OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_ID=$( export OPENSTACK_PUBLIC_NETWORK_ID=$(openstack network show public -f value -c id) -source /tmp/coldfront_venv/bin/activate +if [[ ! "${CI}" == "true" ]]; then + source /tmp/coldfront_venv/bin/activate +fi export DJANGO_SETTINGS_MODULE="local_settings" export FUNCTIONAL_TESTS="True" @@ -25,6 +27,6 @@ export KEYCLOAK_USER="admin" export KEYCLOAK_PASS="nomoresecret" export KEYCLOAK_REALM="master" -coldfront test coldfront_plugin_openstack.tests.functional +coldfront test coldfront_plugin_openstack.tests.functional.openstack openstack application credential delete $OPENSTACK_DEVSTACK_APPLICATION_CREDENTIAL_ID diff --git a/ci/setup.sh b/ci/setup.sh index 60d2f1c..e1deb20 100755 --- a/ci/setup.sh +++ b/ci/setup.sh @@ -1,7 +1,10 @@ set -xe -virtualenv -p python3 /tmp/coldfront_venv -source /tmp/coldfront_venv/bin/activate +# If running on Github actions, don't create a virtualenv +if [[ ! "${CI}" == "true" ]]; then + virtualenv -p python3 /tmp/coldfront_venv + source /tmp/coldfront_venv/bin/activate +fi pip3 install -r test-requirements.txt pip3 install -e . diff --git a/src/coldfront_plugin_openstack/attributes.py b/src/coldfront_plugin_openstack/attributes.py index 364d80b..ec9fc8d 100644 --- a/src/coldfront_plugin_openstack/attributes.py +++ b/src/coldfront_plugin_openstack/attributes.py @@ -1,4 +1,4 @@ -RESOURCE_AUTH_URL = 'OpenStack Auth URL' +RESOURCE_AUTH_URL = 'OpenStack Auth URL' # TODO: remove OpenStack prefix RESOURCE_FEDERATION_PROTOCOL = 'OpenStack Federation Protocol' RESOURCE_IDP = 'OpenStack Identity Provider' RESOURCE_PROJECT_DOMAIN = 'OpenStack Domain for Projects' @@ -16,6 +16,7 @@ RESOURCE_DEFAULT_PUBLIC_NETWORK, RESOURCE_DEFAULT_NETWORK_CIDR] +# TODO: Migration to rename the OpenStack specific prefix out of these attrs ALLOCATION_PROJECT_ID = 'OpenStack Project ID' ALLOCATION_PROJECT_NAME = 'OpenStack Project Name' diff --git a/src/coldfront_plugin_openstack/base.py b/src/coldfront_plugin_openstack/base.py index 3fffedc..9e70d3b 100644 --- a/src/coldfront_plugin_openstack/base.py +++ b/src/coldfront_plugin_openstack/base.py @@ -1,8 +1,11 @@ import abc +import functools from coldfront.core.allocation import models as allocation_models from coldfront.core.resource import models as resource_models +from coldfront_plugin_openstack import attributes + class ResourceAllocator(abc.ABC): @@ -19,6 +22,14 @@ def get_or_create_federated_user(self, username): user = self.create_federated_user(username) return user + @functools.cached_property + def auth_url(self): + return self.resource.get_attribute(attributes.RESOURCE_AUTH_URL).rstrip("/") + + @functools.cached_property + def member_role_name(self): + return self.resource.get_attribute(attributes.RESOURCE_ROLE) or 'member' + @abc.abstractmethod def create_project(self, project_name) -> str: pass diff --git a/src/coldfront_plugin_openstack/management/commands/add_openshift_resource.py b/src/coldfront_plugin_openstack/management/commands/add_openshift_resource.py new file mode 100644 index 0000000..a70249d --- /dev/null +++ b/src/coldfront_plugin_openstack/management/commands/add_openshift_resource.py @@ -0,0 +1,45 @@ +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_openstack import attributes + + +class Command(BaseCommand): + help = 'Create OpenShift resource' + + def add_arguments(self, parser): + parser.add_argument('--name', type=str, required=True, + help='Name of OpenShift resource') + parser.add_argument('--auth-url', type=str, required=True, + help='URL of the openshift-acct-mgt endpoint') + parser.add_argument('--role', type=str, default='edit', + help='Role for user when added to project (default: edit)') + + def handle(self, *args, **options): + openshift, _ = Resource.objects.get_or_create( + resource_type=ResourceType.objects.get(name='OpenShift'), + parent_resource=None, + name=options['name'], + description='OpenShift cloud 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=openshift, + value=options['auth_url'] + ) + ResourceAttribute.objects.get_or_create( + resource_attribute_type=ResourceAttributeType.objects.get( + name=attributes.RESOURCE_ROLE), + resource=openshift, + value=options['role'] + ) diff --git a/src/coldfront_plugin_openstack/management/commands/register_openstack_attributes.py b/src/coldfront_plugin_openstack/management/commands/register_cloud_attributes.py similarity index 85% rename from src/coldfront_plugin_openstack/management/commands/register_openstack_attributes.py rename to src/coldfront_plugin_openstack/management/commands/register_cloud_attributes.py index 1d2c6c5..ee5b11b 100644 --- a/src/coldfront_plugin_openstack/management/commands/register_openstack_attributes.py +++ b/src/coldfront_plugin_openstack/management/commands/register_cloud_attributes.py @@ -7,7 +7,7 @@ class Command(BaseCommand): - help = 'Add default OpenStack allocation related choices' + help = 'Add attributes for OpenStack and OpenShift resources/allocations' def register_allocation_attributes(self): def register(attribute_name, attribute_type): @@ -36,7 +36,11 @@ def register_resource_attributes(self): def register_resource_type(self): resource_models.ResourceType.objects.get_or_create( - name='OpenStack', description='OpenStack Cloud') + name='OpenStack', description='OpenStack Cloud' + ) + resource_models.ResourceType.objects.get_or_create( + name='OpenShift', description='OpenShift Cloud' + ) def handle(self, *args, **options): self.register_resource_type() diff --git a/src/coldfront_plugin_openstack/openshift.py b/src/coldfront_plugin_openstack/openshift.py new file mode 100644 index 0000000..cdc3854 --- /dev/null +++ b/src/coldfront_plugin_openstack/openshift.py @@ -0,0 +1,122 @@ +import functools +import json +import os +import requests +from requests.auth import HTTPBasicAuth +import time +import uuid + +from coldfront_plugin_openstack import attributes, base, utils + + +class ApiException(Exception): + def __init__(self, message): + self.message = message + + +class NotFound(ApiException): + pass + + +class OpenShiftResourceAllocator(base.ResourceAllocator): + + resource_type = 'openshift' + + @functools.cached_property + def session(self): + var_name = utils.env_safe_name(self.resource.name) + username = os.getenv(f'OPENSHIFT_{var_name}_USERNAME') + password = os.getenv(f'OPENSHIFT_{var_name}_PASSWORD') + + session = requests.session() + if username and password: + session.auth = HTTPBasicAuth(username, password) + + functional_tests = os.environ.get('FUNCTIONAL_TESTS', '').lower() + verify = os.getenv(f'OPENSHIFT_{var_name}_VERIFY', '').lower() + if functional_tests == 'true' or verify == 'false': + session.verify = False + + return session + + @staticmethod + def check_response(response: requests.Response): + if 200 <= response.status_code < 300: + return response.json() + if response.status_code == 404: + raise NotFound(f"{response.status_code}: {response.text}") + elif 'does not exist' in response.text or 'not found' in response.text: + raise NotFound(f"{response.status_code}: {response.text}") + else: + raise ApiException(f"{response.status_code}: {response.text}") + + def create_project(self, project_name): + project_id = uuid.uuid4().hex + self._create_project(project_name, project_id) + return project_id + + def set_quota(self, project_id): + pass + + def create_project_defaults(self, project_id): + pass + + def disable_project(self, project_id): + url = f"{self.auth_url}/projects/{project_id}" + r = self.session.delete(url) + self.check_response(r) + + def reactivate_project(self, project_id): + project_name = self.allocation.get_attribute(attributes.ALLOCATION_PROJECT_NAME) + self._create_project(project_name, project_id) + + def get_federated_user(self, username): + url = f"{self.auth_url}/users/{username}" + try: + r = self.session.get(url) + self.check_response(r) + return {'username': username} + except NotFound: + pass + + def create_federated_user(self, unique_id): + url = f"{self.auth_url}/users/{unique_id}" + r = self.session.put(url) + self.check_response(r) + + def assign_role_on_user(self, username, project_id): + # /users//projects//roles/ + url = (f"{self.auth_url}/users/{username}/projects/{project_id}" + f"/roles/{self.member_role_name}") + r = self.session.put(url) + self.check_response(r) + + def remove_role_from_user(self, username, project_id): + # /users//projects//roles/ + url = (f"{self.auth_url}/users/{username}/projects/{project_id}" + f"/roles/{self.member_role_name}") + r = self.session.delete(url) + self.check_response(r) + + def _create_project(self, project_name, project_id): + url = f"{self.auth_url}/projects/{project_id}" + payload = {"displayName": project_name} + r = self.session.put(url, data=json.dumps(payload)) + self.check_response(r) + + def _get_role(self, username, project_id): + # /users//projects//roles/ + url = (f"{self.auth_url}/users/{username}/projects/{project_id}" + f"/roles/{self.member_role_name}") + r = self.session.get(url) + return self.check_response(r) + + def _get_project(self, project_id): + url = f"{self.auth_url}/projects/{project_id}" + r = self.session.get(url) + return self.check_response(r) + + def _delete_user(self, username): + url = f"{self.auth_url}/users/{username}" + r = self.session.delete(url) + return self.check_response(r) diff --git a/src/coldfront_plugin_openstack/openstack.py b/src/coldfront_plugin_openstack/openstack.py index 1d4ce53..f1a5996 100644 --- a/src/coldfront_plugin_openstack/openstack.py +++ b/src/coldfront_plugin_openstack/openstack.py @@ -12,7 +12,7 @@ from neutronclient.v2_0 import client as neutronclient from novaclient import client as novaclient -from coldfront_plugin_openstack import attributes, base +from coldfront_plugin_openstack import attributes, base, utils logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def get_session_for_resource(resource): # uppercase. # This allows for the possibility of managing multiple OpenStack clouds # via multiple resources. - var_name = resource.name.replace(' ', '_').replace('-', '_').upper() + var_name = utils.env_safe_name(resource.name) auth = v3.ApplicationCredential( auth_url=auth_url, application_credential_id=os.environ.get( @@ -187,9 +187,7 @@ def create_federated_user(self, unique_id): return create_response.json()['user'] def assign_role_on_user(self, username, project_id): - role_name = self.resource.get_attribute(attributes.RESOURCE_ROLE) or 'member' - - role = self.identity.roles.find(name=role_name) + role = self.identity.roles.find(name=self.member_role_name) user = self.get_federated_user(username) self.identity.roles.grant(user=user['id'], @@ -197,8 +195,7 @@ def assign_role_on_user(self, username, project_id): role=role) def remove_role_from_user(self, username, project_id): - role_name = self.resource.get_attribute(attributes.RESOURCE_ROLE) or 'member' - role = self.identity.roles.find(name=role_name) + role = self.identity.roles.find(name=self.member_role_name) if user := self.get_federated_user(username): self.identity.roles.revoke(user=user['id'], project=project_id, role=role) diff --git a/src/coldfront_plugin_openstack/tasks.py b/src/coldfront_plugin_openstack/tasks.py index 57ad924..b4bd32a 100644 --- a/src/coldfront_plugin_openstack/tasks.py +++ b/src/coldfront_plugin_openstack/tasks.py @@ -6,7 +6,11 @@ from coldfront.core.allocation.models import (Allocation, AllocationUser) -from coldfront_plugin_openstack import attributes, base, openstack, utils +from coldfront_plugin_openstack import (attributes, + base, + openstack, + openshift, + utils) logger = logging.getLogger(__name__) @@ -23,7 +27,8 @@ attributes.QUOTA_FLOATING_IPS: 0, attributes.QUOTA_OBJECT_GB: 1, attributes.QUOTA_GPU: 0, - } + }, + 'openshift': dict() } # The amount of quota that every projects gets, @@ -33,13 +38,15 @@ 'openstack': { attributes.QUOTA_FLOATING_IPS: 2, attributes.QUOTA_GPU: 0, - } + }, + 'openshift': dict() } def find_allocator(allocation) -> base.ResourceAllocator: allocators = { 'openstack': openstack.OpenStackResourceAllocator, + 'openshift': openshift.OpenShiftResourceAllocator, } # TODO(knikolla): It doesn't seem to be possible to select multiple resources # when requesting a new allocation, so why is this multivalued? diff --git a/src/coldfront_plugin_openstack/tests/base.py b/src/coldfront_plugin_openstack/tests/base.py index ee1fb24..be54eb6 100644 --- a/src/coldfront_plugin_openstack/tests/base.py +++ b/src/coldfront_plugin_openstack/tests/base.py @@ -32,7 +32,7 @@ def setUp(self) -> None: backup, sys.stdout = sys.stdout, open(devnull, 'a') call_command('initial_setup', ) call_command('load_test_data') - call_command('register_openstack_attributes') + call_command('register_cloud_attributes') sys.stdout = backup @staticmethod @@ -59,6 +59,17 @@ def new_resource(name=None, auth_url=None) -> Resource: ) return Resource.objects.get(name=resource_name) + @staticmethod + def new_openshift_resource(name=None, auth_url=None) -> Resource: + resource_name = name or uuid.uuid4().hex + + call_command( + 'add_openshift_resource', + name=resource_name, + auth_url=auth_url or 'https://onboarding-onboarding.cluster.local', + ) + return Resource.objects.get(name=resource_name) + def new_project(self, title=None, pi=None) -> Project: title = title or uuid.uuid4().hex pi = pi or self.new_user() diff --git a/src/coldfront_plugin_openstack/tests/functional/openshift/__init__.py b/src/coldfront_plugin_openstack/tests/functional/openshift/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/coldfront_plugin_openstack/tests/functional/openshift/test_allocation.py b/src/coldfront_plugin_openstack/tests/functional/openshift/test_allocation.py new file mode 100644 index 0000000..c8126ee --- /dev/null +++ b/src/coldfront_plugin_openstack/tests/functional/openshift/test_allocation.py @@ -0,0 +1,83 @@ +import os +import time +import unittest + +from coldfront_plugin_openstack import attributes, openshift, tasks, utils +from coldfront_plugin_openstack.tests import base + + +@unittest.skipUnless(os.getenv('FUNCTIONAL_TESTS'), 'Functional tests not enabled.') +class TestAllocation(base.TestBase): + + def setUp(self) -> None: + super().setUp() + self.resource = self.new_openshift_resource( + name='Microshift', + auth_url=os.getenv('OS_AUTH_URL') + ) + + def test_new_allocation(self): + user = self.new_user() + project = self.new_project(pi=user) + allocation = self.new_allocation(project, self.resource, 1) + allocator = openshift.OpenShiftResourceAllocator(self.resource, + allocation) + + tasks.activate_allocation(allocation.pk) + allocation.refresh_from_db() + + # Check project + project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) + self.assertIsNotNone(project_id) + self.assertIsNotNone(allocation.get_attribute(attributes.ALLOCATION_PROJECT_NAME)) + + allocator._get_project(project_id) + + # Check user and roles + allocator.get_federated_user(user.username) + + allocator._get_role(user.username, project_id) + + allocator.remove_role_from_user(user.username, project_id) + + with self.assertRaises(openshift.NotFound): + allocator._get_role(user.username, project_id) + + allocator.disable_project(project_id) + + # Deleting a project is not instantaneous on OpenShift + time.sleep(10) + with self.assertRaises(openshift.NotFound): + allocator._get_project(project_id) + + def test_add_remove_user(self): + user = self.new_user() + project = self.new_project(pi=user) + project_user = self.new_project_user(user, project) + allocation = self.new_allocation(project, self.resource, 1) + allocation_user = self.new_allocation_user(allocation, user) + allocator = openshift.OpenShiftResourceAllocator(self.resource, + allocation) + + user2 = self.new_user() + project_user2 = self.new_project_user(user2, project) + allocation_user2 = self.new_allocation_user(allocation, user2) + + tasks.activate_allocation(allocation.pk) + allocation.refresh_from_db() + + project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) + + tasks.add_user_to_allocation(allocation_user2.pk) + allocator._get_role(user.username, project_id) + + allocator.get_federated_user(user2.username) + + allocator._get_role(user.username, project_id) + allocator._get_role(user2.username, project_id) + + tasks.remove_user_from_allocation(allocation_user2.pk) + + allocator._get_role(user.username, project_id) + with self.assertRaises(openshift.NotFound): + allocator._get_role(user2.username, project_id) diff --git a/src/coldfront_plugin_openstack/tests/functional/openstack/__init__.py b/src/coldfront_plugin_openstack/tests/functional/openstack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/coldfront_plugin_openstack/tests/functional/test_allocation.py b/src/coldfront_plugin_openstack/tests/functional/openstack/test_allocation.py similarity index 100% rename from src/coldfront_plugin_openstack/tests/functional/test_allocation.py rename to src/coldfront_plugin_openstack/tests/functional/openstack/test_allocation.py diff --git a/src/coldfront_plugin_openstack/utils.py b/src/coldfront_plugin_openstack/utils.py index 85a167a..6303fa2 100644 --- a/src/coldfront_plugin_openstack/utils.py +++ b/src/coldfront_plugin_openstack/utils.py @@ -2,6 +2,10 @@ AllocationAttributeType) +def env_safe_name(name): + return name.replace(' ', '_').replace('-', '_').upper() + + def set_attribute_on_allocation(allocation, attribute_type, attribute_value): allocation_attribute_type_obj = AllocationAttributeType.objects.get( name=attribute_type)