# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved. import json import re import splunk.rest as rest from ITOA.itoa_common import get_conf_data, normalize_bool_flag, convert_to_bytes from ITOA.saved_search_utility import SavedSearch from itsi.upgrade.constants import CONF_COLLECTIONS, CONF_TRANSFORMS, OP_SET_EQUAL, OP_BOOL_EQUAL, OP_GREATER_EQUAL, \ OP_EQUAL, EXPECTED_COLLECTIONS, COLLECTION_STATS_URL, KVSTORE_COLLECTION_SIZE_LIMIT_DEFAULT, CONF_COMMANDS, \ CONF_SAVEDSEARCHES from migration.migration_precheck import MigrationPreCheck class EAPrechecks(MigrationPreCheck): def __init__(self, session_key, logger, pre_checks, skip_pre_checks): MigrationPreCheck.__init__(self, session_key, logger, pre_checks, skip_pre_checks) def precheck_RECHK(self): ''' check if rules engine is enabled/disabled and populate precheck_results list accordingly @rtype: list @return: list of dictionaries that has following key/values: {bool} if the precheck passed {string} corresponding recommendation ''' self.rules_engine_saved_search = SavedSearch.get_search( self.session_key, 'itsi_event_grouping') if int(self.rules_engine_saved_search['disabled']) == 1: return [self._make_result(False, 'Rules Engine is not running', 'Warn')] return [self._make_result(True, 'Rules Engine is up and running', 'Success' )] def precheck_KVSIZECHK(self): ''' check if kv store size checks limits are not exceeded and populate precheck_results list accordingly @rtype: list @return: list of dictionaries that has following key/values: {bool} if the precheck passed {string} corresponding recommendation ''' results = self.get_collection_sizes() if isinstance(results, tuple): return [self._make_result(results[0], results[1], 'Error')] kv_store_collection_size_limit = KVSTORE_COLLECTION_SIZE_LIMIT_DEFAULT stanzas = get_conf_data(self.session_key, 'itsi_event_management', app='SA-ITOA') if 'precheck' in stanzas and 'kv_store_collection_size_limit' in stanzas['precheck']: kv_store_collection_size_limit = int(stanzas['precheck']['kv_store_collection_size_limit']) self.logger.info('Got kv_store_collection_size_limit=%s from itsi_event_management.conf' % kv_store_collection_size_limit) ret_list = [] collection_list = [] for each_result in results: self.logger.info('Collection %s for app %s has %s objects.' % (each_result['collection'], each_result['app'], each_result['count'])) if int(each_result['count']) >= kv_store_collection_size_limit: ret_list.append(self._make_result( False, 'The %s collection contains %s objects which exceeds the KV store size limit of %s objects.' % (each_result['collection'], each_result['count'], kv_store_collection_size_limit), 'Warn')) if each_result['collection'] in EXPECTED_COLLECTIONS: collection_list.append(each_result['collection']) if len(collection_list) < len(EXPECTED_COLLECTIONS): ret_list.append('Some KVStore collection(s) are missing: %s' % filter(lambda x: x not in EXPECTED_COLLECTIONS, collection_list)) if len(ret_list) == 0: return [self._make_result(True, 'KV store size checks are within limits', 'Success')] return ret_list def get_collection_sizes(self): ''' Get collection sizes @rtype: list or tuple @return: list of collections and sizes in the system if success, otherwise return tuple for any failures. ''' try: response, content = rest.simpleRequest(COLLECTION_STATS_URL, sessionKey=self.session_key, method='GET', getargs={'output_mode': 'json'}, rawResult=True) except Exception as e: self.logger.exception(e) return False, "Failed to fetch KV store statistics." if response.status != 200: message = 'Error retrieving KV store statistics. Response: %s, Content: %s' % (response, content) self.logger.error(message) return False, message data = json.loads(content) if 'entry' not in data or len(data['entry']) < 1: message = 'Cannot find entry in the collection statistics.' self.logger.error(message) return False, message ret_list = [] itsi_collection = ['SA-ITOA', 'SA-ITSI-ATAD', 'SA-ITSI-MetricAD', 'SA-UserAccess'] try: for d in data['entry'][0]['content']['data']: item = json.loads(d) ns = item['ns'].split('.') if ns[0] in itsi_collection: ret_list.append({ 'app': ns[0], 'collection': ns[1], 'count': int(item['count']) }) except Exception as e: message = 'Cannot load collection entry from response: %s' % e.message self.logger.error(message) return False, message return ret_list def precheck_EACONFIG(self): ''' check if various EA config checks limits are not exceeded and populate precheck_results list accordingly @rtype: list @return: list of dictionaries that has following key/values: {bool} if the precheck passed {string} corresponding recommendation ''' ret_list = self.check_config('collections', CONF_COLLECTIONS) ret_list.extend(self.check_config('transforms', CONF_TRANSFORMS)) ret_list.extend(self.check_config('commands', CONF_COMMANDS)) ret_list.extend(self.check_config('savedsearches', CONF_SAVEDSEARCHES)) if len(ret_list) == 0: return [self._make_result(True, 'Event Analytics configurations are expected', 'Success')] return ret_list def criteria_to_string(self, criteria): ''' Convert criteria to human readable string @type criteria: dict @param criteria: the criteria to be converted @rtype: basestring @return: the human readable string ''' if criteria['op'] == OP_SET_EQUAL: return 'Expected values in the list are %s' % ', '.join(['"%s"' % s for s in criteria['value']]) return 'Expected value is "%s"' % criteria['value'] def check_config(self, conf_file, stanza_config, app='SA-ITOA'): ''' Check whether configuration in a config file satisfy the criteria @type conf_file: basestring @param conf_file: The config file name without .conf extention @type stanza_config: dict @param stanza_config: the configuration of the criterias for the stanzas to be checked. @type app: basestring @param app: the app context @rtype: list of dict @return: the dict of the results ''' ret_list = [] stanza_dict = get_conf_data(self.session_key, conf_file, app=app) for stanza in stanza_dict: if stanza not in stanza_config: continue params = stanza_dict.get(stanza) for param, check in stanza_config[stanza].items(): if param not in params: self.logger.error('Cannot find param %s in stanza %s in file %s' % (param, stanza, conf_file)) continue value = params.get(param) # extract number from a string if conf file is commands.conf # the expected string format is command.arg.1=-J-Xmx8192M tmp = re.search(r'[A-Za-z]*(\d+[KMGTkmgt])', value) if tmp: value = tmp.group(1) value = convert_to_bytes(value) self.logger.info('Verifying parameter %s with criteria %s on value %s (config file: %s, stanza: %s)' % (param, check, value, conf_file, stanza)) result = self.verify_value(check, value) if not result: message = 'Validation failed for parameter in %s.conf: \'%s\' = "%s" in the [%s] stanza. %s' %\ (conf_file, param, value, stanza, self.criteria_to_string(check)) self.logger.warn(message) ret_list.append(self._make_result(False, message, 'Warn')) return ret_list def verify_value(self, check_criteria, actual_value): """ Verify if the actual value match the check criteria @type check_criteria: dict @param check_criteria: the check criteria to pass @type actual_value: basestring @param actual_value: the actual value in string type @rtype: bool @return: True means pass, False means failed. """ if check_criteria['op'] == OP_EQUAL: return actual_value == check_criteria['value'] if check_criteria['op'] == OP_SET_EQUAL: return set([v.strip() for v in actual_value.split(',')]) == set(check_criteria['value']) if check_criteria['op'] == OP_GREATER_EQUAL: return actual_value >= check_criteria['value'] if check_criteria['op'] == OP_BOOL_EQUAL: return normalize_bool_flag(actual_value) == normalize_bool_flag(check_criteria['value'])