5555DEFAULT_PORT = "8089"
5656DEFAULT_SCHEME = "https"
5757
58- @contextmanager
59- def log_duration ():
60- start_time = datetime .now ()
61- yield
62- end_time = datetime .now ()
63- logging .debug ("Operation took %s" , end_time - start_time )
64-
58+ def log_duration (f ):
59+ def new_f (* args , ** kwargs ):
60+ start_time = datetime .now ()
61+ val = f (* args , ** kwargs )
62+ end_time = datetime .now ()
63+ logging .debug ("Operation took %s" , end_time - start_time )
64+ return val
65+ return new_f
66+
67+ # Custom exceptions
6568class AuthenticationError (Exception ):
6669 pass
6770
71+ # Singleton values to eschew None
72+ class NoAuthenticationToken (object ):
73+ pass
74+
6875class UrlEncoded (str ):
6976 """String subclass to represent URL encoded strings.
7077
@@ -205,7 +212,7 @@ def f():
205212 """
206213 @functools .wraps (request_fun )
207214 def wrapper (self , * args , ** kwargs ):
208- if self .token is None :
215+ if self .token is NoAuthenticationToken :
209216 # Not yet logged in.
210217 if self .autologin and self .username and self .password :
211218 # This will throw an uncaught
@@ -377,7 +384,9 @@ class Context(object):
377384 """
378385 def __init__ (self , handler = None , ** kwargs ):
379386 self .http = HttpLib (handler )
380- self .token = kwargs .get ("token" , None )
387+ self .token = kwargs .get ("token" , NoAuthenticationToken )
388+ if self .token is None : # In case someone explicitly passes token=None
389+ self .token = NoAuthenticationToken
381390 self .scheme = kwargs .get ("scheme" , DEFAULT_SCHEME )
382391 self .host = kwargs .get ("host" , DEFAULT_HOST )
383392 self .port = int (kwargs .get ("port" , DEFAULT_PORT ))
@@ -398,7 +407,12 @@ def _auth_headers(self):
398407
399408 :returns: A list of 2-tuples containing key and value
400409 """
401- return [("Authorization" , self .token )]
410+ # Ensure the token is properly formatted
411+ if self .token .startswith ('Splunk' ):
412+ token = self .token
413+ else :
414+ token = 'Splunk %s' % self .token
415+ return [("Authorization" , token )]
402416
403417 def connect (self ):
404418 """Returns an open connection (socket) to the Splunk instance.
@@ -428,6 +442,7 @@ def connect(self):
428442 return sock
429443
430444 @_authentication
445+ @log_duration
431446 def delete (self , path_segment , owner = None , app = None , sharing = None , ** query ):
432447 """DELETE at *path_segment* with the given namespace and query.
433448
@@ -473,11 +488,11 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
473488 path = self .authority + self ._abspath (path_segment , owner = owner ,
474489 app = app , sharing = sharing )
475490 logging .debug ("DELETE request to %s (body: %s)" , path , repr (query ))
476- with log_duration ():
477- response = self .http .delete (path , self ._auth_headers , ** query )
491+ response = self .http .delete (path , self ._auth_headers , ** query )
478492 return response
479493
480494 @_authentication
495+ @log_duration
481496 def get (self , path_segment , owner = None , app = None , sharing = None , ** query ):
482497 """GET from *path_segment* with the given namespace and query.
483498
@@ -523,12 +538,12 @@ def get(self, path_segment, owner=None, app=None, sharing=None, **query):
523538 path = self .authority + self ._abspath (path_segment , owner = owner ,
524539 app = app , sharing = sharing )
525540 logging .debug ("GET request to %s (body: %s)" , path , repr (query ))
526- with log_duration ():
527- response = self .http .get (path , self ._auth_headers , ** query )
541+ response = self .http .get (path , self ._auth_headers , ** query )
528542 return response
529543
530544 @_authentication
531- def post (self , path_segment , owner = None , app = None , sharing = None , ** query ):
545+ @log_duration
546+ def post (self , path_segment , owner = None , app = None , sharing = None , headers = [], ** query ):
532547 """POST to *path_segment* with the given namespace and query.
533548
534549 Named to match the HTTP method. This function makes at least
@@ -540,12 +555,21 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
540555 ``Context``'s default namespace. All other keyword arguments
541556 are included in the URL as query parameters.
542557
558+ Some of Splunk's endpoints, such as receivers/simple and
559+ receivers/stream, require unstructured data in the POST body
560+ and all metadata passed as GET style arguments. If you provide
561+ a ``body`` argument to ``post``, it will be used as the POST
562+ body, and all other keyword arguments will be passed as
563+ GET-style arguments in the URL.
564+
543565 :raises AuthenticationError: when a the ``Context`` is not logged in.
544566 :raises HTTPError: when there was an error in POSTing to *path_segment*.
545567 :param path_segment: A path_segment to POST to.
546568 :type path_segment: string
547569 :param owner, app, sharing: Namespace parameters (optional).
548570 :type owner, app, sharing: string
571+ :param headers: A dict or list of (key,value) pairs to use as headers for
572+ this request.
549573 :param query: All other keyword arguments, used as query parameters.
550574 :type query: values should be strings
551575 :return: The server's response.
@@ -576,11 +600,18 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
576600 path = self .authority + self ._abspath (path_segment , owner = owner ,
577601 app = app , sharing = sharing )
578602 logging .debug ("POST request to %s (body: %s)" , path , repr (query ))
579- with log_duration ():
580- response = self .http .post (path , self ._auth_headers , ** query )
603+ if isinstance (headers , dict ):
604+ all_headers = [(k ,v ) for k ,v in headers .iteritems ()]
605+ elif isinstance (headers , list ):
606+ all_headers = headers
607+ else :
608+ raise ValueError ("headers must be a list or dict (found: %s)" % headers )
609+ all_headers += self ._auth_headers
610+ response = self .http .post (path , all_headers , ** query )
581611 return response
582612
583613 @_authentication
614+ @log_duration
584615 def request (self , path_segment , method = "GET" , headers = [], body = "" ,
585616 owner = None , app = None , sharing = None ):
586617 """Issue an arbitrary HTTP request to *path_segment*.
@@ -643,11 +674,10 @@ def request(self, path_segment, method="GET", headers=[], body="",
643674 all_headers = headers + self ._auth_headers
644675 logging .debug ("%s request to %s (headers: %s, body: %s)" ,
645676 method , path , str (all_headers ), repr (body ))
646- with log_duration ():
647- response = self .http .request (path ,
648- {'method' : method ,
649- 'headers' : all_headers ,
650- 'body' : body })
677+ response = self .http .request (path ,
678+ {'method' : method ,
679+ 'headers' : all_headers ,
680+ 'body' : body })
651681 return response
652682
653683 def login (self ):
@@ -669,6 +699,12 @@ def login(self):
669699 c = binding.Context(...).login()
670700 # Then issue requests...
671701 """
702+ if self .token is not NoAuthenticationToken and \
703+ (self .username and self .password ):
704+ # If we were passed a session token, but no username or
705+ # password, then login is a nop, since we're automatically
706+ # logged in.
707+ return
672708 try :
673709 response = self .http .post (
674710 self .authority + self ._abspath ("/services/auth/login" ),
@@ -686,7 +722,7 @@ def login(self):
686722
687723 def logout (self ):
688724 """Forget the current session token."""
689- self .token = None
725+ self .token = NoAuthenticationToken
690726 return self
691727
692728 def _abspath (self , path_segment ,
@@ -744,8 +780,8 @@ def _abspath(self, path_segment,
744780 if ns .app is None and ns .owner is None :
745781 return UrlEncoded ("/services/%s" % path_segment , skip_encode = skip_encode )
746782
747- oname = "- " if ns .owner is None else ns .owner
748- aname = "- " if ns .app is None else ns .app
783+ oname = "nobody " if ns .owner is None else ns .owner
784+ aname = "system " if ns .app is None else ns .app
749785 path = UrlEncoded ("/servicesNS/%s/%s/%s" % (oname , aname , path_segment ),
750786 skip_encode = skip_encode )
751787 return path
@@ -788,7 +824,9 @@ def connect(**kwargs):
788824 c = binding.connect(...)
789825 response = c.get("apps/local")
790826 """
791- return Context (** kwargs ).login ()
827+ c = Context (** kwargs )
828+ c .login ()
829+ return c
792830
793831# Note: the error response schema supports multiple messages but we only
794832# return the first, although we do return the body so that an exception
@@ -883,10 +921,18 @@ def get(self, url, headers=None, **kwargs):
883921 def post (self , url , headers = None , ** kwargs ):
884922 if headers is None : headers = []
885923 headers .append (("Content-Type" , "application/x-www-form-urlencoded" )),
924+ # We handle GET-style arguments and an unstructured body. This is here
925+ # to support the receivers/stream endpoint.
926+ if 'body' in kwargs :
927+ body = kwargs .pop ('body' )
928+ if len (kwargs ) > 0 :
929+ url = url + UrlEncoded ('?' + encode (** kwargs ), skip_encode = True )
930+ else :
931+ body = encode (** kwargs )
886932 message = {
887933 'method' : "POST" ,
888934 'headers' : headers ,
889- 'body' : encode ( ** kwargs )
935+ 'body' : body
890936 }
891937 return self .request (url , message )
892938
0 commit comments