From 73773dd7cf132b6ba335f6a70ee9db7225d89f0b Mon Sep 17 00:00:00 2001 From: Matt Anderson Date: Mon, 13 Feb 2023 20:42:27 +0000 Subject: [PATCH 1/2] Add Service.macros --- splunklib/client.py | 114 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 10 deletions(-) diff --git a/splunklib/client.py b/splunklib/client.py index 33156bb5..e2046d8f 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -102,6 +102,7 @@ PATH_JOBS = "search/jobs/" PATH_JOBS_V2 = "search/v2/jobs/" PATH_LOGGER = "/services/server/logger/" +PATH_MACROS = "configs/conf-macros/" PATH_MESSAGES = "messages/" PATH_MODULAR_INPUTS = "data/modular-inputs" PATH_ROLES = "authorization/roles/" @@ -674,6 +675,15 @@ def saved_searches(self): """ return SavedSearches(self) + @property + def macros(self): + """Returns the collection of macros. + + :return: A :class:`Macros` collection of :class:`Macro` + entities. + """ + return Macros(self) + @property def settings(self): """Returns the configuration settings for this instance of Splunk. @@ -774,11 +784,11 @@ def get_api_version(self, path): # Default to v1 if undefined in the path # For example, "/services/search/jobs" is using API v1 api_version = 1 - + versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path) if versionSearch: api_version = int(versionSearch.group(1)) - + return api_version def get(self, path_segment="", owner=None, app=None, sharing=None, **query): @@ -852,7 +862,7 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query): # - Fallback from v2+ to v1 if Splunk Version is < 9. # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)): # path = path.replace(PATH_JOBS_V2, PATH_JOBS) - + if api_version == 1: if isinstance(path, UrlEncoded): path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True) @@ -911,14 +921,14 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query): apps.get('nonexistant/path') # raises HTTPError s.logout() apps.get() # raises AuthenticationError - """ + """ if path_segment.startswith('/'): path = path_segment else: if not self.path.endswith('/') and path_segment != "": self.path = self.path + '/' path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing) - + # Get the API version from the path api_version = self.get_api_version(path) @@ -927,7 +937,7 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query): # - Fallback from v2+ to v1 if Splunk Version is < 9. # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)): # path = path.replace(PATH_JOBS_V2, PATH_JOBS) - + if api_version == 1: if isinstance(path, UrlEncoded): path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True) @@ -2827,7 +2837,7 @@ def events(self, **kwargs): :return: The ``InputStream`` IO handle to this job's events. """ kwargs['segmentation'] = kwargs.get('segmentation', 'none') - + # Search API v1(GET) and v2(POST) if self.service.disable_v2_api: return self.get("events", **kwargs).body @@ -2919,7 +2929,7 @@ def results(self, **query_params): :return: The ``InputStream`` IO handle to this job's results. """ query_params['segmentation'] = query_params.get('segmentation', 'none') - + # Search API v1(GET) and v2(POST) if self.service.disable_v2_api: return self.get("results", **query_params).body @@ -2964,7 +2974,7 @@ def preview(self, **query_params): :return: The ``InputStream`` IO handle to this job's preview results. """ query_params['segmentation'] = query_params.get('segmentation', 'none') - + # Search API v1(GET) and v2(POST) if self.service.disable_v2_api: return self.get("results_preview", **query_params).body @@ -3459,6 +3469,90 @@ def create(self, name, search, **kwargs): return Collection.create(self, name, search=search, **kwargs) +class Macro(Entity): + """This class represents a search macro.""" + def __init__(self, service, path, **kwargs): + Entity.__init__(self, service, path, **kwargs) + + @property + def args(self): + """Returns the macro arguments. + :return: The macro arguments. + :rtype: ``string`` + """ + return self._state.content.get('args', '') + + @property + def definition(self): + """Returns the macro definition. + :return: The macro definition. + :rtype: ``string`` + """ + return self._state.content.get('definition', '') + + @property + def errormsg(self): + """Returns the validation error message for the macro. + :return: The validation error message for the macro. + :rtype: ``string`` + """ + return self._state.content.get('errormsg', '') + + @property + def iseval(self): + """Returns the eval-based definition status of the macro. + :return: The iseval value for the macro. + :rtype: ``string`` + """ + return self._state.content.get('iseval', '0') + + def update(self, definition=None, **kwargs): + """Updates the server with any changes you've made to the current macro + along with any additional arguments you specify. + :param `definition`: The macro definition (optional). + :type definition: ``string`` + :param `kwargs`: Additional arguments (optional). Available parameters are: + 'disabled', 'iseval', 'validation', and 'errormsg'. + :type kwargs: ``dict`` + :return: The :class:`Macro`. + """ + # Updates to a macro *require* that the definition be + # passed, so we pass the current definition if a value wasn't + # provided by the caller. + if definition is None: definition = self.content.definition + Entity.update(self, definition=definition, **kwargs) + return self + + @property + def validation(self): + """Returns the validation expression for the macro. + :return: The validation expression for the macro. + :rtype: ``string`` + """ + return self._state.content.get('validation', '') + + +class Macros(Collection): + """This class represents a collection of macros. Retrieve this + collection using :meth:`Service.macros`.""" + def __init__(self, service): + Collection.__init__( + self, service, PATH_MACROS, item=Macro) + + def create(self, name, definition, **kwargs): + """ Creates a macro. + :param name: The name for the macro. + :type name: ``string`` + :param definition: The macro definition. + :type definition: ``string`` + :param kwargs: Additional arguments (optional). Available parameters are: + 'disabled', 'iseval', 'validation', and 'errormsg'. + :type kwargs: ``dict`` + :return: The :class:`Macros` collection. + """ + return Collection.create(self, name, definition=definition, **kwargs) + + class Settings(Entity): """This class represents configuration settings for a Splunk service. Retrieve this collection using :meth:`Service.settings`.""" @@ -3912,4 +4006,4 @@ def batch_save(self, *documents): data = json.dumps(documents) - return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) \ No newline at end of file + return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8')) From 788ab3ad67a44740dd3af7c13b1db71afea9b81b Mon Sep 17 00:00:00 2001 From: Matt Anderson Date: Mon, 13 Feb 2023 20:44:14 +0000 Subject: [PATCH 2/2] Add tests for macros --- tests/test_macro.py | 164 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100755 tests/test_macro.py diff --git a/tests/test_macro.py b/tests/test_macro.py new file mode 100755 index 00000000..25b72e4d --- /dev/null +++ b/tests/test_macro.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# +# Copyright 2011-2015 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. + +from __future__ import absolute_import +from tests import testlib +import logging + +import splunklib.client as client + +import pytest + +@pytest.mark.smoke +class TestMacro(testlib.SDKTestCase): + def setUp(self): + super(TestMacro, self).setUp() + macros = self.service.macros + logging.debug("Macros namespace: %s", macros.service.namespace) + self.macro_name = testlib.tmpname() + definition = '| eval test="123"' + self.macro = macros.create(self.macro_name, definition) + + def tearDown(self): + super(TestMacro, self).setUp() + for macro in self.service.macros: + if macro.name.startswith('delete-me'): + self.service.macros.delete(macro.name) + + def check_macro(self, macro): + self.check_entity(macro) + expected_fields = ['definition', + 'iseval', + 'args', + 'validation', + 'errormsg'] + for f in expected_fields: + macro[f] + is_eval = macro.iseval + self.assertTrue(is_eval == '1' or is_eval == '0') + + def test_create(self): + self.assertTrue(self.macro_name in self.service.macros) + self.check_macro(self.macro) + + def test_create_with_args(self): + macro_name = testlib.tmpname() + '(1)' + definition = '| eval value="$value$"' + kwargs = { + 'args': 'value', + 'validation': '$value$ > 10', + 'errormsg': 'value must be greater than 10' + } + macro = self.service.macros.create(macro_name, definition=definition, **kwargs) + self.assertTrue(macro_name in self.service.macros) + self.check_macro(macro) + self.assertEqual(macro.iseval, '0') + self.assertEqual(macro.args, kwargs.get('args')) + self.assertEqual(macro.validation, kwargs.get('validation')) + self.assertEqual(macro.errormsg, kwargs.get('errormsg')) + self.service.macros.delete(macro_name) + + def test_delete(self): + self.assertTrue(self.macro_name in self.service.macros) + self.service.macros.delete(self.macro_name) + self.assertFalse(self.macro_name in self.service.macros) + self.assertRaises(client.HTTPError, + self.macro.refresh) + + def test_update(self): + new_definition = '| eval updated="true"' + self.macro.update(definition=new_definition) + self.macro.refresh() + self.assertEqual(self.macro['definition'], new_definition) + + is_eval = testlib.to_bool(self.macro['iseval']) + self.macro.update(iseval=not is_eval) + self.macro.refresh() + self.assertEqual(testlib.to_bool(self.macro['iseval']), not is_eval) + + def test_cannot_update_name(self): + new_name = self.macro_name + '-alteration' + self.assertRaises(client.IllegalOperationException, + self.macro.update, name=new_name) + + def test_name_collision(self): + opts = self.opts.kwargs.copy() + opts['owner'] = '-' + opts['app'] = '-' + opts['sharing'] = 'user' + service = client.connect(**opts) + logging.debug("Namespace for collision testing: %s", service.namespace) + macros = service.macros + name = testlib.tmpname() + + dispatch1 = '| eval macro_one="1"' + dispatch2 = '| eval macro_two="2"' + namespace1 = client.namespace(app='search', sharing='app') + namespace2 = client.namespace(owner='admin', app='search', sharing='user') + new_macro2 = macros.create( + name, dispatch2, + namespace=namespace1) + new_macro1 = macros.create( + name, dispatch1, + namespace=namespace2) + + self.assertRaises(client.AmbiguousReferenceException, + macros.__getitem__, name) + macro1 = macros[name, namespace1] + self.check_macro(macro1) + macro1.update(**{'definition': '| eval number=1'}) + macro1.refresh() + self.assertEqual(macro1['definition'], '| eval number=1') + macro2 = macros[name, namespace2] + macro2.update(**{'definition': '| eval number=2'}) + macro2.refresh() + self.assertEqual(macro2['definition'], '| eval number=2') + self.check_macro(macro2) + + def test_no_equality(self): + self.assertRaises(client.IncomparableException, + self.macro.__eq__, self.macro) + + def test_acl(self): + self.assertEqual(self.macro.access["perms"], None) + self.macro.acl_update(sharing="app", owner="admin", **{"perms.read": "admin, nobody"}) + self.assertEqual(self.macro.access["owner"], "admin") + self.assertEqual(self.macro.access["sharing"], "app") + self.assertEqual(self.macro.access["perms"]["read"], ['admin', 'nobody']) + + def test_acl_fails_without_sharing(self): + self.assertRaisesRegex( + ValueError, + "Required argument 'sharing' is missing.", + self.macro.acl_update, + owner="admin", app="search", **{"perms.read": "admin, nobody"} + ) + + def test_acl_fails_without_owner(self): + self.assertRaisesRegex( + ValueError, + "Required argument 'owner' is missing.", + self.macro.acl_update, + sharing="app", app="search", **{"perms.read": "admin, nobody"} + ) + + +if __name__ == "__main__": + try: + import unittest2 as unittest + except ImportError: + import unittest + unittest.main()