6262import datetime
6363import json
6464import logging
65+ import re
6566import socket
6667from datetime import datetime , timedelta
6768from time import sleep
99100PATH_INDEXES = "data/indexes/"
100101PATH_INPUTS = "data/inputs/"
101102PATH_JOBS = "search/jobs/"
103+ PATH_JOBS_V2 = "search/v2/jobs/"
102104PATH_LOGGER = "/services/server/logger/"
103105PATH_MESSAGES = "messages/"
104106PATH_MODULAR_INPUTS = "data/modular-inputs"
@@ -419,6 +421,7 @@ def __init__(self, **kwargs):
419421 super (Service , self ).__init__ (** kwargs )
420422 self ._splunk_version = None
421423 self ._kvstore_owner = None
424+ self ._instance_type = None
422425
423426 @property
424427 def apps (self ):
@@ -570,6 +573,8 @@ def parse(self, query, **kwargs):
570573 :type kwargs: ``dict``
571574 :return: A semantic map of the parsed search query.
572575 """
576+ if not self .disable_v2_api :
577+ return self .post ("search/v2/parser" , q = query , ** kwargs )
573578 return self .get ("search/parser" , q = query , ** kwargs )
574579
575580 def restart (self , timeout = None ):
@@ -691,6 +696,22 @@ def splunk_version(self):
691696 self ._splunk_version = tuple ([int (p ) for p in self .info ['version' ].split ('.' )])
692697 return self ._splunk_version
693698
699+ @property
700+ def splunk_instance (self ):
701+ if self ._instance_type is None :
702+ splunk_info = self .info ;
703+ if hasattr (splunk_info , 'instance_type' ) :
704+ self ._instance_type = splunk_info ['instance_type' ]
705+ else :
706+ self ._instance_type = ''
707+ return self ._instance_type
708+
709+ @property
710+ def disable_v2_api (self ):
711+ if self .splunk_instance .lower () == 'cloud' :
712+ return self .splunk_version < (9 ,0 ,2209 )
713+ return self .splunk_version < (9 ,0 ,2 )
714+
694715 @property
695716 def kvstore_owner (self ):
696717 """Returns the KVStore owner for this instance of Splunk.
@@ -741,6 +762,25 @@ def __init__(self, service, path):
741762 self .service = service
742763 self .path = path
743764
765+ def get_api_version (self , path ):
766+ """Return the API version of the service used in the provided path.
767+
768+ Args:
769+ path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").
770+
771+ Returns:
772+ int: Version of the API (for example, 1)
773+ """
774+ # Default to v1 if undefined in the path
775+ # For example, "/services/search/jobs" is using API v1
776+ api_version = 1
777+
778+ versionSearch = re .search ('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/' , path )
779+ if versionSearch :
780+ api_version = int (versionSearch .group (1 ))
781+
782+ return api_version
783+
744784 def get (self , path_segment = "" , owner = None , app = None , sharing = None , ** query ):
745785 """Performs a GET operation on the path segment relative to this endpoint.
746786
@@ -803,6 +843,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
803843 app = app , sharing = sharing )
804844 # ^-- This was "%s%s" % (self.path, path_segment).
805845 # That doesn't work, because self.path may be UrlEncoded.
846+
847+ # Get the API version from the path
848+ api_version = self .get_api_version (path )
849+
850+ # Search API v2+ fallback to v1:
851+ # - In v2+, /results_preview, /events and /results do not support search params.
852+ # - Fallback from v2+ to v1 if Splunk Version is < 9.
853+ # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
854+ # path = path.replace(PATH_JOBS_V2, PATH_JOBS)
855+
856+ if api_version == 1 :
857+ if isinstance (path , UrlEncoded ):
858+ path = UrlEncoded (path .replace (PATH_JOBS_V2 , PATH_JOBS ), skip_encode = True )
859+ else :
860+ path = path .replace (PATH_JOBS_V2 , PATH_JOBS )
861+
806862 return self .service .get (path ,
807863 owner = owner , app = app , sharing = sharing ,
808864 ** query )
@@ -855,13 +911,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
855911 apps.get('nonexistant/path') # raises HTTPError
856912 s.logout()
857913 apps.get() # raises AuthenticationError
858- """
914+ """
859915 if path_segment .startswith ('/' ):
860916 path = path_segment
861917 else :
862918 if not self .path .endswith ('/' ) and path_segment != "" :
863919 self .path = self .path + '/'
864920 path = self .service ._abspath (self .path + path_segment , owner = owner , app = app , sharing = sharing )
921+
922+ # Get the API version from the path
923+ api_version = self .get_api_version (path )
924+
925+ # Search API v2+ fallback to v1:
926+ # - In v2+, /results_preview, /events and /results do not support search params.
927+ # - Fallback from v2+ to v1 if Splunk Version is < 9.
928+ # if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
929+ # path = path.replace(PATH_JOBS_V2, PATH_JOBS)
930+
931+ if api_version == 1 :
932+ if isinstance (path , UrlEncoded ):
933+ path = UrlEncoded (path .replace (PATH_JOBS_V2 , PATH_JOBS ), skip_encode = True )
934+ else :
935+ path = path .replace (PATH_JOBS_V2 , PATH_JOBS )
936+
865937 return self .service .post (path , owner = owner , app = app , sharing = sharing , ** query )
866938
867939
@@ -1846,8 +1918,6 @@ class StoragePasswords(Collection):
18461918 instance. Retrieve this collection using :meth:`Service.storage_passwords`.
18471919 """
18481920 def __init__ (self , service ):
1849- if service .namespace .owner == '-' or service .namespace .app == '-' :
1850- raise ValueError ("StoragePasswords cannot have wildcards in namespace." )
18511921 super (StoragePasswords , self ).__init__ (service , PATH_STORAGE_PASSWORDS , item = StoragePassword )
18521922
18531923 def create (self , password , username , realm = None ):
@@ -1865,6 +1935,9 @@ def create(self, password, username, realm=None):
18651935
18661936 :return: The :class:`StoragePassword` object created.
18671937 """
1938+ if self .service .namespace .owner == '-' or self .service .namespace .app == '-' :
1939+ raise ValueError ("While creating StoragePasswords, namespace cannot have wildcards." )
1940+
18681941 if not isinstance (username , six .string_types ):
18691942 raise ValueError ("Invalid name: %s" % repr (username ))
18701943
@@ -1896,6 +1969,9 @@ def delete(self, username, realm=None):
18961969 :return: The `StoragePassword` collection.
18971970 :rtype: ``self``
18981971 """
1972+ if self .service .namespace .owner == '-' or self .service .namespace .app == '-' :
1973+ raise ValueError ("app context must be specified when removing a password." )
1974+
18991975 if realm is None :
19001976 # This case makes the username optional, so
19011977 # the full name can be passed in as realm.
@@ -2660,7 +2736,14 @@ def oneshot(self, path, **kwargs):
26602736class Job (Entity ):
26612737 """This class represents a search job."""
26622738 def __init__ (self , service , sid , ** kwargs ):
2663- path = PATH_JOBS + sid
2739+ # Default to v2 in Splunk Version 9+
2740+ path = "{path}{sid}"
2741+ # Formatting path based on the Splunk Version
2742+ if service .disable_v2_api :
2743+ path = path .format (path = PATH_JOBS , sid = sid )
2744+ else :
2745+ path = path .format (path = PATH_JOBS_V2 , sid = sid )
2746+
26642747 Entity .__init__ (self , service , path , skip_refresh = True , ** kwargs )
26652748 self .sid = sid
26662749
@@ -2714,7 +2797,11 @@ def events(self, **kwargs):
27142797 :return: The ``InputStream`` IO handle to this job's events.
27152798 """
27162799 kwargs ['segmentation' ] = kwargs .get ('segmentation' , 'none' )
2717- return self .get ("events" , ** kwargs ).body
2800+
2801+ # Search API v1(GET) and v2(POST)
2802+ if self .service .disable_v2_api :
2803+ return self .get ("events" , ** kwargs ).body
2804+ return self .post ("events" , ** kwargs ).body
27182805
27192806 def finalize (self ):
27202807 """Stops the job and provides intermediate results for retrieval.
@@ -2802,7 +2889,11 @@ def results(self, **query_params):
28022889 :return: The ``InputStream`` IO handle to this job's results.
28032890 """
28042891 query_params ['segmentation' ] = query_params .get ('segmentation' , 'none' )
2805- return self .get ("results" , ** query_params ).body
2892+
2893+ # Search API v1(GET) and v2(POST)
2894+ if self .service .disable_v2_api :
2895+ return self .get ("results" , ** query_params ).body
2896+ return self .post ("results" , ** query_params ).body
28062897
28072898 def preview (self , ** query_params ):
28082899 """Returns a streaming handle to this job's preview search results.
@@ -2843,7 +2934,11 @@ def preview(self, **query_params):
28432934 :return: The ``InputStream`` IO handle to this job's preview results.
28442935 """
28452936 query_params ['segmentation' ] = query_params .get ('segmentation' , 'none' )
2846- return self .get ("results_preview" , ** query_params ).body
2937+
2938+ # Search API v1(GET) and v2(POST)
2939+ if self .service .disable_v2_api :
2940+ return self .get ("results_preview" , ** query_params ).body
2941+ return self .post ("results_preview" , ** query_params ).body
28472942
28482943 def searchlog (self , ** kwargs ):
28492944 """Returns a streaming handle to this job's search log.
@@ -2932,7 +3027,12 @@ class Jobs(Collection):
29323027 """This class represents a collection of search jobs. Retrieve this
29333028 collection using :meth:`Service.jobs`."""
29343029 def __init__ (self , service ):
2935- Collection .__init__ (self , service , PATH_JOBS , item = Job )
3030+ # Splunk 9 introduces the v2 endpoint
3031+ if not service .disable_v2_api :
3032+ path = PATH_JOBS_V2
3033+ else :
3034+ path = PATH_JOBS
3035+ Collection .__init__ (self , service , path , item = Job )
29363036 # The count value to say list all the contents of this
29373037 # Collection is 0, not -1 as it is on most.
29383038 self .null_count = 0
@@ -3208,12 +3308,15 @@ def fired_alerts(self):
32083308 item = AlertGroup )
32093309 return c
32103310
3211- def history (self ):
3311+ def history (self , ** kwargs ):
32123312 """Returns a list of search jobs corresponding to this saved search.
32133313
3314+ :param `kwargs`: Additional arguments (optional).
3315+ :type kwargs: ``dict``
3316+
32143317 :return: A list of :class:`Job` objects.
32153318 """
3216- response = self .get ("history" )
3319+ response = self .get ("history" , ** kwargs )
32173320 entries = _load_atom_entries (response )
32183321 if entries is None : return []
32193322 jobs = []
@@ -3770,4 +3873,4 @@ def batch_save(self, *documents):
37703873
37713874 data = json .dumps (documents )
37723875
3773- return json .loads (self ._post ('batch_save' , headers = KVStoreCollectionData .JSON_HEADER , body = data ).body .read ().decode ('utf-8' ))
3876+ return json .loads (self ._post ('batch_save' , headers = KVStoreCollectionData .JSON_HEADER , body = data ).body .read ().decode ('utf-8' ))
0 commit comments