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.

390 lines
12 KiB

#
# 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,
)