def rack_get(request): """ List all racks within specified range. """ range_serializer = RackRangeSerializer(data=request.data) if not range_serializer.is_valid(): return JsonResponse( { "failure_message": Status.INVALID_INPUT.value + parse_serializer_errors(range_serializer.errors), "errors": str(range_serializer.errors), }, status=HTTPStatus.BAD_REQUEST, ) (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response racks = Rack.objects.filter( datacenter=range_serializer.get_datacenter(), rack_num__range=range_serializer.get_number_range(), # inclusive range row_letter__range=range_serializer.get_row_range(), ) return get_rack_detailed_response(racks, change_plan)
def change_plan_detail(request, id): """ Retrieve a single change plan. """ (change_plan, failure_response) = get_change_plan(id) if failure_response: return failure_response if request.user != change_plan.owner: return JsonResponse( { "failure_message": Status.ERROR.value + "You do not have access to this change plan.", "errors": "User " + request.user.username + " does not own change plan with id=" + str(id), }, status=HTTPStatus.BAD_REQUEST, ) change_plan_serializer = GetChangePlanSerializer(change_plan) modifications = get_modifications_in_cp(change_plan) return JsonResponse( {"change_plan": change_plan_serializer.data, "modifications": modifications,}, status=HTTPStatus.OK, )
def decommission_asset(request): """ Decommission a live asset """ data = JSONParser().parse(request) (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response if "id" not in data: return JsonResponse( { "failure_message": Status.DECOMMISSION_ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include 'id' when decommissioning an asset", }, status=HTTPStatus.BAD_REQUEST, ) asset_id = data["id"] success_response, failure_response = decommission_asset_parameterized( asset_id, request.query_params, request.user, change_plan) if failure_response: return failure_response if not change_plan: blades = Asset.objects.filter(chassis=asset_id) for blade in blades: blade.delete() Asset.objects.get(id=asset_id).delete() return success_response
def change_plan_resolve_conflict(request, id): """ Resolve a merge conflict """ (change_plan, failure_response) = get_change_plan(id) if failure_response: return failure_response failure_response = get_cp_already_executed_response(change_plan) if failure_response: return failure_response data = JSONParser().parse(request) if "asset_cp" not in data or "override_live" not in data: return JsonResponse( { "failure_message": Status.ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include both 'asset_cp' and `override_live` when resolving a merge conflict", }, status=HTTPStatus.BAD_REQUEST, ) asset_cp = data["asset_cp"] override_live = data["override_live"] try: asset_cp = AssetCP.objects.get(id=asset_cp) except ObjectDoesNotExist: return JsonResponse( { "failure_message": Status.ERROR.value + "AssetCP" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing Asset CP with id=" + str(asset_cp), }, status=HTTPStatus.BAD_REQUEST, ) try: if override_live: asset_cp.is_conflict = False asset_cp.save() else: asset_cp.delete() except Exception as error: return JsonResponse( { "failure_message": Status.DELETE_ERROR.value + "Change Plan" + GenericFailure.ON_DELETE.value, "errors": str(error), }, status=HTTPStatus.BAD_REQUEST, ) return JsonResponse( {"success_message": Status.SUCCESS.value + "Sucessfully resolved conflict "}, status=HTTPStatus.OK, )
def change_plan_modify(request): """ Modify single existing change plan """ data = JSONParser().parse(request) if "id" not in data: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include 'id' when modifying a change plan", }, status=HTTPStatus.BAD_REQUEST, ) id = data["id"] (existing_change_plan, failure_response) = get_change_plan(id) if failure_response: return failure_response failure_response = get_cp_already_executed_response(existing_change_plan) if failure_response: return failure_response for field in data.keys(): value = data[field] setattr(existing_change_plan, field, value) try: existing_change_plan.save() return JsonResponse( { "success_message": Status.SUCCESS.value + "Change Plan " + str(existing_change_plan.name) + " modified", "related_id": str(existing_change_plan.id), }, status=HTTPStatus.OK, ) except Exception as error: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + parse_save_validation_error(error, "Asset"), "errors": str(error), }, status=HTTPStatus.BAD_REQUEST, )
def asset_detail(request, id): """ Retrieve a single asset. """ (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan") ) if failure_response: return failure_response try: if change_plan: assets, assets_cp = get_assets_for_cp( change_plan.id, show_decommissioned=True ) # if id of live asset is given on a change plan, just return the corresponding related assetCP if assets_cp.filter(related_asset=id).exists(): asset = assets_cp.get(related_asset=id) serializer = RecursiveAssetCPSerializer(asset) elif assets_cp.filter(id=id).exists(): asset = assets_cp.get(id=id) serializer = RecursiveAssetCPSerializer(asset) else: asset = assets.get(id=id) serializer = RecursiveAssetSerializer(asset) else: asset = Asset.objects.get(id=id) serializer = RecursiveAssetSerializer(asset) except Asset.DoesNotExist: try: decommissioned_asset = DecommissionedAsset.objects.get(live_id=id) except DecommissionedAsset.DoesNotExist: return JsonResponse( { "failure_message": Status.ERROR.value + "Asset" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing asset with id=" + str(id), }, status=HTTPStatus.BAD_REQUEST, ) else: serializer = GetDecommissionedAssetSerializer(decommissioned_asset) return JsonResponse(serializer.data, status=HTTPStatus.OK)
def decommissioned_asset_page_count(request): """ Return total number of pages according to page size, which must be specified as query parameter. """ (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response if change_plan: return get_page_count_response_for_cp(request, change_plan, decommissioned=True) else: return get_page_count_response( DecommissionedAsset, request.query_params, data_for_filters=request.data, )
def offline_storage_asset_page_count(request): """ Return total number of pages according to page size, which must be specified as query parameter. """ (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan") ) if failure_response: return failure_response if change_plan: return get_page_count_response_for_cp(request, change_plan, stored=True) else: stored_assets = assets_offline_queryset() return get_page_count_response( Asset, request.query_params, data_for_filters=request.data, premade_object_query=stored_assets, )
def change_plan_delete(request): """ Delete a single existing change plan """ data = JSONParser().parse(request) if "id" not in data: return JsonResponse( { "failure_message": Status.DELETE_ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include 'id' when deleting a Change Plan", }, status=HTTPStatus.BAD_REQUEST, ) (existing_change_plan, failure_response) = get_change_plan(data["id"]) if failure_response: return failure_response failure_response = get_cp_already_executed_response(existing_change_plan) if failure_response: return failure_response try: existing_change_plan.delete() except Exception as error: return JsonResponse( { "failure_message": Status.DELETE_ERROR.value + "Change Plan" + GenericFailure.ON_DELETE.value, "errors": str(error), }, status=HTTPStatus.BAD_REQUEST, ) return JsonResponse( { "success_message": Status.SUCCESS.value + "Change Plan " + str(existing_change_plan.name) + " deleted" }, status=HTTPStatus.OK, )
def rack_get_all(request): """ List all racks """ datacenter_id = request.query_params.get("datacenter") if not datacenter_id: return JsonResponse( { "failure_message": Status.ERROR.value + "Must specifiy datacenter", "errors": "Query parameter 'datacenter' is required", }, status=HTTPStatus.BAD_REQUEST, ) (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response racks = Rack.objects.filter(datacenter=datacenter_id) return get_rack_detailed_response(racks, change_plan)
def offline_storage_asset_many(request): """ List many assets in offline storage. If page is not specified as a query parameter, all assets are returned. If page is specified as a query parameter, page size must also be specified, and a page of assets will be returned. """ (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan") ) if failure_response: return failure_response if change_plan: return get_many_assets_response_for_cp(request, change_plan, stored=True) else: stored_assets = assets_offline_queryset() return get_many_response( Asset, RecursiveAssetSerializer, "assets", request, premade_object_query=stored_assets, )
def decommissioned_asset_many(request): """ List many decommissioned assets. If page is not specified as a query parameter, all assets are returned. If page is specified as a query parameter, page size must also be specified, and a page of assets will be returned. """ (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response if change_plan: return get_many_assets_response_for_cp( request, change_plan, decommissioned=True, ) else: return get_many_response( DecommissionedAsset, GetDecommissionedAssetSerializer, "assets", request, )
def change_plan_remove_asset(request, id): """ Remove a single assetCP from a change plan """ (change_plan, failure_response) = get_change_plan(id) if failure_response: return failure_response failure_response = get_cp_already_executed_response(change_plan) if failure_response: return failure_response data = JSONParser().parse(request) if "asset_cp" not in data: return JsonResponse( { "failure_message": Status.ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include 'asset_cp' when removing an asset from change plan", }, status=HTTPStatus.BAD_REQUEST, ) asset_cp = data["asset_cp"] try: asset_cp_object = AssetCP.objects.get(id=asset_cp) except ObjectDoesNotExist: return JsonResponse( { "failure_message": Status.ERROR.value + "AssetCP" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing Asset CP with id=" + str(asset_cp), }, status=HTTPStatus.BAD_REQUEST, ) try: if asset_cp_object.model.is_blade_chassis(): blades = AssetCP.objects.filter(chassis=asset_cp_object, change_plan=change_plan) remove_chassis = True for blade in blades: if len(get_changes_on_asset(blade.related_asset, blade)) == 0: remove_chassis = False if remove_chassis: asset_cp_object.delete() else: for field in asset_cp.related_asset._meta.fields: if ( field.name != "id" and field.name != "assetid_ptr" and field.name != "chassis" ): setattr(asset_cp, field.name, getattr(asset_cp.related_asset, field.name)) else: asset_cp_object.delete() except Exception as error: return JsonResponse( { "failure_message": Status.DELETE_ERROR.value + "Change Plan" + GenericFailure.ON_DELETE.value, "errors": str(error), }, status=HTTPStatus.BAD_REQUEST, ) return JsonResponse( { "success_message": Status.SUCCESS.value + "Asset successfully removed from change plan" }, status=HTTPStatus.OK, )
def pdu_port_availability(request): rack_id = request.query_params.get("id") if not rack_id: return JsonResponse( { "failure_message": Status.ERROR.value + GenericFailure.INTERNAL.value, "errors": "Query parameter 'id' is required", }, status=HTTPStatus.BAD_REQUEST, ) try: rack = Rack.objects.get(id=rack_id) except Rack.DoesNotExist: return JsonResponse( { "failure_message": Status.ERROR.value + "Rack" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing rack with id=" + str(rack_id), }, status=HTTPStatus.BAD_REQUEST, ) (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan")) if failure_response: return failure_response if change_plan: assets, assets_cp = get_assets_for_cp(change_plan.id) assets_cp = assets_cp.filter(rack=rack.id) assets = assets.filter(rack=rack.id) else: assets_cp = None assets = Asset.objects.filter(rack=rack.id) available_left = list(range(1, 25)) available_right = list(range(1, 25)) try: remove_unavailable_pdu_ports(available_left, available_right, assets, PowerPort) except Exception as error: return JsonResponse( {"failure_message": Status.ERROR.value + str(error)}, status=HTTPStatus.BAD_REQUEST, ) if change_plan: try: remove_unavailable_pdu_ports(available_left, available_right, assets_cp, PowerPortCP) except Exception as error: return JsonResponse( {"failure_message": Status.ERROR.value + str(error)}, status=HTTPStatus.BAD_REQUEST, ) suggest = None for index in available_left: if index in available_right: suggest = index break return JsonResponse( { "left_available": available_left, "right_available": available_right, "left_suggest": suggest, "right_suggest": suggest, }, status=HTTPStatus.OK, )
def asset_add(request): """ Add a new asset. """ data = JSONParser().parse(request) if "id" in data: return JsonResponse( { "failure_message": Status.CREATE_ERROR.value + GenericFailure.INTERNAL.value, "errors": "Don't include 'id' when creating an asset", }, status=HTTPStatus.BAD_REQUEST, ) (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan") ) if failure_response: return failure_response chassis_id_live = None if change_plan: failure_response = get_cp_already_executed_response(change_plan) if failure_response: return failure_response data["change_plan"] = change_plan.id if data["chassis"] and not AssetCP.objects.filter(id=data["chassis"]).exists(): # ignore the chassis because we will replace it with a new chassis on AssetCP later chassis_id_live = data["chassis"] del data["chassis"] serializer = AssetCPSerializer(data=data) else: serializer = AssetSerializer(data=data) if not serializer.is_valid(raise_exception=False): return JsonResponse( { "failure_message": Status.INVALID_INPUT.value + parse_serializer_errors(serializer.errors), "errors": str(serializer.errors), }, status=HTTPStatus.BAD_REQUEST, ) try: validate_user_permission_on_new_asset_data( request.user, serializer.validated_data, data_is_validated=True, change_plan=change_plan, chassis_id_live=chassis_id_live, ) except UserAssetPermissionException as auth_error: return JsonResponse( {"failure_message": Status.AUTH_ERROR.value + str(auth_error)}, status=HTTPStatus.UNAUTHORIZED, ) except Exception as error: return JsonResponse( {"failure_message": Status.CREATE_ERROR.value + str(error)}, status=HTTPStatus.BAD_REQUEST, ) if not ( "offline_storage_site" in serializer.validated_data and serializer.validated_data["offline_storage_site"] ): if serializer.validated_data["model"].is_rackmount(): if ( "rack" not in serializer.validated_data or not serializer.validated_data["rack"] or "rack_position" not in serializer.validated_data or not serializer.validated_data["rack_position"] ): return JsonResponse( { "failure_message": Status.INVALID_INPUT.value + "Must include rack and rack position to add a rackmount asset. " }, status=HTTPStatus.BAD_REQUEST, ) rack_id = serializer.validated_data["rack"].id rack_position = serializer.validated_data["rack_position"] height = serializer.validated_data["model"].height try: validate_asset_location_in_rack( rack_id, rack_position, height, change_plan=change_plan ) except LocationException as error: return JsonResponse( {"failure_message": Status.CREATE_ERROR.value + str(error)}, status=HTTPStatus.BAD_REQUEST, ) else: if ( ( ( "chassis" not in serializer.validated_data or not serializer.validated_data["chassis"] ) and not chassis_id_live ) or "chassis_slot" not in serializer.validated_data or not serializer.validated_data["chassis_slot"] ): return JsonResponse( { "failure_message": Status.INVALID_INPUT.value + "Must include chassis and chassis slot to add a blade asset. " }, status=HTTPStatus.BAD_REQUEST, ) if chassis_id_live: try: chassis_live = Asset.objects.get(id=chassis_id_live) except ObjectDoesNotExist: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + "Chassis" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing chassis with id=" + str(chassis_id_live), }, status=HTTPStatus.BAD_REQUEST, ) chassis_cp = add_chassis_to_cp(chassis_live, change_plan) chassis_id = chassis_cp.id serializer.validated_data["chassis"] = chassis_cp else: chassis_id = serializer.validated_data["chassis"].id chassis_slot = serializer.validated_data["chassis_slot"] try: validate_asset_location_in_chassis( chassis_id, chassis_slot, change_plan=change_plan ) except LocationException as error: return JsonResponse( {"failure_message": Status.CREATE_ERROR.value + str(error)}, status=HTTPStatus.BAD_REQUEST, ) try: asset = serializer.save() except Exception as error: return JsonResponse( { "failure_message": Status.CREATE_ERROR.value + parse_save_validation_error(error, "Asset"), "errors": str(error), }, status=HTTPStatus.BAD_REQUEST, ) warning_message = save_all_connection_data( data, asset, request.user, change_plan=change_plan ) if warning_message: return JsonResponse({"warning_message": warning_message}, status=HTTPStatus.OK) if change_plan: return JsonResponse( { "success_message": Status.SUCCESS.value + "Asset created on change plan " + change_plan.name, "related_id": change_plan.id, }, status=HTTPStatus.OK, ) else: log_action(request.user, asset, Action.CREATE) return JsonResponse( { "success_message": Status.SUCCESS.value + "Asset " + str(asset.asset_number) + " created" }, status=HTTPStatus.OK, )
def asset_modify(request): """ Modify a single existing asset """ data = JSONParser().parse(request) if "id" not in data: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + GenericFailure.INTERNAL.value, "errors": "Must include 'id' when modifying an asset", }, status=HTTPStatus.BAD_REQUEST, ) asset_id = data["id"] (change_plan, failure_response) = get_change_plan( request.query_params.get("change_plan") ) if failure_response: return failure_response create_new_asset_cp = False existing_asset = None if change_plan: failure_response = get_cp_already_executed_response(change_plan) if failure_response: return failure_response if not does_asset_exist(asset_id, change_plan): return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + "Asset" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing asset with id=" + str(asset_id), }, status=HTTPStatus.BAD_REQUEST, ) try: existing_asset = AssetCP.objects.get( id=asset_id, change_plan=change_plan.id ) except ObjectDoesNotExist: create_new_asset_cp = True if create_new_asset_cp or not change_plan: try: existing_asset = Asset.objects.get(id=asset_id) except ObjectDoesNotExist: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + "Model" + GenericFailure.DOES_NOT_EXIST.value, "errors": "No existing asset with id=" + str(asset_id), }, status=HTTPStatus.BAD_REQUEST, ) try: validate_user_permission_on_existing_asset(request.user, existing_asset) except UserAssetPermissionException as auth_error: return JsonResponse( {"failure_message": Status.AUTH_ERROR.value + str(auth_error)}, status=HTTPStatus.UNAUTHORIZED, ) try: validate_user_permission_on_new_asset_data( request.user, data, data_is_validated=False, change_plan=change_plan ) except UserAssetPermissionException as auth_error: return JsonResponse( {"failure_message": Status.AUTH_ERROR.value + str(auth_error)}, status=HTTPStatus.UNAUTHORIZED, ) try: validate_hostname_deletion(data, existing_asset) except AssetModificationException as modifcation_exception: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + "Invalid hostname deletion. " + str(modifcation_exception) }, status=HTTPStatus.BAD_REQUEST, ) try: validate_location_modification(data, existing_asset, change_plan=change_plan) except Exception as error: return JsonResponse( { "failure_message": Status.MODIFY_ERROR.value + "Invalid location change. " + str(error) }, status=HTTPStatus.BAD_REQUEST, ) asset_cp = None if change_plan: (asset_cp, failure_message) = save_all_field_data_cp( data, existing_asset, change_plan, create_new_asset_cp ) else: failure_message = save_all_field_data_live(data, existing_asset) if failure_message: return JsonResponse( {"failure_message": Status.MODIFY_ERROR.value + failure_message}, status=HTTPStatus.BAD_REQUEST, ) if asset_cp: existing_asset = asset_cp warning_message = save_all_connection_data( data, existing_asset, request.user, change_plan=change_plan ) if warning_message: return JsonResponse({"warning_message": warning_message}, status=HTTPStatus.OK) if change_plan: return JsonResponse( { "success_message": Status.SUCCESS.value + "Asset modified on change plan " + change_plan.name, "related_id": change_plan.id, }, status=HTTPStatus.OK, ) else: log_action(request.user, existing_asset, Action.MODIFY) return JsonResponse( { "success_message": Status.SUCCESS.value + "Asset " + str(existing_asset.asset_number) + " modified" }, status=HTTPStatus.OK, )
def change_plan_execute(request, id): """ Execute all changes associated with a change plan. """ (change_plan, failure_response) = get_change_plan(id) if failure_response: return failure_response failure_response = get_cp_already_executed_response(change_plan) if failure_response: return failure_response if request.user != change_plan.owner: return JsonResponse( { "failure_message": Status.ERROR.value + "You do not have access to execute this change plan.", "errors": "User " + request.user.username + " does not own change plan with id=" + str(id), }, status=HTTPStatus.BAD_REQUEST, ) assets_cp = AssetCP.objects.filter(change_plan=change_plan) for asset_cp in assets_cp: if get_cp_modification_conflicts(asset_cp): return JsonResponse( { "failure_message": Status.ERROR.value + "All conflicts must be resolved before a change " + "plan can be executed.", "errors": "Conflict found on AssetCP with id=" + str(asset_cp.id), }, status=HTTPStatus.BAD_REQUEST, ) change_plan.execution_time = datetime.now() change_plan.save() num_created = 0 num_modified = 0 num_decommissioned = 0 updated_asset_mappings = {} # rackmount assets first rackmount_assets_cp = assets_cp.filter( ~Q(model__model_type=ModelType.BLADE_ASSET.value) ) for asset_cp in rackmount_assets_cp: changes = [] if asset_cp.related_asset: changes = get_changes_on_asset(asset_cp.related_asset, asset_cp) updated_asset, created = get_updated_asset(asset_cp) updated_asset_mappings[asset_cp] = updated_asset differs_from_live = True if created and not asset_cp.is_decommissioned: num_created += 1 log_action( request.user, updated_asset, Action.CREATE, change_plan=change_plan, ) elif not asset_cp.is_decommissioned: if len(changes) > 0: num_modified += 1 log_action( request.user, updated_asset, Action.MODIFY, change_plan=change_plan, ) else: differs_from_live = False asset_cp.differs_from_live = differs_from_live asset_cp.save() update_network_ports(updated_asset, asset_cp, change_plan) update_power_ports(updated_asset, asset_cp, change_plan) ##blade assets blade_assets_cp = assets_cp.filter(model__model_type=ModelType.BLADE_ASSET.value) for asset_cp in blade_assets_cp: changes = [] if asset_cp.related_asset: changes = get_changes_on_asset(asset_cp.related_asset, asset_cp) chassis_live = updated_asset_mappings[asset_cp.chassis] updated_asset, created = get_updated_asset(asset_cp, chassis_live) updated_asset_mappings[asset_cp] = updated_asset differs_from_live = True if created: num_created += 1 log_action( request.user, updated_asset, Action.CREATE, change_plan=change_plan, ) elif not asset_cp.is_decommissioned: if len(changes) > 0: num_modified += 1 log_action( request.user, updated_asset, Action.MODIFY, change_plan=change_plan, ) else: differs_from_live = False asset_cp.differs_from_live = differs_from_live asset_cp.save() update_network_ports(updated_asset, asset_cp, change_plan) update_power_ports(updated_asset, asset_cp, change_plan) updated_asset.save() ## decomission blades first blades_cp = assets_cp.filter(model__model_type=ModelType.BLADE_ASSET.value) rackmount_cp = assets_cp.filter(~Q(model__model_type=ModelType.BLADE_ASSET.value)) for asset_cp_query in [blades_cp, rackmount_cp]: for asset_cp in asset_cp_query: # Decommission only after all changes have been made to all CP assets updated_asset = updated_asset_mappings[asset_cp] if asset_cp.is_decommissioned: failure_response = decommission_asset_cp( updated_asset, asset_cp, change_plan, ) if failure_response: return failure_response num_decommissioned += 1 log_action( request.user, None, Action.DECOMMISSION, change_plan=change_plan, ) log_execute_change_plan( request.user, change_plan.name, num_created, num_modified, num_decommissioned, ) return JsonResponse( { "success_message": "Change Plan '" + change_plan.name + "' executed: " + str(num_created) + " assets created, " + str(num_modified) + " assets modified, " + str(num_decommissioned) + " assets decommissioned." }, status=HTTPStatus.OK, )