blob: a0e45bded51e81b3a01e3226c102f62cfae21b7f [file] [log] [blame]
#
# Copyright 2015-2016 IBM Corporation
#
# Licensed 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.
#
import json
import httplib
import urllib
import time
from datetime import datetime, timedelta
import copy
from wskitem import Item
from wskutil import (addAuthenticatedCommand, apiBase, bold, parseQName,
getQName, request, responseError, getPrettyJson)
try:
long # Python 2
except NameError:
long = int # Python 3 drops 'long'
# how many seconds to sleep between polls
SLEEP_SECONDS = 2
# a parameter -- how many seconds might it take for an activation
# completion to propagate into cloudant
SLACK_SECONDS = 5
# the timestamp of the latest activation we have already reported
lastTime = 0
#
# 'wsk activations' CLI
#
class Activation(Item):
def __init__(self):
super(Activation, self).__init__('activation', 'activations')
def getItemSpecificCommands(self, parser, props):
subcmd = parser.add_parser('list', help='retrieve activations')
subcmd.add_argument('name', nargs='?', help='the namespace or action to list')
addAuthenticatedCommand(subcmd, props)
subcmd.add_argument('-s', '--skip', help='skip this many entities from the head of the collection', type=int, default=0)
subcmd.add_argument('-l', '--limit', help='only return this many entities from the collection', type=int, default=30)
subcmd.add_argument('-f', '--full', help='return full documents for each activation', action='store_true')
subcmd.add_argument('--upto', help='return activations with timestamps earlier than UPTO; measured in milliseconds since Thu, 01 Jan 1970 00:00:00', type=long, default=0)
subcmd.add_argument('--since', help='return activations with timestamps later than SINCE; measured in milliseconds since Thu, 01 Jan 1970 00:00:00', type=long, default=0)
subcmd = parser.add_parser('get', help='get %s' % self.name)
subcmd.add_argument('name', help='the name of the %s' % self.name)
subcmd.add_argument('project', nargs='?', help='project only this property')
addAuthenticatedCommand(subcmd, props)
subcmd.add_argument('-s', '--summary', help='summarize entity details', action='store_true')
subcmd = parser.add_parser('logs', help='get the logs of an activation')
subcmd.add_argument('id', help='the activation id')
addAuthenticatedCommand(subcmd, props)
subcmd.add_argument('-s', '--strip', help='strip timestamp and stream information', action='store_true')
subcmd = parser.add_parser('result', help='get the result of an activation')
subcmd.add_argument('id', help='the invocation id')
addAuthenticatedCommand(subcmd, props)
# poll
subcmd = parser.add_parser('poll', help='poll continuously for log messages from currently running actions')
subcmd.add_argument('name', nargs='?', help='the namespace to poll')
addAuthenticatedCommand(subcmd, props)
subcmd.add_argument('-e', '--exit', help='exit after this many seconds', type=int, default=-1)
subcmd.add_argument('-ss', '--since-seconds', help='start polling for activations this many seconds ago', type=long, default=0, dest='since_secs')
subcmd.add_argument('-sm', '--since-minutes', help='start polling for activations this many minutes ago', type=long, default=0, dest='since_mins')
subcmd.add_argument('-sh', '--since-hours', help='start polling for activations this many hours ago', type=long, default=0, dest='since_hrs')
subcmd.add_argument('-sd', '--since-days', help='start polling for activations this many days ago', type=long, default=0, dest='since_days')
def cmd(self, args, props):
if args.subcmd == 'list':
return self.list(args, props)
elif args.subcmd == 'get':
return self.get(args, props)
elif args.subcmd == 'logs':
return self.logs(args, props)
elif args.subcmd == 'result':
return self.result(args, props)
elif args.subcmd == 'poll':
return self.poll(args, props)
else:
print('error: unexpected sub command')
return 2
def create(self, args, props, update):
"""not allowed"""
return 2
def postProcess(self, entity):
# entity['logs'] = json.loads(entity['logs'])
return entity
def get(self, args, props):
args.name = getQName(args.name, '_') # kludge: use default namespace unless explicitly specified
return Item.get(self, args, props)
def list(self, args, props):
name = args.name if args.name else '/_'
args.name = getQName(name, '_') # kludge: use default namespace unless explicitly specified
res = self.listCmd(args, props)
if res.status == httplib.OK:
result = json.loads(res.read())
print(bold('activations'))
for a in result:
if args.full:
print(getPrettyJson(a))
else:
print('{:<45}{:<40}'.format(a['activationId'], a['name']))
return 0
else:
return responseError(res)
def getEntitySummary(self, entity):
kind = self.name
fullName = getQName(entity['name'], entity['namespace'])
end = datetime.fromtimestamp(entity['end'] / 1000)
status = entity['response']['status']
result = getPrettyJson(entity['response']['result'])
summary = '%s result for %s (%s at %s):\n%s' % (kind, fullName, status,
end, result)
return summary
def result(self, args, props):
fqid = getQName(args.id, '_') # kludge: use default namespace unless explicitly specified
namespace, aid = parseQName(fqid, props)
url = '%(apibase)s/namespaces/%(namespace)s/activations/%(id)s/result' % {
'apibase': apiBase(props),
'namespace': urllib.quote(namespace),
'id': aid
}
res = request('GET', url, auth=args.auth, verbose=args.verbose)
if res.status == httplib.OK:
response = json.loads(res.read())
if 'result' in response:
result = response['result']
print(getPrettyJson(result))
return 0
else:
return responseError(res)
def logs(self, args, props):
fqid = getQName(args.id, '_') # kludge: use default namespace unless explicitly specified
namespace, aid = parseQName(fqid, props)
url = '%(apibase)s/namespaces/%(namespace)s/activations/%(id)s/logs' % {
'apibase': apiBase(props),
'namespace': urllib.quote(namespace),
'id': aid
}
res = request('GET', url, auth=args.auth, verbose=args.verbose)
if res.status == httplib.OK:
result = json.loads(res.read())
logs = result['logs']
if args.strip:
logs = map(stripTimeStampAndString, logs)
print('\n'.join(logs))
return 0
else:
return responseError(res)
# implementation of wsk activations poll
def poll(self, args, props):
name = args.name if args.name else '/_'
args.name = getQName(name, '_') # kludge: use default namespace unless explicitly specified
print('Hit Ctrl-C to exit.')
try:
self.console(args, props)
except KeyboardInterrupt:
print('')
# return the result of a 'wsk activation list' call, an HTTPResponse
def listCmd(self, args, props):
namespace, pname = parseQName(args.name, props)
url = '%(apibase)s/namespaces/%(namespace)s/activations?docs=%(full)s&skip=%(skip)s&limit=%(limit)s&%(filter)s' % {
'apibase': apiBase(props),
'namespace': urllib.quote(namespace),
'collection': self.collection,
'full': 'true' if args.full else 'false',
'skip': args.skip,
'limit': args.limit,
'filter': 'name=%s' % urllib.quote(pname) if pname else ''
}
if args.upto > 0:
url = url + '&upto=' + str(args.upto)
if args.since > 0:
url = url + '&since=' + str(args.since)
res = request('GET', url, auth=args.auth, verbose=args.verbose)
return res
#
# main routine for polling loop
#
def console(self, args, props):
global lastTime
if args.since_secs == args.since_mins == args.since_hrs == args.since_days == 0:
# initialize lastTime to just past the most recent activation
lastActivation = self.fetchMostRecent(args, props)
if lastActivation is not None:
lastTime = extractTimestamp(lastActivation) + 1 + SLACK_SECONDS * 1000
else:
# initialize lastTime to utcnow - args.since_[secs,mins,hrs,days]
n = datetime.utcnow()
d = timedelta(seconds=args.since_secs, minutes=args.since_mins,
hours=args.since_hrs, days=args.since_days)
e = datetime(1970, 1, 1)
lastTime = int((n - d - e).total_seconds() * 1000)
reported = set()
localStartTime = int(time.time())
print('Polling for logs')
while True:
if (args.exit > 0):
localDuration = int(time.time()) - localStartTime
if (localDuration > args.exit):
return
L = self.fetchActivations(lastTime, args, props)
if L:
printLogs(reversed(L), reported)
time.sleep(SLEEP_SECONDS)
#
# Fetch the most recent activation
#
# Return the activation record, or None
#
def fetchMostRecent(self, args, props):
a = copy.deepcopy(args)
a.action = None
a.name = args.name
a.full = True
a.skip = 0
a.limit = 1
a.upto = 0
a.since = 0
pr = copy.deepcopy(props)
res = self.listCmd(a, pr)
if res.status == httplib.OK:
result = json.loads(res.read())
return None if len(result) == 0 else result[0]
else:
responseError(res)
return None
#
# Fetch all activation records since a timestamp
#
def fetchActivations(self, beginMillis, args, props):
# fetch all activations starting from SLACK_SECONDS seconds in the past
if beginMillis > SLACK_SECONDS * 1000:
beginMillis = beginMillis - (SLACK_SECONDS * 1000)
a = copy.deepcopy(args)
a.name = args.name
a.full = True
a.skip = 0
a.limit = 0
a.upto = 0
a.since = beginMillis
pr = copy.deepcopy(props)
res = self.listCmd(a, pr)
if res.status == httplib.OK:
result = json.loads(res.read())
return result
else:
responseError(res)
return None
#
# Extract the timestamp from an activation, or return 0
#
def extractTimestamp(activation):
return activation['start']
#
# Print all the logs for an activation record
#
# auth: whisk auth key
#
def printLogsForActivation(activation):
global lastTime
print('Activation: %s (%s)' % (activation['name'],
activation['activationId']))
if 'logs' in activation:
for element in activation['logs']:
print(element)
# update the lastTime global timestamp if necessary to record the
# latest start time yet fetched
if activation['start'] > lastTime:
lastTime = activation['start']
#
# Print all the logs for a list of activation records
#
# L: set of activation records
# reported: set of ids already reported to the user
#
def printLogs(L, reported):
for a in L:
if not a['activationId'] in reported:
reported.add(a['activationId'])
printLogsForActivation(a)
#
# Strips timestamp and stream information from log line
#
def stripTimeStampAndString(line):
# log line should be formatted to fixed width and stripping
# first 38 characters should do
return line[39:]