# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved. import uuid from enum import Enum import itsi_py3 from ITOA.itoa_object import ItoaObject, CRUDMethodTypes from ITOA.setup_logging import logger from ITOA.itoa_exceptions import ItoaError class Status(int, Enum): STATUS_UNKNOWN = 0 STATUS_EDIT = 1 STATUS_SAVE = 2 STATUS_SYNC = 3 STATUS_VALIDATE = 4 STATUS_PUBLISH = 5 STATUS_IMPORT = 6 STATUS_ADD_SERVICE = 7 STATUS_DELETE = 8 STATUS_PUBLISH_SUCCESS = 9 STATUS_PARTIAL_PUBLISH = 10 STATUS_PUBLISH_REVERT = 11 STATUS_PUBLISH_RESET = 12 @staticmethod def pretty_string(status=STATUS_UNKNOWN): """ Retrun pretty string of the Status. @type status: Status @param status: status of the sandbox """ status_string = "Unknown" if status == Status.STATUS_EDIT: status_string = "Edit" elif status == Status.STATUS_SAVE: status_string = "Saving" elif status == Status.STATUS_SYNC: status_string = "Synchronizing" elif status == Status.STATUS_PUBLISH: status_string = "Publishing" elif status == Status.STATUS_IMPORT: status_string = "Importing" elif status == Status.STATUS_ADD_SERVICE: status_string = "Adding Sandbox Service" elif status == Status.STATUS_DELETE: status_string = "Deleting" elif status == Status.STATUS_PUBLISH_SUCCESS: status_string = "Published" elif status == Status.STATUS_PARTIAL_PUBLISH: status_string = "Partially published" elif status == Status.STATUS_PUBLISH_REVERT: status_string = "Reverting published services" elif status == Status.STATUS_PUBLISH_RESET: status_string = "Removing published services from sandbox" return status_string @staticmethod def get_allowed_statuses(intermediate_status): """ Returns the list of allowed status for the given intermediate status @param intermediate_status: Intermediate status of the sandbox @type enum: Status enum @rtype: list @return: list of status that are allowed for the intermediate status """ allowed_statuses_by_intermediate_status = {Status.STATUS_DELETE: [Status.STATUS_EDIT, Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH], Status.STATUS_PUBLISH_REVERT: [ Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH], Status.STATUS_PUBLISH_RESET: [Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH]} return allowed_statuses_by_intermediate_status.get(intermediate_status, [Status.STATUS_EDIT]) class ItsiSandbox(ItoaObject): """ Implements ITSI Sandbox object """ logger = logger log_prefix = '[ITSI Sandbox] ' collection_name = 'itsi_sandbox' object_type = 'sandbox' MAX_SERVICE_SANDBOX_LIMIT = 20 @staticmethod def get_allowed_statuses(intermediate_status): """ Returns the list of allowed status for the given intermediate status @param intermediate_status: Intermediate status of the sandbox @type enum: Status enum @rtype: list @return: list of status that are allowed for the intermediate status """ allowed_statuses_by_intermediate_status = {Status.STATUS_DELETE: [Status.STATUS_EDIT, Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH], Status.STATUS_PUBLISH_REVERT: [Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH], Status.STATUS_PUBLISH_RESET: [Status.STATUS_PUBLISH_SUCCESS, Status.STATUS_PARTIAL_PUBLISH], Status.STATUS_SYNC: [Status.STATUS_EDIT, Status.STATUS_PUBLISH, Status.STATUS_PARTIAL_PUBLISH] } return allowed_statuses_by_intermediate_status.get(intermediate_status, [Status.STATUS_EDIT]) def __init__(self, session_key, current_user_name): logger.info('Instantiating Sandbox Object') self.session_key = session_key self.current_user_name = current_user_name super(ItsiSandbox, self).__init__(session_key, current_user_name, self.object_type, collection_name=self.collection_name, is_securable_object=False) def create(self, owner, data, dupname_tag=None, transaction_id=None): """ Create object passed in. Extending the functionality to check for maximum allowed Service Sandbox @type owner: string @param owner: user who is performing this operation @type data: dictionary @param data: object to create @rtype: string @return: id of object created if successful, throws exceptions on errors """ results = self.get_bulk( owner, filter_data={}, fields=['_key'], transaction_id=transaction_id ) if ( len(results) >= self.MAX_SERVICE_SANDBOX_LIMIT ): raise ItoaError( "The maximum number of service sandboxes has been reached.\ Delete an existing sandbox before creating a new one.", logger, status_code=400) return super(ItsiSandbox, self).create( owner, data, dupname_tag, transaction_id) def do_additional_setup(self, owner, objects, req_source='unknown', method=CRUDMethodTypes.METHOD_UPSERT, transaction_id=None, skip_local_failure=False): """ Override ItoaObject.do_additional_setup to add additional attributes to itsi_sandbox before a write operation (create or update) is invoked on this object @type owner: string @param owner: user who is performing this operation @type objects: list of dictionary @param objects: list of objects being written @type req_source: string @param req_source: string identifying source of this request @return: none, throws exceptions on errors """ if method == CRUDMethodTypes.METHOD_CREATE: for json_data in objects: json_data['creator'] = self.current_user_name json_data['status'] = Status.STATUS_EDIT json_data['count_of_services'] = 0 json_data['count_of_unique_service_templates'] = 0 def can_be_deleted(self, owner, objects, raise_error=False, transaction_id=None): """ Override ItoaObject.can_be_deleted to not delete the default sandbox @type owner: string @param owner: context of the request `nobody` vs an actual user @type objects: list @param objects: sandbox objects to check deletability on @type raise_error: boolean @param raise_error: if true, an error will be raised if no objects could be deleted @rtype: list @return: list - list of deletable objects """ results = [] objects = self._get_security_enforcer().enforce_security_on_delete(owner, self, objects, transaction_id=transaction_id) for sandbox in objects: if sandbox and sandbox.get('_key') != 'default_itsi_sandbox': results.append(sandbox) return results def delete(self, owner, object_id, req_source='unknown', transaction_id=None): """ Overriding the Itoaobject Delete method Delete object by id @type owner: string @param owner: user who is performing this operation @type object_id: string @param object_id: id of object to delete @type req_source: string @param req_source: identified source initiating the operation @rtype: string @return: id of object deleted on success, throws exceptions on errors """ from itsi.objects.itsi_sandbox_sync_log import (ItsiSandboxSyncLog, ACTION_TYPE_DELETE, STATUS_IN_PROGRESS, STATUS_SUCCESS, STATUS_FAILED) from itsi.sandbox.sandbox_utils import SandboxServiceUtils stored_object = self.get(owner, object_id, req_source=req_source, transaction_id=transaction_id) if not self.can_be_deleted(owner, [stored_object], raise_error=True, transaction_id=transaction_id): return sandbox_sync_log_interface = ItsiSandboxSyncLog(self.session_key, self.current_user_name) if transaction_id is None: transaction_id = uuid.uuid1().hex allowed_statuses = Status.get_allowed_statuses(Status.STATUS_DELETE) self.validate_and_return_current_sandbox_status(owner, object_id, req_source, transaction_id, allowed_statuses) record = { 'action_type': ACTION_TYPE_DELETE, 'action_status': STATUS_IN_PROGRESS, 'errors': [], } sandbox_sync_log_interface.create_record( owner, transaction_id, self.object_type, object_id, record, ) results = [] try: sand_utils = SandboxServiceUtils(self.session_key, self.current_user_name) sand_utils.perform_sandbox_service_delete(owner, object_id, transaction_id) sand_utils.clear_publish_references_from_sandbox(owner, object_id, transaction_id) results = super(ItsiSandbox, self).delete(owner, object_id, req_source, transaction_id) record_complete = { 'action_type': ACTION_TYPE_DELETE, 'action_status': STATUS_SUCCESS, 'errors': [], } except Exception as e: record_complete = { 'action_type': ACTION_TYPE_DELETE, 'action_status': STATUS_FAILED, 'errors': [str(e)], } sandbox_sync_log_interface.update_record(owner, transaction_id, record_complete) return results def delete_bulk( self, owner, filter_data=None, req_source='unknown', transaction_id=None ): """ Deletes objects matching criteria, if no filtering specified, deletes all objects of this object type @type owner: string @param owner: user who is performing this operation @type filter_data: dictionary @param filter_data: json filter constructed to filter data. Follows mongodb syntax @type req_source: string @param req_source: identified source initiating the operation @return: none, throws exceptions on errors """ if transaction_id is None: transaction_id = uuid.uuid1().hex delete_objects = self.get_bulk(owner, filter_data=filter_data, fields=self.delete_object_fields, transaction_id=transaction_id) for delete_object in delete_objects: sandbox_id = delete_object.get('_key') # Special case for create record for delete operation new_transaction_id = transaction_id + '-' + sandbox_id self.delete(owner, sandbox_id, req_source, new_transaction_id) def post_delete(self, owner, deleted_sandbox_ids, req_source='unknown', method=CRUDMethodTypes.METHOD_UPSERT, transaction_id=None): return def validate_and_return_current_sandbox_status(self, owner, sandbox_id, req_source='Unknown', transaction_id=None, allowed_initial_statuses=[Status.STATUS_EDIT]): """ Validates if sandbox exists and if the status is Edit @type string @param sandbox_id: key of the sandbox """ sandbox_detail = self.get(owner, sandbox_id, req_source, transaction_id) if not sandbox_detail: logger.error( "Unable to retrieve the sandbox details tid=%s sandbox_id=%s", transaction_id, sandbox_id) self.raise_error_bad_validation(logger, 'Could not retrieve service sandbox details. Check that the inputted key is correct.', 409) # This doesn't make sense, but the MR changes are piling up elif sandbox_detail.get('status', Status.STATUS_EDIT) not in allowed_initial_statuses: status_pretty = Status.pretty_string(sandbox_detail.get('status')) logger.error( ("This action can’t be completed because of a conflicting action. Refresh after a minute and try again. Current status is {}").format(status_pretty)) self.raise_error_bad_validation( logger, 'This action can’t be completed because of a conflicting action. Refresh after a minute and try again.', 409, ) return sandbox_detail.get('status', Status.STATUS_EDIT)