|
|
# Copyright (C) 2005-2025 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)
|