# coding=UTF-8 from __future__ import absolute_import from builtins import range import splunk from splunk import auth, entity, rest, util from future.moves.urllib import parse as urllib_parse import splunk.search # we cant import it more simply as 'search' because it collides with several 'search' arguments in these functions. import splunk.safe_lxml_etree as et import logging import splunk.util logger = logging.getLogger('splunk.saved') # Saved Search constants SAVED_SEARCHES_ENDPOINT_ENTITY_PATH = 'saved/searches' SAVED_SEARCHES_HISTORY_ENTITY_PATH = 'saved/searches/%s/history' SAVED_SEARCH_ESCAPE_CHR = u'"' SAVED_SEARCH_DISPATCH_ARG_MAP = { 'ttl': 'dispatch.ttl', 'buckets': 'dispatch.buckets', 'maxCount': 'dispatch.max_count', 'maxTime': 'dispatch.max_time', 'lookups': 'dispatch.lookups', 'spawnProcess': 'dispatch.spawn_process', 'timeFormat': 'dispatch.time_format', 'earliestTime': 'dispatch.earliest_time', 'latestTime': 'dispatch.latest_time' } # //////////////////////////////////////////////////////////////////////////// # Saved Search functions # //////////////////////////////////////////////////////////////////////////// def dispatchSavedSearch(savedSearchName, sessionKey=None, namespace=None, owner=None, hostPath=None, now=0, triggerActions=0, **kwargs): """Initiates a new job based on a saved search.""" uri = entity.buildEndpoint(['saved', 'searches', savedSearchName, 'dispatch'], namespace=namespace, owner=owner) if hostPath: uri = splunk.mergeHostPath(hostPath) + uri args = { 'now': now, 'trigger_actions' : triggerActions } for key, val in kwargs.items(): if key in SAVED_SEARCH_DISPATCH_ARG_MAP: args[SAVED_SEARCH_DISPATCH_ARG_MAP[key]] = val # Pass through for dispatch.* formated kwargs elif key.startswith('dispatch.'): args[key] = val serverResponse, serverContent = rest.simpleRequest(uri, postargs=args, sessionKey=sessionKey) root = et.fromstring(serverContent) # normal messages from splunkd are propogated via SplunkdException; if not 201 == serverResponse.status: extractedMessages = rest.extractMessages(root) for msg in extractedMessages: raise splunk.SearchException(msg['text']) # get the search ID sid = root.findtext('sid').strip() # instantiate result object return splunk.search.SearchJob(sid, hostPath, sessionKey, namespace, owner) def createSavedSearch(search, label, sessionKey=None, namespace=None, owner=None, earliestTime=None, latestTime=None, hostPath=None): '''Save a search given a search string and a search label.''' output = entity.Entity(SAVED_SEARCHES_ENDPOINT_ENTITY_PATH, label, namespace=namespace, owner=owner) # Manually set the properties... output['search'] = search output['name'] = label output['dispatch.earliest_time'] = earliestTime output['dispatch.latest_time'] = latestTime if hostPath: output.hostPath = hostPath entity.setEntity(output, sessionKey=sessionKey) return output def getSavedSearchWithTimes(label, et, lt, namespace=None, sessionKey=None, owner=None, hostPath=None): '''Retrieve a list of UTC times that a saved searches was schedule to run.''' return entity.getEntity(SAVED_SEARCHES_ENDPOINT_ENTITY_PATH, label, uri="/servicesNS/"+owner+"/"+namespace+"/saved/searches/"+urllib_parse.quote_plus(label)+"/scheduled_times", earliest_time=et, latest_time=lt, namespace=namespace, owner=owner, sessionKey=sessionKey, hostPath=hostPath) def listSavedSearches(namespace=None, sessionKey=None, owner=None, hostPath=None, count=None): '''Retrieve a list of saved searches.''' return entity.getEntities(SAVED_SEARCHES_ENDPOINT_ENTITY_PATH, namespace=namespace, owner=owner, sessionKey=sessionKey, hostPath=hostPath, count=count) def getSavedSearch(label, namespace=None, sessionKey=None, owner=None, hostPath=None, uri=None): '''Retrieve a single saved search.''' return entity.getEntity(SAVED_SEARCHES_ENDPOINT_ENTITY_PATH, label, namespace=namespace, owner=owner, sessionKey=sessionKey, hostPath=hostPath, uri=uri) def getSavedSearchHistory(label, namespace=None, sessionKey=None, ignoreExpired=True, owner=None, sortKey=None, sortDir='desc', ignoreRunning=False, search=None, hostPath=None, uri=None): ''' Retrieve the history of a saved search. ignoreExpired: {True|False} When True only return saved searches that have a TTL > 0 or they have been saved by the user. When False return everything, including searches with a TTL >= 0 regardless of the job's saved status. ''' #SPL-35662 done RT searches are alert artifacts which should be of no use to UI, # dashboards should always reference running rt searches search = (search if search is not None else '') + " NOT (isRealTimeSearch=1 AND isDone=1)" entities = entity.getEntities(['saved', 'searches', label, 'history'], namespace=namespace, owner=owner, sessionKey=sessionKey, search=search, hostPath=hostPath, count=0, uri=uri) if ignoreExpired or ignoreRunning: for sid, job in entities.items(): if ignoreExpired \ and splunk.search.normalizeJobPropertyValue('ttl', job.get('ttl')) <= 0 \ and not splunk.search.normalizeJobPropertyValue('isSaved', job.get('isSaved')): del entities[sid] continue # real time searches are never done so no need to ignore running isRTSearch = splunk.search.normalizeJobPropertyValue('isRealTimeSearch', job.get('isRealTimeSearch')) isDone = splunk.search.normalizeJobPropertyValue('isDone', job.get('isDone')) if ignoreRunning and not isDone and not isRTSearch: del entities[sid] if sortKey: reverse = True if sortDir == 'asc': reverse = False try: entities = sorted(entities.items(), key=lambda x: x[1].__dict__[sortKey], reverse=reverse) except KeyError as e: logger.warning("Attempted to sort on a key (%s) from a saved search's history that doesn't exist" % sortKey) return util.OrderedDict(entities) def getSavedSearchJobs(label, namespace=None, owner=None, ignoreExpired=True, ignoreRunning=False, sortKey='createTime', sortDir='desc', sessionKey=None, hostPath=None, **kw): '''Retrieve the saved search's history as search.SearchJob objects.''' jobs = [] history = getSavedSearchHistory(label, namespace=namespace, owner=owner, ignoreExpired=ignoreExpired, ignoreRunning=ignoreRunning, sortKey=sortKey, sortDir=sortDir, sessionKey=sessionKey, hostPath=hostPath) for sid in history: jobs.append(splunk.search.getJob(sid, hostPath=hostPath, sessionKey=sessionKey)) return jobs def getJobForSavedSearch(label, useHistory=None, namespace=None, sessionKey=None, ignoreExpired=True, owner=None, ignoreRunning=True, sortKey='createTime', sortDir='desc', search=None, hostPath=None, **kw): ''' Retrieve the last job run for a saved search. == WARNING == This is meant to be a convenience method for accessing jobs from saved searches that are typically run by the splunkd scheduler. As such dispatching a job from a saved search and then attempting to immediately call getJobForSavedSearch with the param ignoreRunning == True will result in a second job being dispatched. == / WARNING == useHistory dictates how getJobForSavedSearch attempts to fetch a job. useHistory=None implies that if the saved search has a history of jobs relevant to the saved search, it will return the last run saved search. If no jobs can be found a new one will be dispatched and returned. useHistory=True implies that the last run job for the saved search will be returned. If no jobs exist None will be returned instead. useHistory=False is effectively the same as calling dispatchSavedSearch(label) in that it does not check for a previously run job, and instead forces a new job to be created and returned. This option is left for convenience. ''' job = None useHistory = util.normalizeBoolean(useHistory) if isinstance(useHistory, splunk.util.string_type): #verified while fixing SPL-47422 #pylint: disable=E1103 if useHistory.lower() in ('none', 'auto'): useHistory = None else: raise ValueError('Invalid option passed for useHistory: %s' % useHistory) logger.debug('getJobForSavedSearch - label=%s namespace=%s owner=%s' % (label, namespace, owner)) # Attempt to get the saved search history if useHistory is None or useHistory is True: history = getSavedSearchHistory(label, namespace=namespace, sessionKey=sessionKey, ignoreExpired=ignoreExpired, owner=owner, ignoreRunning=ignoreRunning, sortKey=sortKey, sortDir=sortDir, search=search, hostPath=hostPath, uri=kw.get('historyURI')) if len(history) > 0: job = splunk.search.getJob(list(history.keys())[0], hostPath=hostPath, sessionKey=sessionKey) logger.debug('getJobForSavedSearch - found job artifact sid=%s' % job.id) # Dispatch a new search if there is no history for the search if (useHistory is False) or (useHistory is None and job is None): logger.debug('getJobForSavedSearch - no artifact found; dispatching new job') job = dispatchSavedSearch(label, sessionKey=sessionKey, namespace=namespace, owner=owner, hostPath=hostPath, **kw) # If the user specified useHistory = yes and no history was found, this may return None return job def deleteSavedSearch(label, namespace=None, sessionKey=None, owner=None, hostPath=None): '''Delete a saved search.''' return entity.deleteEntity(SAVED_SEARCHES_ENDPOINT_ENTITY_PATH, label, namespace=namespace, owner=owner, sessionKey=sessionKey, hostPath=hostPath) def getSavedSearchFromSID(sid, sessionKey=None, hostPath=None): ''' Takes a search job id and attempts to find the associated saved search object, if set. Returns a splunk.entity.Entity() object if found, None otherwise. ''' job = splunk.search.getJob(sid, sessionKey=sessionKey, hostPath=hostPath) # the eai key contains a : which makes python accessors unhappy; use alt means jobProps = job.toJsonable() namespace = jobProps['eai:acl']['app'] owner = jobProps['eai:acl']['owner'] if job.isSavedSearch and len(job.label) > 0: # first try to fetch saved search from explicit user container try: return getSavedSearch(job.label, namespace=namespace, owner=owner, sessionKey=sessionKey, hostPath=hostPath) except: pass # if fail, try from shared context try: return getSavedSearch(job.label, namespace=namespace, owner=entity.EMPTY_OWNER_NAME, sessionKey=sessionKey, hostPath=hostPath) except splunk.ResourceNotFound: pass # else raise any other exception return None def savedSearchJSONIsAlert(savedSearchJSON): content = savedSearchJSON['entry'][0]['content'] is_scheduled = util.normalizeBoolean(content['is_scheduled']) alert_type = content['alert_type'] alert_track = util.normalizeBoolean(content['alert.track']) actions = util.normalizeBoolean(content.get('actions')) isRealtime = content['dispatch.earliest_time'].startswith('rt') and content['dispatch.latest_time'].startswith('rt') return is_scheduled and ((alert_type != 'always') or alert_track or (isRealtime and actions))