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.
341 lines
14 KiB
341 lines
14 KiB
# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
|
|
|
|
import itsi_py3
|
|
|
|
from ITOA.itoa_common import is_valid_str, normalize_num_field
|
|
from ITOA.setup_logging import getLogger, setup_logging
|
|
from ITOA.itoa_factory import instantiate_object
|
|
from itsi.itsi_time_block_utils import ItsiTimeBlockUtils, CRON_ELEMENT_TYPES
|
|
|
|
|
|
logger = getLogger()
|
|
|
|
|
|
def migrate_timezones(splunkd_session_key, tool_options):
|
|
"""
|
|
In addition to the above migration handlers, we need a manual tool for admins to apply timezone offsets
|
|
to objects that were configures in browser timezone which we cannot auto detect. This tool will be exposed
|
|
as a mode in existing kvstore_to_json tool. But keeping its implementation logic here owing to relevance
|
|
in this migration code
|
|
|
|
Performs timezone offset operations for mode 3 of the tool
|
|
|
|
@param splunkd_session_key: Session key for Splunkd operations
|
|
@param tool_options: options passed in from the tool
|
|
@return: None, output is written to stdout
|
|
"""
|
|
owner = 'nobody'
|
|
|
|
# Validate group options
|
|
object_type = tool_options.object_type
|
|
object_title = tool_options.object_title
|
|
is_get = tool_options.is_get
|
|
|
|
tz_logger = setup_logging(logger=logger, is_console_header=True)
|
|
|
|
object_title_filter = None
|
|
if is_valid_str(object_title):
|
|
object_title_filter = {'title': object_title}
|
|
|
|
try:
|
|
object_type_instance = instantiate_object(splunkd_session_key, owner, object_type, logger=tz_logger)
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
raise Exception('Specified object type "%s" is invalid.' % object_type)
|
|
|
|
objects = object_type_instance.get_bulk(owner, filter_data=object_title_filter)
|
|
|
|
if len(objects) < 1:
|
|
print('No objects matched request. Stopping here')
|
|
return
|
|
|
|
print('\n%s object(s) match request' % len(objects) if objects is not None else 0)
|
|
if is_get:
|
|
print('Retrieved requested object(s):\n' + str(objects))
|
|
else:
|
|
print('Applying timezone change on requested object(s): ' + str([data.get('title') for data in objects]))
|
|
object_attributes_map = {
|
|
'maintenance_calendar': ['start_time', 'end_time']
|
|
}
|
|
|
|
offset_in_sec = None
|
|
try:
|
|
offset_in_sec = float(tool_options.offset_in_sec)
|
|
except Exception:
|
|
pass # Will detect below and show meaningful error
|
|
if not isinstance(offset_in_sec, float):
|
|
raise Exception('Specified timezone offset to apply is invalid. Must pick a number.')
|
|
|
|
for json_data in objects:
|
|
if object_type == 'service':
|
|
apply_timezone_offset_for_service(json_data, int(offset_in_sec / 60))
|
|
elif object_type == 'kpi_threshold_template':
|
|
apply_timezone_offset_for_kpi_threshold_template(json_data, int(offset_in_sec / 60))
|
|
else:
|
|
for json_field in object_attributes_map[object_type]:
|
|
# Only convert offset if end_time is not set to "Indefinite" for maintenance calendars
|
|
|
|
if object_type == 'maintenance_calendar' and json_field == 'end_time' and \
|
|
_is_maintenance_end_time_indefinite(json_data):
|
|
continue
|
|
apply_timezone_offset(json_data, json_field, offset_in_sec)
|
|
|
|
object_type_instance.save_batch(owner, objects, True)
|
|
print('Timezone offset has been applied on the objects requested.\n')
|
|
|
|
|
|
def apply_timezone_offset(json_data, json_field, offset_in_sec):
|
|
"""
|
|
Utility method to apply a timezone offset to an epoch field
|
|
|
|
@type json_data: json dict
|
|
@param json_data: the payload to update
|
|
|
|
@type json_field: basestring
|
|
@param json_field: the name of the field in the payload containing epoch value to update
|
|
|
|
@type offset_in_sec: float
|
|
@param offset_in_sec: the seconds offset to apply to the epoch
|
|
|
|
@rtype: None
|
|
@return: None, the json payload is updated in place
|
|
"""
|
|
if not isinstance(json_data, dict):
|
|
message = '`json_data` is not a valid dictionary, found type %s. Cannot apply timezone offset.' % type(
|
|
json_data).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
if not isinstance(json_field, itsi_py3.string_type):
|
|
message = '`json_field` is not a valid string, found type %s. Cannot apply timezone offset.' % type(
|
|
json_field).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
if not (isinstance(offset_in_sec, float) or isinstance(offset_in_sec, int)):
|
|
message = '`offset_in_sec` is not a valid number, found type %s. Cannot apply timezone offset.' % type(
|
|
offset_in_sec).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
if json_field in json_data:
|
|
normalize_num_field(json_data, json_field, numclass=float)
|
|
json_data[json_field] += offset_in_sec
|
|
|
|
|
|
def apply_timezone_offset_for_service(service, offset_in_min):
|
|
"""
|
|
Given the number of hours needed to offset time_blocks, apply the change to all KPIs in a service
|
|
|
|
@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
|
|
|
|
@rtype: None
|
|
@return: None although service input reference will be updated
|
|
"""
|
|
# Compute the day/hour/minute change as needed for the time blocks based on the offset
|
|
if not (-1440 <= offset_in_min <= 1440):
|
|
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 ValueError(message)
|
|
|
|
if not isinstance(service, dict):
|
|
message = '`service` is not a valid dictionary, found type %s. Cannot apply timezone offset.' % type(service).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
if not isinstance(offset_in_min, int):
|
|
message = '`offset_in_min` is not a valid int, found type %s. Cannot apply timezone offset.' % type(offset_in_min).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
# Only migrate valid time blocks, ignore bad config
|
|
for kpi in service.get('kpis', []):
|
|
if not isinstance(kpi, dict):
|
|
logger.warn('KPI looks invalid, skipping timezone offset adjustment for KPI "%s"', kpi)
|
|
continue # Ignore
|
|
|
|
policy_spec = kpi.get('time_variate_thresholds_specification')
|
|
if not isinstance(policy_spec, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
policies = policy_spec.get('policies')
|
|
if not isinstance(policies, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
for policy_key, policy in policies.items():
|
|
if not isinstance(policy, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
time_blocks = policy.get('time_blocks')
|
|
if not isinstance(time_blocks, list):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
kpi['time_variate_thresholds_specification']['policies'][policy_key]['time_blocks'] = get_new_time_blocks(time_blocks, offset_in_min)
|
|
|
|
|
|
def apply_timezone_offset_for_kpi_threshold_template(kpi_threshold_template, offset_in_min):
|
|
"""
|
|
Given the number of hours needed to offset time_blocks, apply the change to the given KPI threshold template
|
|
|
|
@type kpi_threshold_template: object
|
|
@param kpi_threshold_template: KPI threshold template object instance to update
|
|
|
|
@type offset_in_min: int
|
|
@param offset_in_min: the # of hours of offset to apply
|
|
|
|
@rtype: None
|
|
@return: None although kpi_threshold_template input reference will be updated
|
|
"""
|
|
# Compute the day/hour/minute change as needed for the time blocks based on the offset
|
|
if not (-1440 <= offset_in_min <= 1440):
|
|
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 ValueError(message)
|
|
|
|
if not isinstance(kpi_threshold_template, dict):
|
|
message = '`kpi_threshold_template` is not a valid dictionary, found type %s. Cannot apply timezone offset.' % type(kpi_threshold_template).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
if not isinstance(offset_in_min, int):
|
|
message = '`offset_in_min` is not a valid int, found type %s. Cannot apply timezone offset.' % type(offset_in_min).__name__
|
|
logger.error(message)
|
|
raise TypeError(message)
|
|
|
|
policy_spec = kpi_threshold_template.get('time_variate_thresholds_specification')
|
|
if not isinstance(policy_spec, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
|
|
policies = policy_spec.get('policies')
|
|
if not isinstance(policies, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
|
|
for policy_key, policy in policies.items():
|
|
if not isinstance(policy, dict):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
time_blocks = policy.get('time_blocks')
|
|
if not isinstance(time_blocks, list):
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
kpi_threshold_template['time_variate_thresholds_specification']['policies'][policy_key]['time_blocks'] = get_new_time_blocks(time_blocks, offset_in_min)
|
|
|
|
|
|
def get_new_time_blocks(time_blocks, offset_in_min):
|
|
"""
|
|
Return a list of new time_blocks by manipulating current time_blocks using the offset_in_min value
|
|
|
|
@type time_blocks: list
|
|
@param time_blocks: time blocks list that needs to be updated
|
|
|
|
@type offset_in_min: int
|
|
@param offset_in_min: the # of hours of offset to apply
|
|
|
|
@rtype: list
|
|
@return: empty list if time_blocks is empty or else non-empty list with updated time blocks
|
|
"""
|
|
new_time_blocks = []
|
|
for time_block in time_blocks:
|
|
try:
|
|
# validate time block before applying offset
|
|
ItsiTimeBlockUtils.expand_time_block(time_block)
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
# expand time block into time blocks with single numbers only aka no ranges
|
|
expanded_time_blocks = ItsiTimeBlockUtils.expand_time_block_cron(time_block)
|
|
if not len(expanded_time_blocks) > 0:
|
|
logger.warn('KPI time block policy looks invalid, skipping timezone offset adjustment for policy')
|
|
continue # Ignore
|
|
|
|
shifted_time_blocks = []
|
|
for expanded_time_block in expanded_time_blocks:
|
|
split_time_block_cron = expanded_time_block[0].split(' ')
|
|
|
|
expanded_time_block_min = int(split_time_block_cron[CRON_ELEMENT_TYPES.index('minute')])
|
|
expanded_time_block_hour = int(split_time_block_cron[CRON_ELEMENT_TYPES.index('hour')])
|
|
expanded_time_block_day = int(split_time_block_cron[CRON_ELEMENT_TYPES.index('day_of_week')])
|
|
|
|
new_time_block_min = (expanded_time_block_min + offset_in_min) % 60
|
|
overflow_hours = (expanded_time_block_min + offset_in_min) // 60
|
|
new_time_block_hour = (expanded_time_block_hour + overflow_hours) % 24
|
|
overflow_days = (expanded_time_block_hour + overflow_hours) // 24
|
|
new_time_block_day = (expanded_time_block_day + overflow_days) % 7
|
|
|
|
new_time_block = [
|
|
' '.join([
|
|
str(new_time_block_min),
|
|
str(new_time_block_hour),
|
|
'*',
|
|
'*',
|
|
str(new_time_block_day)
|
|
]),
|
|
expanded_time_block[1]
|
|
]
|
|
shifted_time_blocks.append(new_time_block)
|
|
|
|
# collapse shifted time blocks into a single time block
|
|
list_of_days = [int(shifted_time_block[0].split(' ')[CRON_ELEMENT_TYPES.index('day_of_week')]) for shifted_time_block in shifted_time_blocks]
|
|
collapsed_list_of_days = ItsiTimeBlockUtils.convert_numbers_to_cron_element(list_of_days)
|
|
|
|
first_time_block = shifted_time_blocks[0]
|
|
first_split_time_block_cron = first_time_block[0].split(' ')
|
|
time_block_to_add = [
|
|
' '.join([
|
|
str(first_split_time_block_cron[0]),
|
|
str(first_split_time_block_cron[1]),
|
|
'*',
|
|
'*',
|
|
collapsed_list_of_days
|
|
]),
|
|
first_time_block[1]
|
|
]
|
|
|
|
new_time_blocks.append(time_block_to_add)
|
|
return new_time_blocks
|
|
|
|
|
|
"""
|
|
UI interprets indefinite end time for maintenance windows as JS Date(2038, 0, 18)
|
|
The epoch value for JS Date(2038, 0, 18) can vary for different timezones
|
|
The minimum value for indefinite maintenance window epoch will therefore be considered
|
|
JS Date(2038, 0, 18) - 24 hours (maximum possible timezone offset) = 2147414400 - 86400 = 2147328000
|
|
Backend will consider any epoch end time greater than 2147328000 as indefinite end time
|
|
"""
|
|
_maintenance_calendar_min_indefinite_end_time = 2147328000
|
|
|
|
|
|
def _is_maintenance_end_time_indefinite(maintenance_calendar_json):
|
|
"""
|
|
Helper method to detect if maintenance calendar is configured with indefinite end time
|
|
|
|
@type maintenance_calendar_json: JSON dict
|
|
@param maintenance_calendar_json: JSON payload of maintenance configuration
|
|
|
|
@rtype: boolean
|
|
@return: True if yes, False if no
|
|
"""
|
|
if not isinstance(maintenance_calendar_json, dict):
|
|
return False
|
|
|
|
end_time = maintenance_calendar_json.get('end_time')
|
|
if isinstance(end_time, str) or isinstance(end_time, int):
|
|
try:
|
|
end_time = float(end_time)
|
|
except (TypeError, ValueError):
|
|
# Ignore any conversion failures
|
|
pass
|
|
return isinstance(end_time, float) and end_time >= _maintenance_calendar_min_indefinite_end_time
|