# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved. import ITOA.itoa_common as utils from ITOA.controller_utils import ITOAError from ITOA.itoa_factory import instantiate_object from ITOA.setup_logging import getLogger from itsi.upgrade.timezones import get_new_time_blocks, _is_maintenance_end_time_indefinite, \ apply_timezone_offset, apply_timezone_offset_for_kpi_threshold_template logger = getLogger(logger_name="ITOA.itoa_shift_time_offset") logger.debug("Initialized itoa shift time offset handler log") MAX_TIME_OFFSET_IN_MINUTES = 1440 def construct_filter_for_linked_thr_tmp(thr_tmp_object_ids): """ Create a filter for bulk filtering services containing specified kpi threshold template IDs @type: list @param thr_tmp_object_ids: list of object IDs of kpi threshold template to filter @rtype dict | None @return Filter object for services """ if len(thr_tmp_object_ids) >= 1 and len(thr_tmp_object_ids[0]) > 0: get_bulk_filter = {'$or': []} for thr_tmp_object_id in thr_tmp_object_ids: get_bulk_filter['$or'].append({'kpis.kpi_threshold_template_id': thr_tmp_object_id}) return get_bulk_filter else: return None def construct_filter_for_object_keys(object_keys_list): """ Create a filter for bulk filtering objects containing specified object keys @type: list @param object_keys_list: list of object IDs of specific object to filter @rtype: dict @return: Filter object for specific object type """ if len(object_keys_list) >= 1 and len(object_keys_list[0]) > 0: get_bulk_filter = {'$or': []} for object_key in object_keys_list: get_bulk_filter['$or'].append({'_key': object_key}) return get_bulk_filter else: return {} def apply_timezone_offset_for_service(service, offset_in_min, change_linked=False, threshold_template_ids=[]): """ Given the number of hours needed to offset time_blocks, apply the change to unlinked KPIs in a service if change_linked is false, else update only KPIs linked with threshold template @type service: object @param service: Service object instance to update @type offset_in_min: int @param offset_in_min: the # of hours of offset to apply @type change_linked: boolean @param change_linked: if True change only KPIs with linked threshold template @type threshold_template_ids: list @param threshold_template_ids: List of threshold template IDs whose linked KPIs to update @rtype: None @return: None although service input reference will be updated """ if not (MAX_TIME_OFFSET_IN_MINUTES * -1 <= offset_in_min <= MAX_TIME_OFFSET_IN_MINUTES): message = 'Timezone offset specified is invalid. Must be within a 24-hour (1440 minute) range. Specified value: %s ' % offset_in_min logger.error(message) raise ITOAError(status=400, message=message) for kpi in service.get('kpis', []): if not isinstance(kpi, dict): continue if kpi.get('_key').startswith('SHKPI'): logger.debug('Skipping ServiceHealthScore kpi for service: %s', service.get('title')) continue if kpi.get('time_variate_thresholds') is False: logger.debug('Skipping KPI %s as time policy is not enabled.', kpi.get('title')) continue linked_thr_tmp_id = kpi.get('kpi_threshold_template_id') if change_linked: if linked_thr_tmp_id == '' or linked_thr_tmp_id is None or linked_thr_tmp_id == 'custom' or linked_thr_tmp_id not in threshold_template_ids: # Skipping the KPIs which are not linked with specific Threshold templates logger.debug('Skipping the KPI "%s" which is not linked with any KPI threshold template', kpi.get('title')) continue else: if linked_thr_tmp_id is not None and linked_thr_tmp_id != '' and linked_thr_tmp_id != 'custom': # Skipping the KPIs which are linked with specific Threshold templates logger.debug('Skipping the KPI "%s" which are already linked with KPI threshold template %s', kpi.get('title'), linked_thr_tmp_id) continue policy_spec = kpi.get('time_variate_thresholds_specification') if not isinstance(policy_spec, dict): continue policies = policy_spec.get('policies') if not isinstance(policies, dict): continue for policy_key, policy in policies.items(): if not isinstance(policy, dict): continue time_blocks = policy.get('time_blocks') if not isinstance(time_blocks, list): continue kpi['time_variate_thresholds_specification']['policies'][policy_key]['time_blocks'] = get_new_time_blocks(time_blocks, offset_in_min) class ShiftTimeOffsetUtils(object): """ Class for Utility of shift time offset functions """ def __init__(self, session_key, owner): self._session_key = session_key self.owner = owner def get_objects_to_update(self, object_type, payload_data): """ Helper function to get bulk objects for object type based on the filtering of payload data @type: string @param object_type: the ITOA object type @type: dict @param payload_data: data passed in the endpoint to apply time offset to @rtype: dict @return: Instance Object, Keys of Objects fetched, Objects fetched upon filtering """ if '_keys' not in payload_data.get(object_type): message = '_keys is a mandatory attrbute.' logger.error(message) raise ITOAError(status=400, message=message) object_keys = utils.get_object(payload_data.get(object_type).get('_keys')) object_key_filter = construct_filter_for_object_keys(object_keys) try: object_type_instance = instantiate_object(self._session_key, self.owner, object_type) except Exception as e: logger.exception(e) raise ITOAError(status=400, message=e) return object_type_instance, object_keys, object_type_instance.get_bulk(self.owner, filter_data=object_key_filter) def update_object_time_policy(self, data, offset): """ Function to handle all object types in data from shift_time_offset endpoint and change the time blocks for specified object keys @type data: dict @param data: payload data from the endpoint argument @type offset: int @param offset: offset in seconds to shift time policy with @rtype: dict @return: Success message or ITOA Error(if any) """ if 'service' in data: object_type_instance, service_object_keys, objects = self.get_objects_to_update('service', data) if len(objects) < 1: logger.debug('No service objects matched request') else: for json_data in objects: apply_timezone_offset_for_service(json_data, int(offset / 60)) # Here, passing skip_kpi_consistancy_check True, and ignore_refresh_impacted_objects True as # there is no need to perform additional validations on objects that are already created object_type_instance.save_batch(self.owner, objects, validate_names=False, ignore_refresh_impacted_objects=True, skip_kpi_consistancy_check=True) if 'kpi_threshold_template' in data: object_type_instance, thr_tmp_object_keys, objects = self.get_objects_to_update('kpi_threshold_template', data) if len(objects) < 1: logger.debug('No kpi threshold template objects matched request') else: for json_data in objects: apply_timezone_offset_for_kpi_threshold_template(json_data, int(offset / 60)) object_type_instance.save_batch(self.owner, objects, False) # Now get all linked KPI services if len(thr_tmp_object_keys) == 0: thr_tmp_object_keys = [data.get('_key') for data in objects] linked_thr_tmp_filter = construct_filter_for_linked_thr_tmp(thr_tmp_object_keys) if linked_thr_tmp_filter is not None: try: object_type_instance = instantiate_object(self._session_key, self.owner, 'service') except Exception as e: logger.exception(e) raise ITOAError(status=400, message=e) service_objects = object_type_instance.get_bulk(self.owner, filter_data=linked_thr_tmp_filter) if len(service_objects) > 0: for json_data in service_objects: apply_timezone_offset_for_service(json_data, int(offset / 60), True, thr_tmp_object_keys) # Here, passing skip_kpi_consistancy_check True, and ignore_refresh_impacted_objects True as # there is no need to perform additional validations on objects that are already created object_type_instance.save_batch(self.owner, service_objects, validate_names=False, ignore_refresh_impacted_objects=True, skip_kpi_consistancy_check=True) else: logger.debug("No Service Kpis are linked to Specified KPI threshold template " + str([data.get('_key') for data in objects])) if 'maintenance_calendar' in data: object_type_instance, mtw_object_keys, mw_objects = self.get_objects_to_update('maintenance_calendar', data) if len(mw_objects) < 1: logger.debug('No maintenance calendar objects matched request') else: for json_data in mw_objects: for json_field in ['start_time', 'end_time']: # Only convert offset if end_time is not set to "Indefinite" for maintenance calendars if json_field == 'end_time' and _is_maintenance_end_time_indefinite(json_data): continue apply_timezone_offset(json_data, json_field, offset) object_type_instance.save_batch(self.owner, mw_objects, False) return {"message": "success"}