From f18f37db4cba59c721a5b74a1290e172964a14da Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Tue, 1 May 2012 10:15:54 -0700 Subject: [PATCH 1/7] Fixed a couple typos in comments of binding.py --- splunklib/binding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splunklib/binding.py b/splunklib/binding.py index 2cb997f4a..8052b1d66 100644 --- a/splunklib/binding.py +++ b/splunklib/binding.py @@ -47,7 +47,7 @@ # these three values and the library will reconcile the triple, overriding the # provided values as appropriate. # -# Finally, if no namspacing is specified the library will make use of the +# Finally, if no namespacing is specified the library will make use of the # `/services` branch of the REST API which provides a namespaced view of # Splunk resources equivelent to using owner={currentUser} and app={defaultApp} # @@ -296,7 +296,7 @@ def __init__(self, response): self.body = body # -# The HTTP interface used by the Splunk binding layer abstracts the unerlying +# The HTTP interface used by the Splunk binding layer abstracts the underlying # HTTP library using request & response 'messages' which are implemented as # dictionaries with the following structure: # From 7c8ecefd07a171fb93089a298138b8b78838ddec Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Tue, 1 May 2012 10:16:55 -0700 Subject: [PATCH 2/7] Improved test running. The tests can now be run in place, without installing the library. I switched to autodiscovery of tests in runtests.py, and added comments in tests/README.md about how to run the tests with py.test if you have it installed. --- tests/.coveragerc | 4 ++-- tests/README.md | 18 ++++++++++++++---- tests/runtests.py | 26 ++------------------------ tests/test_app.py | 4 ++-- tests/test_binding.py | 4 ++-- tests/test_collection.py | 4 ++-- tests/test_conf.py | 4 ++-- tests/test_data.py | 4 ++-- tests/test_event_type.py | 4 ++-- tests/test_fired_alert.py | 4 ++-- tests/test_index.py | 4 ++-- tests/test_input.py | 4 ++-- tests/test_job.py | 4 ++-- tests/test_logger.py | 4 ++-- tests/test_message.py | 4 ++-- tests/test_role.py | 4 ++-- tests/test_saved_search.py | 4 ++-- tests/test_service.py | 5 +++-- tests/test_user.py | 4 ++-- tests/testlib.py | 4 ++++ 20 files changed, 55 insertions(+), 62 deletions(-) diff --git a/tests/.coveragerc b/tests/.coveragerc index b4920f583..905d275bf 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -2,10 +2,10 @@ [run] branch = True -data_file = ../tests/.coverage +data_file = .coverage source = - splunk + ../splunklib ../examples ../tests ../utils diff --git a/tests/README.md b/tests/README.md index b4f18993a..1b961cd2c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -10,12 +10,12 @@ in `test_examples.py`. There are no dependencies to run the tests. You can simply execute: cd tests - python runtests.py + python -m unittest discover -or: +or, if you have py.test installed, cd tests - ./runtests.py + py.test ## Code Coverage @@ -39,7 +39,17 @@ as follows: coverage combine coverage report -Should you want to get an HTML report: +If you are using py.test, youcan replace + + coverage run runtests.py + +with + + coverage run `which py.test` + +which provides better reporting, and lets you specify any additional +options, such as a particular file to test. Should you want to get an +HTML report: coverage html diff --git a/tests/runtests.py b/tests/runtests.py index e085760e9..9a4ed5d8f 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -18,32 +18,10 @@ import os import sys +import unittest # Set up the environment for coverage, even though it might not get used os.environ["COVERAGE_PROCESS_START"] = "../tests/.coveragerc" -files = [ - "test_module.py", - "test_data.py", - "test_binding.py", - "test_collection.py", - "test_app.py", - "test_conf.py", - "test_event_type.py", - "test_fired_alert.py", - "test_index.py", - "test_input.py", - "test_job.py", - "test_logger.py", - "test_message.py", - "test_role.py", - "test_user.py", - "test_saved_search.py", - "test_service.py", - "test_examples.py", -] +unittest.TextTestRunner().run(unittest.TestLoader().discover('.')) -for file in files: - sys.stdout.write("Running: %s " % file) - sys.stdout.flush() - os.system("python %s" % file) diff --git a/tests/test_app.py b/tests/test_app.py index 191fd1015..5bd177ca3 100755 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_app(self, app): self.check_entity(app) diff --git a/tests/test_binding.py b/tests/test_binding.py index 9b7c2ebf8..1ca7b143e 100755 --- a/tests/test_binding.py +++ b/tests/test_binding.py @@ -19,12 +19,12 @@ import uuid from xml.etree.ElementTree import XML +import testlib + import splunklib.binding as binding from splunklib.binding import HTTPError import splunklib.data as data -import testlib - # splunkd endpoint paths PATH_USERS = "authentication/users/" diff --git a/tests/test_collection.py b/tests/test_collection.py index 6cad02595..ac53c8b89 100755 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): # Verify that the given collections interface behaves as expected def check_collection(self, collection): diff --git a/tests/test_conf.py b/tests/test_conf.py index 2881c18dd..49aeb5021 100755 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def test_read(self): service = client.connect(**self.opts.kwargs) diff --git a/tests/test_data.py b/tests/test_data.py index 7012c21d0..6330a6d8f 100755 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -16,10 +16,10 @@ from os import path -import splunklib.data as data - import testlib +import splunklib.data as data + class TestCase(testlib.TestCase): def test_elems(self): result = data.load("") diff --git a/tests/test_event_type.py b/tests/test_event_type.py index 52871262b..bc73cfd39 100755 --- a/tests/test_event_type.py +++ b/tests/test_event_type.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_event_type(self, event_type): self.check_entity(event_type) diff --git a/tests/test_fired_alert.py b/tests/test_fired_alert.py index 38d1b817a..4f490c6cd 100755 --- a/tests/test_fired_alert.py +++ b/tests/test_fired_alert.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + def event_count(index): return int(index.content.totalEventCount) diff --git a/tests/test_index.py b/tests/test_index.py index 52a8625c6..bfe7a4d70 100755 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -16,10 +16,10 @@ from os import path -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_index(self, index): self.check_entity(index) diff --git a/tests/test_input.py b/tests/test_input.py index 82283e5eb..183260154 100755 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_input(self, entity): self.check_entity(entity) diff --git a/tests/test_job.py b/tests/test_job.py index 562e9b661..439e19dff 100755 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -16,11 +16,11 @@ from time import sleep +import testlib + import splunklib.client as client import splunklib.results as results -import testlib - class TestCase(testlib.TestCase): # UNDONE: Shouldn't the following assert something on exit? def check_properties(self, job, properties, secs = 10): diff --git a/tests/test_logger.py b/tests/test_logger.py index 7570b1b91..9e74de55d 100755 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + LEVELS = ["INFO", "WARN", "ERROR", "DEBUG", "CRIT"] class TestCase(testlib.TestCase): diff --git a/tests/test_message.py b/tests/test_message.py index be8db9f8e..e22372b3a 100755 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_message(self, message): self.check_entity(message) diff --git a/tests/test_role.py b/tests/test_role.py index ff2655207..ae5f7e57e 100755 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_role(self, role): self.check_entity(role) diff --git a/tests/test_saved_search.py b/tests/test_saved_search.py index 8eb62234c..58193db2b 100755 --- a/tests/test_saved_search.py +++ b/tests/test_saved_search.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_saved_search(self, saved_search): self.check_entity(saved_search) diff --git a/tests/test_service.py b/tests/test_service.py index a3ab90bf2..b73f9c0ec 100755 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -14,11 +14,12 @@ # License for the specific language governing permissions and limitations # under the License. +import testlib + + import splunklib.client as client from splunklib.binding import HTTPError -import testlib - class TestCase(testlib.TestCase): def test_capabilities(self): service = client.connect(**self.opts.kwargs) diff --git a/tests/test_user.py b/tests/test_user.py index c9841e5bd..76f453f40 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. -import splunklib.client as client - import testlib +import splunklib.client as client + class TestCase(testlib.TestCase): def check_user(self, user): self.check_entity(user) diff --git a/tests/testlib.py b/tests/testlib.py index 725c1d878..a56d49483 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -21,6 +21,10 @@ from time import sleep import unittest +# Run the test suite on the SDK without installing it. +sys.path.insert(0, '../') +sys.path.insert(0, '../examples') + from utils import parse def delete_app(service, name): From fc0d930e9702e5c50b167379595f72649605c2c0 Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Tue, 1 May 2012 11:01:32 -0700 Subject: [PATCH 3/7] test_examples working; updated test suite slightly. Had to add sys.path.insert(0, '../') to the top of examples/**/*.py. test_examples.py runs subprocesses with CWD=../examples. runtests.py has been modified to autodiscover tests. --- examples/async/async.py | 1 + examples/binding1.py | 1 + examples/conf.py | 1 + examples/event_types.py | 1 + examples/fired_alerts.py | 1 + examples/follow.py | 1 + examples/handlers/handler_certs.py | 1 + examples/handlers/handler_debug.py | 1 + examples/handlers/handler_proxy.py | 1 + examples/handlers/handler_urllib2.py | 1 + examples/index.py | 1 + examples/info.py | 1 + examples/inputs.py | 1 + examples/job.py | 1 + examples/loggers.py | 1 + examples/oneshot.py | 1 + examples/saved_searches.py | 1 + examples/search.py | 1 + examples/spcmd.py | 1 + examples/spurl.py | 1 + examples/submit.py | 1 + examples/upload.py | 2 +- tests/runtests.py | 1 + tests/test_examples.py | 6 +++--- 24 files changed, 26 insertions(+), 4 deletions(-) diff --git a/examples/async/async.py b/examples/async/async.py index b2ac56d10..f97a51fc2 100755 --- a/examples/async/async.py +++ b/examples/async/async.py @@ -23,6 +23,7 @@ import sys, datetime import urllib from time import sleep +sys.path.insert(0, '../') import splunklib.binding as binding import splunklib.client as client diff --git a/examples/binding1.py b/examples/binding1.py index b9b92e0e6..8e0d4240d 100755 --- a/examples/binding1.py +++ b/examples/binding1.py @@ -20,6 +20,7 @@ entities and 'method-like' endpoints.""" import sys +sys.path.insert(0, '../') from splunklib.binding import connect diff --git a/examples/conf.py b/examples/conf.py index 8e6270d10..196f6a80c 100755 --- a/examples/conf.py +++ b/examples/conf.py @@ -17,6 +17,7 @@ """Create, delete or list stanza information from/to Splunk confs.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect from utils import error, parse diff --git a/examples/event_types.py b/examples/event_types.py index a02a47b7b..bd84e637c 100755 --- a/examples/event_types.py +++ b/examples/event_types.py @@ -17,6 +17,7 @@ """A command line utility that lists Splunk event types.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect diff --git a/examples/fired_alerts.py b/examples/fired_alerts.py index c630aea50..669abdb53 100755 --- a/examples/fired_alerts.py +++ b/examples/fired_alerts.py @@ -17,6 +17,7 @@ """A command line utility that prints out fired alerts.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect diff --git a/examples/follow.py b/examples/follow.py index 9465afe7f..5c9371eba 100755 --- a/examples/follow.py +++ b/examples/follow.py @@ -19,6 +19,7 @@ from pprint import pprint import sys +sys.path.insert(0, '../') import time import splunklib.client as client diff --git a/examples/handlers/handler_certs.py b/examples/handlers/handler_certs.py index 5b664ba24..f7aa85a42 100755 --- a/examples/handlers/handler_certs.py +++ b/examples/handlers/handler_certs.py @@ -37,6 +37,7 @@ import ssl import socket import sys +sys.path.insert(0, '../') import urllib import urlparse diff --git a/examples/handlers/handler_debug.py b/examples/handlers/handler_debug.py index 9e82297c3..e3ae05108 100755 --- a/examples/handlers/handler_debug.py +++ b/examples/handlers/handler_debug.py @@ -20,6 +20,7 @@ from pprint import pprint from StringIO import StringIO import sys +sys.path.insert(0, '../') import urllib2 import splunklib.binding as binding diff --git a/examples/handlers/handler_proxy.py b/examples/handlers/handler_proxy.py index b729ab533..f3611c162 100755 --- a/examples/handlers/handler_proxy.py +++ b/examples/handlers/handler_proxy.py @@ -29,6 +29,7 @@ from pprint import pprint from StringIO import StringIO import sys +sys.path.insert(0, '../') import urllib2 import splunklib.client as client diff --git a/examples/handlers/handler_urllib2.py b/examples/handlers/handler_urllib2.py index 7d88112d3..4c219e4e5 100755 --- a/examples/handlers/handler_urllib2.py +++ b/examples/handlers/handler_urllib2.py @@ -19,6 +19,7 @@ from pprint import pprint from StringIO import StringIO import sys +sys.path.insert(0, '../') import urllib2 import splunklib.client as client diff --git a/examples/index.py b/examples/index.py index 2f8c859c9..17e19b1b6 100755 --- a/examples/index.py +++ b/examples/index.py @@ -17,6 +17,7 @@ """A command line utility for interacting with Splunk indexes.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect diff --git a/examples/info.py b/examples/info.py index 4937d6633..c88113591 100755 --- a/examples/info.py +++ b/examples/info.py @@ -17,6 +17,7 @@ """An example that prints Splunk service info & settings.""" import sys +sys.path.insert(0, '../') import splunklib.client as client diff --git a/examples/inputs.py b/examples/inputs.py index 07013bb1d..9e4ada5b4 100755 --- a/examples/inputs.py +++ b/examples/inputs.py @@ -17,6 +17,7 @@ """A command line utility for interacting with Splunk inputs.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect diff --git a/examples/job.py b/examples/job.py index d8c15bbe2..7fcd79085 100755 --- a/examples/job.py +++ b/examples/job.py @@ -23,6 +23,7 @@ from pprint import pprint import sys +sys.path.insert(0, '../') from splunklib.client import connect from utils import error, parse, cmdline diff --git a/examples/loggers.py b/examples/loggers.py index 41b61a52b..b765b5889 100755 --- a/examples/loggers.py +++ b/examples/loggers.py @@ -18,6 +18,7 @@ current logging level.""" import sys +sys.path.insert(0, '../') import splunklib.client as client diff --git a/examples/oneshot.py b/examples/oneshot.py index c278ed158..dc0dd8199 100755 --- a/examples/oneshot.py +++ b/examples/oneshot.py @@ -19,6 +19,7 @@ from pprint import pprint import socket import sys +sys.path.insert(0, '../') from splunklib.client import connect import splunklib.results as results diff --git a/examples/saved_searches.py b/examples/saved_searches.py index 82d529ffc..b0a6183e8 100755 --- a/examples/saved_searches.py +++ b/examples/saved_searches.py @@ -17,6 +17,7 @@ """A command line utility that lists saved searches.""" import sys +sys.path.insert(0, '../') from splunklib.client import connect diff --git a/examples/search.py b/examples/search.py index a1e23dc9e..9629a676b 100755 --- a/examples/search.py +++ b/examples/search.py @@ -19,6 +19,7 @@ from pprint import pprint import sys +sys.path.insert(0, '../') from time import sleep from splunklib.binding import HTTPError diff --git a/examples/spcmd.py b/examples/spcmd.py index d2afe06ff..2cd32a394 100755 --- a/examples/spcmd.py +++ b/examples/spcmd.py @@ -29,6 +29,7 @@ except ImportError: pass import sys +sys.path.insert(0, '../') import splunklib.client as client diff --git a/examples/spurl.py b/examples/spurl.py index a05aa80d3..e3f85f13f 100755 --- a/examples/spurl.py +++ b/examples/spurl.py @@ -17,6 +17,7 @@ """A simple command line interface for the Splunk REST APIs.""" import sys +sys.path.insert(0, '../') from xml.etree import ElementTree import splunklib.binding as binding diff --git a/examples/submit.py b/examples/submit.py index 4d59af852..bc9df8057 100755 --- a/examples/submit.py +++ b/examples/submit.py @@ -17,6 +17,7 @@ """A command line utility that submits event data to Splunk from stdin.""" import sys +sys.path.insert(0, '../') import splunklib.client as client diff --git a/examples/upload.py b/examples/upload.py index df37ff8a2..fef6846e8 100755 --- a/examples/upload.py +++ b/examples/upload.py @@ -18,7 +18,7 @@ from os import path import sys - +sys.path.insert(0, '../') import splunklib.client as client from utils import * diff --git a/tests/runtests.py b/tests/runtests.py index 9a4ed5d8f..9d238500c 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -19,6 +19,7 @@ import os import sys import unittest +import testlib # Set up the environment for coverage, even though it might not get used os.environ["COVERAGE_PROCESS_START"] = "../tests/.coveragerc" diff --git a/tests/test_examples.py b/tests/test_examples.py index 77b2cd72e..1958418a3 100755 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -19,10 +19,10 @@ import time import sys -import splunklib.client as client - import testlib +import splunklib.client as client + def check_multiline(testcase, first, second, message=None): """Assert that two multi-line strings are equal.""" testcase.assertTrue(isinstance(first, basestring), @@ -48,7 +48,7 @@ def start(script, stdin=None, stdout=PIPE, stderr=None): if isinstance(script, str): script = script.split() script = ["python"] + script - return Popen(script, stdin=stdin, stdout=stdout, stderr=stderr) + return Popen(script, stdin=stdin, stdout=stdout, stderr=stderr, cwd='../examples') # Rudimentary sanity check for each of the examples class TestCase(testlib.TestCase): From 98637c0870f0458d4ae470cc4ff2ea24758c26a6 Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Tue, 1 May 2012 11:54:13 -0700 Subject: [PATCH 4/7] Added DeploymentTenant binding. DeploymentTenant subclasses Entity and wraps /deployment/tenants/{name}; DeploymentTenants subclasses Collection and wraps /deployment/tenants. --- splunklib/client.py | 32 ++++++++++++++++++++++++++++++++ tests/test_deployment.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/test_deployment.py diff --git a/splunklib/client.py b/splunklib/client.py index 727480dba..292fb3558 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -52,6 +52,7 @@ PATH_CAPABILITIES = "authorization/capabilities/" PATH_CONF = "configs/conf-%s/" PATH_CONFS = "properties/" +PATH_DEPLOYMENT_TENANTS = "deployment/tenants/" PATH_EVENT_TYPES = "saved/eventtypes/" PATH_FIRED_ALERTS = "alerts/fired_alerts/" PATH_INDEXES = "data/indexes/" @@ -204,6 +205,10 @@ def capabilities(self): response = self.get(PATH_CAPABILITIES) return _load_atom(response, MATCH_ENTRY_CONTENT).capabilities + @property + def deployment_tenants(self): + return Collection(self, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) + @property def event_types(self): """Returns a collection of saved event types.""" @@ -1012,3 +1017,30 @@ class NotSupportedError(Exception): """Raised for operations that are not supported on a given object.""" pass +class DeploymentTenant(Entity): + """Represents a DeploymentTenant.""" + @property + def check_new(self): + """Will the server inform clients of updated configuration?""" + return self.state.content.get('check-new', False) + + @property + def disabled(self): + """Is this tenant disabled?""" + return self.state.content['disabled'] == 1 + + @property + def whitelist0(self): + """Criterion for allowing clients access to this deployment server.""" + return self.state.content['whitelist.0'] + +class DeploymentTenants(Collection): + """Multitenant configuration of this Splunk instance.""" + def __init__(self, service): + Collection.__init__(self, service, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) + + def list(self, count=0, **kwargs): + Collection.list(self, count=count, **kwargs) + + + diff --git a/tests/test_deployment.py b/tests/test_deployment.py new file mode 100644 index 000000000..c85e5aacd --- /dev/null +++ b/tests/test_deployment.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# +# Copyright 2011-2012 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 testlib + +import splunklib.client as client + +class TestCase(testlib.TestCase): + def test_deployment_tenants(self): + service = client.connect(**self.opts.kwargs) + deployment_tenants = service.deployment_tenants + self.assertEqual(len(deployment_tenants.list(count=1)), 1) + + def test_deployment_tenant(self): + service = client.connect(**self.opts.kwargs) + deployment_tenants = service.deployment_tenants + dt = deployment_tenants.list(count=1)[0] + self.assertTrue(isinstance(dt.disabled, bool)) + self.assertTrue(isinstance(dt.whitelist0, str)) + self.assertTrue(isinstance(dt.check_new, bool)) + +if __name__ == "__main__": + testlib.main() From 6067ae4432c4b48824cba04e6db9a4d59835a5eb Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Tue, 1 May 2012 13:17:48 -0700 Subject: [PATCH 5/7] Added deployment/serverclass Also fixed a few test issues in deployment/tenants --- splunklib/client.py | 89 ++++++++++++++++++++++++++++++++++++++-- tests/test_deployment.py | 23 +++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/splunklib/client.py b/splunklib/client.py index 292fb3558..f1b6795c1 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -53,6 +53,7 @@ PATH_CONF = "configs/conf-%s/" PATH_CONFS = "properties/" PATH_DEPLOYMENT_TENANTS = "deployment/tenants/" +PATH_DEPLOYMENT_SERVERCLASSES = "deployment/serverclass/" PATH_EVENT_TYPES = "saved/eventtypes/" PATH_FIRED_ALERTS = "alerts/fired_alerts/" PATH_INDEXES = "data/indexes/" @@ -207,7 +208,7 @@ def capabilities(self): @property def deployment_tenants(self): - return Collection(self, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) + return DeploymentTenants(self) @property def event_types(self): @@ -279,6 +280,10 @@ def saved_searches(self): """Returns a collection of saved searches.""" return SavedSearches(self) + @property + def serverclasses(self): + return DeploymentServerClasses(self) + @property def settings(self): """Returns configuration settings for the service.""" @@ -1018,7 +1023,7 @@ class NotSupportedError(Exception): pass class DeploymentTenant(Entity): - """Represents a DeploymentTenant.""" + """Binding for /deployments/tenants/{name}.""" @property def check_new(self): """Will the server inform clients of updated configuration?""" @@ -1027,6 +1032,7 @@ def check_new(self): @property def disabled(self): """Is this tenant disabled?""" + print self.state.content['disabled'] return self.state.content['disabled'] == 1 @property @@ -1034,13 +1040,88 @@ def whitelist0(self): """Criterion for allowing clients access to this deployment server.""" return self.state.content['whitelist.0'] + def update(self, **kwargs): + if 'check_new' in kwargs: + kwargs['check-new'] = kwargs.pop('check_new') + self.service.post(PATH_DEPLOYMENT_TENANTS + self.name, **kwargs) + return self + class DeploymentTenants(Collection): - """Multitenant configuration of this Splunk instance.""" + """Multitenant configuration of this Splunk instance. + + Binding for /deployments/tenants. + """ def __init__(self, service): Collection.__init__(self, service, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) def list(self, count=0, **kwargs): - Collection.list(self, count=count, **kwargs) + return Collection.list(self, count=count, **kwargs) + +class DeploymentServerClass(Entity): + """Represents a deployment server class. + + Binds /deployments/serverclass/{name}. + """ + def delete(self, **kwargs): + raise NotSupportedError("Cannot delete server classes via the REST API") + + @property + def whitelist(self): + if 'whitelist' in self.content: + return self.content.whitelist.split(',') + else: + return None + + @property + def blacklist(self): + if 'blacklist' in self.content: + return self.content.blacklist.split(',') + else: + return None + + @property + def filter_type(self): + return self.content.get('filterType', None) + @property + def endpoint(self): + return self.content.get('endpoint', None) + + @property + def tmpfolder(self): + return self.content.get('tmpfolder', None) + + @property + def repository_location(self): + return self.content.get('repositoryLocation', None) + + @property + def target_repository_location(self): + return self.content.get('targetRepositoryLocation', None) + + @property + def continue_matching(self): + return self.content.get('continueMatching', None) + +class DeploymentServerClasses(Collection): + """Binding for /deployment/serverclasses""" + def __init__(self, service): + Collection.__init__(self, service, PATH_DEPLOYMENT_SERVERCLASSES, item=DeploymentServerClass) + + def list(self, count=0, **kwargs): + return Collection.list(self, count=count, **kwargs) + + def create(self, name, **kwargs): + if 'blacklist' in kwargs: + for i,v in enumerate(kwargs['blacklist']): + kwargs['blacklist.%d' % i] = v + kwargs.pop('blacklist') + if 'whitelist' in kwargs: + for i,v in enumerate(kwargs['whitelist']): + kwargs['whitelist.%d' % i] = v + kwargs.pop('whitelist') + if not 'filterType' in kwargs: + kwargs['filterType'] = 'blacklist' + return Collection.create(self, name, **kwargs) diff --git a/tests/test_deployment.py b/tests/test_deployment.py index c85e5aacd..9c2bff4a9 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -31,6 +31,29 @@ def test_deployment_tenant(self): self.assertTrue(isinstance(dt.disabled, bool)) self.assertTrue(isinstance(dt.whitelist0, str)) self.assertTrue(isinstance(dt.check_new, bool)) + self.assertTrue(isinstance(dt.name, str)) + + def test_deployment_serverclass(self): + name = 'pythonsdk_serverclass6' + service = client.connect(**self.opts.kwargs) + if not(name in service.serverclasses): + sc = service.serverclasses.create(name, + filterType='blacklist', + whitelist=['*.wanda.biz', 'ftw.*.leroy.ru'], + blacklist=['*.gov', '*.ch']) + else: + sc = service.serverclasses[name] + sc.refresh() + self.assertEqual(sc.whitelist, ['*.wanda.biz', 'ftw.*.leroy.ru']) + self.assertEqual(sc.blacklist, ['*.gov', '*.ch']) + self.assertEqual(sc.filter_type, 'blacklist') + self.assertEqual(sc.endpoint, None) + self.assertEqual(sc.tmpfolder, None) + self.assertTrue(sc.repository_location.endswith('etc/deployment-apps')) + self.assertEqual(sc.continue_matching, None) + + + if __name__ == "__main__": testlib.main() From 86af15fb644c05af6c8b62c9abb354e1b4b10b62 Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Wed, 2 May 2012 09:12:10 -0700 Subject: [PATCH 6/7] More app bindings; made content keys pretend to be fields. Now you can call app.author instead of app['author']. --- splunklib/client.py | 214 +++++++++++++++++++++++++++++++++------ tests/test_app.py | 11 ++ tests/test_deployment.py | 25 ++++- 3 files changed, 218 insertions(+), 32 deletions(-) diff --git a/splunklib/client.py b/splunklib/client.py index f1b6795c1..ea6f0a9c7 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -52,7 +52,9 @@ PATH_CAPABILITIES = "authorization/capabilities/" PATH_CONF = "configs/conf-%s/" PATH_CONFS = "properties/" +PATH_DEPLOYMENT_CLIENTS = "deployment/client/" PATH_DEPLOYMENT_TENANTS = "deployment/tenants/" +PATH_DEPLOYMENT_SERVERS = "deployment/server/" PATH_DEPLOYMENT_SERVERCLASSES = "deployment/serverclass/" PATH_EVENT_TYPES = "saved/eventtypes/" PATH_FIRED_ALERTS = "alerts/fired_alerts/" @@ -193,7 +195,7 @@ def __init__(self, **kwargs): @property def apps(self): """Returns a collection of Splunk applications.""" - return Collection(self, PATH_APPS) + return Collection(self, PATH_APPS, item=Application) @property def confs(self): @@ -206,9 +208,17 @@ def capabilities(self): response = self.get(PATH_CAPABILITIES) return _load_atom(response, MATCH_ENTRY_CONTENT).capabilities + @property + def deployment_clients(self): + return DeploymentCollection(self, PATH_DEPLOYMENT_CLIENTS, item=DeploymentClient) + + @property + def deployment_servers(self): + return DeploymentCollection(self, PATH_DEPLOYMENT_SERVERS, item=DeploymentServer) + @property def deployment_tenants(self): - return DeploymentTenants(self) + return DeploymentCollection(self, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) @property def event_types(self): @@ -321,6 +331,10 @@ def post(self, relpath="", **kwargs): # kwargs: path, app, owner, sharing, state class Entity(Endpoint): """This class is a base class for all entity objects.""" + # Subclasses can override this to provide the default values for + # optional fields. + defaults = {} + def __init__(self, service, path, **kwargs): Endpoint.__init__(self, service, path) self._state = None @@ -329,8 +343,14 @@ def __init__(self, service, path, **kwargs): def __call__(self, *args): return self.content(*args) + def __getattr__(self, attr): + return self[attr] + def __getitem__(self, key): - return self.content[key] + if key in self.content: + return self.content[key] + else: + return self.defaults[key] # Load the Atom entry record from the given response - this is a method # because the "entry" record varies slightly by entity and this allows @@ -750,6 +770,8 @@ def list(self, *args): class Job(Entity): """This class represents a search job.""" + defaults = {'sid': None} + def __init__(self, service, path, **kwargs): Entity.__init__(self, service, path, **kwargs) @@ -848,11 +870,6 @@ def set_priority(self, value): self.post('control', action="setpriority", priority=value) return self - @property - def sid(self): - """Returns this job's search ID (sid).""" - return self.content.get('sid', None) - def summary(self, **kwargs): """Returns an InputStream IO handle to the job's summary. @@ -1022,6 +1039,19 @@ class NotSupportedError(Exception): """Raised for operations that are not supported on a given object.""" pass +class DeploymentCollection(Collection): + def __init__(self, service, path, item): + Collection.__init__(self, service, path, item=item) + + def create(self, name, **kwargs): + raise NotSupportedError("Cannot create %s with the REST API." % self.__class__.__name__) + + def delete(self, name): + raise NotSupportedError("Cannot delete %s with the REST API." % self.__class__.__name__) + + def list(self, count=0, **kwargs): + return Collection.list(self, count=count, **kwargs) + class DeploymentTenant(Entity): """Binding for /deployments/tenants/{name}.""" @property @@ -1046,22 +1076,12 @@ def update(self, **kwargs): self.service.post(PATH_DEPLOYMENT_TENANTS + self.name, **kwargs) return self -class DeploymentTenants(Collection): - """Multitenant configuration of this Splunk instance. - - Binding for /deployments/tenants. - """ - def __init__(self, service): - Collection.__init__(self, service, PATH_DEPLOYMENT_TENANTS, item=DeploymentTenant) - - def list(self, count=0, **kwargs): - return Collection.list(self, count=count, **kwargs) - class DeploymentServerClass(Entity): """Represents a deployment server class. Binds /deployments/serverclass/{name}. """ + defaults = {'endpoint': None, 'tmpfolder': None} def delete(self, **kwargs): raise NotSupportedError("Cannot delete server classes via the REST API") @@ -1083,14 +1103,6 @@ def blacklist(self): def filter_type(self): return self.content.get('filterType', None) - @property - def endpoint(self): - return self.content.get('endpoint', None) - - @property - def tmpfolder(self): - return self.content.get('tmpfolder', None) - @property def repository_location(self): return self.content.get('repositoryLocation', None) @@ -1103,14 +1115,11 @@ def target_repository_location(self): def continue_matching(self): return self.content.get('continueMatching', None) -class DeploymentServerClasses(Collection): +class DeploymentServerClasses(DeploymentCollection): """Binding for /deployment/serverclasses""" def __init__(self, service): Collection.__init__(self, service, PATH_DEPLOYMENT_SERVERCLASSES, item=DeploymentServerClass) - def list(self, count=0, **kwargs): - return Collection.list(self, count=count, **kwargs) - def create(self, name, **kwargs): if 'blacklist' in kwargs: for i,v in enumerate(kwargs['blacklist']): @@ -1124,4 +1133,147 @@ def create(self, name, **kwargs): kwargs['filterType'] = 'blacklist' return Collection.create(self, name, **kwargs) +class DeploymentServer(Entity): + """Binding for /deployment/server/{name}""" + @property + def whitelist(self): + return self.content.get('whitelist.0', None) + + @property + def check_new(self): + return self.content.get('check-new', False) + + @property + def disabled(self): + if 'disabled' in self.content: + return self.content['disabled'] == 1 + else: + return None + + def update(self, **kwargs): + if 'disabled' in kwargs: + kwargs['disabled'] = '1' if kwargs['disabled'] else '0' + if 'check_new' in kwargs: + kwargs['check-new'] = kwargs.pop('check_new') + if 'whitelist' in kwargs: + kwargs['whitelist.0'] = kwargs.pop('whitelist') + self.service.post(PATH_DEPLOYMENT_SERVERS + self.name, **kwargs) + return self + +class DeploymentServers(DeploymentCollection): + """Binding for /deployment/server""" + def __init__(self, service): + Collection.__init__(self, service, PATH_DEPLOYMENT_SERVERS, item=DeploymentServer) + # Override this because Collection defaults to count=-1 + def list(self, count=0, **kwargs): + return Collection.list(self, count=count, **kwargs) + + def create(self, name, **kwargs): + raise NotSupportedError("Cannot create deployment servers with the REST API.") + +class DeploymentClient(Entity): + """Binding for /deployment/client/{name}""" + def disable(self): + self.service.post(PATH_DEPLOYMENT_CLIENTS + self.name, {'disabled': '1'}) + return self + + @property + def disabled(self): + if 'disabled' in self.content: + return self.content['disabled'] == 1 + else: + return None + + def enable(self): + self.service.post(PATH_DEPLOYMENT_CLIENTS + self.name, {'disabled': '0'}) + return self + + def reload(self): + self.service.get(PATH_DEPLOYMENT_CLIENTS + self.name + "/reload") + return self + + @property + def serverclasses(self): + if 'serverClasses' in self.content: + return self.content['serverClasses'].split(',') + else: + return [] + + @property + def target_uri(self): + return self.content.get('targetUri', None) + + +class Application(Entity): + """Binding for /apps/local/{name}.""" + # TODO: This doesn't work. + # @property + # def check_for_updates(self): + # return self.content.get('check_for_updates', False) == '1' + + @property + def setupInfo(self): + return self.content.get('eai:setup', None) + + def package(self): + return ApplicationPackage(self.service, self.name) + + +class ApplicationPackage(Entity): + """Create a compressed archive of an application on the server. + + Usually an ApplicationPackage is created by calling + Application.package(). It has three fields: + + - `appname` :: The name of the application packaged (e.g., `search`, `Splunk_for_Exchange`). + - `url` :: A URL at which you can download the packaged application. + - `path` :: An absolute path on disk to the packaged application. + """ + def __init__(self, service, appname): + Entity.__init__(self, service, PATH_APPS + appname + "/package") + # 'name' is used by the SDK for the title of the endpoint, so we + # have to give a different label for the application name. + @property + def appname(self): + return self.content['name'] + + @property + def filepath(self): + return self.content['path'] + +# This endpoint is almost undocumented, and there doesn't seem to be a +# good way to test it. +class ApplicationUpdate(Entity): + """Binding for /apps/local/{name}/update.""" + @property + def app_url(self): + return self.content.get('update.appurl', None) + + @property + def checksum(self): + return self.content.get('update.checksum', None) + + @property + def checksum_type(self): + return self.content.get('update.checksum.type', None) + + @property + def homepage(self): + return self.content.get('update.homepage', None) + + @property + def update_name(self): + return self.content.get('update.name', None) + + @property + def size(self): + return self.content.get('update.size', None) + + @property + def version(self): + return self.content.get('update.version', None) + + @property + def implicit_id_required(self): + return self.content.get('update.implicit_id_required', False) diff --git a/tests/test_app.py b/tests/test_app.py index 5bd177ca3..7ce0c9bdc 100755 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -66,6 +66,17 @@ def test_crud(self): self.assertEqual(app['manageable'], "0") self.assertEqual(app['visible'], "0") + self.assertEqual(app.setupInfo, None) + + pkg = app.package() + + print pkg.appname + print pkg.url + print pkg.path + self.assertEqual(pkg.appname, "sdk-test-app") + self.assertTrue(pkg.url.endswith("static/app-packages/sdk-test-app.spl")) + self.assertTrue(pkg.filepath.endswith("etc/system/static/app-packages/sdk-test-app.spl")) + testlib.delete_app(service, appname) self.assertFalse(appname in service.apps) diff --git a/tests/test_deployment.py b/tests/test_deployment.py index 9c2bff4a9..c097336f5 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -44,6 +44,7 @@ def test_deployment_serverclass(self): else: sc = service.serverclasses[name] sc.refresh() + self.assertRaises(client.NotSupportedError, client.DeploymentServerClass.delete, sc) self.assertEqual(sc.whitelist, ['*.wanda.biz', 'ftw.*.leroy.ru']) self.assertEqual(sc.blacklist, ['*.gov', '*.ch']) self.assertEqual(sc.filter_type, 'blacklist') @@ -52,8 +53,30 @@ def test_deployment_serverclass(self): self.assertTrue(sc.repository_location.endswith('etc/deployment-apps')) self.assertEqual(sc.continue_matching, None) + def test_server(self): + name = 'pythonsdk_server' + service = client.connect(**self.opts.kwargs) + self.assertRaises(client.NotSupportedError, + client.DeploymentCollection.create, + service.deployment_servers, name, + check_new=True, disabled=True) + servers = service.deployment_servers.list() + if len(servers) > 0: + for s in servers: + self.assertTrue(isinstance(s.whitelist, str)) + self.assertTrue(isinstance(s.check_new, bool) or s.check_new is None) + print s.disabled + self.assertTrue(isinstance(s.disabled, bool) or s.disabled is None) - + def test_client(self): + service = client.connect(**self.opts.kwargs) + clients = service.deployment_clients.list() + self.assertRaises(client.NotSupportedError, service.deployment_clients.delete, 'asdf') + if len(clients) > 0: + for c in clients: + self.assertTrue(c.disabled is None or isinstance(c.disabled, bool)) + self.assertTrue(isinstance(c.serverclasses, list)) + self.assertTrue(c.target_uri is None or isinstance(c.disabled, str)) if __name__ == "__main__": testlib.main() From 273e8c05bf8ce828f167fe8688f9600ec3f4ed38 Mon Sep 17 00:00:00 2001 From: Frederick Ross Date: Wed, 2 May 2012 11:18:19 -0700 Subject: [PATCH 7/7] New lookup scheme for Entity, and refactored a couple subclasses to use it. --- splunklib/client.py | 129 +++++++++++++++++++++------------------ tests/test_deployment.py | 7 ++- tests/test_util.py | 28 +++++++++ 3 files changed, 102 insertions(+), 62 deletions(-) create mode 100644 tests/test_util.py diff --git a/splunklib/client.py b/splunklib/client.py index ea6f0a9c7..ddb528c85 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -36,6 +36,7 @@ from time import sleep from urllib import urlencode, quote +from collections import defaultdict from splunklib.binding import Context, HTTPError import splunklib.data as data @@ -344,13 +345,29 @@ def __call__(self, *args): return self.content(*args) def __getattr__(self, attr): - return self[attr] + return self._lookup(attr, try_attr=False) def __getitem__(self, key): - if key in self.content: - return self.content[key] - else: - return self.defaults[key] + return self._lookup(key) + + def _restify(self, key): + return key.replace('_', '-') + + def _lookup(self, key, try_attr=True): + # 1. Is there a method on the object that we can call? + if try_attr and key in dir(self): + return getattr(self, key) + # 2. When we replace _ with -, is there an exact match to some + # key in the content? + rkey = self._restify(key) + found = gethierarchy(self.content, rkey) + if found != {}: + return found + # 3. Look in defaults for a key. + if rkey in self.defaults: + return self.defaults[rkey] + # 5. Throw an error. + raise KeyError("No such attribute %s of %s" % (key, self.__class__.__name__)) # Load the Atom entry record from the given response - this is a method # because the "entry" record varies slightly by entity and this allows @@ -993,6 +1010,9 @@ def update(self, search=None, **kwargs): Entity.update(self, search=search, **kwargs) return self +class SavedSearchSchedule(Entity): + pass + class SavedSearches(Collection): """This class represents a collection of saved searches.""" def __init__(self, service): @@ -1081,16 +1101,12 @@ class DeploymentServerClass(Entity): Binds /deployments/serverclass/{name}. """ - defaults = {'endpoint': None, 'tmpfolder': None} - def delete(self, **kwargs): - raise NotSupportedError("Cannot delete server classes via the REST API") - - @property - def whitelist(self): - if 'whitelist' in self.content: - return self.content.whitelist.split(',') - else: - return None + defaults = {'endpoint': None, + 'tmpfolder': None, + 'filterType': None, + 'targetRepositoryLocation': None, + 'repositoryLocation': None, + 'continueMatching': None} @property def blacklist(self): @@ -1099,21 +1115,15 @@ def blacklist(self): else: return None - @property - def filter_type(self): - return self.content.get('filterType', None) - - @property - def repository_location(self): - return self.content.get('repositoryLocation', None) - - @property - def target_repository_location(self): - return self.content.get('targetRepositoryLocation', None) + def delete(self, **kwargs): + raise NotSupportedError("Cannot delete server classes via the REST API") @property - def continue_matching(self): - return self.content.get('continueMatching', None) + def whitelist(self): + if 'whitelist' in self.content: + return self.content.whitelist.split(',') + else: + return None class DeploymentServerClasses(DeploymentCollection): """Binding for /deployment/serverclasses""" @@ -1228,7 +1238,7 @@ class ApplicationPackage(Entity): - `appname` :: The name of the application packaged (e.g., `search`, `Splunk_for_Exchange`). - `url` :: A URL at which you can download the packaged application. - - `path` :: An absolute path on disk to the packaged application. + - `filepath` :: An absolute path on disk to the packaged application. """ def __init__(self, service, appname): Entity.__init__(self, service, PATH_APPS + appname + "/package") @@ -1246,34 +1256,35 @@ def filepath(self): # good way to test it. class ApplicationUpdate(Entity): """Binding for /apps/local/{name}/update.""" - @property - def app_url(self): - return self.content.get('update.appurl', None) - - @property - def checksum(self): - return self.content.get('update.checksum', None) - - @property - def checksum_type(self): - return self.content.get('update.checksum.type', None) - - @property - def homepage(self): - return self.content.get('update.homepage', None) - - @property - def update_name(self): - return self.content.get('update.name', None) - - @property - def size(self): - return self.content.get('update.size', None) - - @property - def version(self): - return self.content.get('update.version', None) + defaults = {"update.appurl": None, + "update.checksum": None, + "update.checksum.type": None, + "update.homepage": None, + "update.name": None, + "update.size": None, + "update.version": None, + "update.implicit_id_required": None} + + def _lookup(self, key): + return Entity._lookup("update." + key) + +def gethierarchy(dct, key, sep='.'): + def tree(): return defaultdict(tree) + if key in dct: + return dct[key] + key = key + sep + result = tree() + for k,v in dct.iteritems(): + if not k.startswith(key): + continue + suffix = k[len(key):] + if '.' in suffix: + ks = suffix.split(sep) + z = result + for x in ks[:-1]: + z = z[x] + z[ks[-1]] = v + else: + result[suffix] = v + return result - @property - def implicit_id_required(self): - return self.content.get('update.implicit_id_required', False) diff --git a/tests/test_deployment.py b/tests/test_deployment.py index c097336f5..a7a9a98dc 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -46,12 +46,13 @@ def test_deployment_serverclass(self): sc.refresh() self.assertRaises(client.NotSupportedError, client.DeploymentServerClass.delete, sc) self.assertEqual(sc.whitelist, ['*.wanda.biz', 'ftw.*.leroy.ru']) + self.assertEqual(sc['whitelist'], ['*.wanda.biz', 'ftw.*.leroy.ru']) self.assertEqual(sc.blacklist, ['*.gov', '*.ch']) - self.assertEqual(sc.filter_type, 'blacklist') + self.assertEqual(sc.filterType, 'blacklist') self.assertEqual(sc.endpoint, None) self.assertEqual(sc.tmpfolder, None) - self.assertTrue(sc.repository_location.endswith('etc/deployment-apps')) - self.assertEqual(sc.continue_matching, None) + self.assertTrue(sc.repositoryLocation.endswith('etc/deployment-apps')) + self.assertEqual(sc.continueMatching, None) def test_server(self): name = 'pythonsdk_server' diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 000000000..30574eda3 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# +# Copyright 2011-2012 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 testlib + +import splunklib.client as client + +class TestCase(testlib.TestCase): + def test_gethierarchy(test): + test.assertEqual(client.gethierarchy({'apple':0, + 'pear.alpha':1, + 'pear.beta':2, + 'pear.gamma.meep':3}, + 'pear'), + {'alpha':1, 'beta':2, 'gamma': {'meep':3}})