diff --git a/src/coldfront_plugin_cloud/attributes.py b/src/coldfront_plugin_cloud/attributes.py index 9f8bb02c..e4d899c7 100644 --- a/src/coldfront_plugin_cloud/attributes.py +++ b/src/coldfront_plugin_cloud/attributes.py @@ -24,6 +24,7 @@ class CloudAllocationAttribute: RESOURCE_API_URL = "OpenShift API Endpoint URL" RESOURCE_IDENTITY_NAME = "OpenShift Identity Provider Name" RESOURCE_ROLE = "Role for User in Project" +RESOURCE_IBM_AVAILABLE = "IBM Spectrum Scale Storage Available" RESOURCE_FEDERATION_PROTOCOL = "OpenStack Federation Protocol" RESOURCE_IDP = "OpenStack Identity Provider" @@ -42,6 +43,7 @@ class CloudAllocationAttribute: CloudResourceAttribute(name=RESOURCE_IDP), CloudResourceAttribute(name=RESOURCE_PROJECT_DOMAIN), CloudResourceAttribute(name=RESOURCE_ROLE), + CloudResourceAttribute(name=RESOURCE_IBM_AVAILABLE), CloudResourceAttribute(name=RESOURCE_USER_DOMAIN), CloudResourceAttribute(name=RESOURCE_EULA_URL), CloudResourceAttribute(name=RESOURCE_DEFAULT_PUBLIC_NETWORK), @@ -86,7 +88,10 @@ class CloudAllocationAttribute: QUOTA_LIMITS_CPU = "OpenShift Limit on CPU Quota" QUOTA_LIMITS_MEMORY = "OpenShift Limit on RAM Quota (MiB)" QUOTA_LIMITS_EPHEMERAL_STORAGE_GB = "OpenShift Limit on Ephemeral Storage Quota (GiB)" -QUOTA_REQUESTS_STORAGE = "OpenShift Request on Storage Quota (GiB)" +QUOTA_REQUESTS_NESE_STORAGE = "OpenShift Request on NESE Storage Quota (GiB)" +QUOTA_REQUESTS_IBM_STORAGE = ( + "OpenShift Request on IBM Spectrum Scale Storage Quota (GiB)" +) QUOTA_REQUESTS_GPU = "OpenShift Request on GPU Quota" QUOTA_REQUESTS_VM_GPU_A100_SXM4 = "OpenShift Request on GPU A100 SXM4" QUOTA_REQUESTS_VM_GPU_V100 = "OpenShift Request on GPU V100" @@ -107,7 +112,8 @@ class CloudAllocationAttribute: CloudAllocationAttribute(name=QUOTA_LIMITS_CPU), CloudAllocationAttribute(name=QUOTA_LIMITS_MEMORY), CloudAllocationAttribute(name=QUOTA_LIMITS_EPHEMERAL_STORAGE_GB), - CloudAllocationAttribute(name=QUOTA_REQUESTS_STORAGE), + CloudAllocationAttribute(name=QUOTA_REQUESTS_NESE_STORAGE), + CloudAllocationAttribute(name=QUOTA_REQUESTS_IBM_STORAGE), CloudAllocationAttribute(name=QUOTA_REQUESTS_GPU), CloudAllocationAttribute(name=QUOTA_REQUESTS_VM_GPU_A100_SXM4), CloudAllocationAttribute(name=QUOTA_REQUESTS_VM_GPU_V100), diff --git a/src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py b/src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py index 95c8193f..8e05ef4c 100644 --- a/src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py +++ b/src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py @@ -44,6 +44,11 @@ def add_arguments(self, parser): action="store_true", help="Indicates this is an OpenShift Virtualization resource (default: False)", ) + parser.add_argument( + "--ibm-storage-available", + action="store_true", + help="Indicates that Ibm Scale storage is available in this resource (default: False)", + ) def handle(self, *args, **options): self.validate_role(options["role"]) @@ -86,3 +91,11 @@ def handle(self, *args, **options): resource=openshift, value=options["role"], ) + + ResourceAttribute.objects.get_or_create( + resource_attribute_type=ResourceAttributeType.objects.get( + name=attributes.RESOURCE_IBM_AVAILABLE + ), + resource=openshift, + value="true" if options["ibm_storage_available"] else "false", + ) diff --git a/src/coldfront_plugin_cloud/management/commands/calculate_storage_gb_hours.py b/src/coldfront_plugin_cloud/management/commands/calculate_storage_gb_hours.py index 45668394..425803f1 100644 --- a/src/coldfront_plugin_cloud/management/commands/calculate_storage_gb_hours.py +++ b/src/coldfront_plugin_cloud/management/commands/calculate_storage_gb_hours.py @@ -300,9 +300,9 @@ def process_invoice_row(allocation, attrs, su_name, rate): allocation, [ attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB, - attributes.QUOTA_REQUESTS_STORAGE, + attributes.QUOTA_REQUESTS_NESE_STORAGE, ], - "OpenShift Storage", + "OpenShift NESE Storage", openshift_storage_rate, ) diff --git a/src/coldfront_plugin_cloud/management/commands/register_cloud_attributes.py b/src/coldfront_plugin_cloud/management/commands/register_cloud_attributes.py index 8adb9967..94effebe 100644 --- a/src/coldfront_plugin_cloud/management/commands/register_cloud_attributes.py +++ b/src/coldfront_plugin_cloud/management/commands/register_cloud_attributes.py @@ -22,6 +22,10 @@ ("OpenStack Project ID", {"name": "Allocated Project ID"}), ("OpenStack Project Name", {"name": "Allocated Project Name"}), ("OpenShift Limit on RAM Quota", {"name": "OpenShift Limit on RAM Quota (MB)"}), + ( + "OpenShift Request on Storage Quota (GiB)", + {"name": "OpenShift Request on NESE Storage Quota (GiB)"}, + ), ("OpenStack Volume Quota", {"name": "OpenStack Number of Volumes Quota"}), ("OpenStack Compute RAM Quota", {"name": "OpenStack Compute RAM Quota (MiB)"}), ("OpenStack Volume GB Quota", {"name": "OpenStack Volume Quota (GiB)"}), diff --git a/src/coldfront_plugin_cloud/openshift.py b/src/coldfront_plugin_cloud/openshift.py index 89f1b59f..0dc6c7fe 100644 --- a/src/coldfront_plugin_cloud/openshift.py +++ b/src/coldfront_plugin_cloud/openshift.py @@ -70,7 +70,12 @@ class OpenShiftResourceAllocator(base.ResourceAllocator): attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: lambda x: { "limits.ephemeral-storage": f"{x}Gi" }, - attributes.QUOTA_REQUESTS_STORAGE: lambda x: {"requests.storage": f"{x}Gi"}, + attributes.QUOTA_REQUESTS_NESE_STORAGE: lambda x: { + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": f"{x}Gi" + }, + attributes.QUOTA_REQUESTS_IBM_STORAGE: lambda x: { + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": f"{x}Gi" + }, attributes.QUOTA_REQUESTS_GPU: lambda x: {"requests.nvidia.com/gpu": f"{x}"}, attributes.QUOTA_PVC: lambda x: {"persistentvolumeclaims": f"{x}"}, } diff --git a/src/coldfront_plugin_cloud/openshift_vm.py b/src/coldfront_plugin_cloud/openshift_vm.py index b7c1fc51..17b47b8a 100644 --- a/src/coldfront_plugin_cloud/openshift_vm.py +++ b/src/coldfront_plugin_cloud/openshift_vm.py @@ -8,7 +8,12 @@ class OpenShiftVMResourceAllocator(openshift.OpenShiftResourceAllocator): attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: lambda x: { "limits.ephemeral-storage": f"{x}Gi" }, - attributes.QUOTA_REQUESTS_STORAGE: lambda x: {"requests.storage": f"{x}Gi"}, + attributes.QUOTA_REQUESTS_NESE_STORAGE: lambda x: { + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": f"{x}Gi" + }, + attributes.QUOTA_REQUESTS_IBM_STORAGE: lambda x: { + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": f"{x}Gi" + }, attributes.QUOTA_REQUESTS_VM_GPU_A100_SXM4: lambda x: { "requests.nvidia.com/A100_SXM4_40GB": f"{x}" }, diff --git a/src/coldfront_plugin_cloud/tasks.py b/src/coldfront_plugin_cloud/tasks.py index fca94a31..262f6845 100644 --- a/src/coldfront_plugin_cloud/tasks.py +++ b/src/coldfront_plugin_cloud/tasks.py @@ -34,7 +34,8 @@ attributes.QUOTA_LIMITS_CPU: 1, attributes.QUOTA_LIMITS_MEMORY: 4096, attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: 5, - attributes.QUOTA_REQUESTS_STORAGE: 20, + attributes.QUOTA_REQUESTS_NESE_STORAGE: 20, + attributes.QUOTA_REQUESTS_IBM_STORAGE: 0, attributes.QUOTA_REQUESTS_GPU: 0, attributes.QUOTA_PVC: 2, }, @@ -42,7 +43,8 @@ attributes.QUOTA_LIMITS_CPU: 1, attributes.QUOTA_LIMITS_MEMORY: 4096, attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: 5, - attributes.QUOTA_REQUESTS_STORAGE: 20, + attributes.QUOTA_REQUESTS_NESE_STORAGE: 20, + attributes.QUOTA_REQUESTS_IBM_STORAGE: 0, attributes.QUOTA_REQUESTS_VM_GPU_A100_SXM4: 0, attributes.QUOTA_REQUESTS_VM_GPU_V100: 0, attributes.QUOTA_REQUESTS_VM_GPU_H100: 0, @@ -74,7 +76,21 @@ def get_expected_attributes(allocator: base.ResourceAllocator): """Based on the allocator's resource type, return the expected quotas attributes the allocation should have""" resource_name = allocator.resource_type - return list(UNIT_QUOTA_MULTIPLIERS[resource_name].keys()) + resource_expected_quotas = UNIT_QUOTA_MULTIPLIERS[resource_name].copy() + + # If the resource attribute is not set (i.e for OpenStack resources), get_attribute returns None + is_ibm_storage_available = allocator.resource.get_attribute( + attributes.RESOURCE_IBM_AVAILABLE + ) + is_ibm_storage_available = ( + is_ibm_storage_available and is_ibm_storage_available.lower() == "true" + ) + if "openshift" in resource_name and not is_ibm_storage_available: + resource_expected_quotas.pop( + attributes.QUOTA_REQUESTS_IBM_STORAGE, None + ) # The resource may or may not already have this attribute + + return list(resource_expected_quotas.keys()) def find_allocator(allocation) -> base.ResourceAllocator: diff --git a/src/coldfront_plugin_cloud/tests/base.py b/src/coldfront_plugin_cloud/tests/base.py index a1d06b5c..5a19857c 100644 --- a/src/coldfront_plugin_cloud/tests/base.py +++ b/src/coldfront_plugin_cloud/tests/base.py @@ -80,7 +80,11 @@ def new_openstack_resource(name=None, auth_url=None) -> Resource: @staticmethod def new_openshift_resource( - name=None, api_url=None, idp=None, for_virtualization=False + name=None, + api_url=None, + idp=None, + for_virtualization=False, + ibm_storage_available=False, ) -> Resource: resource_name = name or uuid.uuid4().hex @@ -90,6 +94,7 @@ def new_openshift_resource( api_url=api_url or "https://onboarding-onboarding.cluster.local:6443", idp=idp or "developer", for_virtualization=for_virtualization, + ibm_storage_available=ibm_storage_available, ) return Resource.objects.get(name=resource_name) diff --git a/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py b/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py index fdb9dbe9..a3450c2e 100644 --- a/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py +++ b/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py @@ -18,6 +18,7 @@ def setUp(self) -> None: self.resource = self.new_openshift_resource( name="Microshift", api_url=os.getenv("OS_API_URL"), + ibm_storage_available=True, ) def test_new_allocation(self): @@ -135,7 +136,7 @@ def test_new_allocation_quota(self): 2 * 5, ) self.assertEqual( - allocation.get_attribute(attributes.QUOTA_REQUESTS_STORAGE), 2 * 20 + allocation.get_attribute(attributes.QUOTA_REQUESTS_NESE_STORAGE), 2 * 20 ) self.assertEqual(allocation.get_attribute(attributes.QUOTA_REQUESTS_GPU), 2 * 0) self.assertEqual(allocation.get_attribute(attributes.QUOTA_PVC), 2 * 2) @@ -149,7 +150,8 @@ def test_new_allocation_quota(self): "limits.cpu": "2", "limits.memory": "8Gi", "limits.ephemeral-storage": "10Gi", - "requests.storage": "40Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "40Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", "requests.nvidia.com/gpu": "0", "persistentvolumeclaims": "4", }, @@ -164,7 +166,7 @@ def test_new_allocation_quota(self): allocation, attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB, 50 ) utils.set_attribute_on_allocation( - allocation, attributes.QUOTA_REQUESTS_STORAGE, 100 + allocation, attributes.QUOTA_REQUESTS_NESE_STORAGE, 100 ) utils.set_attribute_on_allocation(allocation, attributes.QUOTA_REQUESTS_GPU, 1) utils.set_attribute_on_allocation(allocation, attributes.QUOTA_PVC, 10) @@ -175,7 +177,7 @@ def test_new_allocation_quota(self): allocation.get_attribute(attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB), 50 ) self.assertEqual( - allocation.get_attribute(attributes.QUOTA_REQUESTS_STORAGE), 100 + allocation.get_attribute(attributes.QUOTA_REQUESTS_NESE_STORAGE), 100 ) self.assertEqual(allocation.get_attribute(attributes.QUOTA_REQUESTS_GPU), 1) self.assertEqual(allocation.get_attribute(attributes.QUOTA_PVC), 10) @@ -192,7 +194,8 @@ def test_new_allocation_quota(self): "limits.cpu": "6", "limits.memory": "8Gi", "limits.ephemeral-storage": "50Gi", - "requests.storage": "100Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "100Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", "requests.nvidia.com/gpu": "1", "persistentvolumeclaims": "10", }, @@ -221,7 +224,8 @@ def test_reactivate_allocation(self): "limits.cpu": "2", "limits.memory": "8Gi", "limits.ephemeral-storage": "10Gi", - "requests.storage": "40Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "40Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", "requests.nvidia.com/gpu": "0", "persistentvolumeclaims": "4", }, @@ -242,7 +246,8 @@ def test_reactivate_allocation(self): "limits.cpu": "3", "limits.memory": "8Gi", "limits.ephemeral-storage": "10Gi", - "requests.storage": "40Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "40Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", "requests.nvidia.com/gpu": "0", "persistentvolumeclaims": "4", }, @@ -375,3 +380,106 @@ def test_allocation_new_attribute(self): "limits.memory": "8Gi", }, ) + + def test_migrate_quota_field_names(self): + """When a quota key in QUOTA_KEY_MAPPING changes to a new value, validate_allocations should update the quota.""" + 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() + + project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) + + quota = allocator.get_quota(project_id) + self.assertEqual( + quota, + { + "limits.cpu": "1", + "limits.memory": "4Gi", + "limits.ephemeral-storage": "5Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "20Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", + "requests.nvidia.com/gpu": "0", + "persistentvolumeclaims": "2", + }, + ) + + # Now migrate NESE Storage quota field (ocs-external...) to fake storage quota + with unittest.mock.patch.dict( + openshift.OpenShiftResourceAllocator.QUOTA_KEY_MAPPING, + { + attributes.QUOTA_REQUESTS_NESE_STORAGE: lambda x: { + "fake-storage.storageclass.storage.k8s.io/requests.storage": f"{x}Gi" + } + }, + ): + call_command("validate_allocations", apply=True) + + # Check the quota after migration + quota = allocator.get_quota(project_id) + self.assertEqual( + quota, + { + "limits.cpu": "1", + "limits.memory": "4Gi", + "limits.ephemeral-storage": "5Gi", + "fake-storage.storageclass.storage.k8s.io/requests.storage": "20Gi", # Migrated key + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", + "requests.nvidia.com/gpu": "0", + "persistentvolumeclaims": "2", + }, + ) + + def test_ibm_storage_not_available(self): + """If IBM Scale storage is not available, the corresponding quotas should not be set.""" + user = self.new_user() + project = self.new_project(pi=user) + + # Set ibm storage as not available + self.resource.resourceattribute_set.filter( + resource_attribute_type__name=attributes.RESOURCE_IBM_AVAILABLE + ).update(value="false") + allocation = self.new_allocation(project, self.resource, 1) + allocator = openshift.OpenShiftResourceAllocator(self.resource, allocation) + + tasks.activate_allocation(allocation.pk) + allocation.refresh_from_db() + + project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) + + quota = allocator.get_quota(project_id) + self.assertEqual( + quota, + { + "limits.cpu": "1", + "limits.memory": "4Gi", + "limits.ephemeral-storage": "5Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "20Gi", + "requests.nvidia.com/gpu": "0", + "persistentvolumeclaims": "2", + }, + ) + + # Now set IBM Scale storage as available + self.resource.resourceattribute_set.filter( + resource_attribute_type__name=attributes.RESOURCE_IBM_AVAILABLE + ).update(value="true") + + call_command("validate_allocations", apply=True) + + quota = allocator.get_quota(project_id) + self.assertEqual( + quota, + { + "limits.cpu": "1", + "limits.memory": "4Gi", + "limits.ephemeral-storage": "5Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "20Gi", + "ibm-spectrum-scale-fileset.storageclass.storage.k8s.io/requests.storage": "0", # Newly added IBM key + "requests.nvidia.com/gpu": "0", + "persistentvolumeclaims": "2", + }, + ) diff --git a/src/coldfront_plugin_cloud/tests/functional/openshift_vm/test_allocation.py b/src/coldfront_plugin_cloud/tests/functional/openshift_vm/test_allocation.py index e4c4cb0c..4e8b4c4b 100644 --- a/src/coldfront_plugin_cloud/tests/functional/openshift_vm/test_allocation.py +++ b/src/coldfront_plugin_cloud/tests/functional/openshift_vm/test_allocation.py @@ -36,7 +36,7 @@ def test_new_allocation(self): 2 * 5, ) self.assertEqual( - allocation.get_attribute(attributes.QUOTA_REQUESTS_STORAGE), 2 * 20 + allocation.get_attribute(attributes.QUOTA_REQUESTS_NESE_STORAGE), 2 * 20 ) self.assertEqual( allocation.get_attribute(attributes.QUOTA_REQUESTS_VM_GPU_A100_SXM4), 2 * 0 @@ -58,7 +58,7 @@ def test_new_allocation(self): "limits.cpu": "2", "limits.memory": "8Gi", "limits.ephemeral-storage": "10Gi", - "requests.storage": "40Gi", + "ocs-external-storagecluster-ceph-rbd.storageclass.storage.k8s.io/requests.storage": "40Gi", "requests.nvidia.com/A100_SXM4_40GB": "0", "requests.nvidia.com/GV100GL_Tesla_V100": "0", "requests.nvidia.com/H100_SXM5_80GB": "0",