# # Copyright 2025 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import copy import json from multiprocessing.pool import ThreadPool from splunklib.binding import HTTPError from ..rest_handler.handler import RestHandler from ..rest_handler.schema import RestSchema __all__ = [ "GlobalConfigError", "Configuration", "Inputs", "Configs", "Settings", ] class GlobalConfigError(Exception): pass class Configuration: """ Splunk Configuration Handler. """ FILTERS = ["eai:appName", "eai:acl", "eai:userName"] ENTITY_NAME = "name" SETTINGS = "settings" NOT_FOUND = "[404]: Not Found" def __init__(self, splunkd_client, schema): """ :param splunkd_client: SplunkRestClient :param schema: """ self._client = splunkd_client self._schema = schema def load(self, *args, **kwargs): """ Load all stored configuration for given schema. :param args: :param kwargs: :return: """ raise NotImplementedError() def save_stanza(self, item): """ Save configuration with type_name and configuration :param item: :return: error while save the configuration """ return self._save_configuration(item[0], item[1]) def save(self, payload): """ Save configuration. Return error while saving. It includes creating and updating. That is, it will try to update first, then create if NOT FOUND error occurs. :param payload: same format with return of ``load``. :return: Usage:: >>> from splunktaucclib.global_config import GlobalConfig >>> global_config = GlobalConfig() >>> payload = { >>> 'settings': [ >>> { >>> 'name': 'proxy', >>> 'proxy_host': '1.2.3.4', >>> 'proxy_port': '5678', >>> }, >>> { >>> 'name': 'logging', >>> 'level': 'DEBUG', >>> } >>> ] >>> } >>> global_config.settings.save(payload) """ # expand the payload to task_list task_list = [] for type_name, configurations in payload.items(): task_list.extend( [(type_name, configuration) for configuration in configurations] ) task_len = len(task_list) # return empty error list if task list is empty if not task_list: return [] task_len = min(8, task_len) pool = ThreadPool(processes=task_len) errors = pool.map(self.save_stanza, task_list) pool.close() pool.join() return errors @property def internal_schema(self): """ Get the schema for inputs, configs and settings :return: """ return self._schema.inputs + self._schema.configs + self._schema.settings def _save_configuration(self, type_name, configuration): schema = self._search_configuration_schema( type_name, configuration[self.ENTITY_NAME], ) configuration = copy.copy(configuration) self._dump_multiple_select(configuration, schema) # update try: self._update(type_name, copy.copy(configuration)) except HTTPError as exc: if self.NOT_FOUND in str(exc): # not exists, go to create pass else: return exc except Exception as exc: return exc else: return None # create try: self._create(type_name, configuration) except Exception as exc: return exc else: return None def _create(self, type_name, configuration): self._save_endpoint( type_name, configuration, ) def _update(self, type_name, configuration): name = configuration[self.ENTITY_NAME] del configuration[self.ENTITY_NAME] self._save_endpoint( type_name, configuration, name=name, ) @classmethod def _filter_fields(cls, entity): for (k, v) in list(entity.items()): if k in cls.FILTERS: del entity[k] def _load_endpoint(self, name, schema): query = { "output_mode": "json", "count": "0", "--cred--": "1", } response = self._client.get( RestHandler.path_segment(self._endpoint_path(name)), **query ) body = response.body.read() cont = json.loads(body) entities = [] for entry in cont["entry"]: entity = entry["content"] entity[self.ENTITY_NAME] = entry["name"] self._load_multiple_select(entity, schema) entities.append(entity) return entities def _save_endpoint(self, endpoint, content, name=None): endpoint = self._endpoint_path(endpoint) self._client.post(RestHandler.path_segment(endpoint, name=name), **content) @classmethod def _load_multiple_select(cls, entity, schema): for field in schema: field_type = field.get("type") value = entity.get(field["field"]) if field_type != "multipleSelect" or not value: continue delimiter = field["options"]["delimiter"] entity[field["field"]] = value.split(delimiter) @classmethod def _dump_multiple_select(cls, entity, schema): for field in schema: field_type = field.get("type") value = entity.get(field["field"]) if field_type != "multipleSelect" or not value: continue if not isinstance(value, list): continue delimiter = field["options"]["delimiter"] entity[field["field"]] = delimiter.join(value) def _endpoint_path(self, name): return "{admin_match}/{endpoint_name}".format( admin_match=self._schema.admin_match, endpoint_name=RestSchema.endpoint_name(name, self._schema.namespace), ) def _search_configuration_schema(self, type_name, configuration_name): for item in self.internal_schema: # add support for settings schema if item["name"] == type_name or ( type_name == self.SETTINGS and item["name"] == configuration_name ): return item["entity"] else: raise GlobalConfigError( "Schema Not Found for Configuration, " "configuration_type={configuration_type}, " "configuration_name={configuration_name}".format( configuration_type=type_name, configuration_name=configuration_name, ), ) class Inputs(Configuration): def __init__(self, splunkd_client, schema): super().__init__(splunkd_client, schema) self._splunkd_client = splunkd_client self._schema = schema self._references = None def load(self, input_type=None): """ :param input_type: :return: Usage:: >>> from splunktaucclib.global_config import GlobalConfig >>> global_config = GlobalConfig() >>> inputs = global_config.inputs.load() """ # move configs read operation out of init method if not self._references: self._references = Configs(self._splunkd_client, self._schema).load() inputs = {} for input_item in self.internal_schema: if input_type is None or input_item["name"] == input_type: input_entities = self._load_endpoint( input_item["name"], input_item["entity"] ) # filter unused fields in response for input_entity in input_entities: self._filter_fields(input_entity) # expand referenced entity self._reference( input_entities, input_item, self._references, ) inputs[input_item["name"]] = input_entities return inputs @property def internal_schema(self): return self._schema.inputs @classmethod def _reference(cls, input_entities, input_item, configs): for input_entity in input_entities: cls._input_reference( input_item["name"], input_entity, input_item["entity"], configs ) @classmethod def _input_reference(cls, input_type, input_entity, input_schema, configs): for field in input_schema: options = field.get("options", {}) config_type = options.get("referenceName") config_name = input_entity.get(field["field"]) if not config_type or not config_name: continue for config in configs.get(config_type, []): if config["name"] == config_name: input_entity[field["field"]] = config break else: raise GlobalConfigError( "Config Not Found for Input, " "input_type={input_type}, " "input_name={input_name}, " "config_type={config_type}, " "config_name={config_name}".format( input_type=input_type, input_name=input_entity["name"], config_type=config_type, config_name=config_name, ) ) class Configs(Configuration): def load(self, config_type=None): """ :param config_type: :return: Usage:: >>> from splunktaucclib.global_config import GlobalConfig >>> global_config = GlobalConfig() >>> configs = global_config.configs.load() """ configs = {} for config in self.internal_schema: if config_type is None or config["name"] == config_type: config_entities = self._load_endpoint(config["name"], config["entity"]) for config_entity in config_entities: self._filter_fields(config_entity) configs[config["name"]] = config_entities return configs @property def internal_schema(self): return self._schema.configs class Settings(Configuration): TYPE_NAME = "settings" def load(self): """ :return: Usage:: >>> from splunktaucclib.global_config import GlobalConfig >>> global_config = GlobalConfig() >>> settings = global_config.settings.load() """ settings = [] for setting in self.internal_schema: setting_entity = self._load_endpoint( "settings/%s" % setting["name"], setting["entity"] ) self._load_multiple_select(setting_entity[0], setting["entity"]) entity = setting_entity[0] self._filter_fields(entity) settings.append(entity) return {Settings.TYPE_NAME: settings} @property def internal_schema(self): return self._schema.settings def _search_configuration_schema(self, type_name, configuration_name): return super()._search_configuration_schema( configuration_name, configuration_name, )