"""
Interface with the Splunk search API.
.. important::
This module requires the general :ref:`Splunk setup <splunk-setup>`.
"""
import logging
import urllib.parse
import salt.utils.yaml
from salt.utils.odict import OrderedDict
HAS_LIBS = False
try:
import requests
import splunklib.client
HAS_LIBS = True
except ImportError:
pass
log = logging.getLogger(__name__)
# Don't shadow built-in's.
__func_alias__ = {"list_": "list"}
__virtualname__ = "splunk_search"
def __virtual__():
if HAS_LIBS:
return __virtualname__
return (
False,
"The splunk_search execution module failed to load: "
"requires both the requests and the splunk-sdk python library to be installed.",
)
def _get_splunk(profile):
"""
Return the splunk client, cached into __context__ for performance
"""
config = __salt__["config.option"](profile)
key = "splunk_search.{}:{}:{}:{}".format(
config.get("host"),
config.get("port"),
config.get("username"),
config.get("password"),
)
if key not in __context__:
__context__[key] = splunklib.client.connect(
host=config.get("host"),
port=config.get("port"),
username=config.get("username"),
password=config.get("password"),
)
return __context__[key]
def _get_splunk_search_props(search):
"""
Get splunk search properties from an object
"""
props = search.content
props["app"] = search.access.app
props["sharing"] = search.access.sharing
return props
[docs]
def get(name, profile="splunk"):
"""
Get a splunk search
CLI Example:
splunk_search.get 'my search name'
"""
client = _get_splunk(profile)
search = None
# uglyness of splunk lib
try:
search = client.saved_searches[name]
except KeyError:
pass
return search
[docs]
def update(name, profile="splunk", **kwargs):
"""
Update a splunk search
CLI Example:
splunk_search.update 'my search name' sharing=app
"""
client = _get_splunk(profile)
search = client.saved_searches[name]
props = _get_splunk_search_props(search)
updates = kwargs
update_needed = False
update_set = {}
diffs = []
for key in sorted(kwargs):
old_value = props.get(key, None)
new_value = updates.get(key, None)
if isinstance(old_value, str):
old_value = old_value.strip()
if isinstance(new_value, str):
new_value = new_value.strip()
if old_value != new_value:
update_set[key] = new_value
update_needed = True
diffs.append(f"{key}: '{old_value}' => '{new_value}'")
if update_needed:
search.update(**update_set).refresh()
return update_set, diffs
return False
[docs]
def create(name, profile="splunk", **kwargs):
"""
Create a splunk search
CLI Example:
splunk_search.create 'my search name' search='error msg'
"""
client = _get_splunk(profile)
search = client.saved_searches.create(name, **kwargs)
# use the REST API to set owner and permissions
# this is hard-coded for now; all managed searches are app scope and
# readable by all
config = __salt__["config.option"](profile)
url = "https://{}:{}".format(config.get("host"), config.get("port"))
auth = (config.get("username"), config.get("password"))
data = {
"owner": config.get("username"),
"sharing": "app",
"perms.read": "*",
}
_req_url = "{}/servicesNS/{}/search/saved/searches/{}/acl".format(
url, config.get("username"), urllib.parse.quote(name)
)
requests.post(_req_url, auth=auth, verify=True, data=data, timeout=120)
return _get_splunk_search_props(search)
[docs]
def delete(name, profile="splunk"):
"""
Delete a splunk search
CLI Example:
splunk_search.delete 'my search name'
"""
client = _get_splunk(profile)
try:
client.saved_searches.delete(name)
return True
except KeyError:
return None
[docs]
def list_(profile="splunk"):
"""
List splunk searches (names only)
CLI Example:
splunk_search.list
"""
client = _get_splunk(profile)
searches = [x["name"] for x in client.saved_searches]
return searches
[docs]
def list_all(
prefix=None,
app=None,
owner=None,
description_contains=None,
name_not_contains=None,
profile="splunk",
):
"""
Get all splunk search details. Produces results that can be used to create
an sls file.
if app or owner are specified, results will be limited to matching saved
searches.
if description_contains is specified, results will be limited to those
where "description_contains in description" is true if name_not_contains is
specified, results will be limited to those where "name_not_contains not in
name" is true.
If prefix parameter is given, alarm names in the output will be prepended
with the prefix; alarms that have the prefix will be skipped. This can be
used to convert existing alarms to be managed by salt, as follows:
CLI Example:
1. Make a "backup" of all existing searches
$ salt-call splunk_search.list_all --out=txt | sed "s/local: //" > legacy_searches.sls
2. Get all searches with new prefixed names
$ salt-call splunk_search.list_all "prefix=**MANAGED BY SALT** " --out=txt | sed "s/local: //" > managed_searches.sls
3. Insert the managed searches into splunk
$ salt-call state.sls managed_searches.sls
4. Manually verify that the new searches look right
5. Delete the original searches
$ sed s/present/absent/ legacy_searches.sls > remove_legacy_searches.sls
$ salt-call state.sls remove_legacy_searches.sls
6. Get all searches again, verify no changes
$ salt-call splunk_search.list_all --out=txt | sed "s/local: //" > final_searches.sls
$ diff final_searches.sls managed_searches.sls
"""
client = _get_splunk(profile)
# splunklib doesn't provide the default settings for saved searches.
# so, in order to get the defaults, we create a search with no
# configuration, get that search, and then delete it. We use its contents
# as the default settings
name = "splunk_search.list_all get defaults"
try:
client.saved_searches.delete(name)
except Exception: # pylint: disable=broad-except
pass
search = client.saved_searches.create(name, search="nothing")
defaults = dict(search.content)
client.saved_searches.delete(name)
# stuff that splunk returns but that you should not attempt to set.
# cf http://dev.splunk.com/view/python-sdk/SP-CAAAEK2
readonly_keys = (
"triggered_alert_count",
"action.email",
"action.populate_lookup",
"action.rss",
"action.script",
"action.summary_index",
"qualifiedSearch",
"next_scheduled_time",
)
results = OrderedDict()
# sort the splunk searches by name, so we get consistent output
searches = sorted((s.name, s) for s in client.saved_searches)
for name, search in searches:
if app and search.access.app != app:
continue
if owner and search.access.owner != owner:
continue
if name_not_contains and name_not_contains in name:
continue
if prefix:
if name.startswith(prefix):
continue
name = prefix + name
# put name in the OrderedDict first
d = [{"name": name}]
# add the rest of the splunk settings, ignoring any defaults
description = ""
for k, v in sorted(search.content.items()):
if k in readonly_keys:
continue
if k.startswith("display."):
continue
if not v:
continue
if k in defaults and defaults[k] == v:
continue
d.append({k: v})
if k == "description":
description = v
if description_contains and description_contains not in description:
continue
results["manage splunk search " + name] = {"splunk_search.present": d}
return salt.utils.yaml.safe_dump(results, default_flow_style=False, width=120)