You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1078 lines
62 KiB
1078 lines
62 KiB
# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
|
|
|
|
import itsi_py3
|
|
from itsi_py3 import _
|
|
import copy
|
|
import ITOA.itoa_common as utils
|
|
from ITOA.setup_logging import logger
|
|
from ITOA.itoa_object import ItoaObject, CRUDMethodTypes
|
|
from itsi.objects.itsi_service import ItsiService
|
|
from itsi.objects.itsi_kpi import ANOMALY_DETECTION_ATTRBUTES, GENERATED_SEARCH_ATTRIBUTES, BACKFILL_ATTRIBUTES
|
|
from ITOA.datamodel_interface import DatamodelInterface
|
|
from ITOA.storage import itoa_storage
|
|
from ITOA.itoa_exceptions import ItoaDatamodelContextError
|
|
from itsi.objects.itsi_kpi_base_search import ItsiKPIBaseSearch, BASE_SEARCH_KPI_ATTRIBUTES, BASE_SEARCH_METRIC_KPI_ATTRIBUTES
|
|
from itsi.itsi_utils import ITOAInterfaceUtils
|
|
from itsi.objects.itsi_backup_restore import ItsiBackupRestore
|
|
from itsi.objects.itsi_upgrade_readiness_prechecks import ItsiUpgradeReadinessPrechecks
|
|
from migration_utility.constants import MODES, UPGRADE_READINESS_JOB_TIMEOUT_LIMIT
|
|
from itsi.objects.itsi_sandbox_service import ItsiSandboxService
|
|
from itsi.objects.itsi_sandbox import ItsiSandbox
|
|
from itsi.objects.changehandlers.sandbox_base_service_template_update_handler import SandboxBaseServiceTemplateUpdateHandler
|
|
|
|
|
|
class ItsiBaseServiceTemplate(ItoaObject):
|
|
"""
|
|
Implements ITSI Service Template ItoaObject methods
|
|
"""
|
|
def __init__(self, session_key, current_user_name):
|
|
super(ItsiBaseServiceTemplate, self).__init__(session_key,
|
|
current_user_name,
|
|
'base_service_template',
|
|
collection_name='itsi_base_service_template',
|
|
is_securable_object=True)
|
|
self.service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
self.sandbox_service_interface = ItsiSandboxService(self.session_key, self.current_user_name)
|
|
self.sandbox_interface = ItsiSandbox(self.session_key, self.current_user_name)
|
|
self.services = {}
|
|
# used to skip service template processing for services
|
|
# this flag is only used in the backup and restore case for the base service template object.
|
|
self.skip_service_template_update = False
|
|
self.persisted_service_template_map = {}
|
|
self.sandbox_update_jobs = []
|
|
|
|
##################################################
|
|
# Helper methods
|
|
##################################################
|
|
|
|
def _validate_payload(self, owner, objects, method=CRUDMethodTypes.METHOD_UPSERT, transaction_id=None):
|
|
"""
|
|
Perform validation on Base Service Template object payload, before processing
|
|
it and saving to kvstore.
|
|
|
|
@type owner: basestring
|
|
@param owner: request owner. "nobody" or some username.
|
|
|
|
@type objects: list
|
|
@param objects: List of base service template type objects
|
|
|
|
@type method: basestring
|
|
@param method: operation type. Defaults to upsert.
|
|
|
|
@type transaction_id: basestring
|
|
@param transaction_id: transaction id for end-end tracing.
|
|
"""
|
|
logger.info('Validating payload to upsert Base Service Template object(s). transaction_id="%s"', transaction_id)
|
|
|
|
def _validate_kpis(object):
|
|
# use service validation utility to validate a base service template
|
|
self.service_interface.validate_kpis(owner, [object], method, transaction_id,
|
|
for_base_service_template=True)
|
|
# check if all kpis are shared base kpis in request
|
|
# Note: we might allow cloning an adhoc/datamodel kpi in service template payload in the future
|
|
# since service and kpi object are already validated above, we skip it here
|
|
for kpi in object.get('kpis', []):
|
|
if kpi.get('search_type') != 'shared_base':
|
|
self.raise_error_bad_validation(logger, _('Invalid kpi type for {} service template. '
|
|
'Expected only shared base search kpis, '
|
|
'received {}.').format(method.lower(),
|
|
kpi.get('search_type')))
|
|
|
|
if method in (CRUDMethodTypes.METHOD_UPSERT, CRUDMethodTypes.METHOD_UPDATE):
|
|
for obj in objects:
|
|
_validate_kpis(obj)
|
|
|
|
elif method == CRUDMethodTypes.METHOD_CREATE:
|
|
if not utils.is_valid_list(objects):
|
|
self.raise_error_bad_validation(logger,
|
|
_('Invalid data for creating the service template. '
|
|
'Expected a list of JSON objects, received {}.').format(
|
|
type(objects).__name__))
|
|
|
|
for object in objects:
|
|
if not isinstance(object, dict):
|
|
self.raise_error_bad_validation(logger, _('Invalid data for creating service template '
|
|
'from service. Expected a JSON object, '
|
|
'received {}.').format(type(objects).__name__))
|
|
# check _immutable to see if service template object is imported from static conf file
|
|
# if true, its kpis need to be validated before creation.
|
|
is_immutable = object.get('_immutable', False)
|
|
if is_immutable:
|
|
_validate_kpis(object)
|
|
# If not static import, a new base service template can be created
|
|
# from a service or from an existing service template (clone operation).
|
|
# Therefore, only performing the validation on payload sent by UI for creation of template.
|
|
# Since, base service template is a templatized service
|
|
# (already in kvstore), no need to perform the validation on the created base service template.
|
|
else:
|
|
service_id = object.get('service_id', None)
|
|
base_service_template_id = object.get('base_service_template_id', None)
|
|
if service_id is None and base_service_template_id is None:
|
|
self.raise_error_bad_validation(logger,
|
|
_('The `service_id` or the `base_service_template_id` field is required to create'
|
|
' a service template.'))
|
|
|
|
@staticmethod
|
|
def _cleanup_base_service_template_content(base_service_template):
|
|
"""
|
|
Cleanup fields from base service templates, which are redundant.
|
|
@param base_service_template: base service template object
|
|
"""
|
|
keys_to_cleanup = ['reschedule_sync_msg']
|
|
if not len(base_service_template.get('serviceTemplateId', '')) > 0:
|
|
keys_to_cleanup.append('serviceTemplateId')
|
|
|
|
for k in keys_to_cleanup:
|
|
base_service_template.pop(k, None)
|
|
|
|
logger.debug('Cleaned up fields="%s", from service_template="%s"',
|
|
keys_to_cleanup, base_service_template.get('_key', ''))
|
|
|
|
@staticmethod
|
|
def _cleanup_base_kpi_template_content(base_service_template):
|
|
"""
|
|
Cleanup fields from KPIs in base service templates, which are redundant.
|
|
@param base_service_template: base service template object
|
|
"""
|
|
base_kpi_templates = base_service_template.get('kpis', [])
|
|
|
|
for kpi_template in base_kpi_templates:
|
|
keys_to_cleanup = GENERATED_SEARCH_ATTRIBUTES + ANOMALY_DETECTION_ATTRBUTES + BACKFILL_ATTRIBUTES + \
|
|
['service_id', 'enabled', 'type', 'linked_kpi_thresholds_updated']
|
|
|
|
if kpi_template.get('search_type', '') != 'shared_base':
|
|
keys_to_cleanup.extend(['base_search_metric', 'base_search_id'])
|
|
if kpi_template.get('search_type', '') != 'datamodel':
|
|
keys_to_cleanup.extend(['datamodel', 'datamodel_filter'])
|
|
if kpi_template.get('kpi_template_kpi_id', None) == '':
|
|
keys_to_cleanup.extend(['kpi_template_kpi_id'])
|
|
if 'aggregate_outlier_detection_enabled' in kpi_template:
|
|
keys_to_cleanup.extend([
|
|
'aggregate_outlier_detection_enabled',
|
|
'outlier_detection_algo',
|
|
'outlier_detection_sensitivity'
|
|
])
|
|
|
|
for k in keys_to_cleanup:
|
|
kpi_template.pop(k, None)
|
|
logger.debug(
|
|
'Cleaned up fields="%s", from kpi="%s" in service_template="%s"',
|
|
keys_to_cleanup, kpi_template.get('_key', ''), base_service_template.get('_key', '')
|
|
)
|
|
|
|
@staticmethod
|
|
def _if_kpis_updated(old_kpis, new_kpis):
|
|
"""
|
|
Compares new kpis with persisted kpis of service template,
|
|
to determine if kpis are being changed in update request or not.
|
|
|
|
@type old_kpis: list of dict
|
|
@param old_kpis: kpis in persisted service template
|
|
@type new_kpis: list of dict
|
|
@param new_kpis: kpis in update request for service template
|
|
@return: bool
|
|
"""
|
|
kpis_updated = False
|
|
if len(old_kpis) != len(new_kpis):
|
|
kpis_updated = True
|
|
else:
|
|
for old_kpi, new_kpi in zip(old_kpis, new_kpis):
|
|
if new_kpi.get('_key') != old_kpi.get('_key'):
|
|
kpis_updated = True
|
|
break
|
|
elif new_kpi != old_kpi:
|
|
kpis_updated = True
|
|
break
|
|
return kpis_updated
|
|
|
|
@staticmethod
|
|
def _normalize_overwrite_kpi_thresholds_flag(flag):
|
|
"""
|
|
Normalizes overwrite_kpi_thresholds flag value.
|
|
@param flag:
|
|
@return:
|
|
"""
|
|
value = 'none'
|
|
if isinstance(flag, itsi_py3.string_type):
|
|
if flag.lower() == 'all':
|
|
value = 'all'
|
|
elif flag.lower() == 'unchanged':
|
|
value = 'unchanged'
|
|
return value
|
|
|
|
def _get_linked_services_update_job(self, service_template, persisted_service_template_map, transaction_id=None,
|
|
affected_sandbox_services=[]):
|
|
"""
|
|
Checks if update of service template requires refresh of linked services. If so, returns
|
|
list of refresh jobs for service template (typically one job). If not, returns empty list.
|
|
|
|
@type service_template: dict
|
|
@param service_template: service template object for which update is requested
|
|
@type persisted_service_template_map: dict
|
|
@param persisted_service_template_map: persisted service template map in kvstore for which
|
|
update is requested
|
|
@type transaction_id: string
|
|
@param transaction_id: for instrumentation purposes
|
|
@return: list of refresh jobs to update services linked to template
|
|
"""
|
|
linked_services_update_jobs = []
|
|
change_type = 'base_service_template_update'
|
|
overwrite_entity_rules = utils.normalize_bool_flag(service_template.get('overwrite_entity_rules', False))
|
|
overwrite_health_scores = utils.normalize_bool_flag(service_template.get('overwrite_health_scores', False))
|
|
overwrite_kpi_thresholds = self._normalize_overwrite_kpi_thresholds_flag(
|
|
service_template.get('overwrite_kpi_thresholds', 'none')
|
|
)
|
|
filter_data = {"base_service_template_id": service_template.get("_key")}
|
|
|
|
affected_sandbox_services = self.sandbox_service_interface.get_bulk('nobody', fields=["_key", "sandbox_id"],
|
|
filter_data=filter_data)
|
|
if len(service_template.get('linked_services', [])) > 0 or len(affected_sandbox_services):
|
|
old_service_template_tags = persisted_service_template_map.get(service_template.get('_key'),
|
|
{}).get('template_tags', [])
|
|
new_service__template_tags = service_template.get('template_tags', [])
|
|
service_tags_update_required = False
|
|
if set(old_service_template_tags) != set(new_service__template_tags):
|
|
service_tags_update_required = True
|
|
|
|
old_healthscore_by_entity = persisted_service_template_map.get(
|
|
service_template.get('_key'), {}).get('is_healthscore_calculate_by_entity_enabled', 1)
|
|
new_healthscore_by_entity = service_template.get('is_healthscore_calculate_by_entity_enabled', 1)
|
|
healthscore_by_entity_update_required = False
|
|
if old_healthscore_by_entity != new_healthscore_by_entity:
|
|
healthscore_by_entity_update_required = True
|
|
|
|
old_kpis = persisted_service_template_map.get(service_template.get('_key'), {}).get('kpis', [])
|
|
# if overwrite flag for health score or thresholds is set, consider, kpi update is required.
|
|
# no need to perform old kpis and new kpis comparison to find out, if kpis actually changed.
|
|
# as, user may want to revert linked services to service template without making any
|
|
# changes to template.
|
|
if overwrite_health_scores or overwrite_kpi_thresholds.strip().lower() in ('all', 'unchanged'):
|
|
kpi_update_required = True
|
|
else:
|
|
new_kpis = service_template.get('kpis', [])
|
|
kpi_update_required = self._if_kpis_updated(old_kpis, new_kpis)
|
|
|
|
if (kpi_update_required or overwrite_entity_rules or service_tags_update_required
|
|
or healthscore_by_entity_update_required):
|
|
if overwrite_entity_rules:
|
|
logger.debug(
|
|
'Entity rules update is needed for services linked to service template. service_template="%s". '
|
|
'transaction_id="%s"', service_template.get('_key'), transaction_id)
|
|
|
|
change_detail = {
|
|
service_template.get('_key'): {
|
|
'kpi_update_required': kpi_update_required,
|
|
'service_tags_update_required': service_tags_update_required,
|
|
'healthscore_by_entity_update_required': healthscore_by_entity_update_required,
|
|
'overwrite_health_scores': overwrite_health_scores,
|
|
'overwrite_kpi_thresholds': overwrite_kpi_thresholds,
|
|
'overwrite_entity_rules': overwrite_entity_rules
|
|
}
|
|
}
|
|
if kpi_update_required:
|
|
logger.debug(
|
|
'KPIs update is needed for services linked to service template. service_template="%s". '
|
|
'transaction_id="%s"', service_template.get('_key'), transaction_id)
|
|
old_kpi_key_to_title_map = {}
|
|
for old_kpi in old_kpis:
|
|
old_kpi_key_to_title_map[old_kpi['_key']] = old_kpi.get('title')
|
|
change_detail[service_template.get('_key')]['old_kpis'] = copy.deepcopy(old_kpi_key_to_title_map)
|
|
|
|
if len(service_template.get('linked_services', [])) > 0:
|
|
linked_services_update_jobs.append(
|
|
self.get_refresh_job_meta_data(
|
|
change_type,
|
|
service_template.get('_key'),
|
|
self.object_type,
|
|
change_detail,
|
|
transaction_id
|
|
)
|
|
)
|
|
if len(affected_sandbox_services) > 0 :
|
|
sandbox_change_type = 'sandbox_base_service_template_update'
|
|
change_detail[service_template.get('_key')]['affected_sandbox_services'] = affected_sandbox_services
|
|
change_detail[service_template.get('_key')]['updates_entity_rules'] = service_template.get('entity_rules', [])
|
|
change_detail[service_template.get('_key')]['affected_sandbox_services'] = affected_sandbox_services
|
|
refresh_job = self.get_refresh_job_meta_data(sandbox_change_type,
|
|
service_template.get('_key'),
|
|
self.object_type, change_detail,
|
|
transaction_id)
|
|
self.sandbox_update_jobs.append(refresh_job)
|
|
if not service_template.get('scheduled_time', False):
|
|
logger.info('Added refresh job of type base_service_template_update, to push out service '
|
|
'template changes to its linked services. service_template="%s", transaction_id="%s"',
|
|
service_template.get('_key'), transaction_id)
|
|
|
|
return linked_services_update_jobs
|
|
|
|
def _convert_adhoc_datamodel_metric_search(self, owner, templatized_service, service_to_update):
|
|
"""
|
|
Convert adhoc search and data model search into shared base search. Update service template and service object at the same time
|
|
|
|
@type owner: basestring
|
|
@param owner: request owner. "nobody" or some username
|
|
@type templatized_service: dict
|
|
@param templatized_service: service template object
|
|
@type service_to_update: dict
|
|
@param service_to_update: service object in kvstore
|
|
@return: None. Raise error if something goes wrong
|
|
"""
|
|
|
|
def _generate_kpi_base_search_title(service_title, kpi_title, kpi_key):
|
|
"""
|
|
Generate kpi base search title
|
|
|
|
@type service_title: basestring
|
|
@param service_title: service title
|
|
@type kpi_title: basestring
|
|
@param kpi_title: kpi title
|
|
@type kpi_key: basestring
|
|
@param kpi_key: kpi key
|
|
@return: basestring. Generated Kpi base search title
|
|
"""
|
|
# enforce a limit on title length and add the last 8 digits of kpi key to avoid duplicate title
|
|
return "{}:{}_{}".format(service_title,
|
|
kpi_title,
|
|
kpi_key[:8])
|
|
|
|
kpi_map = {}
|
|
|
|
for kpi in service_to_update.get('kpis', []):
|
|
if kpi.get('_key', '').startswith(self.service_interface.SHKPI_STARTS_WITH):
|
|
continue
|
|
kpi_map[kpi.get('title')] = kpi
|
|
|
|
for kpi in templatized_service.get('kpis', []):
|
|
|
|
if kpi.get('search_type') == 'shared_base':
|
|
continue
|
|
|
|
# Create a kpi base search from adhoc search
|
|
kpi_base_search = ITOAInterfaceUtils.generate_kpi_base_search()
|
|
kpi_base_search_id = ITOAInterfaceUtils.generate_backend_key()
|
|
kpi_base_search['_key'] = kpi_base_search_id
|
|
kpi_base_search['title'] = _generate_kpi_base_search_title(templatized_service.get('title'),
|
|
kpi.get('title'), kpi.get('_key'))
|
|
|
|
# Copy common fields from kpi
|
|
common_fields = BASE_SEARCH_KPI_ATTRIBUTES + ['description', 'metric']
|
|
common_fields.remove('sec_grp')
|
|
|
|
metric_fields = BASE_SEARCH_METRIC_KPI_ATTRIBUTES
|
|
|
|
for common_field in common_fields:
|
|
# alert_period has to be explicitly converted to str or the kpi load fails
|
|
# Please note the inconsisitency between alert_period in adhoc kpi (int) and shared base kpi (string)
|
|
if common_field == 'alert_period':
|
|
kpi_base_search[common_field] = str(kpi.get(common_field, ''))
|
|
else:
|
|
if common_field in kpi:
|
|
kpi_base_search[common_field] = kpi.get(common_field, '')
|
|
|
|
# Create metric.
|
|
# For now, a new kpi base search is created for every kpi.
|
|
# The performance could be further improved by merging similar base searches into one
|
|
# This is out of the scope of PBL-12547
|
|
metric = {}
|
|
metric_key = ITOAInterfaceUtils.generate_backend_key()
|
|
for metric_field in metric_fields:
|
|
metric[metric_field] = kpi.get(metric_field, '')
|
|
metric['title'] = '{}({})'.format(kpi.get('aggregate_statop', ' '), kpi.get('threshold_field', ' '))
|
|
metric['_key'] = metric_key
|
|
|
|
if 'metrics' in kpi_base_search:
|
|
kpi_base_search['metrics'].append(metric)
|
|
else:
|
|
kpi_base_search['metrics'] = [metric]
|
|
|
|
# Special handling for datamodel search
|
|
if kpi.get('search_type') == 'datamodel':
|
|
if not utils.is_valid_dict(kpi.get('datamodel')):
|
|
self.raise_error_bad_validation(logger,
|
|
_("Invalid data model defined for KPI {}").format(kpi.get('title')))
|
|
|
|
datamodel = kpi.get('datamodel').get('datamodel')
|
|
datamodel_obj = kpi.get('datamodel').get('object')
|
|
|
|
datamodel_dict = DatamodelInterface.get_datamodel(self.session_key,
|
|
'',
|
|
itoa_storage.ITOAStorage().get_app_name(),
|
|
datamodel
|
|
)
|
|
|
|
dm = datamodel_dict.get(datamodel)
|
|
if dm is None:
|
|
message = _("Could not locate specified data model {}").format(datamodel)
|
|
logger.error(message)
|
|
raise ItoaDatamodelContextError(message, logger)
|
|
dm_objects = dm['objects']
|
|
for object in dm_objects:
|
|
if object.get('objectName') == datamodel_obj:
|
|
break
|
|
|
|
# Use ObjectSearch String
|
|
# If datamodel filter exists, append it to the search string
|
|
search_string = object.get('objectSearch')
|
|
if kpi.get('datamodel_filter_clauses'):
|
|
search_string += ' | search ' + kpi['datamodel_filter_clauses']
|
|
kpi_base_search['base_search'] = search_string
|
|
|
|
if kpi.get('search_type') == 'metric':
|
|
kpi_base_search['is_metric'] = True
|
|
kpi_base_search['base_search'] = '*'
|
|
else:
|
|
kpi_base_search['is_metric'] = False
|
|
# save kpi base search
|
|
kpi_base_search_interface = ItsiKPIBaseSearch(self.session_key, self.current_user_name)
|
|
kpi_base_search_interface.create(owner, kpi_base_search)
|
|
|
|
logger.debug(
|
|
'kpi base search ="{}" is created from kpi ="{}", service = "{}"'.format(kpi_base_search['title'],
|
|
kpi.get('title'),
|
|
service_to_update.get(
|
|
'title')))
|
|
|
|
# update kpi object
|
|
# no need to generate searches since it's generated automatically
|
|
for each_kpi in [kpi, kpi_map[kpi.get('title')]]:
|
|
each_kpi['search_type'] = 'shared_base'
|
|
each_kpi['base_search_id'] = kpi_base_search_id
|
|
each_kpi['base_search_metric'] = metric_key
|
|
each_kpi['base_search'] = kpi_base_search['base_search']
|
|
each_kpi['is_metric'] = kpi_base_search['is_metric']
|
|
|
|
logger.info(
|
|
'successfully converted kpi = "{}" in service = "{}" to kpi base search ="{}"'.format(kpi.get('title'),
|
|
service_to_update.get(
|
|
'title'),
|
|
kpi_base_search[
|
|
'title']))
|
|
|
|
def generate_persisted_service_template_map(self, owner, objects, transaction_id=None):
|
|
"""
|
|
Utility function to generate persisted service template map. It's shared by do_addtional_setup and identify_dependencies
|
|
@type owner: basestring
|
|
@param owner: request owner. "nobody" or some username.
|
|
|
|
@type objects: list
|
|
@param objects: List of base service template type objects
|
|
|
|
@type transaction_id: basestring
|
|
@param transaction_id: transaction id for end-end tracing.
|
|
"""
|
|
templates_get_filter = {
|
|
'$or': []
|
|
}
|
|
templates_keys_list = []
|
|
for service_template in objects:
|
|
templates_get_filter['$or'].append({
|
|
'_key': service_template.get('_key')
|
|
})
|
|
templates_keys_list.append(service_template.get('_key'))
|
|
|
|
if len(templates_get_filter['$or']) > 0:
|
|
persisted_service_templates = self.get_bulk(
|
|
owner, filter_data=templates_get_filter,
|
|
fields=['_key', 'kpis',
|
|
'entity_rules',
|
|
'linked_services',
|
|
'sync_status',
|
|
'is_healthscore_calculate_by_entity_enabled'],
|
|
transaction_id=transaction_id
|
|
)
|
|
for service_template in persisted_service_templates:
|
|
self.persisted_service_template_map[service_template.get('_key')] = service_template
|
|
|
|
##################################################
|
|
# ItoaObject specific methods
|
|
##################################################
|
|
|
|
def do_additional_setup(self, owner, objects, req_source='unknown', method=CRUDMethodTypes.METHOD_UPSERT,
|
|
transaction_id=None, skip_local_failure=False):
|
|
"""
|
|
Any additional setup to be done for Base Service Template object(s).
|
|
@type owner: basestring
|
|
@param owner: request owner. "nobody" or some username.
|
|
|
|
@type objects: list
|
|
@param objects: List of base service template type objects
|
|
|
|
@type req_source: basestring
|
|
@param req_source: Source requesting this operation.
|
|
|
|
@type method: basestring
|
|
@param method: operation type. Defaults to upsert.
|
|
|
|
@type transaction_id: basestring
|
|
@param transaction_id: transaction id for end-end tracing.
|
|
"""
|
|
def _normalize_enabled_flag_to_binary(input_):
|
|
false_things = [0, '0', 'false']
|
|
if input_ in false_things:
|
|
return 0
|
|
else:
|
|
return 1
|
|
|
|
self._validate_payload(owner, objects, method, transaction_id)
|
|
|
|
if method in (CRUDMethodTypes.METHOD_UPDATE, CRUDMethodTypes.METHOD_UPSERT):
|
|
self.generate_persisted_service_template_map(owner, objects, transaction_id)
|
|
|
|
for object in objects:
|
|
"""
|
|
User can create a base service template:
|
|
1. from a static conf file
|
|
creation payload will be the normalized content of the conf file, with _immutable set to true
|
|
|
|
2. from a service or from a service template
|
|
Therefore, creation payload will look something like below:
|
|
eg. {
|
|
'service_id': <key_of_service_to_create_template_from>,
|
|
'title': 'new_base_service_template',
|
|
'_owner': 'nobody',
|
|
'sec_grp': <key_of_sec_grp>,
|
|
....
|
|
}
|
|
i.e either 'service_id' or 'base_service_template_id' need to be present in the request payload.
|
|
"""
|
|
|
|
def _add_sync_status_metadata(service_template_object):
|
|
"""
|
|
Adds sync_status('synced') and last_sync_error('') to the service template object being created
|
|
@type service_template_object: dict
|
|
@param service_template_object: object that needs to be created
|
|
"""
|
|
service_template_object['sync_status'] = 'synced'
|
|
service_template_object['last_sync_error'] = ''
|
|
|
|
def _overwrite_title_and_description(request_object, service_template_object):
|
|
"""
|
|
Overwrites the title and description of the service template object with the fields from the request object
|
|
@type request_object: dict
|
|
@param request_object: request object
|
|
@type: service_template_object: dict
|
|
@param service_template_object: object that needs to be updated
|
|
"""
|
|
service_template_object['title'] = request_object.get('title', '')
|
|
service_template_object['description'] = request_object.get('description', '')
|
|
|
|
if method == CRUDMethodTypes.METHOD_CREATE:
|
|
base_service_template_id = object.get('base_service_template_id', None)
|
|
# if '_immutable' is set to true, it's an import operation with no further enrichment needed.
|
|
is_immutable = object.get('_immutable', False)
|
|
if is_immutable:
|
|
logger.debug('Importing base service template title="%s" to create base service template',
|
|
object.get('title'))
|
|
# if 'base_service_template_id' and 'service_id' are both present, we will treat it as a clone operation.
|
|
# This assumption is made since the base_service_template_id is not part of the service template object,
|
|
# it needs to be explicitly provided which means its an intended clone operation.
|
|
elif base_service_template_id:
|
|
logger.debug(
|
|
'Cloning base service template base_service_template_id="%s" to create base service template. '
|
|
'transaction_id="%s"', base_service_template_id, transaction_id)
|
|
cloned_service_template = self._clone_base_service_template(owner, base_service_template_id,
|
|
req_source, transaction_id)
|
|
|
|
# update the object with templatized service
|
|
object.update(cloned_service_template)
|
|
|
|
# remove 'base_service_template_id' field from the request object
|
|
object.pop('base_service_template_id', None)
|
|
# remove 'service_id' from the request since we have handled this as a clone operation
|
|
object.pop('service_id', None)
|
|
else:
|
|
service_id = object.get('service_id', None)
|
|
logger.debug('Templatizing service_id="%s" to create base service template. '
|
|
'transaction_id="%s"', service_id, transaction_id)
|
|
|
|
templatized_service = self.service_interface.templatize(owner, service_id, req_source=req_source,
|
|
for_base_service_template=True)
|
|
|
|
service_to_update = self.service_interface.get(owner, service_id, req_source=req_source,
|
|
transaction_id=transaction_id)
|
|
|
|
if templatized_service is None:
|
|
self.raise_error_bad_validation(logger,
|
|
_('Could not find service object with id `{}`. Cannot create base '
|
|
'service template from non-existent service').format(
|
|
service_id))
|
|
|
|
# Convert adhoc searches / data model search/ metric search to shared base search
|
|
self._convert_adhoc_datamodel_metric_search(owner, templatized_service, service_to_update)
|
|
|
|
self.services[service_to_update.get('_key')] = service_to_update
|
|
|
|
# link service to template
|
|
templatized_service['linked_services'] = []
|
|
templatized_service['linked_services'].append(service_id)
|
|
|
|
# overwrite title and description from the request
|
|
_overwrite_title_and_description(object, templatized_service)
|
|
|
|
# When linking a service to a template, adds the service tags to the template
|
|
# In the post_save_setup method, the update to the service will be made
|
|
# (move service_tags.tags to service_tags.template_tags)
|
|
templatized_service['template_tags'] = []
|
|
if 'service_tags' in templatized_service:
|
|
if 'tags' in templatized_service['service_tags']:
|
|
templatized_service['template_tags'] = templatized_service['service_tags']['tags']
|
|
# Clean up
|
|
templatized_service.pop('service_tags', None)
|
|
|
|
# update the object with templatized service
|
|
object.update(templatized_service)
|
|
|
|
# add sync status metadata to the updated object
|
|
_add_sync_status_metadata(object)
|
|
|
|
# If the flag is not passed in, enable the
|
|
# is_healthscore_calculate_by_entity on service object
|
|
object['is_healthscore_calculate_by_entity_enabled'] = _normalize_enabled_flag_to_binary(
|
|
object.get('is_healthscore_calculate_by_entity_enabled', 1))
|
|
|
|
# set the value field for entity rule_item to empty string for rule_type matchesblank or notmatchesblank
|
|
def _set_value_for_blank_entity_rule_types(template_object):
|
|
for entity_rule in template_object.get('entity_rules', []):
|
|
for rule_item in entity_rule.get('rule_items', []):
|
|
if rule_item.get('rule_type', None) == 'matchesblank' or rule_item.get('rule_type',
|
|
None) == 'notmatchesblank':
|
|
rule_item['value'] = ''
|
|
|
|
_set_value_for_blank_entity_rule_types(object)
|
|
|
|
if not len(object.get('entity_rules', [])) > 0:
|
|
object['entity_rules'] = []
|
|
|
|
# cleanup service and kpi content.
|
|
ItsiBaseServiceTemplate._cleanup_base_service_template_content(object)
|
|
ItsiBaseServiceTemplate._cleanup_base_kpi_template_content(object)
|
|
|
|
# replace linked_services and sync_status fields with the value stored in kvstore
|
|
if method == CRUDMethodTypes.METHOD_UPDATE or method == CRUDMethodTypes.METHOD_UPSERT:
|
|
if not self.skip_service_template_update and object.get('_key') in self.persisted_service_template_map:
|
|
object['linked_services'] = self.persisted_service_template_map[object.get('_key')].get(
|
|
'linked_services', [])
|
|
object['sync_status'] = self.persisted_service_template_map[object.get('_key')].get('sync_status',
|
|
'synced')
|
|
|
|
object['total_linked_services'] = len(object.get('linked_services', []))
|
|
|
|
# generate key for new base service template
|
|
if not utils.is_valid_str(object.get('_key', '')):
|
|
object['_key'] = ITOAInterfaceUtils.generate_backend_key()
|
|
|
|
logger.info('Completed additional setup to upsert Base Service Template objects. transaction_id="%s"',
|
|
transaction_id)
|
|
|
|
def _clone_base_service_template(self, owner, base_service_template_id, req_source, transaction_id):
|
|
"""
|
|
Clones the base service template with id base_service_template_id. Removes fields that should not be part of the clone
|
|
Regenerates _key for all kpis.
|
|
@type owner: basestring
|
|
@param owner: 'nobody' or user performing the operation
|
|
|
|
@type base_service_template_id: basestring
|
|
@param base_service_template_id: service template id that is to be cloned
|
|
|
|
@type req_source: basestring
|
|
@param req_source: request source
|
|
|
|
@type transaction_id: basestring
|
|
@param transaction_id: transaction id for end-end tracing
|
|
|
|
@rtype: object
|
|
@return:
|
|
{object}: cloned base service template object
|
|
"""
|
|
base_service_template_to_clone = self.get(owner, base_service_template_id, req_source=req_source,
|
|
transaction_id=transaction_id)
|
|
|
|
if base_service_template_to_clone is None:
|
|
self.raise_error_bad_validation(logger,
|
|
_('Could not find service template object with id `{}`. Cannot create base '
|
|
'service template from non-existent service template').format(
|
|
base_service_template_id))
|
|
|
|
# clean up keys not required in the new service template object and the ones we want to retain from the request
|
|
# NOTE: we will not retain the 'service_id' field since this template is not created from a service
|
|
# NOTE: not removing sec_grp, since for service template it should always be global and cannot be changed
|
|
internal_fields = [key for key in list(base_service_template_to_clone.keys()) if key.startswith('mod_')]
|
|
internal_fields += ['acl', '_user', '_owner']
|
|
keys_to_cleanup = ['_key', 'linked_services', 'service_id', 'base_service_template_id', 'title', 'description',
|
|
'identifying_name', 'scheduled_time', 'scheduled_job'] + internal_fields
|
|
for key in keys_to_cleanup:
|
|
base_service_template_to_clone.pop(key, None)
|
|
base_service_template_to_clone['linked_services'] = []
|
|
base_service_template_to_clone['_immutable'] = 0
|
|
base_service_template_to_clone['_is_from_conf'] = 0
|
|
|
|
for kpi in base_service_template_to_clone.get('kpis', []):
|
|
kpi['_key'] = ITOAInterfaceUtils.generate_backend_key()
|
|
|
|
return base_service_template_to_clone
|
|
|
|
def identify_dependencies(self, owner, base_service_templates, method, req_source='unknown', transaction_id=None,
|
|
skip_local_failure=False):
|
|
"""
|
|
Identity dependencies of service templates and add refresh jobs.
|
|
NOTE: one refresh job is being added for each service template update, as there could
|
|
be 100s of linked services to be updated after service template update.
|
|
|
|
@type owner: basestring
|
|
@param {string} owner: user which is performing this operation
|
|
@type base_service_templates: list
|
|
@param base_service_templates: service templates to validate for dependency
|
|
@type method: basestring
|
|
@param method: method name
|
|
@type req_source: basestring
|
|
@param req_source: request source
|
|
@rtype: tuple
|
|
@return:
|
|
{boolean} set to true/false if dependency update is required
|
|
{list} list - list of refresh job
|
|
"""
|
|
refresh_jobs = []
|
|
affected_sandbox_services = []
|
|
if self.skip_service_template_update:
|
|
return len(refresh_jobs) > 0, refresh_jobs
|
|
|
|
if not self.persisted_service_template_map and method in (
|
|
CRUDMethodTypes.METHOD_UPDATE, CRUDMethodTypes.METHOD_UPSERT, CRUDMethodTypes.METHOD_DELETE):
|
|
self.generate_persisted_service_template_map(owner, base_service_templates, transaction_id)
|
|
|
|
if method == CRUDMethodTypes.METHOD_UPDATE or method == CRUDMethodTypes.METHOD_UPSERT:
|
|
# setting this as we will be using this in the post save method
|
|
self.before_save_base_service_templates = copy.deepcopy(base_service_templates)
|
|
|
|
for service_template in base_service_templates:
|
|
# getting all the sandbox services that are affected by the template
|
|
filter_data = {'$or': [{"base_service_template_id": service_template.get("_key")},
|
|
{"service_template_id": service_template.get("_key")}]}
|
|
affected_sandbox_services = self.sandbox_service_interface.get_bulk('nobody',
|
|
fields=["_key", "sandbox_id"],
|
|
filter_data=filter_data)
|
|
refresh_job = self._get_linked_services_update_job(service_template,
|
|
self.persisted_service_template_map,
|
|
transaction_id=transaction_id,
|
|
affected_sandbox_services=affected_sandbox_services)
|
|
|
|
# update service template to scheduled if there are linked services
|
|
if refresh_job:
|
|
service_template['sync_status'] = 'sync scheduled'
|
|
|
|
backup_restore_interface = ItsiBackupRestore(self.session_key, self.current_user_name)
|
|
if backup_restore_interface.is_any_backup_restore_job_in_progress(owner, req_source=req_source):
|
|
if not service_template.get('scheduled_time'):
|
|
# if backup/restore is in progress, reschedule the sync job from
|
|
# current time to a later time
|
|
service_template['scheduled_time'] = utils.get_current_utc_epoch()
|
|
# set localized message
|
|
service_template['reschedule_sync_msg'] = _('Service template changes cannot be pushed now'
|
|
' because a backup/restore is currently in '
|
|
'progress. The service template sync has been '
|
|
'scheduled for a later time.')
|
|
|
|
upgrade_readiness_interface = ItsiUpgradeReadinessPrechecks(self.session_key,
|
|
self.current_user_name)
|
|
if upgrade_readiness_interface.get_in_progress_upgrade_readiness_prechecks(
|
|
lookback_time=UPGRADE_READINESS_JOB_TIMEOUT_LIMIT, mode=MODES["AUTO_REMEDIATION"]
|
|
):
|
|
if not service_template.get('scheduled_time'):
|
|
# if upgrade readiness auto-remediation is in progress, reschedule the sync job from
|
|
# current time to a later time
|
|
service_template['scheduled_time'] = utils.get_current_utc_epoch()
|
|
# set localized message
|
|
service_template['reschedule_sync_msg'] = _('Service template changes cannot be pushed now'
|
|
' because an upgrade readiness auto-remediation'
|
|
' is currently in progress. The service template'
|
|
' sync has been scheduled for a later time.')
|
|
|
|
if service_template.get('scheduled_time'):
|
|
# refresh queue job processor requires changed_object_key to be a list
|
|
# manually convert it here
|
|
refresh_job[0]['changed_object_key'] = [refresh_job[0]['changed_object_key']]
|
|
# we don't want to overwrite an existing scheduled job completely, as we might loose
|
|
# some information in change_detail needed for correctly updating linked services
|
|
if service_template.get('scheduled_job'):
|
|
persisted_change_detail = service_template['scheduled_job'].get('change_detail', {})
|
|
new_change_detail = refresh_job[0].get('change_detail', {})
|
|
template_key = service_template.get('_key')
|
|
if template_key in persisted_change_detail:
|
|
# only update old_kpis dict, if last service template update did not
|
|
# update any of KPIs content
|
|
if not persisted_change_detail[template_key].get('kpi_update_required', False):
|
|
persisted_change_detail[template_key]['old_kpis'] = \
|
|
new_change_detail.get(template_key).get('old_kpis', {})
|
|
|
|
if persisted_change_detail[template_key].get('kpi_update_required', False) or \
|
|
new_change_detail.get(template_key).get('kpi_update_required', False):
|
|
persisted_change_detail[template_key]['kpi_update_required'] = True
|
|
else:
|
|
persisted_change_detail[template_key]['kpi_update_required'] = False
|
|
|
|
# always update all the overwrite options provided by user
|
|
persisted_change_detail[template_key]['overwrite_entity_rules'] = \
|
|
new_change_detail.get(template_key).get('overwrite_entity_rules', False)
|
|
persisted_change_detail[template_key]['overwrite_health_scores'] = \
|
|
new_change_detail.get(template_key).get('overwrite_health_scores', False)
|
|
persisted_change_detail[template_key]['overwrite_kpi_thresholds'] = \
|
|
new_change_detail.get(template_key).get('overwrite_kpi_thresholds', 'none')
|
|
# no change details found for previously scheduled job. overwrite it completely
|
|
else:
|
|
service_template['scheduled_job'] = refresh_job[0]
|
|
# there's no previously defined scheduled job, which has not executed yet.
|
|
else:
|
|
service_template['scheduled_job'] = refresh_job[0]
|
|
else:
|
|
refresh_jobs.extend(refresh_job)
|
|
service_template.pop('scheduled_job', None)
|
|
service_template.pop('overwrite_health_scores', None)
|
|
service_template.pop('overwrite_kpi_thresholds', None)
|
|
service_template.pop('overwrite_entity_rules', None)
|
|
service_template.pop('is_scheduled', None)
|
|
|
|
logger.info('Total refresh jobs added after update of service template objects = %s. transaction_id="%s".'
|
|
' updated_service_templates="%s"', len(refresh_jobs), transaction_id,
|
|
[service_template.get('_key') for service_template in base_service_templates])
|
|
|
|
if method == CRUDMethodTypes.METHOD_DELETE:
|
|
change_type = 'delete_base_service_template'
|
|
linked_services_map = {}
|
|
linked_sandbox_services_map = {}
|
|
deleted_service_template_ids = []
|
|
for service_template in list(self.persisted_service_template_map.values()):
|
|
# getting all the sandbox services that are affected by the template
|
|
filter_data = {'$or': [{"base_service_template_id": service_template.get("_key")},
|
|
{"service_template_id": service_template.get("_key")}]}
|
|
affected_sandbox_services = self.sandbox_service_interface.get_bulk('nobody',
|
|
fields=["_key", "sandbox_id"],
|
|
filter_data=filter_data)
|
|
deleted_service_template_ids.append(service_template['_key'])
|
|
linked_services_map[service_template['_key']] = service_template.get('linked_services', [])
|
|
linked_sandbox_services_map[service_template['_key']] = affected_sandbox_services
|
|
|
|
if len(deleted_service_template_ids) > 0:
|
|
refresh_jobs.append(
|
|
self.get_refresh_job_meta_data(
|
|
change_type,
|
|
deleted_service_template_ids,
|
|
self.object_type,
|
|
change_detail=linked_services_map,
|
|
transaction_id=transaction_id
|
|
)
|
|
)
|
|
logger.info('Added refresh job of type delete_base_service_template, to unlink or delete services '
|
|
'linked to deleted service template(s). service_template="%s", transaction_id="%s"',
|
|
deleted_service_template_ids, transaction_id)
|
|
if len(affected_sandbox_services) > 0 and len(deleted_service_template_ids) > 0:
|
|
change_type = 'sandbox_delete_base_service_template'
|
|
change = self.get_refresh_job_meta_data(change_type,
|
|
deleted_service_template_ids,
|
|
self.object_type,
|
|
change_detail=linked_sandbox_services_map,
|
|
transaction_id=transaction_id)
|
|
refresh_jobs.append(change)
|
|
return len(refresh_jobs) > 0, refresh_jobs
|
|
|
|
def post_save_setup(self, owner, ids, base_service_templates, req_source='unknown',
|
|
method=CRUDMethodTypes.METHOD_UPSERT, transaction_id=None, skip_local_failure=False):
|
|
"""
|
|
Link service to service template after creating service template from service.
|
|
NOTE: Not needed in case of create of service template from service template (clone). In this case 'service_id' is None
|
|
NOTE: Not needed in case of update of service template.
|
|
|
|
@type owner: string
|
|
@param owner: user who is performing this operation
|
|
@type ids: List of dict identifiers in format {"_key": <key>} returned by kvstore, pairity with objects passed in
|
|
@param ids: list of dict
|
|
@type base_service_templates: list of dictionary
|
|
@param base_service_templates: list of objects being written
|
|
@type req_source: string
|
|
@param req_source: string identifying source of this request
|
|
@type method: string
|
|
@param method: method name
|
|
@type transaction_id: basestring
|
|
@param transaction_id: transaction id for instrumentation purposes.
|
|
@return: none, throws exceptions on errors
|
|
"""
|
|
if method == CRUDMethodTypes.METHOD_CREATE:
|
|
# Looping through the list, even though there would only be one service template
|
|
# in the list, as we're only addressing CREATE request here.
|
|
for service_template in base_service_templates:
|
|
service_template_to_unlink = None
|
|
service_id = service_template.get('service_id', None)
|
|
service_template_id = service_template.get('_key')
|
|
|
|
service_to_update = self.services.get(service_id) if service_id else None
|
|
|
|
if service_id and service_to_update is None:
|
|
self.raise_error_bad_validation(logger,
|
|
_('Could not find cached service `{}`. Please debug.').format(
|
|
service_id))
|
|
|
|
if not service_to_update:
|
|
logger.info(
|
|
'service_template="%s" created from another service template. No service linkage update '
|
|
'required ', service_template_id)
|
|
continue
|
|
|
|
if utils.is_valid_str(service_to_update.get('base_service_template_id', '')):
|
|
service_template_to_unlink = service_to_update.get('base_service_template_id')
|
|
|
|
# When creating a service template from a service, move the services tags to its template tags.
|
|
# Any 'old' template_tags in the service is ignored.
|
|
if 'service_tags' not in service_to_update:
|
|
service_to_update['service_tags'] = dict()
|
|
old_tags = service_to_update['service_tags'].get('tags', [])
|
|
service_to_update['service_tags']['tags'] = []
|
|
service_to_update['service_tags']['template_tags'] = old_tags
|
|
service_to_update['base_service_template_id'] = service_template_id
|
|
for kpi in service_to_update.get('kpis', []):
|
|
if kpi.get('_key', '').startswith(self.service_interface.SHKPI_STARTS_WITH):
|
|
continue
|
|
kpi['base_service_template_id'] = service_template_id
|
|
if (kpi.get('is_recommended_time_policies', False)):
|
|
kpi['linked_kpi_thresholds_updated'] = True
|
|
|
|
# enable below flag to skip service template linking by service interface, to avoid circular updates
|
|
# NOTE: Once we start supporting bulk creation of service templates through this endpoint, start
|
|
# using bulk update instead of single update to update services with the link to service template.
|
|
self.service_interface.skip_service_template_update = True
|
|
self.service_interface.update(owner, service_id, service_to_update, transaction_id=transaction_id)
|
|
|
|
logger.info('service="%s" linked to service template, after creation '
|
|
'of service_template="%s"', service_id, service_template_id)
|
|
|
|
# if service used for template creation was previously linked to another template, unlink that
|
|
# service template from the service.
|
|
if service_template_to_unlink:
|
|
previously_linked_template = self.get(owner, service_template_to_unlink, req_source=req_source,
|
|
transaction_id=transaction_id)
|
|
|
|
updated_linked_services = [linked_service for linked_service in
|
|
previously_linked_template.get('linked_services', [])
|
|
if linked_service != service_id]
|
|
previously_linked_template['linked_services'] = updated_linked_services
|
|
self.skip_service_template_update = True
|
|
self.update(owner, service_template_to_unlink, previously_linked_template,
|
|
transaction_id=transaction_id)
|
|
self.skip_service_template_update = False
|
|
|
|
logger.info('service="%s" which was used to create service_template="%s", was previously linked to '
|
|
'another service template. Previously linked service template %s, has been unlinked from'
|
|
' the service.', service_id, service_template_id, service_template_to_unlink)
|
|
if method in (CRUDMethodTypes.METHOD_UPSERT, CRUDMethodTypes.METHOD_UPDATE):
|
|
if self.sandbox_update_jobs:
|
|
backup_restore_interface = ItsiBackupRestore(self.session_key, self.current_user_name)
|
|
if backup_restore_interface.is_any_backup_restore_job_in_progress(owner, req_source=req_source):
|
|
if not service_template.get('scheduled_time'):
|
|
# if backup/restore is in progress, reschedule the sync job from
|
|
# current time to a later time
|
|
service_template['scheduled_time'] = utils.get_current_utc_epoch()
|
|
# set localized message
|
|
service_template['reschedule_sync_msg'] = _('Service template changes cannot be pushed now'
|
|
' because a backup/restore is currently in '
|
|
'progress. The service template sync has been '
|
|
'scheduled for a later time.')
|
|
|
|
upgrade_readiness_interface = ItsiUpgradeReadinessPrechecks(self.session_key,
|
|
self.current_user_name)
|
|
if upgrade_readiness_interface.get_in_progress_upgrade_readiness_prechecks(
|
|
lookback_time=UPGRADE_READINESS_JOB_TIMEOUT_LIMIT, mode=MODES["AUTO_REMEDIATION"]
|
|
):
|
|
if not service_template.get('scheduled_time'):
|
|
# if upgrade readiness auto-remediation is in progress, reschedule the sync job from
|
|
# current time to a later time
|
|
service_template['scheduled_time'] = utils.get_current_utc_epoch()
|
|
# set localized message
|
|
service_template['reschedule_sync_msg'] = _('Service template changes cannot be pushed now'
|
|
' because an upgrade readiness auto-remediation'
|
|
' is currently in progress. The service template'
|
|
' sync has been scheduled for a later time.')
|
|
for change in self.sandbox_update_jobs:
|
|
# changed_object_keys = []
|
|
# changed_object_keys.append(change.get('changed_object_key', []))
|
|
# change['changed_object_key'] = changed_object_keys
|
|
SandboxBaseServiceTemplateUpdateHandler(logger, self.session_key).deferred(change=change, scheduled_for_later=False)
|
|
|
|
def can_be_deleted(self, owner, objects, raise_error=False, transaction_id=None):
|
|
# Do not allow delete of base service template if it's immutable
|
|
results = []
|
|
for template in objects:
|
|
immutable = template.get('_immutable', False)
|
|
if not immutable:
|
|
results.append(template)
|
|
return results
|
|
|
|
def save_batch(
|
|
self,
|
|
owner,
|
|
service_templates_list,
|
|
validate_names,
|
|
dupname_tag=None,
|
|
req_source='unknown',
|
|
ignore_refresh_impacted_objects=False,
|
|
method=CRUDMethodTypes.METHOD_UPSERT,
|
|
is_partial_data=False,
|
|
transaction_id=None,
|
|
skip_local_failure=False):
|
|
"""
|
|
Only address bulk update requests. If, request is for bulk creation of service templates,
|
|
then raise error.
|
|
|
|
@type owner: string
|
|
@param owner: user who is performing this operation
|
|
@type service_templates_list: list
|
|
@param service_templates_list: list of objects to upsert
|
|
@type validate_names: bool
|
|
@param validate_names: validate_names is a means for search commands and csv load to by pass
|
|
perf hit from name validation in scenarios they can safely skip
|
|
@type req_source: string
|
|
@param req_source: string identifying source of this request
|
|
@type is_partial_data: bool
|
|
@param is_partial_data: indicates if payload passed into each entry in data_list is a subset of object structure
|
|
when True, payload passed into data is a subset of object structure
|
|
when False, payload passed into data is the entire object structure
|
|
Note that KV store API does not support partial updates
|
|
This argument only applies to update entries since on create, entire payload is a MUST
|
|
@rtype: list of strings
|
|
@return: ids of objects upserted on success, throws exceptions on errors
|
|
"""
|
|
if len(service_templates_list) > 0 and not self.skip_service_template_update:
|
|
get_bulk_filter = {
|
|
'$or': []
|
|
}
|
|
object_key_list = [] # used to skip duplicate keys
|
|
for service_template in service_templates_list:
|
|
# if _key is missing for a service template, assume it's a create request
|
|
if not utils.is_valid_str(service_template.get('_key', '')):
|
|
self.raise_error(logger,
|
|
_('Bulk creation of service template objects is not supported or ID field '
|
|
'missing for at least one of the objects in bulk update request.'),
|
|
status_code=405)
|
|
elif service_template.get('_key') not in object_key_list:
|
|
object_key_list.append(service_template.get('_key'))
|
|
get_bulk_filter['$or'].append({
|
|
'_key': service_template.get('_key')
|
|
})
|
|
|
|
persisted_service_templates = self.get_bulk(owner, req_source=req_source, filter_data=get_bulk_filter,
|
|
fields=['_key'], transaction_id=transaction_id)
|
|
|
|
# if some of the template objects in request do no exist in kvstore.
|
|
# means, it is a create request for some of the objects in requested objects list.
|
|
if len(get_bulk_filter['$or']) > len(persisted_service_templates):
|
|
self.raise_error(logger,
|
|
_('Bulk creation of Service Template objects is not supported.'),
|
|
status_code=405)
|
|
|
|
# if data list contains no service template creation request, then proceed with the normal save batch process.
|
|
result_ids = super(ItsiBaseServiceTemplate, self).save_batch(owner, service_templates_list, validate_names,
|
|
dupname_tag=dupname_tag,
|
|
req_source=req_source,
|
|
ignore_refresh_impacted_objects=ignore_refresh_impacted_objects,
|
|
method=method, is_partial_data=is_partial_data,
|
|
transaction_id=transaction_id)
|
|
self.skip_service_template_update = False
|
|
return result_ids
|