You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
9.4 KiB
223 lines
9.4 KiB
import appsmanager
|
|
import splunk
|
|
import splunk.admin as admin
|
|
import splunk.appbuilder as appbuilder
|
|
import re
|
|
import os
|
|
import json
|
|
from splunk.clilib.bundle_paths import make_splunkhome_path
|
|
import splunk.clilib.cli_common as cli_common
|
|
import splunk.spl2helper as spl2helper
|
|
|
|
HTTP_POST_TEMPLATE = "template"
|
|
HTTP_POST_LABEL = "label"
|
|
HTTP_POST_DESC = "description"
|
|
HTTP_POST_VISIBLE = "visible"
|
|
HTTP_POST_AUTHOR = "author"
|
|
HTTP_POST_CONFIGURED = "configured"
|
|
HTTP_POST_VERSION = "version"
|
|
|
|
ACTION_PACKAGE = "package"
|
|
ACTION_DEPENDENCIES = "dependencies"
|
|
ACTION_SYNC_OBJECTS = "syncfs"
|
|
EDIT_LOCAL_APPS_CAP_EXPR = "edit_local_apps"
|
|
AAO_OR_EDIT_LOCAL_APPS_CAP_EXPR = "admin_all_objects or edit_local_apps"
|
|
|
|
DEFAULT_TEMPLATE = "barebones"
|
|
|
|
class LocalAppsHandler(admin.MConfigHandler):
|
|
def __init__(self, scriptMode, ctxInfo):
|
|
admin.MConfigHandler.__init__(self, scriptMode, ctxInfo)
|
|
# This handler already lists upon create/edit, turn this off.
|
|
self.shouldAutoList = False
|
|
|
|
'''
|
|
Set up supported arguments
|
|
'''
|
|
def setup(self):
|
|
enableInstallApps = cli_common.getConfKeyValue("limits", "auth", "enable_install_apps")
|
|
|
|
if self.requestedAction == admin.ACTION_CREATE:
|
|
# Let the C++ handler do all the validation work.
|
|
self.supportedArgs.addOptArg('*')
|
|
# SPL-207748: implementing auth for localapps-python endpoint
|
|
if (splunk.util.normalizeBoolean(enableInstallApps)):
|
|
self.setWriteCapability(EDIT_LOCAL_APPS_CAP_EXPR)
|
|
else:
|
|
# default
|
|
self.setWriteCapability(AAO_OR_EDIT_LOCAL_APPS_CAP_EXPR)
|
|
|
|
if self.customAction == ACTION_PACKAGE:
|
|
# SPL-158999: to support merge-local-meta and exclude-local-meta parameters
|
|
self.supportedArgs.addOptArg('merge-local-meta')
|
|
self.supportedArgs.addOptArg('exclude-local-meta')
|
|
|
|
if (splunk.util.normalizeBoolean(enableInstallApps)):
|
|
self.customActionCap = EDIT_LOCAL_APPS_CAP_EXPR
|
|
return
|
|
|
|
# default
|
|
self.customActionCap = AAO_OR_EDIT_LOCAL_APPS_CAP_EXPR
|
|
|
|
if self.customAction == ACTION_SYNC_OBJECTS:
|
|
self.supportedArgs.addReqArg('object_types')
|
|
self.supportedArgs.addOptArg('dest')
|
|
self.supportedArgs.addOptArg('filter')
|
|
|
|
self.customActionCap = AAO_OR_EDIT_LOCAL_APPS_CAP_EXPR
|
|
|
|
|
|
'''
|
|
Create a new application
|
|
'''
|
|
def handleCreate(self, confInfo):
|
|
args = self.callerArgs.data
|
|
|
|
# Sanity checking for app ID: no special chars and shorter than 100 chars
|
|
appName = self.callerArgs.id
|
|
if not appName or len(appName) == 0:
|
|
raise admin.ArgValidationException('App folder name is not set.')
|
|
|
|
if re.search('[^A-Za-z0-9._-]', appName):
|
|
raise admin.ArgValidationException('App folder name cannot contain spaces or special characters.')
|
|
|
|
if len(appName) > 100:
|
|
raise admin.ArgValidationException('App folder name cannot be longer than 100 characters.')
|
|
|
|
kwargs = {
|
|
'label' : _getFieldValue(args, HTTP_POST_LABEL, appName, maxLen=100),
|
|
'visible' : _getFieldValue(args, HTTP_POST_VISIBLE, 'true'),
|
|
'author' : _getFieldValue(args, HTTP_POST_AUTHOR, '', maxLen=100),
|
|
'description' : _getFieldValue(args, HTTP_POST_DESC, '', maxLen=500),
|
|
'configured' : _getFieldValue(args, HTTP_POST_CONFIGURED, '0'),
|
|
'version' : _getFieldValue(args, HTTP_POST_VERSION, '1.0.0')
|
|
}
|
|
template = _getFieldValue(args, HTTP_POST_TEMPLATE, DEFAULT_TEMPLATE)
|
|
|
|
if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}(\s?\w[\w\d]{,9})?$", kwargs['version']) is None:
|
|
raise admin.ArgValidationException("Version '%s' is invalid. Use the version format 'major.minor.patch', for example '1.0.0'." % kwargs['version'])
|
|
|
|
try:
|
|
url = appbuilder.createApp(appName, template, **kwargs)
|
|
appbuilder.addUploadAssets(appName)
|
|
except splunk.RESTException as e:
|
|
raise admin.InternalException(e.msg)
|
|
|
|
confInfo[appName].append('name', appName)
|
|
for field in kwargs:
|
|
confInfo[appName].append(field, kwargs[field])
|
|
|
|
'''
|
|
Controls local applications
|
|
'''
|
|
def handleEdit(self, confInfo):
|
|
appName = self.callerArgs.id
|
|
appbuilder.addUploadAssets(appName)
|
|
|
|
'''
|
|
Handles other commands
|
|
'''
|
|
def handleCustom(self, confInfo):
|
|
action = self.customAction
|
|
actionType = self.requestedAction
|
|
if self.customAction == ACTION_PACKAGE:
|
|
if appsmanager.isCloud(self.getSessionKey()):
|
|
raise admin.BadActionException("'package' action is not supported for Splunk Cloud.")
|
|
|
|
# Create a package of an application
|
|
confInfo.addWarnMsg('The package action has been deprecated.')
|
|
appName = self.callerArgs.id
|
|
# obtain merge-local-meta and exclude-local-meta parameters
|
|
# if the respective arguments are not passed, we want to package local.meta and default.meta as is
|
|
mergeLocalMeta = self.callerArgs.get('merge-local-meta', ['f'])[0]
|
|
excludeLocalMeta = self.callerArgs.get('exclude-local-meta', ['f'])[0]
|
|
try:
|
|
mergeLocalMeta = splunk.util.normalizeBoolean(mergeLocalMeta, enableStrictMode=True)
|
|
excludeLocalMeta = splunk.util.normalizeBoolean(excludeLocalMeta, enableStrictMode=True)
|
|
except ValueError as e:
|
|
raise admin.ArgValidationException("Incorrect parameter value. Please specify one of the following: "
|
|
"true/false, t/f, or 0/1.")
|
|
if (mergeLocalMeta and excludeLocalMeta):
|
|
raise admin.ArgValidationException("Invalid parameter specification. Both -merge-local-meta and "
|
|
"-exclude-local-meta cannot be \"true\" at the same time.")
|
|
|
|
try:
|
|
url, path = appbuilder.packageApp(appName, excludeLocalMeta=excludeLocalMeta, mergeLocalMeta=mergeLocalMeta)
|
|
|
|
confInfo['Package'].append('name', appName)
|
|
confInfo['Package'].append('url', url)
|
|
confInfo['Package'].append('path', path)
|
|
|
|
except splunk.RESTException as e:
|
|
raise admin.ArgValidationException(e.msg)
|
|
elif self.customAction == ACTION_DEPENDENCIES:
|
|
self.getAppDependencies(self.callerArgs.id, confInfo)
|
|
elif self.customAction == ACTION_SYNC_OBJECTS:
|
|
"""
|
|
Sync objects to the app's local file-system which are maintained externally (eg: kv-store etc)
|
|
"""
|
|
app_name = self.callerArgs.id
|
|
object_types_arg = self.callerArgs.get('object_types', [""])[0]
|
|
confInfo['SyncFs'].append('name', app_name)
|
|
|
|
app_location = make_splunkhome_path(['etc', 'apps', app_name])
|
|
if not os.path.isdir(app_location):
|
|
raise splunk.ResourceNotFound('App %s does not exist.' % app_name)
|
|
|
|
try:
|
|
for ob_type in list(object_types_arg.split(',')):
|
|
ob_type = ob_type.lower().strip()
|
|
if ob_type.startswith("spl2"):
|
|
spl2helper.sync_spl2_objects(self.callerArgs, ob_type, self.getSessionKey(), app_name)
|
|
except splunk.RESTException as e:
|
|
raise admin.InternalException(e.msg)
|
|
|
|
'''
|
|
This handler is overridden by UIAppsHandler
|
|
'''
|
|
def handleList(self, confInfo):
|
|
pass
|
|
|
|
'''
|
|
Get application dependencies from its manifest file
|
|
'''
|
|
|
|
def getAppDependencies(self, app, confInfo):
|
|
app_location = make_splunkhome_path(['etc', 'apps', app])
|
|
if not os.path.isdir(app_location):
|
|
raise splunk.ResourceNotFound('App %s does not exist.' % app)
|
|
|
|
manifestFile = os.path.join(app_location, 'app.manifest')
|
|
if not os.path.exists(str(manifestFile)):
|
|
# not an error, app manifest is optional
|
|
confInfo.addInfoMsg('Application manifest file is missing.')
|
|
return
|
|
|
|
# read app manifest chopping possible comments (that start with '#')
|
|
manifest = ''.join([line.split('#')[0] for line in open(manifestFile, 'r').readlines()])
|
|
|
|
# parse manifest, possible errors will bubble to REST reply
|
|
manifest = json.loads(manifest)
|
|
|
|
dependencies = manifest.get('dependencies')
|
|
if dependencies is None:
|
|
confInfo.addInfoMsg("Application manifest doesn't include dependencies.")
|
|
return
|
|
|
|
# construct reply
|
|
reply = confInfo[app]
|
|
for dependency, properties in dependencies.items():
|
|
# Convert properties to STR as it returns unicode
|
|
reply[dependency] = str(properties.get("version"))
|
|
|
|
def _getFieldValue(args, fieldName, defaultVal=None, maxLen=None):
|
|
value = args[fieldName][0] or defaultVal if fieldName in args else defaultVal
|
|
if value and maxLen and len(value) > maxLen:
|
|
raise admin.ArgValidationException('App %(fieldName)s cannot be longer than %(maxLen)s characters.'
|
|
% {'fieldName' : fieldName, 'maxLen' : maxLen} )
|
|
return value
|
|
|
|
# initialize the handler, and add the actions it supports.
|
|
admin.init(LocalAppsHandler, admin.CONTEXT_NONE)
|
|
|