blob: 74b8ebaad547e72331fbdce9fd59fd1ae0bda091 [file] [log] [blame]
#!/usr/bin/env python
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##
# Whisk Admin command line interface
##
import argparse
import json
import os
import random
import re
from subprocess import Popen, PIPE, STDOUT
import string
import sys
import traceback
import uuid
import wskprop
if sys.version_info.major >= 3:
from urllib.parse import quote_plus
else:
from urllib import quote_plus
try:
import argcomplete
except ImportError:
argcomplete = False
from wskutil import request
DB_PROTOCOL = 'DB_PROTOCOL'
DB_HOST = 'DB_HOST'
DB_PORT = 'DB_PORT'
DB_USERNAME = 'DB_USERNAME'
DB_PASSWORD = 'DB_PASSWORD'
DB_WHISK_AUTHS = 'DB_WHISK_AUTHS'
DB_WHISK_ACTIONS = 'DB_WHISK_ACTIONS'
DB_WHISK_ACTIVATIONS = 'DB_WHISK_ACTIVATIONS'
LOGS_DIR = 'WHISK_LOGS_DIR'
# SCRIPT_DIR is going to be traversing all links and point to tools/cli/wsk
CLI_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))
# ROOT_DIR is the repository root
ROOT_DIR = os.path.join(os.path.join(CLI_DIR, os.pardir), os.pardir)
def main():
requiredprops = [
DB_PROTOCOL, DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD,
DB_WHISK_AUTHS, DB_WHISK_ACTIONS, DB_WHISK_ACTIVATIONS,
LOGS_DIR ]
whiskprops = wskprop.importPropsIfAvailable(wskprop.propfile(ROOT_DIR))
(valid, props, deferredInfo) = wskprop.checkRequiredProperties(requiredprops, whiskprops)
exitCode = 0 if valid else 2
if valid:
try:
args = parseArgs()
if (args.verbose):
print(deferredInfo)
exitCode = {
'user' : userCmd,
'db' : dbCmd,
'syslog' : syslogCmd,
'limits': limitsCmd
}[args.cmd](args, props)
except Exception as e:
print('Exception: ', e)
print('Informative: ', deferredInfo)
traceback.print_exc()
exitCode = 1
sys.exit(exitCode)
def parseArgs():
parser = argparse.ArgumentParser(description='OpenWhisk admin command line tool')
parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')
subparsers = parser.add_subparsers(title='available commands', dest='cmd')
propmenu = subparsers.add_parser('user', help='manage users')
subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
subcmd = subparser.add_parser('create', help='create a user and show authorization key')
subcmd.add_argument('subject', help='the subject to create')
subcmd.add_argument('-u', '--auth', help='the uuid:key to initialize the subject authorization key with')
subcmd.add_argument('-ns', '--namespace', help='create key for given namespace instead (defaults to subject id')
subcmd = subparser.add_parser('delete', help='delete a user')
subcmd.add_argument('subject', help='the subject to delete')
subcmd.add_argument('-ns', '--namespace', help='delete key for given namespace only')
subcmd = subparser.add_parser('get', help='get authorization key for user')
subcmd.add_argument('subject', help='the subject to get key for')
subcmd.add_argument('-ns', '--namespace', help='the namespace to get the key for, defaults to subject id')
subcmd.add_argument('-a', '--all', help='list all namespaces and their keys', action='store_true')
subcmd = subparser.add_parser('whois', help='identify user from an authorization key')
subcmd.add_argument('authkey', help='the credentials to look up')
subcmd = subparser.add_parser('block', help='block one or more users')
subcmd.add_argument('subjects', nargs='+', help='one or more users to block')
subcmd = subparser.add_parser('unblock', help='unblock one or more users')
subcmd.add_argument('subjects', nargs='+', help='one or more users to unblock')
subcmd = subparser.add_parser('list', help='list authorization keys associated with a namespace')
subcmd.add_argument('namespace', help='the namespace to lookup')
subcmd.add_argument('-p', '--pick', metavar='N', help='show no more than N identities', type=int, default=1)
subcmd.add_argument('-a', '--all', help='show all identities', action='store_true')
subcmd.add_argument('-k', '--key', help='show only the keys', action='store_true')
propmenu = subparsers.add_parser('limits', help='manage namespace-specific limits')
subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
subcmd = subparser.add_parser('set', help='set limits for a given namespace')
subcmd.add_argument('namespace', help='the namespace to set limits for')
subcmd.add_argument('--invocationsPerMinute', help='invocations per minute allowed', type=int)
subcmd.add_argument('--firesPerMinute', help='trigger fires per minute allowed', type=int)
subcmd.add_argument('--concurrentInvocations', help='concurrent invocations allowed for this namespace', type=int)
subcmd = subparser.add_parser('get', help='get limits for a given namespace (if none exist, system defaults apply)')
subcmd.add_argument('namespace', help='the namespace to get limits for')
subcmd = subparser.add_parser('delete', help='delete limits for a given namespace (system defaults apply)')
subcmd.add_argument('namespace', help='the namespace to delete limits for')
propmenu = subparsers.add_parser('db', help='work with dbs')
subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
subcmd = subparser.add_parser('get', help='get contents of database')
subcmd.add_argument('database', help='the database name')
subcmd.add_argument('-v', '--view', help='the view in the database to get')
subcmd.add_argument('--docs', help='include document contents', action='store_true')
propmenu = subparsers.add_parser('syslog', help='work with system logs')
subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')
subcmd = subparser.add_parser('get', help='get logs')
subcmd.add_argument('components', help='components, one or more of [controllerN, invokerN] where N is the instance', nargs='*', default=['controller0', 'invoker0'])
subcmd.add_argument('-t', '--tid', help='retrieve logs for the transaction id')
subcmd.add_argument('-g', '--grep', help='retrieve logs that match grep expression')
if argcomplete:
argcomplete.autocomplete(parser)
return parser.parse_args()
def userCmd(args, props):
if args.subcmd == 'create':
return createUserCmd(args, props)
elif args.subcmd == 'delete':
return deleteUserCmd(args, props)
elif args.subcmd == 'get':
return getUserCmd(args, props)
elif args.subcmd == 'whois':
return whoisUserCmd(args, props)
elif args.subcmd == 'list':
return listUserCmd(args, props)
elif args.subcmd == 'block':
return blockUserCmd(args, props)
elif args.subcmd == 'unblock':
return unblockUserCmd(args, props)
else:
print('unknown command')
return 2
def dbCmd(args, props):
if args.subcmd == 'get':
return getDbCmd(args, props)
else:
print('unknown command')
return 2
def syslogCmd(args, props):
if args.subcmd == 'get':
return getLogsCmd(args, props)
else:
print('unknown command')
return 2
def limitsCmd(args, props):
if args.subcmd == 'set':
return setLimitsCmd(args, props)
elif args.subcmd == 'get':
return getLimitsCmd(args, props)
elif args.subcmd == 'delete':
return deleteLimitsCmd(args, props)
else:
print('unknown command')
return 2
def createUserCmd(args, props):
subject = args.subject.strip()
if len(subject) < 5:
print('Subject name must be at least 5 characters')
return 2
if args.namespace and args.namespace.strip() == '':
print('Namespace must not be empty')
return 2
else:
desiredNamespace = subject if not args.namespace else args.namespace.strip()
if args.auth:
try:
parts = args.auth.split(':')
try:
uid = str(uuid.UUID(parts[0], version = 4))
except ValueError:
print('authorization id is not a valid UUID')
return 2
key = parts[1]
if len(key) < 64:
print('authorization key must be at least 64 characters long')
return 2
except Exception as e:
print('failed to determine authorization id and key: %s' % e)
return 2
else:
uid = str(uuid.uuid4())
key = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
(doc, res) = getDocumentFromDb(props, args.subject, args.verbose)
if doc is None:
doc = {
'_id': subject,
'subject': subject,
'namespaces': [
{
'name': desiredNamespace,
'uuid': uid,
'key': key
}
]
}
else:
if not doc.get('blocked'):
namespaces = [ns for ns in doc['namespaces'] if ns['name'] == desiredNamespace]
if len(namespaces) == 0:
doc['namespaces'].append({
'name': desiredNamespace,
'uuid': uid,
'key': key
})
else:
print('Namespace already exists')
return 1
else:
print('The subject you want to edit is blocked')
return 1
res = insertIntoDatabase(props, doc, args.verbose)
if res.status in [201, 202]:
print('%s:%s' % (uid, key))
else:
print('Failed to create subject (%s)' % res.read().strip())
return 1
def getUserCmd(args, props):
(doc, res) = getDocumentFromDb(props, args.subject, args.verbose)
if doc is not None:
if args.all is True:
# tabulate name of each space and its key
for ns in doc['namespaces']:
print('%s\t%s:%s' % (ns['name'], ns['uuid'], ns['key']))
return 0
else:
# if requesting key for specific namespace, report only that key;
# use default namespace if no namespace provided
namespaceName = args.namespace if args.namespace is not None else args.subject
namespaces = [ns for ns in doc['namespaces'] if ns['name'] == namespaceName]
if len(namespaces) == 1:
ns = namespaces[0]
print('%s:%s' % (ns['uuid'], ns['key']))
return 0
else:
print('namespace "%s" not found for "%s"' % (namespaceName, args.subject))
return 1
else:
print('Failed to get subject (%s)' % res.read().strip())
return 1
def listUserCmd(args, props):
(nslist, res) = getIdentitiesFromNamespace(args, props)
if args.pick < 1:
print('pick at least 1 identity to show')
return 2
if nslist is not None:
nslist = nslist if args.all is True else nslist[:args.pick]
if len(nslist) > 0:
for p in nslist:
print('%s:%s%s' % (p['uuid'], p['key'], "\t%s" % p['subject'] if not args.key else ""))
return 0
else:
print('no identities found for namespace "%s"' % args.namespace)
return 0
else:
print('Failed to get namespace key (%s)' % res.read().strip())
return 1
def getDocumentFromDb(props, doc, verbose):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s' % {
'protocol': protocol,
'host' : host,
'port' : port,
'database': database,
'subject' : doc
}
headers = {
'Content-Type': 'application/json',
}
res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=verbose)
if res.status == 200:
doc = json.loads(res.read())
return (doc, res)
else:
return (None, res)
def getIdentitiesFromNamespace(args, props):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/subjects/_view/identities?key=["%(ns)s"]' % {
'protocol': protocol,
'host' : host,
'port' : port,
'username': username,
'database': database,
'ns' : args.namespace
}
headers = {
'Content-Type': 'application/json',
}
res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
nslist = None
if res.status == 200:
doc = json.loads(res.read())
nslist = []
if 'rows' in doc and len(doc['rows']) > 0:
for row in doc['rows']:
if 'id' in row:
nslist.append({"subject": row["id"], "uuid": row['value']['uuid'], "key": row['value']['key']})
return (nslist, res)
def deleteUserCmd(args, props):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
if args.subject.strip() == '':
print('Subject must not be empty')
return 2
if args.namespace and args.namespace.strip() == '':
print('Namespace must not be empty')
return 2
(prev, res) = getDocumentFromDb(props, args.subject, args.verbose)
if prev is None:
print('Failed to delete subject (%s)' % res.read().strip())
return 1
if not args.namespace:
url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s?rev=%(rev)s' % {
'protocol': protocol,
'host' : host,
'port' : port,
'database': database,
'subject' : args.subject.strip(),
'rev' : prev['_rev']
}
headers = {
'Content-Type': 'application/json',
}
res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
if res.status in [200, 202]:
print('Subject deleted')
else:
print('Failed to delete subject (%s)' % res.read().strip())
return 1
else:
namespaceToDelete = args.namespace.strip()
namespaces = [ns for ns in prev['namespaces'] if ns['name'] != namespaceToDelete]
if len(prev['namespaces']) == len(namespaces):
print('Namespace "%s" does not exist for "%s"' % (namespaceToDelete, prev['_id']))
return 1
else:
prev['namespaces'] = namespaces
res = insertIntoDatabase(props, prev, args.verbose)
if res.status in [201, 202]:
print('Namespace deleted')
else:
print('Failed to remove namespace (%s)' % res.read().strip())
return 1
def whoisUserCmd(args, props):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
authParts = args.authkey.split(':')
uuid = authParts[0]
key = authParts[1]
url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/subjects/_view/identities?key=["%(uuid)s","%(key)s"]' % {
'protocol': protocol,
'host' : host,
'port' : port,
'username': username,
'database': database,
'uuid' : uuid,
'key' : key
}
headers = {
'Content-Type': 'application/json',
}
res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
if res.status == 200:
doc = json.loads(res.read())
if 'rows' in doc and len(doc['rows']) > 0:
for row in doc['rows']:
if 'id' in row:
print('subject: %s' % row['id'])
print('namespace: %s' % row['value']['namespace'])
else:
print('Subject id is not recognized')
return 0
print('Failed to get subject (%s)' % res.read().strip())
return 1
def blockUserCmd(args, props):
failed = 0
for subject in args.subjects:
subject = subject.strip()
if len(subject) > 0:
(doc, res) = getDocumentFromDb(props, subject, args.verbose)
if doc is not None:
doc['blocked'] = True
insertRes = insertIntoDatabase(props, doc, args.verbose)
if insertRes.status in [201, 202]:
print('"%s" blocked successfully' % subject)
else:
print('Failed to block "%s" (%s)' % (subject, res.read().strip()))
failed += 1
else:
print('Failed to block "%s" (%s)' % (subject, res.read().strip()))
failed += 1
return failed
def unblockUserCmd(args, props):
failed = 0
for subject in args.subjects:
subject = subject.strip()
if len(subject) > 0:
(doc, res) = getDocumentFromDb(props, subject, args.verbose)
if doc is not None:
doc['blocked'] = False
insertRes = insertIntoDatabase(props, doc, args.verbose)
if insertRes.status in [201, 202]:
print('"%s" unblocked successfully' % subject)
else:
print('Failed to unblock "%s" (%s)' % (subject, res.read().strip()))
failed += 1
else:
print('Failed to unblock "%s" (%s)' % (subject, res.read().strip()))
failed += 1
return failed
def setLimitsCmd(args, props):
argsDict = vars(args)
docId = args.namespace + "/limits"
(dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)
doc = dbDoc or {'_id': docId}
limits = ['invocationsPerMinute', 'firesPerMinute', 'concurrentInvocations']
for limit in limits:
givenLimit = argsDict.get(limit)
toSet = givenLimit if givenLimit != None else doc.get(limit)
if toSet != None:
doc[limit] = toSet
res = insertIntoDatabase(props, doc, args.verbose)
if res.status in [201, 202]:
print('Limits successfully set for "%s"' % args.namespace)
else:
print('Failed to set limits (%s)' % res.read().strip())
return 1
def getLimitsCmd(args, props):
docId = args.namespace + "/limits"
(dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)
if dbDoc is not None:
limits = ['invocationsPerMinute', 'firesPerMinute', 'concurrentInvocations']
for limit in limits:
givenLimit = dbDoc.get(limit)
if givenLimit != None:
print('%s = %s' % (limit, givenLimit))
else:
error = json.loads(res.read())
if error['reason'] == 'missing':
print('No limits found, default system limits apply')
else:
print('Failed to get limits (%s)' % res.read().strip())
return 1
def deleteLimitsCmd(args, props):
docId = quote_plus(args.namespace + "/limits")
(dbDoc, res) = getDocumentFromDb(props, docId, args.verbose)
if dbDoc is None:
print('Failed to delete limits (%s)' % res.read().strip())
return 1
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(docid)s?rev=%(rev)s' % {
'protocol': protocol,
'host' : host,
'port' : port,
'database': database,
'docid' : docId,
'rev' : dbDoc['_rev']
}
headers = {
'Content-Type': 'application/json',
}
res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
if res.status in [200, 202]:
print('Limits deleted')
else:
print('Failed to delete limits (%s)' % res.read().strip())
return 1
def getDbCmd(args, props):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
if args.database == 'subjects':
database = props[DB_WHISK_AUTHS]
elif args.database == 'whisks':
database = props[DB_WHISK_ACTIONS]
elif args.database == 'activations':
database = props[DB_WHISK_ACTIVATIONS]
else:
database = args.database
if args.view:
try:
parts = args.view.split('/')
designdoc = parts[0]
viewname = parts[1]
except:
print('view name "%s" is not formatted correctly, should be design/view' % args.view)
return 2
url = '%(protocol)s://%(host)s:%(port)s/%(database)s%(design)s/%(index)s?reduce=false&include_docs=%(docs)s' % {
'protocol': protocol,
'host' : host,
'port' : port,
'database': database,
'design' : '/_design/' + designdoc +'/_view' if args.view else '',
'index' : viewname if args.view else '_all_docs',
'docs' : 'true' if args.docs else 'false'
}
headers = {
'Content-Type': 'application/json',
}
print('getting contents for %s (%s)' % (database, args.view if args.view else 'primary index'))
res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)
if res.status == 200:
table = json.loads(res.read())
print(json.dumps(table, sort_keys=True, indent=4, separators=(',', ': ')))
return 0
print('Failed to get database (%s)' % res.read().strip())
return 1
def insertIntoDatabase(props, doc, verbose = False):
protocol = props[DB_PROTOCOL]
host = props[DB_HOST]
port = props[DB_PORT]
username = props[DB_USERNAME]
password = props[DB_PASSWORD]
database = props[DB_WHISK_AUTHS]
url = '%(protocol)s://%(host)s:%(port)s/%(database)s' % {
'protocol': protocol,
'host' : host,
'port' : port,
'database': database
}
body = json.dumps(doc)
headers = {
'Content-Type': 'application/json',
}
res = request('POST', url, body, headers, auth='%s:%s' % (username, password), verbose=verbose)
return res
def getLogsCmd(args, props):
def getComponentLogs(component):
path = '%s/%s/%s_logs.log' % (props[LOGS_DIR], component, component)
if args.tid:
cmd = 'grep "\[#tid_%s\]" %s' % (args.tid, path)
elif args.grep:
cmd = 'grep "%s" %s' % (args.grep, path)
else:
cmd = 'cat %s' % path
(output, error) = shell(cmd, verbose = args.verbose)
if output:
return output
if error:
sys.stderr.write(error)
return ''
logs = map(getComponentLogs, args.components)
joined = ''.join(logs)
if joined:
output = joined.strip()
parts = output.split('\n')
filter = [p for p in parts if p != '']
date = map(extractDate, filter)
keyed = zip(date, parts)
sort = sorted(keyed, key=lambda t: t[1])
msgs = unzip(sort)[1]
print('\n'.join(msgs))
return 0
def shell(cmd, data=None, verbose=False):
if verbose:
print(cmd)
if input:
p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, stdin=PIPE)
out, err = p.communicate(input=data)
else:
out, err = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT)
p.wait()
return (out, err)
def unzip(iterable):
return zip(*iterable)
def extractDate(line):
matches = re.search(r'\d{4}-[01]{1}\d{1}-[0-3]{1}\d{1}T[0-2]{1}\d{1}:[0-6]{1}\d{1}:[0-6]{1}\d{1}.\d{3}Z', line)
if matches is not None:
date = matches.group(0)
return date
else:
return None
if __name__ == '__main__':
main()