Skip to content

Commit f76de8d

Browse files
author
Frederick Ross
committed
Lots of random stuff; primarily got all index tests working.
1 parent 122f83f commit f76de8d

7 files changed

Lines changed: 184 additions & 80 deletions

File tree

splunklib/binding.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def __init__(self, handler=None, **kwargs):
309309
self.token = kwargs.get("token", None)
310310
self.scheme = kwargs.get("scheme", DEFAULT_SCHEME)
311311
self.host = kwargs.get("host", DEFAULT_HOST)
312-
self.port = kwargs.get("port", DEFAULT_PORT)
312+
self.port = int(kwargs.get("port", DEFAULT_PORT))
313313
self.authority = _authority(self.scheme, self.host, self.port)
314314
self.namespace = namespace(**kwargs)
315315
self.username = kwargs.get("username", "")
@@ -350,8 +350,11 @@ def connect(self):
350350
socket.write("X-Splunk-Input-Mode: Streaming\r\n")
351351
socket.write("\r\n")
352352
"""
353-
cn = socket.create_connection((self.host, int(self.port)))
354-
return ssl.wrap_socket(cn) if self.scheme == "https" else cn
353+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
354+
if self.scheme == "https":
355+
sock = ssl.wrap_socket(sock)
356+
sock.connect((self.host, self.port))
357+
return sock
355358

356359
def _authentication(self, request_fun):
357360
"""Wrapper to handle autologin and authentication errors.
@@ -606,10 +609,21 @@ def f():
606609
path = self.authority \
607610
+ self._abspath(path_segment, owner=owner,
608611
app=app, sharing=sharing)
609-
headers = headers + self._auth_headers
612+
# all_headers can't be named headers, due to a error in
613+
# Python's implementation of closures. In particular:
614+
# def f(x):
615+
# def g():
616+
# x = x + "a"
617+
# return x
618+
# return g()
619+
# throws UnboundLocalError, claiming that x is not bound.
620+
all_headers = headers + self._auth_headers
621+
print all_headers
622+
print path
623+
print body
610624
return self.http.request(path,
611625
{'method': method,
612-
'headers': headers,
626+
'headers': all_headers,
613627
'body': body})
614628
return self._authentication(f)
615629

@@ -687,13 +701,11 @@ def _abspath(self, path_segment,
687701
'/servicesNS/nobody/system/apps/local/search'
688702
url = c.authority + c._abspath('apps/local/sharing')
689703
"""
704+
skip_encode = isinstance(path_segment, UrlEncoded)
690705
# If path_segment is absolute, escape all forbidden characters
691706
# in it and return it.
692707
if path_segment.startswith('/'):
693-
if isinstance(path_segment, UrlEncoded):
694-
return path_segment
695-
else:
696-
return UrlEncoded(path_segment)
708+
return UrlEncoded(path_segment, skip_encode=skip_encode)
697709

698710
# path_segment is relative, so we need a namespace to build an
699711
# absolute path.
@@ -707,11 +719,12 @@ def _abspath(self, path_segment,
707719
# namespace. If only one of app and owner is specified, use
708720
# '-' for the other.
709721
if ns.app is None and ns.owner is None:
710-
return UrlEncoded("/services/%s" % path_segment)
722+
return UrlEncoded("/services/%s" % path_segment, skip_encode=skip_encode)
711723

712724
oname = "-" if ns.owner is None else ns.owner
713725
aname = "-" if ns.app is None else ns.app
714-
return UrlEncoded("/servicesNS/%s/%s/%s" % (oname, aname, path_segment))
726+
return UrlEncoded("/servicesNS/%s/%s/%s" % (oname, aname, path_segment),
727+
skip_encode=skip_encode)
715728

716729
def connect(**kwargs):
717730
"""Return an authenticated ``Context`` object.
@@ -756,16 +769,13 @@ def connect(**kwargs):
756769
# Note: the error response schema supports multiple messages but we only
757770
# return the first, although we do return the body so that an exception
758771
# handler that wants to read multiple messages can do so.
759-
def read_error_message(response):
760-
body = response.body.read()
761-
return body, XML(body).findtext("./messages/msg")
762-
763772
class HTTPError(Exception):
764773
"""This class is raised for HTTP responses that return an error."""
765774
def __init__(self, response):
766775
status = response.status
767776
reason = response.reason
768-
body, detail = read_error_message(response)
777+
body = response.body.read()
778+
detail = XML(body).findtext("./messages/msg")
769779
message = "HTTP %d %s%s" % (
770780
status, reason, "" if detail is None else " -- %s" % detail)
771781
Exception.__init__(self, message)

splunklib/client.py

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
PATH_SAVED_SEARCHES = "saved/searches/"
7171
PATH_STANZA = "configs/conf-%s/%s" # (file, stanza)
7272
PATH_USERS = "authentication/users/"
73+
PATH_RECEIVERS_STREAM = "receivers/stream"
74+
PATH_RECEIVERS_SIMPLE = "receivers/simple"
7375

7476
XNAMEF_ATOM = "{http://www.w3.org/2005/Atom}%s"
7577
XNAME_ENTRY = XNAMEF_ATOM % "entry"
@@ -297,7 +299,7 @@ def restart(self):
297299
@property
298300
def roles(self):
299301
"""Returns a collection of user roles."""
300-
return Collection(self, PATH_ROLES)
302+
return Roles(self)
301303

302304
def search(self, query, **kwargs):
303305
return self.jobs.create(query, **kwargs)
@@ -493,9 +495,6 @@ def __init__(self, service, path, **kwargs):
493495
self._state = None
494496
self.refresh(kwargs.get('state', None)) # "Prefresh"
495497

496-
def __call__(self, *args):
497-
return self.content(*args)
498-
499498
def __eq__(self, other):
500499
"""Raises IncomparableException.
501500
@@ -991,27 +990,31 @@ def attach(self, host=None, source=None, sourcetype=None):
991990
if host is not None: args['host'] = host
992991
if source is not None: args['source'] = source
993992
if sourcetype is not None: args['sourcetype'] = sourcetype
994-
path = "receivers/stream?%s" % urllib.urlencode(args)
993+
path = UrlEncoded(PATH_RECEIVERS_STREAM + "?" + urllib.urlencode(args), skip_encode=True)
995994

996995
# Since we need to stream to the index connection, we have to keep
997996
# the connection open and use the Splunk extension headers to note
998997
# the input mode
999-
cn = self.service.connect()
1000-
cn.write("POST %s HTTP/1.1\r\n" % self.service._abspath(path))
1001-
cn.write("Host: %s:%s\r\n" % (self.service.host, self.service.port))
1002-
cn.write("Accept-Encoding: identity\r\n")
1003-
cn.write("Authorization: %s\r\n" % self.service.token)
1004-
cn.write("X-Splunk-Input-Mode: Streaming\r\n")
1005-
cn.write("\r\n")
1006-
return cn
998+
sock = self.service.connect()
999+
headers = ["POST %s HTTP/1.1\r\n" % self.service._abspath(path),
1000+
"Host: %s:%s\r\n" % (self.service.host, int(self.service.port)),
1001+
"Accept-Encoding: identity\r\n",
1002+
"Authorization: %s\r\n" % self.service.token,
1003+
"X-Splunk-Input-Mode: Streaming\r\n",
1004+
"\r\n"]
1005+
for h in headers:
1006+
sock.write(h)
1007+
return sock
10071008

10081009
def clean(self, timeout=60):
10091010
"""Deletes the contents of the index.
10101011
10111012
:param `timeout`: The time-out period for the operation, in seconds (the
10121013
default is 60).
10131014
"""
1014-
saved = self.refresh()('maxTotalDataSizeMB', 'frozenTimePeriodInSecs')
1015+
self.refresh()
1016+
tds = self['maxTotalDataSizeMB']
1017+
ftp = self['frozenTimePeriodInSecs']
10151018
self.update(maxTotalDataSizeMB=1, frozenTimePeriodInSecs=1)
10161019
self.roll_hot_buckets()
10171020

@@ -1022,7 +1025,9 @@ def clean(self, timeout=60):
10221025
count += 1
10231026
self.refresh()
10241027

1025-
self.update(**saved) # Restore original values
1028+
# Restore original values
1029+
self.update(maxTotalDataSizeMB=tds,
1030+
frozenTimePeriodInSecs=ftp)
10261031
if self.content.totalEventCount != '0':
10271032
raise OperationError, "Operation timed out."
10281033
return self
@@ -1033,7 +1038,7 @@ def roll_hot_buckets(self):
10331038
return self
10341039

10351040
def submit(self, event, host=None, source=None, sourcetype=None):
1036-
"""Submits an event to the index using ``HTTP POST``.
1041+
"""Submit a single event to the index using ``HTTP POST``.
10371042
10381043
:param `host`: The host value of the event.
10391044
:param `source`: The source value of the event.
@@ -1048,7 +1053,7 @@ def submit(self, event, host=None, source=None, sourcetype=None):
10481053
# is that we are not sending a POST request encoded using
10491054
# x-www-form-urlencoded (as we do not have a key=value body),
10501055
# because we aren't really sending a "form".
1051-
path = UrlEncoded("receivers/simple?%s" % urllib.urlencode(args), skip_encode=True)
1056+
path = UrlEncoded(PATH_RECEIVERS_SIMPLE + "?" + urllib.urlencode(args), skip_encode=True)
10521057
self.service.request(path, method="POST", body=event)
10531058
return self
10541059

@@ -1311,7 +1316,7 @@ def preview(self, **query_params):
13111316
"""Fetch an InputStream IO handle of preview search results.
13121317
13131318
Unlike ``results``, which requires a job to be finished to
1314-
return any results, ``results_preview`` returns whatever
1319+
return any results, ``preview`` returns whatever
13151320
Splunk has so far, whether the job is running or not. The
13161321
returned search results are the raw data from the server. Pass
13171322
the handle returned to ``results.ResultsReader`` to get a
@@ -1321,7 +1326,7 @@ def preview(self, **query_params):
13211326
import splunklib.results as results
13221327
s = client.connect(...)
13231328
job = s.jobs.create("search * | head 5")
1324-
r = results.ResultsReader(job.results_preview())
1329+
r = results.ResultsReader(job.preview())
13251330
if r.is_preview:
13261331
print "Preview of a running search job."
13271332
else:
@@ -1636,8 +1641,15 @@ def __init__(self, service):
16361641
def __getitem__(self, key):
16371642
return Collection.__getitem__(self, key.lower())
16381643

1644+
def __contains__(self, name):
1645+
return Collection.__contains__(self, name.lower())
1646+
16391647
def contains(self, name):
1640-
return Collection.contains(self, name.lower())
1648+
"""Deprecated: Use in operator instead.
1649+
1650+
Check if there is a user *name* in this Splunk instance.
1651+
"""
1652+
return Collection.__contains__(self, name.lower())
16411653

16421654
def create(self, username, password, roles, **params):
16431655
"""Create a new user.
@@ -1667,6 +1679,8 @@ def create(self, username, password, roles, **params):
16671679
raise ValueError("Invalid username: %s" % str(username))
16681680
username = username.lower()
16691681
self.post(name=username, password=password, roles=roles, **params)
1682+
# splunkd doesn't return the user in the POST response body,
1683+
# so we have to make a second round trip to fetch it.
16701684
response = self.get(username)
16711685
entry = _load_atom(response, XNAME_ENTRY).entry
16721686
state = _parse_atom_entry(entry)
@@ -1679,6 +1693,62 @@ def create(self, username, password, roles, **params):
16791693
def delete(self, name):
16801694
return Collection.delete(self, name.lower())
16811695

1696+
class Roles(Collection):
1697+
"""Roles in the Splunk instance."""
1698+
def __init__(self, service):
1699+
return Collection.__init__(self, service, PATH_ROLES)
1700+
1701+
def __getitem__(self, key):
1702+
return Collection.__getitem__(self, key.lower())
1703+
1704+
def __contains__(self, name):
1705+
return Collection.__contains__(self, name.lower())
1706+
1707+
def contains(self, name):
1708+
"""Deprecated: Use in operator instead.
1709+
1710+
Check if there is a user *name* in this Splunk instance.
1711+
"""
1712+
return Collection.__contains__(self, name.lower())
1713+
1714+
def create(self, name, **params):
1715+
"""Create a new role.
1716+
1717+
This function makes two roundtrips to the server, plus at most
1718+
two more if autologin is turned on.
1719+
1720+
:param name: Name for the role
1721+
:type name: string
1722+
:param params: Optional parameters. See the `REST API documentation<http://docs/Documentation/Splunk/4.3.2/RESTAPI/RESTaccess#POST_authorization.2Froles>`_.
1723+
:return: A reference to the new role.
1724+
:rtype: ``Entity``
1725+
1726+
**Example**::
1727+
1728+
import splunklib.client as client
1729+
c = client.connect(...)
1730+
roles = c.roles
1731+
paltry = roles.create("paltry", imported_roles="user", defaultApp="search")
1732+
"""
1733+
if not isinstance(name, basestring):
1734+
raise ValueError("Invalid role name: %s" % str(name))
1735+
name = name.lower()
1736+
self.post(name=name, **params)
1737+
# splunkd doesn't return the user in the POST response body,
1738+
# so we have to make a second round trip to fetch it.
1739+
response = self.get(name)
1740+
entry = _load_atom(response, XNAME_ENTRY).entry
1741+
state = _parse_atom_entry(entry)
1742+
entity = self.item(
1743+
self.service,
1744+
urllib.unquote(state.links.alternate),
1745+
state=state)
1746+
return entity
1747+
1748+
def delete(self, name):
1749+
return Collection.delete(self, name.lower())
1750+
1751+
16821752
class OperationError(Exception):
16831753
"""Raised for a failed operation, such as a time out."""
16841754
pass

splunklib/data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
__all__ = ["load"]
2929

30+
# LNAME refers to element names without namespaces; XNAME is the same
31+
# name, but with an XML namespace.
3032
LNAME_DICT = "dict"
3133
LNAME_ITEM = "item"
3234
LNAME_KEY = "key"

0 commit comments

Comments
 (0)