Skip to content

Commit 9153d7b

Browse files
committed
Refactored validate_allocations to be more resource-agnostic
Some validation steps still require different code paths for OpenStack and OpenShift allocations. Particularly in quota validation Quota validation will now behave the same for both resource types. OpenStack's particular use of default quotas is reflected in a new test in `openstack/test_allocation.py` OpenStack integration code is slightly changed to better handle missing object storage quotas
1 parent 21c90e5 commit 9153d7b

File tree

4 files changed

+190
-192
lines changed

4 files changed

+190
-192
lines changed

src/coldfront_plugin_cloud/management/commands/validate_allocations.py

Lines changed: 135 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
class Command(BaseCommand):
2424
help = "Validates quotas and users in resource allocations."
2525

26+
OPENSTACK_RESOURCE_NAMES = ["OpenStack", "ESI"]
27+
OPENSHIFT_RESOURCE_NAMES = ["OpenShift", "Openshift Virtualization"]
28+
2629
def add_arguments(self, parser):
2730
parser.add_argument(
2831
"--apply",
@@ -91,7 +94,7 @@ def set_default_quota_on_allocation(allocation, allocator, coldfront_attr):
9194
return value
9295

9396
@staticmethod
94-
def parse_quota_value(quota_str: str | None, attr: str) -> int | None:
97+
def parse_openshift_quota_value(quota_str: str | None, attr: str) -> int | None:
9598
PATTERN = r"([0-9]+)(m|k|Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?"
9699

97100
suffix = {
@@ -140,6 +143,97 @@ def parse_quota_value(quota_str: str | None, attr: str) -> int | None:
140143

141144
return quota_str
142145

146+
def validate_project_exists(self, allocator, project_id, resource_name):
147+
if resource_name in self.OPENSHIFT_RESOURCE_NAMES:
148+
allocator._get_project(project_id)
149+
elif resource_name in self.OPENSTACK_RESOURCE_NAMES:
150+
allocator.identity.projects.get(project_id)
151+
152+
def validate_quotas(
153+
self,
154+
allocator,
155+
project_id,
156+
allocation,
157+
allocation_str,
158+
resource_name,
159+
apply: bool,
160+
):
161+
quota = allocator.get_quota(project_id)
162+
for attr in tasks.get_expected_attributes(allocator):
163+
# quota_key = Command.get_quota_key(attr, resource_name)
164+
# Get quota key
165+
if resource_name in self.OPENSHIFT_RESOURCE_NAMES:
166+
key_with_lambda = allocator.QUOTA_KEY_MAPPING.get(attr, None)
167+
# This gives me just the plain key str
168+
quota_key = list(key_with_lambda(1).keys())[0]
169+
elif resource_name in self.OPENSTACK_RESOURCE_NAMES:
170+
quota_key = allocator.QUOTA_KEY_MAPPING_ALL_KEYS.get(attr, None)
171+
if not quota_key:
172+
# Note(knikolla): Some attributes are only maintained
173+
# for bookkeeping purposes and do not have a
174+
# corresponding quota set on the service.
175+
continue
176+
177+
expected_value = allocation.get_attribute(attr)
178+
current_value = quota.get(quota_key, None)
179+
# expected_value, current_value = Command.parse_quota_values(expected_value, current_value, attr, resource_name)
180+
181+
# parse quota values
182+
if resource_name in self.OPENSHIFT_RESOURCE_NAMES:
183+
current_value = Command.parse_openshift_quota_value(current_value, attr)
184+
elif resource_name in self.OPENSTACK_RESOURCE_NAMES:
185+
obj_key = openstack.OpenStackResourceAllocator.QUOTA_KEY_MAPPING[
186+
"object"
187+
]["keys"][attributes.QUOTA_OBJECT_GB]
188+
if quota_key == obj_key and expected_value <= 0:
189+
expected_value = 1
190+
current_value = int(
191+
allocator.object(project_id).head_account().get(obj_key)
192+
)
193+
194+
if current_value is None and expected_value is None:
195+
msg = (
196+
f"Value for quota for {attr} is not set anywhere"
197+
f" on allocation {allocation_str}"
198+
)
199+
200+
if apply:
201+
expected_value = Command.set_default_quota_on_allocation(
202+
allocation, allocator, attr
203+
)
204+
msg = f"Added default quota for {attr} to allocation {allocation_str} to {expected_value}"
205+
logger.warning(msg)
206+
elif current_value is not None and expected_value is None:
207+
msg = (
208+
f'Attribute "{attr}" expected on allocation {allocation_str} but not set.'
209+
f" Current quota is {current_value}."
210+
)
211+
212+
if apply:
213+
utils.set_attribute_on_allocation(allocation, attr, current_value)
214+
expected_value = (
215+
current_value # To pass `current_value != expected_value` check
216+
)
217+
msg = f"{msg} Attribute set to match current quota."
218+
logger.warning(msg)
219+
220+
if current_value != expected_value:
221+
msg = (
222+
f"Value for quota for {attr} = {current_value} does not match expected"
223+
f" value of {expected_value} on allocation {allocation_str}"
224+
)
225+
logger.warning(msg)
226+
227+
if apply:
228+
try:
229+
allocator.set_quota(project_id)
230+
logger.warning(
231+
f"Quota for allocation {project_id} was out of date. Reapplied!"
232+
)
233+
except Exception as e:
234+
logger.error(f"setting openshift quota failed: {e}")
235+
continue
236+
143237
def check_institution_specific_code(self, allocation, apply):
144238
attr = attributes.ALLOCATION_INSTITUTION_SPECIFIC_CODE
145239
isc = allocation.get_attribute(attr)
@@ -152,202 +246,55 @@ def check_institution_specific_code(self, allocation, apply):
152246
logger.warn(f'Attribute "{attr}" added to allocation {alloc_str}')
153247

154248
def handle(self, *args, **options):
155-
# Deal with Openstack and ESI resources
156-
openstack_resources = Resource.objects.filter(
157-
resource_type__name__in=["OpenStack", "ESI"]
158-
)
159-
openstack_allocations = Allocation.objects.filter(
160-
resources__in=openstack_resources,
161-
status=AllocationStatusChoice.objects.get(name="Active"),
162-
)
163-
for allocation in openstack_allocations:
164-
self.check_institution_specific_code(allocation, options["apply"])
165-
allocation_str = f'{allocation.pk} of project "{allocation.project.title}"'
166-
msg = f"Starting resource validation for allocation {allocation_str}."
167-
logger.debug(msg)
168-
169-
failed_validation = False
170-
171-
allocator = tasks.find_allocator(allocation)
172-
173-
project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
174-
if not project_id:
175-
logger.error(f"{allocation_str} is active but has no Project ID set.")
176-
continue
177-
178-
try:
179-
allocator.identity.projects.get(project_id)
180-
except http.NotFound:
181-
logger.error(
182-
f"{allocation_str} has Project ID {project_id}. But"
183-
f" no project found in OpenStack."
184-
)
185-
continue
186-
187-
quota = allocator.get_quota(project_id)
188-
189-
failed_validation = Command.sync_users(
190-
project_id, allocation, allocator, options["apply"]
249+
for resource_name in (
250+
self.OPENSTACK_RESOURCE_NAMES + self.OPENSHIFT_RESOURCE_NAMES
251+
):
252+
resource = Resource.objects.filter(resource_type__name=resource_name)
253+
allocations = Allocation.objects.filter(
254+
resources__in=resource,
255+
status=AllocationStatusChoice.objects.get(name="Active"),
191256
)
192257

193-
obj_key = openstack.OpenStackResourceAllocator.QUOTA_KEY_MAPPING["object"][
194-
"keys"
195-
][attributes.QUOTA_OBJECT_GB]
258+
for allocation in allocations:
259+
allocation_str = (
260+
f'{allocation.pk} of project "{allocation.project.title}"'
261+
)
262+
logger.debug(
263+
f"Starting resource validation for allocation {allocation_str}."
264+
)
196265

197-
for attr in tasks.get_expected_attributes(allocator):
198-
key = allocator.QUOTA_KEY_MAPPING_ALL_KEYS.get(attr, None)
199-
if not key:
200-
# Note(knikolla): Some attributes are only maintained
201-
# for bookkeeping purposes and do not have a
202-
# corresponding quota set on the service.
203-
continue
266+
allocator = tasks.find_allocator(allocation)
267+
project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
204268

205-
expected_value = allocation.get_attribute(attr)
206-
current_value = quota.get(key, None)
207-
if key == obj_key and expected_value <= 0:
208-
expected_obj_value = 1
209-
current_value = int(
210-
allocator.object(project_id).head_account().get(obj_key)
211-
)
212-
if current_value != expected_obj_value:
213-
failed_validation = True
214-
msg = (
215-
f"Value for quota for {attr} = {current_value} does not match expected"
216-
f" value of {expected_obj_value} on allocation {allocation_str}"
217-
)
218-
logger.warning(msg)
219-
elif expected_value is None and current_value:
220-
msg = (
221-
f'Attribute "{attr}" expected on allocation {allocation_str} but not set.'
222-
f" Current quota is {current_value}."
223-
)
224-
if options["apply"]:
225-
utils.set_attribute_on_allocation(
226-
allocation, attr, current_value
227-
)
228-
msg = f"{msg} Attribute set to match current quota."
229-
logger.warning(msg)
230-
elif not current_value == expected_value:
231-
failed_validation = True
232-
msg = (
233-
f"Value for quota for {attr} = {current_value} does not match expected"
234-
f" value of {expected_value} on allocation {allocation_str}"
269+
# Check project ID is set
270+
if not project_id:
271+
logger.error(
272+
f"{allocation_str} is active but has no Project ID set."
235273
)
236-
logger.warning(msg)
274+
continue
237275

238-
if failed_validation and options["apply"]:
276+
# Check project exists in remote cluster
239277
try:
240-
allocator.set_quota(
241-
allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
242-
)
243-
except Exception as e:
278+
self.validate_project_exists(allocator, project_id, resource_name)
279+
except http.NotFound:
244280
logger.error(
245-
f"setting {allocation.resources.first()} quota failed: {e}"
281+
f"{allocation_str} has Project ID {project_id}. But"
282+
f" no project found in {resource_name}."
246283
)
247284
continue
248-
logger.warning(
249-
f"Quota for allocation {allocation_str} was out of date. Reapplied!"
250-
)
251-
252-
# Deal with OpenShift and Openshift VM
253-
254-
openshift_resources = Resource.objects.filter(
255-
resource_type__name__in=["OpenShift", "Openshift Virtualization"]
256-
)
257-
openshift_allocations = Allocation.objects.filter(
258-
resources__in=openshift_resources,
259-
status=AllocationStatusChoice.objects.get(name="Active"),
260-
)
261-
262-
for allocation in openshift_allocations:
263-
self.check_institution_specific_code(allocation, options["apply"])
264-
allocation_str = f'{allocation.pk} of project "{allocation.project.title}"'
265-
logger.debug(
266-
f"Starting resource validation for allocation {allocation_str}."
267-
)
268285

269-
allocator = tasks.find_allocator(allocation)
270-
271-
project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID)
272-
273-
if not project_id:
274-
logger.error(f"{allocation_str} is active but has no Project ID set.")
275-
continue
276-
277-
try:
278-
allocator._get_project(project_id)
279-
except http.NotFound:
280-
logger.error(
281-
f"{allocation_str} has Project ID {project_id}. But"
282-
f" no project found in OpenShift."
283-
)
284-
continue
285-
286-
quota = allocator.get_quota(project_id)
287-
288-
failed_validation = Command.sync_users(
289-
project_id, allocation, allocator, options["apply"]
290-
)
291-
Command.sync_openshift_project_labels(
292-
project_id, allocator, options["apply"]
293-
)
294-
295-
for attr in tasks.get_expected_attributes(allocator):
296-
key_with_lambda = allocator.QUOTA_KEY_MAPPING.get(attr, None)
297-
298-
# This gives me just the plain key
299-
key = list(key_with_lambda(1).keys())[0]
300-
301-
expected_value = allocation.get_attribute(attr)
302-
current_value = quota.get(key, None)
303-
current_value = self.parse_quota_value(current_value, attr)
304-
305-
if expected_value is None and current_value is not None:
306-
msg = (
307-
f'Attribute "{attr}" expected on allocation {allocation_str} but not set.'
308-
f" Current quota is {current_value}."
286+
# Check institution code, users, labels, and quotas
287+
self.check_institution_specific_code(allocation, options["apply"])
288+
Command.sync_users(project_id, allocation, allocator, options["apply"])
289+
if resource_name in self.OPENSHIFT_RESOURCE_NAMES:
290+
Command.sync_openshift_project_labels(
291+
project_id, allocator, options["apply"]
309292
)
310-
if options["apply"]:
311-
utils.set_attribute_on_allocation(
312-
allocation, attr, current_value
313-
)
314-
msg = f"{msg} Attribute set to match current quota."
315-
logger.warning(msg)
316-
else:
317-
# We just checked the case where the quota value is set in the cluster
318-
# but not in coldfront. This is the only case the cluster value is the
319-
# "source of truth" for the quota value
320-
# If the coldfront value is set, it is always the source of truth.
321-
# But first, we need to check if the quota value is set anywhere at all.
322-
# TODO (Quan): Refactor these if statements so that we can remove this comment block
323-
if current_value is None and expected_value is None:
324-
msg = (
325-
f"Value for quota for {attr} is not set anywhere"
326-
f" on allocation {allocation_str}"
327-
)
328-
logger.warning(msg)
329-
330-
if options["apply"]:
331-
expected_value = self.set_default_quota_on_allocation(
332-
allocation, allocator, attr
333-
)
334-
logger.warning(
335-
f"Added default quota for {attr} to allocation {allocation_str} to {expected_value}"
336-
)
337-
338-
if not (current_value == expected_value):
339-
msg = (
340-
f"Value for quota for {attr} = {current_value} does not match expected"
341-
f" value of {expected_value} on allocation {allocation_str}"
342-
)
343-
logger.warning(msg)
344-
345-
if options["apply"]:
346-
try:
347-
allocator.set_quota(project_id)
348-
logger.warning(
349-
f"Quota for allocation {project_id} was out of date. Reapplied!"
350-
)
351-
except Exception as e:
352-
logger.error(f"setting openshift quota failed: {e}")
353-
continue
293+
self.validate_quotas(
294+
allocator,
295+
project_id,
296+
allocation,
297+
allocation_str,
298+
resource_name,
299+
options["apply"],
300+
)

src/coldfront_plugin_cloud/openstack.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,16 +263,17 @@ def get_quota(self, project_id):
263263
key = self.QUOTA_KEY_MAPPING["object"]["keys"][attributes.QUOTA_OBJECT_GB]
264264
try:
265265
swift = self.object(project_id).head_account()
266-
quotas[key] = int(int(swift.get(key)) / GB_IN_BYTES)
267266
except ksa_exceptions.catalog.EndpointNotFound:
268267
logger.debug("No swift available, skipping its quota.")
269268
except swiftclient.exceptions.ClientException as e:
270269
if e.http_status == 403:
271270
self._init_rgw_for_project(project_id)
272-
swift = self.object(project_id).head_account()
273-
quotas[key] = int(int(swift.get(key)) / GB_IN_BYTES)
274271
else:
275272
raise
273+
274+
try:
275+
swift = self.object(project_id).head_account()
276+
quotas[key] = int(int(swift.get(key)) / GB_IN_BYTES)
276277
except (ValueError, TypeError):
277278
logger.info("No swift quota set.")
278279

0 commit comments

Comments
 (0)