from __future__ import print_function
from builtins import object
import os
import struct
import sys
import types
import traceback
import splunk.safe_lxml_etree as et
import splunk
import splunk.util
from splunk.persistconn.packet import PersistentServerConnectionPacketParser
import __main__
def usage():
raise UsageException("Usage: %s [%s]. (Got: %s)" % (sys.argv[0], str.join(str(" | "), ARGS_LIST), sys.argv))
############################## Constants ##
ACTION_CREATE = 1
ACTION_LIST = 2
ACTION_EDIT = 4
ACTION_REMOVE = 8
ACTION_MEMBERS = 16
ACTION_RELOAD = 32
CONTEXT_NONE = 1
CONTEXT_APP_ONLY = 2
CONTEXT_APP_AND_USER = 3
ARG_EXECUTE = "execute"
ARG_PERSISTENT = "persistent"
ARG_SETUP = "setup"
ARGS_LIST = (ARG_SETUP, ARG_EXECUTE, ARG_PERSISTENT)
# TODO: macro this.
EAI_META_PREFIX = "eai:"
EAI_ENTRY_ACL = "eai:acl"
CAP_BYPASS = "bypass_capability_check"
CAP_NONE = "allow_access_to_all"
ADMIN_ALL_OBJECTS = "admin_all_objects"
ATTR_DATATYPE = "datatype"
ATOMTYPE_UNSET = "unset"
ATOMTYPE_STRING = "string"
ATOMTYPE_BOOL = "boolean"
ATOMTYPE_NUMBER = "number"
ATOMTYPE_NULL = "null"
def stdout_write(payload):
if sys.version_info > (3, 0) and isinstance(payload, bytes):
sys.stdout.buffer.write(payload)
sys.stdout.buffer.write(b'\n')
else:
sys.stdout.write(payload)
sys.stdout.write('\n')
############################## Exceptions ##
class AdminManagerException(Exception):
def __init__(self, msg):
# SPL-189027 and SPL-180340
# Make sure messages are passed as regular string instead of
# Byte string for Pyhon 3. For Python 2, both should work.
Exception.__init__(self, splunk.util.toDefaultStrings(msg))
# NOTE: if you add something here, also add it to AdminManagerExternal.cpp's rethrow_python_exception().
class AlreadyExistsException( AdminManagerException): pass
class NotFoundException( AdminManagerException): pass
class ArgValidationException( AdminManagerException): pass
class BadActionException( AdminManagerException): pass
class BadProgrammerException( AdminManagerException): pass
class HandlerSetupException( AdminManagerException): pass
class InternalException( AdminManagerException): pass
class PermissionsException( AdminManagerException): pass
class UsageException( AdminManagerException): pass
class ServiceUnavailableException(AdminManagerException): pass
############################## Entry point ##
def handleException(e, exc_info, traceback):
exType, exMsg, exBT = exc_info
#
root = et.Element("eai_error");
# [true|false]
knownNode = et.SubElement(root, "recognized")
knownNode.text = isinstance(e, AdminManagerException) and "true" or "false"
# [exception class]
typeNode = et.SubElement(root, "type");
typeNode.text = splunk.util.unicode(exType)
# [exception value]
msgNode = et.SubElement(root, "message");
msgNode.text = splunk.util.unicode(exMsg)
# [bt]
stackNode = et.SubElement(root, "stacktrace");
stackNode.text = traceback
#
return et.tostring(root)
def init_persistent(handler, ctxInfo, data):
mode = ARG_PERSISTENT
try:
hand = handler(mode, ctxInfo, data)
if hand.isSetup():
hand.setup()
return hand.toXml(ConfigInfo())
else:
info = ConfigInfo()
hand.execute(info)
return hand.toXml(info)
except Exception as e:
return handleException(e, sys.exc_info(), traceback.format_exc())
class PersistentHandler(PersistentServerConnectionPacketParser):
def __init__(self, handler, ctxInfo):
self._handler = handler
self._ctxInfo = ctxInfo
super(PersistentHandler, self).__init__()
def handle_packet(self, in_packet):
if in_packet.is_first():
self.write("") # Empty error string indicates success
if in_packet.has_block():
reply = init_persistent(self._handler, self._ctxInfo,
in_packet.block)
if reply:
self.write(reply)
def init(handler, ctxInfo):
try:
# TODO: handle dependence on sys.argv intelligently for testing/etc imports.
mode = ((len(sys.argv) > 1) and sys.argv[1] in ARGS_LIST) and sys.argv[1] or usage()
if mode == ARG_PERSISTENT:
h = PersistentHandler(handler, ctxInfo)
h.run()
return
isSetup = False
if ARG_EXECUTE == mode:
hand = handler(mode, ctxInfo)
info = ConfigInfo()
hand.execute(info)
stdout_write(hand.toXml(info))
elif ARG_SETUP == mode:
hand = handler(mode, ctxInfo)
hand.setup()
stdout_write(hand.toXml(ConfigInfo()))
else:
raise InternalException("Can't get here, boss (%s)." % mode)
except Exception as e:
stdout_write(handleException(e, sys.exc_info(), traceback.format_exc()))
############################## Helpers ##
class AdminTyper(object):
types = {}
def add(self, key, itemType):
self.types[key] = itemType
def get(self, key):
if key in self.types:
return self.types[key]
return str
class ArgsList(object):
def __init__(self):
self.data = {}
for member in dir(self.data):
if not member in ("__class__", "__getitem__", "__setitem__"):
setattr(self, member, getattr(self.data, member))
def __iter__(self):
return iter(self.data)
def __getitem__(self, key):
return self.data[key]
def __contains__(self, key):
return key in self.data
def __len__(self):
return len(self.data)
def __setitem__(self, key, val):
if key in self.data:
del(self.data[key])
self.append(key, val)
def append(self, key, val):
if isinstance(val, list):
if not key in self.data:
self.data[key] = val
else:
self.data[key] += val
else:
self.data[key] = val
def datatypeFromNative(self, item):
"""Return a datatype string based on item's Python datatype. Our datatypes follow
the JSON model and are thus restricted to string/boolean/number/null."""
if item == None:
return ATOMTYPE_NULL
# basestring to also support unicode (SPL-60776).
if isinstance(item, splunk.util.string_type):
return ATOMTYPE_STRING
# SPL-140511 - Fundamentally, JSON bool type was never interpreted correctly, since integer eval
# happened before... this change just sets evaluating bool before int.
if isinstance(item, bool):
return ATOMTYPE_BOOL
if isinstance(item, (int, float)):
return ATOMTYPE_NUMBER
if sys.version_info < (3, 0) and isinstance(item, long):
return ATOMTYPE_NUMBER
return ATOMTYPE_UNSET
def toXml(self, parent):
for k, v in list(self.data.items()):
if isinstance(v, list):
# ex1: - val1
- val2
# ex2: empty list: ...note absence of datatype.
itemList = et.SubElement(parent, "attrib", type = "list")
itemList.attrib["name"] = k
# peek at the first item and get the datatype for this list. the datatype is
# per-list, not per-item, so we base it on the first one.
if (0 != len(v)):
itemList.attrib[ATTR_DATATYPE] = self.datatypeFromNative(v[0])
for oneV in v:
item = et.SubElement(itemList, "item")
item.text = splunk.util.unicode(oneV)
else:
# ex: value
item = et.SubElement(parent, "attrib", type = "item")
item.attrib["name"] = k
item.attrib[ATTR_DATATYPE] = self.datatypeFromNative(v)
item.text = splunk.util.unicode(v)
class ArgsInfo(ArgsList):
def __init__(self):
self.id = ""
ArgsList.__init__(self)
def toXml(self, parent):
id = et.SubElement(parent, "id")
id.text = self.id
args = et.SubElement(parent, "args")
ArgsList.toXml(self, args)
class ConfigItem(ArgsList):
def __init__(self):
self.actions = ~ACTION_CREATE & ~ACTION_MEMBERS # default: all but create/members.
self.atomId = ""
self.author = ""
self._customActions = []
self._metadata = {}
self.timePublished = 0
self.timeUpdated = 0
ArgsList.__init__(self)
# whether to show 'members' link in results.
def hasMembers(self):
return self.actions & ACTION_MEMBERS
def setHasMembers(self, canHas):
if canHas:
self.actions |= ACTION_MEMBERS
else:
self.actions &= ~ACTION_MEMBERS
def copyMetadata(self, src):
for k, v in list(src.items()):
if k.startswith(EAI_META_PREFIX):
self.setMetadata(k, v)
def setMetadata(self, key, value):
self._metadata[key] = value
def removeAllActions(self):
self.actions = 0
def addCustomAction(self, ca):
self._customActions.append(ca)
def toXml(self, parent):
metadataNode = et.SubElement(parent, "metadata")
# serialize acl & related metadata.
if EAI_ENTRY_ACL in self._metadata:
# {'eai:acl': {'sharing': 'app', 'perms': {'read': ['*'], 'write': ['*']}, 'app': 'windows',
# 'modifiable': 'true', 'can_write': 'true', 'owner': 'nobody'},
# 'eai:userName': 'nobody', 'eai:appName': 'windows'}
aclNode = et.SubElement(metadataNode, "acl")
settings = self._metadata[EAI_ENTRY_ACL]
for item in ("sharing", "app", "modifiable", "owner", "removable"):
if not item in settings:
continue
tmpNode = et.SubElement(aclNode, item)
tmpNode.text = splunk.util.unicode(settings[item])
# TODO: this could be a little more resilient? unlikely to fail, though.
if "perms" in settings:
permsListNode = et.SubElement(aclNode, "perms")
if settings["perms"] is not None:
for perm, permRoles in list(settings["perms"].items()):
permNode = et.SubElement(permsListNode, "perm")
permNode.attrib["name"] = perm
for role in permRoles:
roleNode = et.SubElement(permNode, "role")
roleNode.text = role
# serialize custom actions.
if len(self._customActions) > 0:
caRootNode = et.SubElement(parent, "customActions")
for actName in self._customActions:
actNode = et.SubElement(caRootNode, "action")
actNameNode = et.SubElement(actNode, "name")
actNameNode.text = splunk.util.unicode(actName)
#add actions
actionNode = et.SubElement(parent, "actions")
actionNode.text = str(self.actions)
# serialize remaining (non-underscore fields).
memberList = dir(self)
dataNode = et.SubElement(parent, "data")
ArgsList.toXml(self, dataNode)
memberList.remove("data")
for item in memberList:
if not item.startswith("_"):
member = getattr(self, item)
if not type(member) in (types.BuiltinMethodType, types.MethodType):
tmp = et.SubElement(parent, "attrib")
tmp.attrib["name"] = item
tmp.text = splunk.util.unicode(getattr(self, item))
class ConfigInfo(object):
def __init__(self):
self.feed_name = ""
self.data = {}
for member in dir(self.data):
if not member in ("__class__", "__getitem__", "__setitem__"):
setattr(self, member, getattr(self.data, member))
self.messages = []
def __iter__(self):
return iter(self.data)
def __getitem__(self, key):
if not key in self.data:
self.data[key] = ConfigItem()
return self.data[key]
def __contains__(self, key):
return key in self.data
def __len__(self):
return len(self.data)
def __setitem__(self, item, keyValPair):
if 2 != len(keyValPair):
raise BadProgrammerException("Value of ConfigInfo key should be key/value tuple (is: %s)." % splunk.util.unicode(keyValPair))
self.data[item] = ConfigItem()
self.data[item].append(keyValPair[0], keyValPair[1])
def copyMetadata(self, src, transformer = None):
"""
Copy EAI metadata from a dictionary in the format:
{"item1": {"foo" : bar,
"eai:data1" : "val",
"eai:foo2" : "val"},
"item2": ...}
"""
for name, settings in list(src.items()):
if transformer:
name = transformer(name)
# only copy metadata for items we have?
if not name in self.data:
continue
self[name].copyMetadata(settings)
def mergeFrom(self, srcConfInfo):
self.data.update(srcConfInfo.data)
def addInfoMsg(self, msg):
self.messages.append({"text": msg, "type": "INFO"})
def addWarnMsg(self, msg):
self.messages.append({"text": msg, "type": "WARN"})
def addErrorMsg(self, msg):
self.messages.append({"text": msg, "type": "ERROR"})
def addDeprecationMsg(self):
self.addWarnMsg("This endpoint has been deprecated")
def toXml(self, parent):
feedNode = et.SubElement(parent, "feed_name")
feedNode.text = self.feed_name
if len(self.messages) > 0:
for msg in self.messages:
msgNode = et.SubElement(parent, "message")
msgNode.attrib["text"] = msg["text"]
msgNode.attrib["type"] = msg["type"]
for k, v in list(self.data.items()):
tmpNode = et.SubElement(parent, "item")
tmpNode.attrib["name"] = k
v.toXml(tmpNode) # calls ArgsList.toXml().
class ArgSpecList(object):
def __init__(self):
self.args = []
def addOptArg(self, argName):
if argName.startswith("_"):
raise HandlerSetupException("Parameters beginning with '_' are reserved for EAI's usage.")
arg = self.ArgSpec()
arg.argName = argName
arg.isRequired = False
self.args.append(arg)
return arg
def addReqArg(self, argName):
if argName.startswith("_"):
raise HandlerSetupException("Parameters beginning with '_' are reserved for EAI's usage.")
arg = self.ArgSpec()
arg.argName = argName
arg.isRequired = True
self.args.append(arg)
return arg
class ArgSpec(object):
argsDependedOn = []
argName = []
isRequired = False
def dependsOn(self, argName):
self.argsDependedOn.append(argName);
def isWildcarded(self):
return self.argName.contains('*');
def toXml(self, parent):
for spec in self.args:
# can't have * in xml tag names...
tmp = et.SubElement(parent, "arg")
tmp.attrib["name"] = spec.argName
isReq = et.SubElement(tmp, "isRequired")
isReq.text = splunk.util.unicode(spec.isRequired)
if len(spec.argsDependedOn) > 0:
dependsOn = et.SubElement(tmp, "argsDependedOn");
for oneDep in spec.argsDependedOn:
dep = et.SubElement(dependsOn, "dep")
dep.text = oneDep
############################## Handler base class ##
class MConfigHandler(object):
"""
Base class for all EAI handlers to implement.
"""
def __init__(self, scriptMode, ctxInfo, request=None):
self.appName = "" # useful for external handlers to save data.
self.callerArgs = ArgsInfo() # args passed in from caller.
self.customAction = "" # set if /handler// was hit.
self.customActionCap = "" # required capability for current custAct, if any.
self.didFilter = False # did the handler filter on its own?
self.didPaginate = False # did the handler paginate on its own?
self.docShowEntry = True # whether to gen docs for this action ctxt.
self.maxCount = 0 # max items to return to caller.
self.posOffset = 0 # offset from beginning of results.
self.requestedAction = 0 # action requested by the caller.
self.requestedFilters = None # any number of filters specified by caller.
self.restartRequired = False # should server be restarted once handler is done?
self.shouldAutoList = True # should handleList be called after create/edit?
self.shouldFilter = False # did caller request filtering?
self.sortAscending = True # false == sort descending.
self.sortByKey = "" # in a given set of config objects, which key to sort by.
self.supportedArgs = ArgSpecList() # args supported by this particular handler.
self.shouldReload = False # call handleReload after handler finishes?
self.userName = "" # useful for external handlers to save data.
self.capabilityRead = ""
self.capabilityWrite = ""
self.context = ctxInfo
self._mode = scriptMode
#pylint: disable=E1103
if sys.stdin.closed:
return; # hope you know what you're doing - this better be for testing or something!
if sys.version_info >= (3, 0):
if request is None:
dataFromSplunkd = sys.stdin.buffer.read()
elif isinstance(request, str):
dataFromSplunkd = request.encode()
else:
dataFromSplunkd = request
elif request is None:
dataFromSplunkd = sys.stdin.read()
else:
dataFromSplunkd = request
if 0 == len(dataFromSplunkd):
raise UsageException("Received no serialized data via stdin (mode: %s). Will not continue." % self._mode)
### open("/tmp/lolwut", "w").write("""
### "------------------data from splunkd---------------------"
### %s
### "--------------------------------------------------------"
### """ % dataFromSplunkd)
# TODO: could compare dir() output before and after the following, and show
# a warning if the count changes (unexpected members).
xmlData = et.fromstring(dataFromSplunkd)
for item in xmlData.find("eai_settings"):
if (None != item.text):
setattr(self, item.tag, item.text)
# convert these guys to nums.
self.maxCount = int(self.maxCount)
self.posOffset = int(self.posOffset)
self.requestedAction = int(self.requestedAction)
#for (k, v) in parsedData["eai_settings"].items():
#setattr(self, k, v)
# bleh, workarounds. need slightly smarter deserialization (TODO).
tmp = xmlData.find("callerArgs")
self.callerArgs = ArgsInfo()
for item in tmp.find("args"):
if item.tag == "item" and "name" in item.attrib:
# TODO: should this just work as self.callerArgs[item.getAttribute("name")].append(item.text)?
if not item.attrib["name"] in self.callerArgs.data:
self.callerArgs.data[item.attrib["name"]] = [item.text]
else:
self.callerArgs.data[item.attrib["name"]].append(item.text)
self.callerArgs.id = tmp.find("id").text
# TODO BUG: requestedFilters is not deserialized!
isSetup = xmlData.find("setup")
sessionKey = xmlData.find("sessionKey")
productType = xmlData.find("productType")
httpHeaders = xmlData.find("headers")
self._setup = isSetup is not None
if sessionKey is not None:
setattr(__main__, '___sessionKey', sessionKey.text)
if productType is not None:
setattr(__main__, '___productType', productType.text)
if httpHeaders is not None:
setattr(__main__, '___httpHeaders', {header.get('key'): header.text for header in httpHeaders})
def isSetup(self):
return self._setup
def readConf(self, confName, typer = None):
import splunk.bundle as bundle
app = self.context != CONTEXT_NONE and self.appName or "-"
user = self.context == CONTEXT_APP_AND_USER and self.userName or "-"
retDict = {}
try:
thing=bundle.getConf(confName, sessionKey=self.getSessionKey(), namespace=app, owner=user)
for s in thing:
retDict[s] = {}
if typer:
for k, v in list(thing[s].items()):
retDict[s][k] = typer.get(k)(v) # apply native datatypes.
else:
retDict[s].update(list(thing[s].items()))
# it's not "wrong" to request a conf file that doesn't exist, just like with PropertyPages.
except splunk.ResourceNotFound:
pass
return retDict
def readConfCtx(self, confName):
"""
This version of readConf should only be used when you're sure there's an
appropriate handler for it. Basically, something at /configs/conf-.
"""
app = self.context != CONTEXT_NONE and self.appName or "-"
user = self.context == CONTEXT_APP_AND_USER and self.userName or "-"
retDict = {}
path = "configs/conf-%s" % confName
import splunk.entity as en
thing=en.getEntities(path, sessionKey=self.getSessionKey(), namespace=app, owner=user, count=-1)
for s in thing:
retDict[s] = {}
retDict[s].update(list(thing[s].items()))
return retDict
def writeConf(self, confName, stanzaName, settingsDict):
import splunk.bundle as bundle
app = self.appName # always save things to SOME app context.
user = self.context == CONTEXT_APP_AND_USER and self.userName or "-"
overwriteStanzas = not (self.requestedAction == ACTION_EDIT or self.requestedAction == ACTION_REMOVE)
try:
confObj = bundle.getConf( confName, sessionKey=self.getSessionKey(), namespace=app, owner=user,
overwriteStanzas=overwriteStanzas)
except splunk.ResourceNotFound:
confObj = bundle.createConf(confName, sessionKey=self.getSessionKey(), namespace=app, owner=user)
confObj.beginBatch()
for k, v in list(settingsDict.items()):
if isinstance(v, list):
confObj[stanzaName][k] = str.join(str(","), v)
else:
confObj[stanzaName][k] = v
confObj.commitBatch()
def setReadCapability(self, capability):
self.capabilityRead = capability
def setWriteCapability(self, capability):
self.capabilityWrite = capability
def getSessionKey(self):
if hasattr(__main__, "___sessionKey"):
return getattr(__main__, "___sessionKey")
raise UsageException("Session key not provided in __main__.")
def getHttpHeaders(self):
if hasattr(__main__, "___httpHeaders"):
return getattr(__main__, "___httpHeaders")
raise UsageException("HTTP headers not provided in __main__.")
def toXml(self, confInfo):
root = et.Element("eai")
setts = et.SubElement(root, "eai_settings")
info = et.SubElement(root, "config_info")
confInfo.toXml(info)
for memberName in dir(self):
member = getattr(self, memberName)
if (not isinstance(member, types.MethodType) and not memberName.startswith("_")):
if "toXml" in dir(member):
tmp = et.SubElement(setts, memberName)
member.toXml(tmp)
elif isinstance(member, list):
for oneItem in member:
tmp = et.SubElement(setts, memberName)
tmp.text = None != member and splunk.util.unicode(member) or u""
else:
tmp = et.SubElement(setts, memberName)
tmp.text = None != member and splunk.util.unicode(member) or u""
# SPL-202410: encoding must be explicitly utf-8; default is ASCII
# (https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring)
return et.tostring(root, encoding="utf-8")
def setup(self):
"""
Must be implemented by the derived class. Defines arguments and validation
info. Called before the handle*() functions.
Should:
- inspect self.requestedAction.
- populate self.supportedArgs via addReqArg() and addOptarg().
- set pipelineName and processorName if appropriate.
"""
raise BadProgrammerException("This python handler has not implemented a setup function. Aborting.")
def execute(self, confInfo):
if 0 != len(self.customAction):
self.handleCustom(confInfo)
else:
if self.requestedAction == ACTION_CREATE: self.handleCreate(confInfo)
if self.requestedAction == ACTION_LIST: self.handleList(confInfo)
if self.requestedAction == ACTION_EDIT: self.handleEdit(confInfo)
if self.requestedAction == ACTION_REMOVE: self.handleRemove(confInfo)
if self.requestedAction == ACTION_MEMBERS: self.handleMembers(confInfo)
if self.requestedAction == ACTION_RELOAD: self.handleReload(confInfo)
def handleCreate(self, confInfo):
"""Called when user invokes the "create" action."""
self.actionNotImplemented()
def handleEdit(self, confInfo):
"""Called when user invokes the "edit" action."""
self.actionNotImplemented()
def handleList(self, confInfo):
"""Called when user invokes the "list" action."""
self.actionNotImplemented()
def handleMembers(self, confInfo):
"""Called when user invokes the "members" action."""
self.actionNotImplemented()
def handleReload(self, confInfo):
"""Called when user invokes the "reload" action."""
self.actionNotImplemented()
def handleRemove(self, confInfo):
"""Called when user invokes the "remove" action."""
self.actionNotImplemented()
def handleCustom(self, confInfo):
"""
Called when user invokes a custom action. Implementer can find out which
action is requested by checking self.customAction and self.requestedAction.
The former is a string, the latter an action type (create/edit/delete/etc).
"""
self.actionNotImplemented()
def actionNotImplemented(self):
raise BadProgrammerException("This handler claims to support this action (%d), but has not implemented it." % self.requestedAction)
def invalidCustomAction(self):
raise BadActionException("Invalid custom action for this python handler (custom action: %s, eai action: %d)." \
% (self.customAction, self.requestedAction))