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.
1839 lines
92 KiB
1839 lines
92 KiB
# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
|
|
|
|
import json
|
|
import time
|
|
|
|
from croniter import croniter
|
|
from datetime import datetime
|
|
|
|
import custom_threshold_windows.constants as CustomThresholdWindowConstants
|
|
from itsi_py3 import _
|
|
from ITOA.itoa_common import calculate_next_cron_time, get_current_utc_epoch, is_valid_dict, is_valid_list, \
|
|
is_valid_num, is_valid_str, normalize_num_field
|
|
from ITOA.itoa_object import ItoaObject, CRUDMethodTypes
|
|
from custom_threshold_windows.utils import calculate_custom_thresholds, get_human_readable_time_str
|
|
from ITOA.setup_logging import logger
|
|
from ITOA.itoa_exceptions import ItoaAccessDeniedError
|
|
from ITOA.controller_utils import ITOAError
|
|
from itsi.itsi_utils import ITOAInterfaceUtils, SplunkMessageHandler
|
|
from itsi.objects.itsi_service import ItsiService
|
|
|
|
|
|
def skip_validation_method(f):
|
|
"""
|
|
Decorator for skipping validation in a single ItsiCustomThresholdWindows method.
|
|
See validate_ctw_schema_skippable() for what this actually skips.
|
|
|
|
:param f: Function to decorate
|
|
:type f: function
|
|
:return: (function)
|
|
"""
|
|
|
|
def wrapper(self, *args, **kwargs):
|
|
existing_skip_validation = self.skip_validation
|
|
self.skip_validation = True
|
|
try:
|
|
return f(self, *args, **kwargs)
|
|
finally:
|
|
self.skip_validation = existing_skip_validation
|
|
|
|
return wrapper
|
|
|
|
|
|
class ItsiCustomThresholdWindows(ItoaObject):
|
|
"""
|
|
Implements ITSI Custom threshold window object type, which supersedes
|
|
existing KPI threshold configurations during specified, special time
|
|
periods. Custom threshold window exist in a one-to-many relationship where
|
|
a single CTW can be used by multiple KPIs.
|
|
|
|
The methods are organized into sections as follows:
|
|
* Class setup and validation methods
|
|
* Non-CRUD REST Endpoint methods (associate, disconnect, stop)
|
|
* CRUD operation methods
|
|
* Miscellaneous helper methods
|
|
* Delete handler methods
|
|
"""
|
|
collection_name = 'itsi_custom_threshold_windows'
|
|
log_prefix = '[ITSI Custom threshold windows]'
|
|
object_type = 'custom_threshold_windows'
|
|
delete_object_fields = ['_key', 'acl', 'sec_grp', '_immutable', 'status']
|
|
|
|
"""
|
|
Class setup and validation methods
|
|
"""
|
|
|
|
def __init__(self, session_key, current_user_name, skip_validation=False):
|
|
"""
|
|
:param session_key: Splunk access token.
|
|
:type session_key: basestring
|
|
:param current_user_name: Splunk user calling this class
|
|
:type current_user_name: basestring
|
|
:param skip_validation: Whether to skip schema-validation for performance improvements (internal-use only)
|
|
:type skip_validation: boolean
|
|
"""
|
|
super(ItsiCustomThresholdWindows, self).__init__(
|
|
session_key,
|
|
current_user_name,
|
|
object_type=self.object_type,
|
|
collection_name=self.collection_name,
|
|
title_validation_required=True
|
|
)
|
|
|
|
# Dependencies
|
|
self.message_handler = SplunkMessageHandler(session_key)
|
|
self.service_object = ItsiService(session_key, self.current_user_name)
|
|
|
|
# Permissioning
|
|
self.skip_validation = skip_validation
|
|
# For linking and unlinking, a less-restrictive set of permissions is required. This controls whether to check
|
|
# a potential update against the `can_edit` or the `can_link` permission
|
|
self.check_against_can_link_instead = False
|
|
# For transitioning states, a less-restrictive set of permissions is required. This controls whether to check
|
|
# a potential update against the `can_edit` or the `can_transition` permission
|
|
self.check_against_can_transition_instead = False
|
|
|
|
def do_object_validation(self, owner, objects, validate_name=True, dupname_tag=None, transaction_id=None,
|
|
skip_local_failure=False):
|
|
"""
|
|
Object validation routine (overwrites ItoaObject.do_object_validation())
|
|
|
|
:param objects: Objects to validate
|
|
:type objects: list
|
|
:param dupname_tag: A special tag to the duplicated titles. This is sometimes used to identify the calling
|
|
process (a bit of a workaround).
|
|
:type dupname_tag: basestring
|
|
"""
|
|
super(ItsiCustomThresholdWindows, self).do_object_validation(owner, objects, validate_name, dupname_tag,
|
|
transaction_id, skip_local_failure)
|
|
# Skip validation for trusted internal process (backup/restore)
|
|
existing_skip_validation = self.skip_validation
|
|
if dupname_tag == CustomThresholdWindowConstants.DUPNAME_TAG_MIGRATION:
|
|
# This flag is only used in do_additional_setup().
|
|
# do_object_validation() is always followed by do_additional_setup() in the code.
|
|
self.skip_validation = True
|
|
try:
|
|
self.validate_ctw_schema(objects)
|
|
finally:
|
|
self.skip_validation = existing_skip_validation
|
|
|
|
def do_additional_setup(self, owner, objects, req_source='unknown', method=CRUDMethodTypes.METHOD_UPSERT,
|
|
transaction_id=None, skip_local_failure=False):
|
|
"""
|
|
Additional setup performed during edit/create operations (overwrites ItoaObject.do_additional_setup()).
|
|
This is called immediately after ItoaObject.do_object_validation().
|
|
This method is called for CREATE, UPDATE, and UPSERT.
|
|
|
|
:param owner: Splunk user to own object
|
|
:type owner: basestring
|
|
:param objects: Objects to setup
|
|
:type objects: list
|
|
:param method: CRUD method that we preform the setup for
|
|
:type method: CRUDMethodTypes enum (basestring)
|
|
:param transaction_id: Transaction ID for logging
|
|
:type transaction_id: basestring
|
|
"""
|
|
self.attach_security_groups(owner, objects, transaction_id)
|
|
# Check that the CTW to edit can be edited
|
|
# Note: can_delete permissions handled by overwritten delete()
|
|
for json_data in objects:
|
|
if method == CRUDMethodTypes.METHOD_UPDATE or method == CRUDMethodTypes.METHOD_UPSERT:
|
|
try:
|
|
ctw_to_update = self.get(owner, json_data.get('_key', ''))
|
|
# Updating merges the object data before saving, so we need a separate mechanism of figuring out
|
|
# if the change was merely KPI linkages or a change to the object itself
|
|
if self.check_against_can_link_instead:
|
|
self.check_ctw_permissions(ctw_to_update, 'can_link')
|
|
elif self.check_against_can_transition_instead:
|
|
self.check_ctw_permissions(ctw_to_update, 'can_transition')
|
|
else:
|
|
self.check_ctw_permissions(ctw_to_update, 'can_edit')
|
|
except ITOAError as e:
|
|
# UPSERT allows creation of new objects
|
|
if method == CRUDMethodTypes.METHOD_UPSERT and e.status == 404:
|
|
continue
|
|
raise e
|
|
# Check if the duration field provided is a non negative integer
|
|
duration = json_data.get('duration')
|
|
if duration and int(duration) < 0:
|
|
error_message = "Error:'Must be a positive integer ({})." \
|
|
.format('duration')
|
|
self.raise_error(logger, _(error_message), 403)
|
|
|
|
def attach_security_groups(self, owner, custom_threshold_windows, transaction_id):
|
|
"""
|
|
Attach inferred security groups to CTW (from services).
|
|
|
|
:param owner: Splunk user to own object
|
|
:type owner: basestring
|
|
:param custom_threshold_windows: CTWs to update
|
|
:type custom_threshold_windows: list
|
|
:param transaction_id: Transaction ID for logging
|
|
:type transaction_id: basestring
|
|
"""
|
|
# This interface uses `owner` instead of the actual user since the actual user may not have access to all
|
|
# services attached to a shared CTW. This shouldn't be a security risk as this function is only called
|
|
# internally, before a CREATE, UPDATE, or UPSERT in itoa_object.py.
|
|
service_interface = ItsiService(self.session_key, owner)
|
|
for threshold_window in custom_threshold_windows:
|
|
associated_sec_grps = []
|
|
# Validate linked objects
|
|
linked_services = threshold_window.get('linked_services', [])
|
|
for linked_service in linked_services:
|
|
try:
|
|
service = service_interface.get(owner,
|
|
linked_service.get("service_id"),
|
|
req_source="CustomThresholdWindowsGet",
|
|
transaction_id=transaction_id)
|
|
if service is None:
|
|
raise ITOAError(status="404", message=_("Linked service not found."))
|
|
if not service.get("permissions", {}).get("write", False):
|
|
raise ITOAError(status="403", message=_("Service write permission denied."))
|
|
if service.get('sec_grp') and not service["sec_grp"] in associated_sec_grps:
|
|
associated_sec_grps.append(service["sec_grp"])
|
|
except ItoaAccessDeniedError:
|
|
raise ITOAError(status="403", message=_("Service permission denied."))
|
|
|
|
if len(associated_sec_grps) == 0:
|
|
threshold_window['sec_grp_list'] = [self._get_security_enforcer().get_default_itsi_security_group_key()]
|
|
else:
|
|
threshold_window['sec_grp_list'] = associated_sec_grps
|
|
|
|
def validate_ctw_schema(self, custom_threshold_windows):
|
|
"""
|
|
Method for validating custom_threshold_window objects and potentially fixing any required fields in the expected
|
|
schema of the CTW object.
|
|
|
|
@type custom_threshold_windows: object list
|
|
@param custom_threshold_windows: list of custom threshold objects being created/updated
|
|
|
|
@return None - exceptions to be raised upon failed validations
|
|
"""
|
|
|
|
def validate_ctw_schema_skippable(custom_threshold_window):
|
|
"""
|
|
Helper function that runs the skippable validations. This function sets default values and runs the check
|
|
for creating CTWs in the past (user shouldn't be able to skip this check, but backup-restores should).
|
|
|
|
:param custom_threshold_window: CTW object
|
|
:type custom_threshold_window: dict
|
|
"""
|
|
if not is_valid_dict(custom_threshold_window):
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_('Invalid custom threshold window specified. Custom Threshold Windows must be in dict format.')
|
|
)
|
|
|
|
for bool_field in ['recurrence']:
|
|
if (
|
|
(bool_field not in custom_threshold_window)
|
|
or (not isinstance(custom_threshold_window[bool_field], bool))
|
|
):
|
|
custom_threshold_window[bool_field] = False
|
|
|
|
for str_field in ['description', 'pause_description', 'cron_schedule', 'window_type', 'status']:
|
|
if (
|
|
(str_field not in custom_threshold_window)
|
|
or (not is_valid_str(custom_threshold_window[str_field]))
|
|
):
|
|
if str_field == 'window_type':
|
|
custom_threshold_window[str_field] = CustomThresholdWindowConstants.TYPE_PERCENTAGE
|
|
elif str_field == 'status':
|
|
custom_threshold_window[str_field] = CustomThresholdWindowConstants.STATUS_SCHEDULED
|
|
else:
|
|
custom_threshold_window[str_field] = ''
|
|
|
|
for num_field in ['duration', 'start_time', 'window_config_percentage']:
|
|
if (
|
|
(num_field not in custom_threshold_window)
|
|
or (not is_valid_num(custom_threshold_window[num_field]))
|
|
):
|
|
if num_field == 'duration':
|
|
custom_threshold_window[num_field] = 24
|
|
elif num_field == 'window_config_percentage':
|
|
custom_threshold_window[num_field] = 10
|
|
elif num_field == 'start_time':
|
|
# Default start to be 24 hours from now
|
|
default_start = 86400
|
|
custom_threshold_window[num_field] = int(datetime.now().timestamp() + default_start)
|
|
|
|
if 'window_config_static' not in custom_threshold_window:
|
|
custom_threshold_window['window_config_static'] = {}
|
|
|
|
# Percentages should be between -200 and 200
|
|
if abs(int(custom_threshold_window.get('window_config_percentage', 0))) > 200:
|
|
custom_threshold_window['window_config_percentage'] = 200 \
|
|
if custom_threshold_window['window_config_percentage'] > 0 else -200
|
|
|
|
# Validate start time for non-recurring window
|
|
if not custom_threshold_window.get('recurrence') and \
|
|
threshold_window['start_time'] < get_current_utc_epoch():
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_('Start time cannot be in the past for a custom threshold window (%s).' %
|
|
custom_threshold_window.get('title')),
|
|
)
|
|
|
|
for threshold_window in custom_threshold_windows:
|
|
# this will be set to false only by associate KPI calls so no need to do validation
|
|
if not self.skip_validation:
|
|
validate_ctw_schema_skippable(threshold_window)
|
|
|
|
# If recurring window and somehow status was set to completed, change back to scheduled
|
|
if threshold_window['status'] == CustomThresholdWindowConstants.STATUS_COMPLETED and \
|
|
threshold_window['recurrence']:
|
|
threshold_window['status'] = CustomThresholdWindowConstants.STATUS_SCHEDULED
|
|
|
|
# Fill in missing times in scheduled CTWs
|
|
if threshold_window.get('status') == CustomThresholdWindowConstants.STATUS_SCHEDULED:
|
|
next_scheduled_time = None
|
|
if not threshold_window.get('start_time', 0):
|
|
next_scheduled_time = self.calculate_next_scheduled_time(threshold_window)
|
|
threshold_window['start_time'] = next_scheduled_time
|
|
if not threshold_window.get('next_scheduled_time', 0):
|
|
next_scheduled_time = next_scheduled_time or self.calculate_next_scheduled_time(threshold_window)
|
|
threshold_window['next_scheduled_time'] = next_scheduled_time
|
|
if not threshold_window.get('end_time', 0):
|
|
threshold_window['end_time'] = self.calculate_end_time(threshold_window)
|
|
|
|
# Time field normalization
|
|
normalize_num_field(threshold_window, 'start_time')
|
|
normalize_num_field(threshold_window, 'next_scheduled_time')
|
|
normalize_num_field(threshold_window, 'end_time')
|
|
|
|
# Security check to avoid basing permissions on passed-in fields (e.g. with updates)
|
|
if 'permissions' in threshold_window:
|
|
threshold_window.pop('permissions', None)
|
|
|
|
def check_ctw_permissions(self, ctw_to_validate, requested_permission, service_kpis_dicts=[]):
|
|
"""
|
|
Method to check that a user can perform a given operation on a CTW.
|
|
|
|
@type ctw_to_validate: Custom Threshold Windows object
|
|
@param ctw_to_validate: object to validate
|
|
|
|
@type requested_permission: basestring
|
|
@param requested_permission: requested permissions
|
|
|
|
@type service_kpis_dicts: list
|
|
@param service_kpis_dicts: list of service-kpi dicts to be unlinked from CTW
|
|
|
|
@return None - Exception raised if operation cannot be done
|
|
"""
|
|
ctw_title = ctw_to_validate.get('title')
|
|
status = ctw_to_validate['status']
|
|
if requested_permission not in CustomThresholdWindowConstants.CAN_PERMISSIONS:
|
|
error_message = 'Unknown custom threshold window permission requested (%s)' % requested_permission
|
|
self.raise_error(logger, _(error_message), 500)
|
|
if self.current_user_name != 'nobody':
|
|
if not ctw_to_validate['permissions']['can_view']:
|
|
error_message = "{ \"error_code\": \"403\", \"message\": \"User does not have permission to view custom threshold window.\"}"
|
|
self.raise_error(logger, _(error_message), 403)
|
|
elif requested_permission == 'can_link' and status not in CustomThresholdWindowConstants.LINKABLE_STATES:
|
|
error_message = "The KPI(s) can't be linked / unlinked because the custom threshold window ({}) is " \
|
|
"in the {} state.".format(ctw_title, status)
|
|
self.raise_error(logger, _(error_message), 422)
|
|
elif requested_permission == 'can_edit' and status not in CustomThresholdWindowConstants.EDITABLE_STATES:
|
|
error_message = "The custom threshold window ({}) can't be edited because it is in the {} state." \
|
|
.format(ctw_title, status)
|
|
self.raise_error(logger, _(error_message), 422)
|
|
elif requested_permission == 'can_transition' and status not in \
|
|
CustomThresholdWindowConstants.TRANSITIONABLE_STATES:
|
|
error_message = "The custom threshold window ({}) can't be transitioned because it is in the {} " \
|
|
"state.".format(ctw_title, status)
|
|
self.raise_error(logger, _(error_message), 422)
|
|
elif requested_permission == 'can_delete' and status not in \
|
|
CustomThresholdWindowConstants.DELETEABLE_STATES:
|
|
error_message = "The custom threshold window ({}) can't be deleted because it is in the {} state."\
|
|
.format(ctw_title, status)
|
|
self.raise_error(logger, _(error_message), 422)
|
|
elif (requested_permission == 'can_link' and not ctw_to_validate['permissions']['can_link']):
|
|
error_message = "Error: User does not have permission to link to custom threshold window " \
|
|
"({}).".format(ctw_title)
|
|
self.raise_error(logger, _(error_message), 403)
|
|
elif requested_permission == 'can_unlink' and 'can_unlink' in ctw_to_validate['permissions']:
|
|
if not service_kpis_dicts:
|
|
error_message = "Error: Not enough data passed in to unlink KPIs from custom " \
|
|
"threshold window ({}).".format(ctw_title)
|
|
self.raise_error(logger, _(error_message), 500)
|
|
for service_and_kpis in service_kpis_dicts:
|
|
ids_of_kpis_to_disconnect = service_and_kpis.get('linked_kpi_ids')
|
|
for kpi_key in ids_of_kpis_to_disconnect:
|
|
# Bad data can be passed in. Validate this
|
|
if ctw_to_validate['permissions']['can_unlink'].get(kpi_key) is None:
|
|
error_message = "Error: Custom Threshold Window ({}) does not have reference to KPI with ID ({}) or user does not have permission to unlink to custom threshold window.".format(ctw_title, kpi_key)
|
|
self.raise_error(logger, _(error_message), 403)
|
|
elif requested_permission == 'can_edit' and not ctw_to_validate['permissions']['can_edit']:
|
|
error_message = "Error: User does not have permission to edit custom threshold window ({})." \
|
|
.format(ctw_title)
|
|
self.raise_error(logger, _(error_message), 403)
|
|
elif requested_permission == 'can_transition' and not ctw_to_validate['permissions']['can_transition']:
|
|
error_message = 'Error: User does not have permission to transition custom threshold window ({}).' \
|
|
.format(ctw_title)
|
|
self.raise_error(logger, _(error_message), 403)
|
|
elif requested_permission == 'can_delete' and not ctw_to_validate['permissions']['can_delete']:
|
|
error_message = "Error: User does not have permission to delete custom threshold window ({})." \
|
|
.format(ctw_title)
|
|
self.raise_error(logger, _(error_message), 403)
|
|
|
|
def validate_ctw_inputs(self, data, is_create_call=False, is_partial_data=False):
|
|
"""
|
|
Helper function implemented to check for inputs passed by user for rest calls.
|
|
We check inputs like linked_services and KPis must not duplicates.
|
|
"""
|
|
if is_partial_data:
|
|
if data.get('end_time', None) is not None:
|
|
if data.get('start_time') is None and data.get('cron_schedule') is None:
|
|
raise ITOAError(status='400', message=_('Custom threshold window update operation cannot update '
|
|
'end_time.'))
|
|
linked_services = data.get('linked_services', [])
|
|
if is_create_call and not linked_services:
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should contain at least one '
|
|
'linked service.') % data.get('title', 'N/A'))
|
|
service_id_list = set()
|
|
for linked_service in linked_services:
|
|
service_id = linked_service.get('service_id', 'undefined')
|
|
if (service_id in service_id_list):
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should not contain duplicates in '
|
|
'linked services.') % data.get('title', 'N/A'))
|
|
else:
|
|
service_id_list.add(service_id)
|
|
linked_kpi_ids = linked_service.get('linked_kpi_ids', [])
|
|
if ITOAInterfaceUtils.check_if_duplicates(linked_kpi_ids):
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should not contain duplicates in '
|
|
'Kpis.') % data.get('title', 'N/A'))
|
|
|
|
"""
|
|
Non-CRUD REST Endpoint methods
|
|
"""
|
|
|
|
@skip_validation_method
|
|
def associate_ctw_kpis(self, owner, object_id, transaction_id=None, **kwargs):
|
|
"""
|
|
This function is used to associate the service and its Kpi's to
|
|
CTW object.
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type object_id: string
|
|
@param object_id: id of CTW object to retrieve
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: unique identifier of a user transaction
|
|
|
|
@type: dict
|
|
@param **kwargs: key word arguments extracted from request
|
|
|
|
@rtype: dictionary
|
|
@return: object with updated association, throws exceptions on errors
|
|
"""
|
|
ctw_to_update = self.get(owner, object_id, req_source='get_ctw_object', transaction_id=transaction_id)
|
|
self.check_ctw_permissions(ctw_to_update, 'can_link')
|
|
|
|
# In the case of a shared CTW, the user linking may not have access to all linked services. However, we still
|
|
# need to attach those as we can do a partial update on a object but not on an object's field.
|
|
ctw_to_update_as_nobody = ItoaObject.get(self, 'nobody', object_id)
|
|
ctw_to_update['linked_services'] = ctw_to_update_as_nobody['linked_services']
|
|
|
|
service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
payload = kwargs.get('data') if 'data' in kwargs else json.loads(kwargs.get('payload'))
|
|
if 'custom_threshold_window' in payload:
|
|
custom_threshold_window_from_payload = payload['custom_threshold_window']
|
|
ctw_to_update = self.update_ctw_object_with_values_received(ctw_to_update,
|
|
custom_threshold_window_from_payload)
|
|
services_in_payload = payload.get('services', [])
|
|
ctw_services_objs_to_update = ctw_to_update.get('linked_services', [])
|
|
list_of_service_ids_in_ctw_obj = []
|
|
if len(ctw_services_objs_to_update) != 0:
|
|
list_of_service_ids_in_ctw_obj = self.populate_services_associated_to_ctw(ctw_services_objs_to_update)
|
|
for service in services_in_payload:
|
|
ctw_services_obj = {}
|
|
service_obj = service_interface.get(owner, service.get('_key'),
|
|
req_source='get_bulk',
|
|
transaction_id=transaction_id)
|
|
ctw_services_obj['service_id'] = service.get('_key')
|
|
if service_obj is not None:
|
|
kpis_in_object = service_obj.get('kpis', [])
|
|
kpi_ids_in_object = [kpi_object.get('_key') for kpi_object in kpis_in_object]
|
|
kpis_in_payload = service.get('kpi_ids')
|
|
if set(kpis_in_payload).issubset(set(kpi_ids_in_object)):
|
|
ctw_services_obj['linked_kpi_ids'] = kpis_in_payload
|
|
else:
|
|
missing_kpis = list(set(kpis_in_payload).difference(set(kpi_ids_in_object)))
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_("The specified KPIs are not part of the service object {}".format(missing_kpis))
|
|
)
|
|
if service.get('_key') in list_of_service_ids_in_ctw_obj:
|
|
for tmp_ctw_obj in ctw_services_objs_to_update:
|
|
if tmp_ctw_obj.get('service_id') == service.get('_key'):
|
|
kpi_ids = tmp_ctw_obj.get('linked_kpi_ids')
|
|
for str in ctw_services_obj.get('linked_kpi_ids'):
|
|
if str not in kpi_ids:
|
|
kpi_ids.append(str)
|
|
tmp_ctw_obj['linked_kpi_ids'] = kpi_ids
|
|
ctw_services_objs_to_update[ctw_services_objs_to_update.index(tmp_ctw_obj)] = tmp_ctw_obj
|
|
elif ctw_services_obj not in ctw_services_objs_to_update:
|
|
ctw_services_objs_to_update.append(ctw_services_obj)
|
|
|
|
else:
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_("Not able to retrive the Service object %s", service.get('_key'))
|
|
)
|
|
ctw_to_update['linked_services'] = ctw_services_objs_to_update
|
|
# Prevent KPI association from deleting fields in the CTW
|
|
ctw_to_update.pop('_marked_for_delete', None)
|
|
try:
|
|
self.check_against_can_link_instead = True
|
|
ctw_object = self.update(owner, object_id, ctw_to_update, is_partial_data=True)
|
|
finally:
|
|
self.check_against_can_link_instead = False
|
|
return ctw_object
|
|
|
|
@skip_validation_method
|
|
def disconnect_kpis_from_custom_threshold_window(self, owner, ctw_id=None, **kwargs):
|
|
"""
|
|
This function is used to disconnect KPIs from a CTW object
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type ctw_id: string
|
|
@param ctw_id: Id of CTW object to retrieve
|
|
|
|
@type: dict
|
|
@param kwargs: Key word arguments extracted from request
|
|
|
|
@rtype: dictionary
|
|
@return: Updated custom threshold object, post-disconnect. Throws exceptions on errors
|
|
|
|
Payload will look like:
|
|
{
|
|
"data": {
|
|
"service_kpis_dict": [
|
|
{
|
|
"service_id": "fake service id",
|
|
"linked_kpi_ids": [
|
|
"fake kpi id"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
"""
|
|
ctw_to_update = self.get(owner, ctw_id)
|
|
list_of_service_kpis_dicts = kwargs.get('data', {}).get('service_kpis_dict', [])
|
|
self.check_ctw_permissions(ctw_to_update, 'can_unlink', list_of_service_kpis_dicts)
|
|
|
|
# In the case of a shared CTW, the user linking may not have access to all linked services. However, we still
|
|
# need to attach those as we can do a partial update on a object but not on an object's field.
|
|
ctw_to_update_as_nobody = ItoaObject.get(self, 'nobody', ctw_id)
|
|
ctw_to_update['linked_services'] = ctw_to_update_as_nobody['linked_services']
|
|
|
|
ctw_linked_services = ctw_to_update.get('linked_services', [])
|
|
ctw_linked_services_keys = [linked_service.get('service_id') for linked_service in ctw_linked_services]
|
|
|
|
# This should not be triggered since ctws need at least 1 service/kpi linked at creation but we'll keep it just in case
|
|
if not ctw_linked_services_keys:
|
|
error_message = 'Error: Custom threshold windows object has no linked_services entries.'
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_(error_message)
|
|
)
|
|
|
|
service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
|
|
for service_kpis_dict in list_of_service_kpis_dicts:
|
|
# Check that CTW has reference to service in linked_services
|
|
dict_service_id = service_kpis_dict.get('service_id')
|
|
if dict_service_id not in ctw_linked_services_keys:
|
|
error_message = 'Error: Custom threshold window does not have reference to service ' \
|
|
'with ID {} in linked_services field.'.format(dict_service_id)
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_(error_message)
|
|
)
|
|
|
|
service_obj = service_interface.get(owner, dict_service_id)
|
|
|
|
ids_of_kpis_to_disconnect = service_kpis_dict.get('linked_kpi_ids')
|
|
kpis_in_service_obj = service_obj.get('kpis')
|
|
kpi_keys_in_service_dict = {}
|
|
for kpi in kpis_in_service_obj:
|
|
kpi_keys_in_service_dict[kpi.get('_key')] = kpi
|
|
|
|
# Grab linked_kpi_ids from ctw
|
|
linked_kpi_ids_in_ctw_for_service = []
|
|
for linked_service in ctw_linked_services:
|
|
if dict_service_id == linked_service.get('service_id'):
|
|
linked_kpi_ids_in_ctw_for_service = linked_service.get('linked_kpi_ids', [])
|
|
break
|
|
|
|
for id_of_kpi_to_disconnect in ids_of_kpis_to_disconnect:
|
|
kpi = kpi_keys_in_service_dict.get(id_of_kpi_to_disconnect, None)
|
|
if kpi is None:
|
|
error_message = 'Service {} does not have a kpi with key {}.'.format(service_obj.get('_key'), id_of_kpi_to_disconnect)
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_(error_message)
|
|
)
|
|
|
|
# Verify that each kpi has reference to CTW
|
|
if ctw_id not in kpi.get('linked_custom_threshold_windows'):
|
|
error_message = 'Error: KPI {} does not have reference to CTW {} in linked_custom_threshold_windows field.'.format(id_of_kpi_to_disconnect, ctw_id)
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_(error_message)
|
|
)
|
|
|
|
# Now, verify that ctw has reference to KPI
|
|
if id_of_kpi_to_disconnect not in linked_kpi_ids_in_ctw_for_service:
|
|
error_message = 'Error: Custom threshold window does not contain a reference to KPI with ID {} ' \
|
|
'for service with ID {}.'.format(id_of_kpi_to_disconnect, dict_service_id)
|
|
self.raise_error_bad_validation(
|
|
logger,
|
|
_(error_message)
|
|
)
|
|
|
|
# Now, modify CTW object
|
|
for linked_service in ctw_linked_services:
|
|
if linked_service.get('service_id') == dict_service_id:
|
|
linked_kpi_ids = linked_service.get('linked_kpi_ids')
|
|
linked_kpi_ids.remove(id_of_kpi_to_disconnect)
|
|
if not linked_kpi_ids:
|
|
ctw_linked_services.remove(linked_service)
|
|
else:
|
|
linked_service['linked_kpi_ids'] = linked_kpi_ids
|
|
ctw_to_update['linked_services'] = ctw_linked_services
|
|
|
|
# Prevent KPI disconnection from deleting fields in the CTW
|
|
ctw_to_update.pop('_marked_for_delete', None)
|
|
try:
|
|
self.check_against_can_link_instead = True
|
|
ctw_object = self.update(owner, ctw_id, ctw_to_update, is_partial_data=True)
|
|
except Exception as e:
|
|
error_msg = 'Error while attempting to update custom threshold window (%s): %s' % (ctw_id, e)
|
|
self.raise_error(logger, error_msg, 500)
|
|
finally:
|
|
self.check_against_can_link_instead = False
|
|
return ctw_object
|
|
|
|
def populate_services_associated_to_ctw(self, list_of_service_associated_to_ctw):
|
|
list_of_service_ids = []
|
|
for service_obj in list_of_service_associated_to_ctw:
|
|
if len(service_obj) != 0:
|
|
list_of_service_ids.append(service_obj.get('service_id', ''))
|
|
return list_of_service_ids
|
|
|
|
def update_received_values_for_ctw_object_update_with_end_time(self, ctw_object, data):
|
|
"""
|
|
Method to fill the end_time at run time for the CTW object updation flow
|
|
We pull in the current CTW object and update the new values pass in
|
|
to get the end_time for CTW.
|
|
|
|
@type ctw_object: object
|
|
@param ctw_object: ctw object to update from datastore
|
|
|
|
@type data: json
|
|
@param data: data from ui/rest to update
|
|
|
|
@return: ypdated json data with end_time populated
|
|
"""
|
|
json_data = self.extract_json_data(data)
|
|
calculate_end_time = False
|
|
if json_data.get('start_time'):
|
|
ctw_object['start_time'] = json_data.get('start_time')
|
|
calculate_end_time = True
|
|
if json_data.get('duration'):
|
|
ctw_object['duration'] = json_data.get('duration')
|
|
calculate_end_time = True
|
|
if json_data.get('user_timezone'):
|
|
ctw_object['user_timezone'] = json_data.get('user_timezone')
|
|
if json_data.get('cron_schedule'):
|
|
ctw_object['cron_schedule'] = json_data.get('cron_schedule')
|
|
ctw_object['start_time'] = 0
|
|
ctw_object['recurrence'] = True
|
|
ctw_object['next_scheduled_time'] = self.calculate_next_scheduled_time(ctw_object)
|
|
ctw_object['start_time'] = ctw_object['next_scheduled_time']
|
|
calculate_end_time = True
|
|
else:
|
|
ctw_object['recurrence'] = False
|
|
|
|
if calculate_end_time:
|
|
data['end_time'] = self.calculate_end_time(ctw_object)
|
|
data['next_scheduled_time'] = self.calculate_next_scheduled_time(ctw_object)
|
|
return data
|
|
|
|
def update_ctw_object_with_values_received(self, db_ctw_object, user_passed_ctw_obj):
|
|
"""
|
|
Method for copying the Values of object received for updation into the DB Object for processing.
|
|
@type db_ctw_object: custom_threshold_window
|
|
@param db_ctw_object: object of custom threshold objects being updated
|
|
|
|
@type user_passed_ctw_obj: object
|
|
@param user_passed_ctw_obj: object containing values to update CTW with
|
|
|
|
@return: custom_threshold_window
|
|
"""
|
|
for key in user_passed_ctw_obj.keys():
|
|
if key in db_ctw_object.keys():
|
|
db_ctw_object[key] = user_passed_ctw_obj.get(key, db_ctw_object[key])
|
|
return db_ctw_object
|
|
|
|
def get_stop_reason_message(self, stop_reason):
|
|
"""
|
|
gets a stop reason message with current user's username name in it
|
|
|
|
@type: string
|
|
@param stop_reason: custom reason message provided by the current user
|
|
|
|
@rtype: string
|
|
@return stop reason message
|
|
"""
|
|
|
|
message = _("Last stopped by {} user.").format(self.current_user_name)
|
|
if stop_reason:
|
|
message = _("Last stopped by {} user with message: {}").format(self.current_user_name, stop_reason)
|
|
return message
|
|
|
|
@skip_validation_method
|
|
def stop_active_ctw(self, owner, ctw_id, stop_reason):
|
|
"""
|
|
Stop an active custom threshold window
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type: string
|
|
@param ctw_id: key of the CTW object
|
|
|
|
@type: string
|
|
@param stop_reason: reason for stopping CTW
|
|
|
|
@rtype: json
|
|
@return: Stopped CTW object
|
|
"""
|
|
ctw_object = self.get(owner, ctw_id)
|
|
self.check_ctw_permissions(ctw_object, 'can_transition')
|
|
inactive_services = []
|
|
inactive_kpis = {}
|
|
for service in ctw_object.get('linked_services', []):
|
|
inactive_services.append(service['service_id'])
|
|
for kpi_id in service.get('linked_kpi_ids'):
|
|
inactive_kpis[kpi_id] = ctw_object['_key']
|
|
|
|
self.update_service_object([], {}, inactive_services, inactive_kpis)
|
|
try:
|
|
self.check_against_can_transition_instead = True
|
|
stop_reason_message = self.get_stop_reason_message(stop_reason)
|
|
self.update_ctw_object_status(
|
|
to_be_active_windows=[],
|
|
to_be_ended_windows=[ctw_object],
|
|
is_stopping=True,
|
|
stop_reason_message=stop_reason_message,
|
|
)
|
|
finally:
|
|
self.check_against_can_transition_instead = False
|
|
return self.get('nobody', ctw_id)
|
|
|
|
def bulk_stop_active_ctws(self, owner, ctw_ids, stop_reason):
|
|
"""
|
|
Stop multiple active custom threshold windows
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
:type: list of string
|
|
:param ctw_ids: List of CTW IDs to stop
|
|
|
|
@type: string
|
|
@param stop_reason: reason for stopping CTW
|
|
|
|
:type: list of json
|
|
:return: List of stopped CTW objects
|
|
"""
|
|
rv = []
|
|
try:
|
|
for i in range(len(ctw_ids)):
|
|
rv.append(self.stop_active_ctw(owner, ctw_ids[i], stop_reason))
|
|
except Exception as e:
|
|
error_msg = 'Custom threshold windows stopped successfully (%s), custom threshold windows that ' \
|
|
'encountered errors in the stopping process (%s), %s' % (ctw_ids[0:i], ctw_ids[i:], e)
|
|
self.raise_error(logger, error_msg, 500)
|
|
return rv
|
|
|
|
"""
|
|
CRUD operation methods
|
|
"""
|
|
|
|
def create(self, owner, data, dupname_tag=None, transaction_id=None):
|
|
"""
|
|
Overriding the itoa_object create function.
|
|
|
|
We create the CTW object and then attach the KPIs to it to make use of existing code. We throw errors here
|
|
instead of in do_additional_setup() because that's called only before the base interface creation code.
|
|
|
|
On associate_ctw_kpis() error, attempt a deletion of the CTW object. This is less addressable by the user, but
|
|
this ensures stricter compliance with the rule that CTWs must have KPIs.
|
|
"""
|
|
service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
linked_services = data.get('linked_services', [])
|
|
self.validate_ctw_inputs(data , is_create_call=True)
|
|
for linked_service in linked_services:
|
|
service_id = linked_service.get('service_id', 'undefined')
|
|
service = service_interface.get(owner, service_id)
|
|
linked_kpi_ids = linked_service.get('linked_kpi_ids', [])
|
|
if not service:
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should have a valid service ID, '
|
|
'service ID %s is not a valid service ID') % (data.get('title', 'N/A'), service_id))
|
|
elif not linked_kpi_ids:
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should have linked KPI Ids') %
|
|
data.get('title', 'N/A'))
|
|
kpi_keys = {kpi['_key'] for kpi in service['kpis']}
|
|
for kpi_id in linked_kpi_ids:
|
|
if kpi_id not in kpi_keys:
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should have a valid KPI '
|
|
'reference, KPI with ID %s is not part of service with ID %s') %
|
|
(data.get('title', 'N/A'), kpi_id, service_id))
|
|
# The main point of the method
|
|
response = super(ItsiCustomThresholdWindows, self).create(
|
|
owner, data, dupname_tag=dupname_tag, transaction_id=transaction_id,
|
|
)
|
|
return response
|
|
|
|
def save_batch(
|
|
self,
|
|
owner,
|
|
data_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
|
|
):
|
|
"""
|
|
Overriding the itoa_object save_batch function.
|
|
|
|
Batch saving is less performant (we don't have a batch-associate), but it's okay for now since this is only used
|
|
in backup-restore and not in other cases.
|
|
|
|
On associate_ctw_kpis() error, attempt a deletion of the CTW object. This is less addressable by the user, but
|
|
this ensures stricter compliance with the rule that CTWs must have KPIs.
|
|
"""
|
|
for json_data in data_list:
|
|
linked_services = json_data.get('linked_services', [])
|
|
if not linked_services and not is_partial_data:
|
|
raise ITOAError(status='400', message=_('Custom threshold window (%s) should contain at least one '
|
|
'linked service.') % json_data.get('title', 'N/A'))
|
|
|
|
response = super(ItsiCustomThresholdWindows, self).save_batch(
|
|
owner, data_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, skip_local_failure=skip_local_failure,
|
|
)
|
|
return response
|
|
|
|
def get(self, owner, object_id, req_source='unknown', transaction_id=None):
|
|
"""
|
|
Overriding the itoa_object get function.
|
|
The CTW object type is not intended to be 'securable' but we can still enforce RBAC filtering.
|
|
|
|
@type owner: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type object_id: string
|
|
@param object_id: id of CTW object to retrieve
|
|
|
|
@type req_source: string
|
|
@param req_source: identified source initiating the operation
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: unique identifier of a user transaction
|
|
|
|
@return: object matching id on success, throws exceptions on errors (including if object is not found)
|
|
"""
|
|
ctw_object = ItoaObject.get(self, owner, object_id, req_source, transaction_id)
|
|
if not ctw_object:
|
|
raise ITOAError(status="404", message=_("Custom threshold window (%s) not found." % object_id))
|
|
services_by_ids, services_sec_grp_ids = self.fetch_service_data(owner, transaction_id)
|
|
self.attach_ctw_permissions(owner, ctw_object, transaction_id, services_by_ids,
|
|
services_sec_grp_ids)
|
|
self.check_ctw_permissions(ctw_object, 'can_view')
|
|
return ctw_object
|
|
|
|
def get_bulk(self,
|
|
owner,
|
|
sort_key=None,
|
|
sort_dir=None,
|
|
filter_data=None,
|
|
fields=None,
|
|
skip=None,
|
|
limit=None,
|
|
req_source='get_bulk',
|
|
transaction_id=None):
|
|
"""
|
|
Overriding the itoa_object get_bulk function.
|
|
The CTW object type is not intended to be 'securable' but we can still enforce RBAC filtering.
|
|
|
|
Note: The `fields` parameter isn't entirely accurate as certain fields are forced into the response
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type sort_key: string
|
|
@param sort_key: string defining keys to sort by
|
|
|
|
@type sort_dir: string
|
|
@param sort_dir: string defining direction for sorting - asc or desc
|
|
|
|
@type filter_data: dictionary
|
|
@param filter_data: json filter constructed to filter data. Follows mongodb syntax
|
|
|
|
@type fields: list
|
|
@param fields: list of fields to retrieve, fetches all fields if not specified
|
|
This isn't fully accurate as certain fields (like permissions, `object_type`, `linked_services`) are forced into
|
|
the response.
|
|
|
|
@type skip: number
|
|
@param skip: number of items to skip from the start
|
|
|
|
@type limit: number
|
|
@param limit: maximum number of items to return
|
|
|
|
@type req_source: string
|
|
@param req_source: identified source initiating the operation
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: unique identifier of a user transaction
|
|
|
|
@rtype: list of dictionary
|
|
@return: objects retrieved on success, throws exceptions on errors,
|
|
list of of CTW objects filtering based on rbac
|
|
"""
|
|
# Special hack to show only the ctws for which user has
|
|
# access in count section of CTW lister page
|
|
if fields is not None:
|
|
if 'linked_services' not in fields:
|
|
fields.append('linked_services')
|
|
if 'sec_grp_list' not in fields:
|
|
fields.append('sec_grp_list')
|
|
if 'user_timezone' not in fields:
|
|
fields.append('user_timezone')
|
|
if 'duration' not in fields:
|
|
fields.append('duration')
|
|
|
|
return self.do_rbac_filtering(owner,
|
|
sort_key,
|
|
sort_dir,
|
|
filter_data,
|
|
fields,
|
|
skip,
|
|
limit,
|
|
req_source,
|
|
transaction_id)
|
|
|
|
def update(self, owner, object_id, data, is_partial_data=False, dupname_tag=None, transaction_id=None):
|
|
"""
|
|
Overridden method for itoa_object.update().
|
|
|
|
Security check is additionally done here instead of do_additional_setup() due to is_partial_data support. We
|
|
don't want to use user-supplied fields, so we scrub out permissions fields before attaching our own. However,
|
|
is_partial_data support patches the objects *before* doing the save operation as KVStore (MongoDB) doesn't
|
|
support partial updates natively. This means that the proper permissions are added, the permissions get removed
|
|
in do_additional_setup(), and then the save happens and checks for permissions.
|
|
"""
|
|
self.validate_ctw_inputs(data , is_partial_data=is_partial_data)
|
|
ctw_object = self.get(owner, object_id, transaction_id=transaction_id)
|
|
if self.check_against_can_link_instead:
|
|
self.check_ctw_permissions(ctw_object, 'can_link')
|
|
else:
|
|
self.check_ctw_permissions(ctw_object, 'can_edit')
|
|
self.update_received_values_for_ctw_object_update_with_end_time(ctw_object, data)
|
|
return super(ItsiCustomThresholdWindows, self).update(owner, object_id, data, is_partial_data=is_partial_data,
|
|
dupname_tag=dupname_tag, transaction_id=transaction_id)
|
|
|
|
def can_be_deleted(self, owner, objects, raise_error=False, transaction_id=None):
|
|
"""
|
|
Overrides itoa_object.can_be_deleted().
|
|
|
|
@type owner: string
|
|
@param {string} owner: user which is performing this operation
|
|
@type objects: list
|
|
@param objects: the objects to validate for dependency
|
|
@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
|
|
"""
|
|
return [obj for obj in objects if obj['permissions']['can_delete']]
|
|
|
|
def delete(self, owner, ctw_id, req_source='unknown', transaction_id=None):
|
|
"""
|
|
Overridden method for itoa_object.delete(). Cleans up references that linked services/kpis have to ctw
|
|
before ctw delete happens
|
|
|
|
@type owner: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type ctw_id: string
|
|
@param ctw_id: Identifier of the object that was deleted
|
|
|
|
@type req_source: string
|
|
@param req_source: String identifying source of this request
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: String identifying source of this request
|
|
|
|
@return: results from the delete
|
|
"""
|
|
ctw_object = self.get(owner, ctw_id)
|
|
self.check_ctw_permissions(ctw_object, 'can_delete')
|
|
|
|
# Use parent delete function to go through normal operations (including identifying dependencies)
|
|
return super(ItsiCustomThresholdWindows, self).delete(owner,
|
|
ctw_id,
|
|
req_source=req_source,
|
|
transaction_id=transaction_id)
|
|
|
|
def delete_bulk(self, owner, filter_data=None, req_source='unknown', transaction_id=None):
|
|
"""
|
|
Overridden method for itoa_object.delete_bulk(). Cleans up references that linked services/kpis have to ctws
|
|
before ctw delete happens
|
|
|
|
@type owner: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type filter_data: string
|
|
@param filter_data: sql Filter condition with Ctw ids
|
|
|
|
@type req_source: string
|
|
@param req_source: String identifying source of this request
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: String identifying source of this request
|
|
|
|
@return: results from the delete
|
|
"""
|
|
|
|
# Use parent delete function to go through normal operations (including identifying dependencies)
|
|
results = []
|
|
delete_objects = self.get_bulk(owner, filter_data=filter_data, fields=self.delete_object_fields)
|
|
for to_be_deleted_object in delete_objects:
|
|
result = self.delete(owner, to_be_deleted_object.get('_key'),
|
|
req_source=req_source, transaction_id=transaction_id)
|
|
results.append(result)
|
|
return results
|
|
|
|
def do_rbac_filtering(self,
|
|
owner,
|
|
sort_key=None,
|
|
sort_dir=None,
|
|
filter_data=None,
|
|
fields=None,
|
|
skip=None,
|
|
limit=None,
|
|
req_source='do_rbac_filtering',
|
|
transaction_id=None):
|
|
"""
|
|
Filtering out the CTW objects which contains:
|
|
- services that current user doesn't have access to
|
|
How is this done?
|
|
1. Fetch all the CTW objects without limit and offset
|
|
2. Fetch the services visible to current user
|
|
3. Compare the security_groups_ids of objects in the CTW objects
|
|
against the services security groups user can see.
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type sort_key: string
|
|
@param sort_key: string defining keys to sort by
|
|
|
|
@type sort_dir: string
|
|
@param sort_dir: string defining direction for sorting - asc or desc
|
|
|
|
@type filter_data: dictionary
|
|
@param filter_data: json filter constructed to filter data. Follows mongodb syntax
|
|
|
|
@type fields: list
|
|
@param fields: list of fields to retrieve, fetches all fields if not specified
|
|
|
|
@type skip: number
|
|
@param skip: number of items to skip from the start
|
|
|
|
@type limit: number
|
|
@param limit: maximum number of items to return
|
|
|
|
@type req_source: string
|
|
@param req_source: identified source initiating the operation
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: unique identifier of a user transaction
|
|
|
|
@rtype: list of dictionary
|
|
@return: objects retrieved on success, throws exceptions on errors,
|
|
list of of CTW objects filtering based on rbac
|
|
"""
|
|
# get all the CTW objects by enforcing skip = None and limit = None to get them all
|
|
ctw_objects_full_list = ItoaObject.get_bulk(self,
|
|
owner,
|
|
sort_key,
|
|
sort_dir,
|
|
filter_data,
|
|
fields,
|
|
None,
|
|
None,
|
|
req_source,
|
|
transaction_id)
|
|
if len(ctw_objects_full_list) > 0:
|
|
services_by_ids, services_sec_grp_ids = self.fetch_service_data(owner, transaction_id)
|
|
ctw_objects_filtered_list = []
|
|
for ctw_object in ctw_objects_full_list:
|
|
# Set permissions on all objects returned
|
|
self.attach_ctw_permissions(owner, ctw_object, transaction_id, services_by_ids, services_sec_grp_ids)
|
|
if ctw_object['permissions']['can_view']:
|
|
ctw_objects_filtered_list.append(ctw_object)
|
|
skip = int(skip) if skip is not None else 0
|
|
limit = int(limit) if limit is not None else 0
|
|
if limit == 0 and skip == 0:
|
|
return ctw_objects_filtered_list
|
|
else:
|
|
return ctw_objects_filtered_list[skip:limit + skip]
|
|
return ctw_objects_full_list
|
|
|
|
def identify_dependencies(self, owner, objects, method, req_source='unknown',
|
|
transaction_id=None, skip_local_failure=False):
|
|
"""
|
|
Assess whether any refresh queue jobs needs to be created based on changes that are occurring
|
|
@param {string} owner: user performing this operation
|
|
@param {list} objects: list of objects being updated
|
|
@param {string} method: method name
|
|
@param {string} req_source: request source
|
|
@return: a tuple
|
|
{boolean} set to true/false if dependency update is required
|
|
{list} list - list of refresh job, each element has the following
|
|
change_type: <identifier of the change used to pick change handler>,
|
|
changed_object_key: <Array of changed objects' keys>,
|
|
changed_object_type: <string of the type of object>,
|
|
change_detail: <Dictionary containing additional information>:
|
|
service_kpi_mapping: <dictionary of services/KPIs affected and needing updates>,
|
|
method: <String representing the type of change to be performed>
|
|
METHODS USED (please update if modified):
|
|
- DELETE (CTW is being deleted)
|
|
- ADD (KPIs added and need to be linked to CTW)
|
|
- DISCONNECT (KPIs removed from CTW and need to be updated)
|
|
|
|
"""
|
|
refresh_jobs = []
|
|
is_refresh_required = False
|
|
if not is_valid_list(objects):
|
|
logger.error("%s resource did not passed valid object list:%s", req_source, objects)
|
|
return is_refresh_required, refresh_jobs
|
|
|
|
ctws_being_updated = []
|
|
services_to_update = {} # service_key: list of kpi ids
|
|
fields_filter = ['_key', 'linked_services']
|
|
|
|
persisted_custom_threshold_windows = self.get_persisted_objects_by_id(
|
|
owner,
|
|
object_ids=[ctw.get('_key') for ctw in objects],
|
|
req_source=req_source,
|
|
fields=fields_filter
|
|
)
|
|
if method == CustomThresholdWindowConstants.METHOD_DELETE:
|
|
for threshold_window in persisted_custom_threshold_windows:
|
|
ctws_being_updated.append(threshold_window.get('_key'))
|
|
linked_services = threshold_window.get('linked_services', [])
|
|
for service in linked_services:
|
|
service_key = service.get('service_id')
|
|
linked_kpi_ids_from_service = service.get('linked_kpi_ids', [])
|
|
if service_key not in services_to_update:
|
|
services_to_update[service_key] = set(linked_kpi_ids_from_service)
|
|
else:
|
|
services_to_update[service_key].update(linked_kpi_ids_from_service)
|
|
|
|
# convert back to a list when creating the refresh queue job
|
|
for service_key in services_to_update:
|
|
services_to_update[service_key] = list(services_to_update[service_key])
|
|
|
|
if len(ctws_being_updated) > 0 and services_to_update:
|
|
# For a deletion, we can create a giant CTW and remove all the CTWs from a KPI
|
|
refresh_jobs.append(
|
|
self.get_refresh_job_meta_data(
|
|
change_type='custom_threshold_window_update',
|
|
changed_object_key=list(ctws_being_updated), # Keys of custom threshold windows being updated
|
|
changed_object_type=self.object_type,
|
|
change_detail={
|
|
'method': CustomThresholdWindowConstants.METHOD_DELETE,
|
|
'service_kpi_mapping': services_to_update
|
|
},
|
|
transaction_id=transaction_id
|
|
))
|
|
else:
|
|
persisted_ctw_dict = dict((ctw['_key'], ctw) for ctw in persisted_custom_threshold_windows)
|
|
|
|
for ctw in objects:
|
|
# Newly created CTWs will not have a key yet at this point in the create call
|
|
if ctw.get('_key') is None:
|
|
ctw['_key'] = ITOAInterfaceUtils.generate_backend_key()
|
|
|
|
# For update operations, we create a job per CTW being updated so that each service/KPI is updated
|
|
# correctly/without wrong CTWs being added
|
|
persisted_ctw = persisted_ctw_dict.get(ctw.get('_key'))
|
|
|
|
if persisted_ctw is None or method == CRUDMethodTypes.METHOD_CREATE:
|
|
# This is a new CTW being created. Need to do associations with all the services/kpis
|
|
newly_linked_kpis = {service.get('service_id'): service.get('linked_kpi_ids')
|
|
for service in ctw.get('linked_services', [])}
|
|
former_linked_kpis = {}
|
|
else:
|
|
# Basic update to the CTW, which can include adding more services/kpis or potentially
|
|
# disconnecting KPIs
|
|
newly_linked_kpis, former_linked_kpis = self.determine_modified_kpis(ctw, persisted_ctw)
|
|
|
|
if len(newly_linked_kpis) > 0:
|
|
refresh_jobs.append(
|
|
self.get_refresh_job_meta_data(
|
|
change_type='custom_threshold_window_update',
|
|
changed_object_key=[ctw.get('_key', '')],
|
|
changed_object_type=self.object_type,
|
|
change_detail={
|
|
'method': CustomThresholdWindowConstants.METHOD_ADD,
|
|
'service_kpi_mapping': newly_linked_kpis
|
|
},
|
|
transaction_id=transaction_id
|
|
))
|
|
if len(former_linked_kpis) > 0:
|
|
refresh_jobs.append(
|
|
self.get_refresh_job_meta_data(
|
|
change_type='custom_threshold_window_update',
|
|
changed_object_key=[ctw.get('_key', '')],
|
|
changed_object_type=self.object_type,
|
|
change_detail={
|
|
'method': CustomThresholdWindowConstants.METHOD_DISCONNECT,
|
|
'service_kpi_mapping': former_linked_kpis
|
|
},
|
|
transaction_id=transaction_id
|
|
))
|
|
|
|
is_refresh_required = len(refresh_jobs) > 0
|
|
return is_refresh_required, refresh_jobs
|
|
|
|
def fetch_service_data(self, owner, transaction_id):
|
|
"""
|
|
Fetch data about services the owner has access to for CTW permissioning
|
|
|
|
:param owner: Splunk user to own object
|
|
:type owner: basestring
|
|
:param transaction_id: Transaction ID for logging
|
|
:type transaction_id: basestring
|
|
:return: Tuple of dict, list
|
|
"""
|
|
fields = ['_key', 'sec_grp', 'permissions', 'kpis._key']
|
|
# get the services which current_user_name has access to.
|
|
service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
services = service_interface.get_bulk(owner,
|
|
fields=fields,
|
|
req_source='get_bulk',
|
|
transaction_id=transaction_id)
|
|
|
|
services_by_ids = {}
|
|
services_sec_grp_ids = []
|
|
for service in services:
|
|
sec_grp = service['sec_grp']
|
|
if sec_grp not in services_sec_grp_ids:
|
|
services_sec_grp_ids.append(sec_grp)
|
|
if service['_key'] not in services_by_ids:
|
|
services_by_ids[service['_key']] = service
|
|
return services_by_ids, services_sec_grp_ids
|
|
|
|
def attach_ctw_permissions(self, owner, custom_threshold_window, transaction_id, services_by_ids={},
|
|
services_sec_grp_ids=['default_itsi_security_group']):
|
|
"""
|
|
Attach explicit permissions as fields to the CTW object.
|
|
|
|
:param owner: Splunk user to own object
|
|
:type owner: basestring
|
|
:param custom_threshold_window: CTW object
|
|
:type custom_threshold_window: dict
|
|
:param transaction_id: Transaction ID for logging
|
|
:type transaction_id: basestring
|
|
:param services_by_ids: Dictionary of services keyed by ID
|
|
:type services_by_ids: dict
|
|
:param services_sec_grp_ids: List of security groups that the user has access to
|
|
:type services_sec_grp_ids: list
|
|
"""
|
|
new_attached_permissions = {
|
|
'can_view': False,
|
|
'can_link': False,
|
|
'can_delete': False,
|
|
'can_edit': False,
|
|
'can_unlink': {},
|
|
'can_write': False
|
|
}
|
|
|
|
def set_all_can_permissions(boolean, permissions_dict):
|
|
for can_permission in CustomThresholdWindowConstants.CAN_PERMISSIONS:
|
|
if can_permission == 'can_unlink':
|
|
if can_permission in permissions_dict:
|
|
for kpi in permissions_dict[can_permission]:
|
|
permissions_dict[can_permission][kpi] = boolean
|
|
else:
|
|
permissions_dict[can_permission] = {}
|
|
else:
|
|
permissions_dict[can_permission] = boolean
|
|
|
|
def reset_existing_permissions():
|
|
for can_permission in CustomThresholdWindowConstants.CAN_PERMISSIONS:
|
|
custom_threshold_window.pop(can_permission, None)
|
|
if 'permissions' in custom_threshold_window:
|
|
for can_permission in CustomThresholdWindowConstants.CAN_PERMISSIONS:
|
|
if can_permission == 'can_unlink':
|
|
if can_permission in custom_threshold_window['permissions']:
|
|
for kpi in custom_threshold_window['permissions'][can_permission]:
|
|
custom_threshold_window['permissions'][can_permission][kpi] = False
|
|
else:
|
|
custom_threshold_window['permissions'][can_permission] = {}
|
|
else:
|
|
if can_permission in custom_threshold_window['permissions']:
|
|
custom_threshold_window['permissions'][can_permission] = False
|
|
else:
|
|
custom_threshold_window['permissions'] = new_attached_permissions.copy()
|
|
|
|
reset_existing_permissions()
|
|
|
|
# When internal processes like Modular Input are trying to save the CTW object (with nobody user), have
|
|
# the can_edit and can_delete flags set to True
|
|
if self.current_user_name == 'nobody':
|
|
set_all_can_permissions(True, custom_threshold_window['permissions'])
|
|
return
|
|
|
|
linked_services = custom_threshold_window.get('linked_services', [])
|
|
total_kpi_count = sum([len(linked_service['linked_kpi_ids']) for linked_service in linked_services])
|
|
# In the case when no association is between CTW and KPI, everyone can edit and see the CTW.
|
|
if len(linked_services) == 0:
|
|
set_all_can_permissions(True, new_attached_permissions)
|
|
|
|
ctw_object_sec_grp_list = custom_threshold_window.get('sec_grp_list', [])
|
|
# One service for which user have access to is enough to continue onto the next step
|
|
for ctw_object_sec_grp in ctw_object_sec_grp_list:
|
|
if ctw_object_sec_grp in services_sec_grp_ids:
|
|
new_attached_permissions['can_view'] = True
|
|
new_attached_permissions['can_link'] = True
|
|
new_attached_permissions['can_delete'] = True
|
|
break
|
|
|
|
fields = ['_key', 'sec_grp', 'permissions', 'kpis._key']
|
|
if new_attached_permissions['can_view'] or len(ctw_object_sec_grp_list) == 0:
|
|
service_interface = ItsiService(self.session_key, self.current_user_name)
|
|
is_edit_blocked = False
|
|
visible_linked_services = []
|
|
for linked_service in linked_services:
|
|
if len(linked_service) != 0:
|
|
# Determine 'can_edit'
|
|
service_id = linked_service.get('service_id')
|
|
try:
|
|
service = None
|
|
if services_by_ids:
|
|
if service_id in services_by_ids:
|
|
# Get the service from dict obj as we have fetched it earlier
|
|
# so no need to get it again.
|
|
service = services_by_ids[service_id]
|
|
|
|
else:
|
|
# TODO: It's possible that the 'simpler' single CTW case should be set up similarly
|
|
# to the multiple CTW case since we iterate over all services.
|
|
# User a get_bulk so that we can limit the fields fetched from the single service
|
|
service = service_interface.get_bulk(owner,
|
|
filter_data={'_key': service_id},
|
|
fields=fields,
|
|
req_source='CustomThresholdWindowGet',
|
|
transaction_id=transaction_id)[0]
|
|
# User can't fetch the service itself
|
|
# If service is None, it's either not fetchable by the current user or it may have been deleted.
|
|
# In either case, it should not affect the permissions of the CTW.
|
|
if not service:
|
|
is_edit_blocked = True
|
|
continue
|
|
# Determine 'can_edit' and, while you're at it, remove the services from CTW for which
|
|
# the user does not have permissions.
|
|
if 'permissions' in service:
|
|
if 'read' in service['permissions'] and not service['permissions']['read']:
|
|
continue
|
|
elif 'write' in service['permissions']:
|
|
visible_linked_services.append(linked_service)
|
|
user_can_write = False
|
|
if service['permissions']['write']:
|
|
# A single existing service allows further linking and unlinking
|
|
new_attached_permissions['can_link'] = True
|
|
user_can_write = True
|
|
else:
|
|
# For readable services, a single uneditable service blocks all editing
|
|
is_edit_blocked = True
|
|
for kpi in linked_service['linked_kpi_ids']:
|
|
if 'can_unlink' not in new_attached_permissions:
|
|
new_attached_permissions['can_unlink'] = {}
|
|
new_attached_permissions['can_unlink'][kpi] = user_can_write
|
|
except ItoaAccessDeniedError:
|
|
continue
|
|
if not is_edit_blocked:
|
|
new_attached_permissions['can_edit'] = True
|
|
|
|
# Replace CTW linked services with a list of services that the user can actually view
|
|
custom_threshold_window['linked_services'] = visible_linked_services
|
|
|
|
# State handling
|
|
if custom_threshold_window.get('status') in CustomThresholdWindowConstants.DELETEABLE_STATES:
|
|
new_attached_permissions['can_delete'] = new_attached_permissions.get('can_edit', False)
|
|
else:
|
|
new_attached_permissions['can_delete'] = False
|
|
|
|
# 'can_transition' is just 'can_edit' that ignores state-handling
|
|
if custom_threshold_window.get('status') in CustomThresholdWindowConstants.TRANSITIONABLE_STATES:
|
|
new_attached_permissions['can_transition'] = new_attached_permissions.get('can_edit', False)
|
|
else:
|
|
new_attached_permissions['can_transition'] = False
|
|
|
|
if custom_threshold_window.get('status') not in CustomThresholdWindowConstants.EDITABLE_STATES:
|
|
new_attached_permissions['can_edit'] = False
|
|
|
|
if custom_threshold_window.get('status') not in CustomThresholdWindowConstants.LINKABLE_STATES:
|
|
new_attached_permissions['can_link'] = False
|
|
for kpi in new_attached_permissions['can_unlink']:
|
|
new_attached_permissions['can_unlink'][kpi] = False
|
|
|
|
# Safety catches (lack of lower permissions blocks higher permissions)
|
|
# can_view -> can_link -> (can_transition) -> can_edit
|
|
# can_transition is separate because it needs to be a "historical" permission
|
|
if not new_attached_permissions['can_view']:
|
|
new_attached_permissions['can_link'] = False
|
|
for kpi in new_attached_permissions['can_unlink']:
|
|
new_attached_permissions['can_unlink'][kpi] = False
|
|
if not new_attached_permissions['can_link']:
|
|
new_attached_permissions['can_edit'] = False
|
|
for kpi in new_attached_permissions['can_unlink']:
|
|
new_attached_permissions['can_unlink'][kpi] = False
|
|
# If there is only one KPI in can_unlink, it should be False regardless because the user should not
|
|
# be able to remove the last KPI from a Custom threshold window
|
|
if len(new_attached_permissions['can_unlink']) == 1 and\
|
|
len(new_attached_permissions['can_unlink']) == total_kpi_count:
|
|
for only_kpi in new_attached_permissions['can_unlink']:
|
|
new_attached_permissions['can_unlink'][only_kpi] = False
|
|
|
|
custom_threshold_window['permissions'] = new_attached_permissions
|
|
|
|
@staticmethod
|
|
def clear_ctw_reference_from_kpi(ctw_id, kpi):
|
|
if kpi.get('linked_custom_threshold_windows'):
|
|
kpi['linked_custom_threshold_windows'].remove(ctw_id)
|
|
|
|
if kpi.get('active_custom_threshold_window') == ctw_id:
|
|
del kpi['active_custom_threshold_window']
|
|
|
|
"""
|
|
Miscellaneous helper methods
|
|
"""
|
|
|
|
def get_kpis_associated_to_ctw(self, custom_threshold_window):
|
|
linked_services = custom_threshold_window.get('linked_services', [])
|
|
kpis_associated_to_ctw = []
|
|
for linked_service in linked_services:
|
|
if len(linked_service) != 0:
|
|
kpis_in_service_object = linked_service.get('linked_kpi_ids', [])
|
|
if len(kpis_in_service_object) != 0:
|
|
kpis_associated_to_ctw.extend(kpis_in_service_object)
|
|
return kpis_associated_to_ctw
|
|
|
|
def calculate_end_time(self, custom_threshold_window):
|
|
"""
|
|
Assumes custom threshold window object is valid.
|
|
Calculates when the current custom threshold window should end
|
|
@param custom_threshold_window: custom threshold window object
|
|
@return: int representing the end time for a threshold window
|
|
"""
|
|
window_duration_hours = custom_threshold_window.get('duration', 24)
|
|
window_duration = window_duration_hours * 3600
|
|
normalize_num_field(custom_threshold_window, 'start_time')
|
|
window_start_time = custom_threshold_window.get('start_time')
|
|
return window_start_time + window_duration
|
|
|
|
def calculate_next_scheduled_time(
|
|
self, custom_threshold_window, use_custom_time=False, custom_time=datetime.now().timestamp()
|
|
):
|
|
"""
|
|
Assumes custom threshold window object is valid.
|
|
Checks to see if recurrent schedule is active. Otherwise, it'll check for a start_time
|
|
|
|
Calculates what the next_scheduled_time should be. If its a non-recurring CTW, then next_scheduled_time will be
|
|
set to an empty string. NOTE - future functions that use this field will need to check if empty string or int
|
|
@param use_custom_time: Bool that indicates whether a custom date time should be used. For testing purposes
|
|
mainly
|
|
@param custom_time: some custom time for calculation. Should only be used for testing purposes
|
|
@param custom_threshold_window: the custom threshold window itself
|
|
@return None - modification is made directly to the custom threshold window
|
|
"""
|
|
if custom_threshold_window.get('start_time') is None and custom_threshold_window.get('cron_schedule') is None:
|
|
self.raise_error_bad_validation(logger, _('Custom time window has no valid start time set.'))
|
|
|
|
if custom_threshold_window.get('recurrence', False):
|
|
# Calculate based off the cron schedule
|
|
threshold_cron_schedule = custom_threshold_window.get('cron_schedule')
|
|
try:
|
|
if use_custom_time:
|
|
logger.debug('calculate_next_scheduled_time is using a custom time for calculation.')
|
|
# This should only enter during a special scenario, usually testing
|
|
next_start_time = int(croniter(threshold_cron_schedule, custom_time).get_next())
|
|
else:
|
|
next_start_time, user_timezone = calculate_next_cron_time(custom_threshold_window)
|
|
logger.debug('Custom threshold window {} is recurring and determined next scheduled time '
|
|
'{} based on the timezone {}'.format(custom_threshold_window.get('_key'),
|
|
next_start_time,
|
|
str(user_timezone))
|
|
)
|
|
custom_threshold_window['next_scheduled_time'] = next_start_time
|
|
except ValueError:
|
|
self.raise_error_bad_validation(logger,
|
|
_('Cron schedule was set incorrectly in custom threshold window - '
|
|
'given {} as schedule.'.format(threshold_cron_schedule)))
|
|
else:
|
|
# If it is not a recurrence, then the next execution will just be the first execution (start time specified)
|
|
normalize_num_field(custom_threshold_window, 'start_time')
|
|
window_start_time = custom_threshold_window.get('start_time')
|
|
current_time = custom_time if use_custom_time else int(datetime.now().timestamp())
|
|
if current_time >= window_start_time:
|
|
custom_threshold_window['next_scheduled_time'] = ''
|
|
else:
|
|
custom_threshold_window['next_scheduled_time'] = window_start_time
|
|
return custom_threshold_window['next_scheduled_time']
|
|
|
|
def determine_modified_kpis(self, updated_ctw, existing_ctw):
|
|
"""
|
|
Helper method that checks to see if there are any new KPIs that were added to the CTW.
|
|
|
|
@type updated_ctw: dict
|
|
@param updated_ctw: The CTW that is being updated
|
|
@type existing_ctw: dict
|
|
@param existing_ctw: The CTW with the same key that exists in the kvstore currently (older version)
|
|
@rtype: dict
|
|
@returns: a dict of the service -> KPI ids that have been added for this specific CTW, which need to
|
|
go through the associate update handler
|
|
"""
|
|
new_service_kpis = {}
|
|
removed_service_kpis = {}
|
|
existing_linked_services = {service.get('service_id'): service.get('linked_kpi_ids', [])
|
|
for service in existing_ctw.get('linked_services', [])}
|
|
|
|
updated_linked_services = {service.get('service_id'): service.get('linked_kpi_ids', [])
|
|
for service in updated_ctw.get('linked_services', [])}
|
|
|
|
affected_service_ids = [*existing_linked_services] + [*updated_linked_services]
|
|
for service_id in set(affected_service_ids):
|
|
existing_service_kpis = set(existing_linked_services.get(service_id, []))
|
|
updated_service_kpis = set(updated_linked_services.get(service_id, []))
|
|
|
|
added_kpis = updated_service_kpis.difference(existing_service_kpis)
|
|
removed_kpis = existing_service_kpis.difference(updated_service_kpis)
|
|
if added_kpis:
|
|
new_service_kpis[service_id] = list(added_kpis)
|
|
if removed_kpis:
|
|
removed_service_kpis[service_id] = list(removed_kpis)
|
|
|
|
return new_service_kpis, removed_service_kpis
|
|
|
|
def update_kpi_objects(self, kpi_list, active_kpis, inactive_kpis):
|
|
"""
|
|
Updates the linked KPIs in Custom Threshold Window.
|
|
|
|
If a window becomes active, calculate the custom thresholds for each KPI affected and save to the KPI object.
|
|
If a window is ending, remove any custom threshold fields from the KPI object
|
|
|
|
@type: object
|
|
@param self: The self reference
|
|
|
|
@type: list
|
|
@param kpi_list: The list of kpis in service
|
|
|
|
@type: dict
|
|
@param active_kpis: Key value pair of the KPIs and the related active CTWs
|
|
|
|
@type: dict
|
|
@param inactive_kpis: Key value pair of the KPIs and the related non-operative CTWs
|
|
|
|
@rtype: list
|
|
@return: The list of updated KPIs
|
|
"""
|
|
updated_kpis = []
|
|
if len(active_kpis) > 0:
|
|
filter_data = {'$or': [{'_key': active_ctw_key} for active_ctw_key in active_kpis.values()]}
|
|
fields = ['_key', 'window_type', 'window_config_percentage']
|
|
active_ctws = self.get_bulk('nobody', filter_data=filter_data, fields=fields)
|
|
active_ctw_map = {ctw.get('_key'): ctw for ctw in active_ctws}
|
|
logger.debug('{} custom threshold windows have become active and fetched. Linked KPIs to be updated.'
|
|
.format(len(active_ctw_map)))
|
|
for kpi in kpi_list:
|
|
if kpi.get('_key', '') in active_kpis.keys():
|
|
ctw_key = active_kpis[kpi.get('_key', '')]
|
|
kpi['active_custom_threshold_window'] = ctw_key
|
|
linked_ctw = kpi.get('linked_custom_threshold_windows', [])
|
|
if ctw_key not in linked_ctw:
|
|
linked_ctw.append(ctw_key)
|
|
kpi['linked_custom_threshold_windows'] = linked_ctw
|
|
logger.debug('Calculating custom thresholds for kpi {} with key {}'
|
|
.format(kpi.get('title'), kpi.get('_key')))
|
|
kpi = calculate_custom_thresholds(kpi, active_ctw_map.get(ctw_key))
|
|
updated_kpis.append(kpi)
|
|
elif kpi.get('_key', '') in inactive_kpis.keys():
|
|
kpi['active_custom_threshold_window'] = ''
|
|
kpi.pop('aggregate_thresholds_custom', None)
|
|
if kpi['time_variate_thresholds'] is True:
|
|
kpi.pop('time_variate_thresholds_specification_custom', None)
|
|
updated_kpis.append(kpi)
|
|
else:
|
|
updated_kpis.append(kpi)
|
|
return updated_kpis
|
|
|
|
def update_service_object(self, active_services, active_kpis, inactive_services, inactive_kpis):
|
|
"""
|
|
Updates the services with the updated KPIs linked to Custom Threshold Windows
|
|
|
|
@type: object
|
|
@param self: The self reference
|
|
|
|
@type: list
|
|
@param active_services: List of the services affected active CTWs
|
|
|
|
@type: list
|
|
@param inactive_services: List of the services affected non-operative CTWs
|
|
|
|
@type: dict
|
|
@param active_kpis: Key value pair of the KPIs and the related active CTWs
|
|
|
|
@type: dict
|
|
@param inactive_kpis: Key value pair of the KPIs and the related non-operative CTWs
|
|
|
|
@rtype: None
|
|
@return: None
|
|
"""
|
|
all_updated_service_ids = active_services + inactive_services
|
|
if len(all_updated_service_ids) > 0:
|
|
filter_data = {'$or': [{'_key': object_id} for object_id in all_updated_service_ids]}
|
|
all_services = self.service_object.get_bulk('nobody', filter_data=filter_data)
|
|
updated_services = []
|
|
for service in all_services:
|
|
updated_kpis = self.update_kpi_objects(service['kpis'], active_kpis, inactive_kpis)
|
|
service['kpis'] = updated_kpis
|
|
updated_services.append(service)
|
|
self.service_object.save_batch('nobody', updated_services, False)
|
|
|
|
def update_ctw_object_status(
|
|
self,
|
|
to_be_active_windows,
|
|
to_be_ended_windows,
|
|
is_stopping=False,
|
|
stop_reason_message=''
|
|
):
|
|
"""
|
|
Updates the status of the Custom Threshold Window
|
|
|
|
|
|
@type: list
|
|
@param to_be_active_windows: List of CTWs to update to the "Active" state
|
|
|
|
@type: list
|
|
@param to_be_ended_windows: List of CTWs to update to the "Scheduled", "Stopped", or "Completed" state
|
|
|
|
@type: string
|
|
@param stop_reason_message: Reason for stopping CTW
|
|
|
|
@rtype: None
|
|
@return: None
|
|
"""
|
|
to_update_windows = to_be_active_windows + to_be_ended_windows
|
|
filter = {'$or': [{'_key': ctw['_key']} for ctw in to_update_windows]}
|
|
to_update_ctws = self.get_bulk(
|
|
'nobody',
|
|
# Note: Don't fetch 'linked_services' to prevent a bug where a status change will move the CTW into a
|
|
# non-editable state that KPIs can't be edited onto.
|
|
fields=['_key', 'start_time', 'end_time', 'next_scheduled_time', 'recurrence', 'status', 'cron_schedule',
|
|
'stop_reason', 'user_timezone', 'duration'],
|
|
filter_data=filter,
|
|
)
|
|
|
|
to_be_active_keys = [window['_key'] for window in to_be_active_windows]
|
|
to_be_ended_keys = [window['_key'] for window in to_be_ended_windows]
|
|
updated_ctw_objects = []
|
|
current_time = int(time.time())
|
|
for window in to_update_ctws:
|
|
# Status change for CTW to Scheduled -> Active
|
|
if window['_key'] in to_be_active_keys:
|
|
if window['status'] != CustomThresholdWindowConstants.STATUS_ACTIVE:
|
|
window['status'] = CustomThresholdWindowConstants.STATUS_ACTIVE
|
|
window['stop_reason'] = ''
|
|
# Calculate next_scheduled_time, start_time, end_time for recurring CTWs
|
|
if window['recurrence']:
|
|
time_now = get_current_utc_epoch()
|
|
next_scheduled_time = window.get('next_scheduled_time', 0)
|
|
if next_scheduled_time < time_now:
|
|
window['start_time'] = window['next_scheduled_time']
|
|
window['next_scheduled_time'] = self.calculate_next_scheduled_time(window)
|
|
else:
|
|
# For non-recurring reset next_scheduled_time
|
|
window['next_scheduled_time'] = ''
|
|
updated_ctw_objects.append(window)
|
|
# Status change for CTW to Active -> Completed or Active -> Stopped
|
|
elif window['_key'] in to_be_ended_keys:
|
|
if is_stopping:
|
|
if window['recurrence']:
|
|
end_status = CustomThresholdWindowConstants.STATUS_SCHEDULED
|
|
window['next_scheduled_time'] = self.calculate_next_scheduled_time(window)
|
|
window['start_time'] = window['next_scheduled_time']
|
|
window['end_time'] = self.calculate_end_time(window)
|
|
else:
|
|
end_status = CustomThresholdWindowConstants.STATUS_STOPPED
|
|
window['next_scheduled_time'] = ''
|
|
window['last_stopped_time'] = current_time
|
|
window['stop_reason'] = stop_reason_message
|
|
else:
|
|
end_status = CustomThresholdWindowConstants.STATUS_COMPLETED
|
|
window['status'] = end_status
|
|
updated_ctw_objects.append(window)
|
|
else:
|
|
logger.error('Unhandled custom threshold window reached in update_ctw_object_status() (%s)' % window)
|
|
|
|
# Save updated CTW after status change
|
|
if len(updated_ctw_objects) > 0:
|
|
self.save_batch('nobody', updated_ctw_objects, False, is_partial_data=True)
|
|
for window in updated_ctw_objects:
|
|
if window['status'] == CustomThresholdWindowConstants.STATUS_ACTIVE:
|
|
self.message_handler.post_or_update_message(
|
|
'ctw_%s' % window['_key'], 'info',
|
|
'Custom threshold window (%s) is now active on %s and will conclude on %s.' %
|
|
(window['title'], get_human_readable_time_str(current_time),
|
|
get_human_readable_time_str(window['end_time'])),
|
|
)
|
|
elif is_stopping:
|
|
self.message_handler.post_or_update_message(
|
|
'ctw_%s' % window['_key'], 'info',
|
|
'Custom threshold window (%s) has been manually stopped (%s to %s)' %
|
|
(window['title'], get_human_readable_time_str(window['start_time']),
|
|
get_human_readable_time_str(current_time)),
|
|
)
|
|
else:
|
|
self.message_handler.post_or_update_message(
|
|
'ctw_%s' % window['_key'], 'info',
|
|
'Custom threshold window (%s) has successfully completed (%s to %s)' %
|
|
(window['title'], get_human_readable_time_str(window['start_time']),
|
|
get_human_readable_time_str(current_time)),
|
|
)
|
|
|
|
"""
|
|
Delete handler methods
|
|
"""
|
|
@skip_validation_method
|
|
def disconnect_deleted_kpis_from_all_ctws(self, owner, kpi_id=None, transaction_id=None):
|
|
"""
|
|
This function is used to disassociate the deleted KPIs from a CTW objects
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type kpi_id: string
|
|
@param kpi_id: id of KPI object deleted
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: Transaction id
|
|
"""
|
|
def remove_kpi_from_linked_services(ctw_obj, kpi_id):
|
|
"""
|
|
Helper function to remove a KPI from linked_services
|
|
|
|
:type: dict
|
|
:param ctw_obj: A CTW object
|
|
|
|
:type: string
|
|
:param kpi_id: KPI ID to remove from the linked_services field
|
|
"""
|
|
linked_services = ctw_obj.get('linked_services', [])
|
|
for linked_service in linked_services:
|
|
kpis_in_service_object = linked_service.get('linked_kpi_ids', [])
|
|
if len(kpis_in_service_object) != 0:
|
|
kpis_in_service_object.remove(kpi_id)
|
|
if not kpis_in_service_object:
|
|
linked_services.remove(linked_service)
|
|
else:
|
|
linked_service['linked_kpi_ids'] = kpis_in_service_object
|
|
ctw_obj['linked_services'] = linked_services
|
|
|
|
# This is the proper syntax for multivalue fields in a KVStore query
|
|
filter_data = {'$or': [{'linked_services.linked_kpi_ids': kpi_id}]}
|
|
ctw_objects = ItoaObject.get_bulk(self, owner, filter_data=filter_data, req_source='get_bulk',
|
|
transaction_id=transaction_id)
|
|
for ctw_obj in ctw_objects:
|
|
kpis_for_threshold_window = self.get_kpis_associated_to_ctw(ctw_obj)
|
|
if kpi_id in kpis_for_threshold_window:
|
|
remove_kpi_from_linked_services(ctw_obj, kpi_id)
|
|
if not ctw_obj.get('linked_services'):
|
|
ctw_obj['status'] = CustomThresholdWindowConstants.STATUS_INVALID
|
|
ctw_obj['last_error'] = 'Custom threshold window invalidated by deletion of last linked KPI ' \
|
|
'%s' % kpi_id
|
|
self.update(owner, ctw_obj.get('_key'), ctw_obj, transaction_id=transaction_id)
|
|
logger.info('Successfully Disassociated the KPI from custom threshold window %s', ctw_obj.get('title'))
|
|
logger.info('Successfully Disassociated the KPI from all custom threshold windows')
|
|
|
|
@skip_validation_method
|
|
def disconnect_deleted_service_from_all_ctws(self, owner, deleted_service_ids=None, transaction_id=None):
|
|
"""
|
|
This function is used to disconnect deleted KPIs from CTW objects
|
|
|
|
@type: string
|
|
@param owner: Splunk user to own object
|
|
|
|
@type deleted_service_ids: list
|
|
@param deleted_service_ids: ids for services deleted
|
|
|
|
@type transaction_id: string
|
|
@param transaction_id: Transaction id
|
|
"""
|
|
def remove_services_from_linked_services(ctw_obj, service_ids_set):
|
|
"""
|
|
Helper function to remove services from linked_services
|
|
|
|
:type: dict
|
|
:param ctw_obj: A CTW object
|
|
|
|
:type: set of string
|
|
:param service_ids_set: Service IDs to remove from the linked_services field
|
|
|
|
:type: list of string
|
|
:return: List of removed service IDs
|
|
"""
|
|
rv = []
|
|
linked_services = ctw_obj.get('linked_services', [])
|
|
for linked_service in linked_services:
|
|
service_id = linked_service.get('service_id', [])
|
|
if service_id in service_ids_set:
|
|
linked_services.remove(linked_service)
|
|
rv.append(service_id)
|
|
ctw_obj['linked_services'] = linked_services
|
|
return rv
|
|
|
|
filter_data = {'$or': [{'linked_services.service_id': service_id} for service_id in deleted_service_ids]}
|
|
ctw_objects = ItoaObject.get_bulk(self, owner, filter_data=filter_data, req_source='get_bulk',
|
|
transaction_id=transaction_id)
|
|
deleted_service_ids_set = set(deleted_service_ids)
|
|
for ctw_obj in ctw_objects:
|
|
removed_service_ids = remove_services_from_linked_services(ctw_obj, deleted_service_ids_set)
|
|
if not ctw_obj.get('linked_services'):
|
|
ctw_obj['status'] = CustomThresholdWindowConstants.STATUS_INVALID
|
|
ctw_obj['last_error'] = 'Custom threshold window invalidated by deletion of last linked service(s) ' \
|
|
'%s' % ', '.join(removed_service_ids)
|
|
self.update(owner, ctw_obj.get('_key'), ctw_obj, transaction_id=transaction_id)
|
|
logger.info('Successfully Disassociated the service and KPI from custom threshold window %s',
|
|
ctw_obj.get('title'))
|
|
logger.info('Successfully Disassociated the service and KPIs from all custom threshold windows')
|