| # |
| # 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 os |
| import json |
| import base64 |
| import httplib |
| import argparse |
| import urllib |
| import subprocess |
| from wskitem import Item |
| from wskutil import addAuthenticatedCommand, bold, request, getParams, getActivationArgument, getAnnotations, responseError, parseQName, getQName, apiBase, getPrettyJson |
| |
| # |
| # 'wsk actions' CLI |
| # |
| class Action(Item): |
| |
| def __init__(self): |
| super(Action, self).__init__('action', 'actions') |
| |
| def getItemSpecificCommands(self, parser, props): |
| subcmd = parser.add_parser('create', help='create new action') |
| subcmd.add_argument('name', help='the name of the action') |
| subcmd.add_argument('artifact', help='artifact (e.g., file name) containing action definition') |
| addAuthenticatedCommand(subcmd, props) |
| subcmd.add_argument('--docker', help='treat artifact as docker image path on dockerhub', action='store_true') |
| subcmd.add_argument('--copy', help='treat artifact as the name of an existing action', action='store_true') |
| subcmd.add_argument('--sequence', help='treat artifact as comma separated sequence of actions to invoke', action='store_true') |
| subcmd.add_argument('--lib', help='add library to artifact (must be a gzipped tar file)', type=argparse.FileType('r')) |
| subcmd.add_argument('--shared', nargs='?', const='yes', choices=['yes', 'no'], help='shared action (default: private)') |
| subcmd.add_argument('-a', '--annotation', help='annotations', nargs=2, action='append') |
| subcmd.add_argument('-p', '--param', help='default parameters', nargs=2, action='append') |
| subcmd.add_argument('-t', '--timeout', help='the timeout limit in milliseconds when the action will be terminated', type=int) |
| subcmd.add_argument('-m', '--memory', help='the memory limit in MB of the container that runs the action', type=int) |
| |
| subcmd = parser.add_parser('update', help='update an existing action') |
| subcmd.add_argument('name', help='the name of the action') |
| subcmd.add_argument('artifact', nargs='?', default=None, help='artifact (e.g., file name) containing action definition') |
| addAuthenticatedCommand(subcmd, props) |
| subcmd.add_argument('--docker', help='treat artifact as docker image path on dockerhub', action='store_true') |
| subcmd.add_argument('--copy', help='treat artifact as the name of an existing action', action='store_true') |
| subcmd.add_argument('--sequence', help='treat artifact as comma separated sequence of actions to invoke', action='store_true') |
| subcmd.add_argument('--lib', help='add library to artifact (must be a gzipped tar file)', type=argparse.FileType('r')) |
| subcmd.add_argument('--shared', nargs='?', const='yes', choices=['yes', 'no'], help='shared action (default: private)') |
| subcmd.add_argument('-a', '--annotation', help='annotations', nargs=2, action='append') |
| subcmd.add_argument('-p', '--param', help='default parameters', nargs=2, action='append') |
| subcmd.add_argument('-t', '--timeout', help='the timeout limit in milliseconds when the action will be terminated', type=int) |
| subcmd.add_argument('-m', '--memory', help='the memory limit in MB of the container that runs the action', type=int) |
| |
| subcmd = parser.add_parser('invoke', help='invoke action') |
| subcmd.add_argument('name', help='the name of the action to invoke') |
| addAuthenticatedCommand(subcmd, props) |
| subcmd.add_argument('-p', '--param', help='parameters', nargs=2, action='append') |
| subcmd.add_argument('-b', '--blocking', action='store_true', help='blocking invoke') |
| subcmd.add_argument('-r', '--result', help='show only activation result if a blocking activation (unless there is a failure)', action='store_true') |
| |
| self.addDefaultCommands(parser, props) |
| |
| def cmd(self, args, props): |
| if args.subcmd == 'invoke': |
| return self.invoke(args, props) |
| else: |
| return super(Action, self).cmd(args, props) |
| |
| def create(self, args, props, update): |
| exe = self.getExec(args, props) |
| if args.sequence: |
| if args.param is None: |
| args.param = [] |
| ns = props['namespace'] |
| actions = self.csvToList(args.artifact) |
| actions = [ getQName(a, ns) for a in actions ] |
| args.param.append([ '_actions', json.dumps(actions)]) |
| |
| validExe = exe is not None and 'kind' in exe |
| if update or validExe: # if create action, then exe must be valid |
| payload = {} |
| if args.annotation: |
| payload['annotations'] = getAnnotations(args) |
| if args.param: |
| payload['parameters'] = getParams(args) |
| # API will accept limits == {} as limits not specified on an update |
| if args.timeout or args.memory: |
| payload['limits'] = self.getLimits(args) |
| if validExe: |
| payload['exec'] = exe |
| if args.shared: |
| self.addPublish(payload, args) |
| return self.put(args, props, update, json.dumps(payload)) |
| else: |
| if not args.copy: |
| print 'the artifact "%s" is not a valid file. If this is a docker image, use --docker.' % args.artifact |
| else: |
| print 'the action "%s" does not exit (or your are not entitled to it).' % args.artifact |
| return 2 |
| |
| def invoke(self, args, props): |
| res = self.doInvoke(args, props) |
| # OK implies successful blocking invoke |
| # ACCEPTED implies non-blocking |
| # All else are failures |
| if res.status == httplib.OK or res.status == httplib.ACCEPTED: |
| result = json.loads(res.read()) |
| if not (args.result and args.blocking and res.status == httplib.OK): |
| print 'ok: invoked %(name)s with id %(id)s' % {'name': args.name, 'id': result['activationId'] } |
| if res.status == httplib.OK and args.result: |
| print getPrettyJson(result['response']['result']) |
| elif res.status == httplib.OK : |
| print bold('response:') |
| print getPrettyJson(result) |
| return 0 |
| else: |
| return responseError(res) |
| |
| # invokes the action and returns HTTP response |
| def doInvoke(self, args, props): |
| namespace, pname = parseQName(args.name, props) |
| url = 'https://%(apibase)s/namespaces/%(namespace)s/actions/%(name)s?blocking=%(blocking)s' % { |
| 'apibase': apiBase(props), |
| 'namespace': urllib.quote(namespace), |
| 'name': self.getSafeName(pname), |
| 'blocking': 'true' if args.blocking else 'false' |
| } |
| payload = json.dumps(getActivationArgument(args)) |
| headers = { |
| 'Content-Type': 'application/json' |
| } |
| res = request('POST', url, payload, headers, auth=args.auth, verbose=args.verbose) |
| return res |
| |
| # creates { timeout: msecs, memory: megabytes } action timeout/memory limits |
| def getLimits(self, args): |
| limits = {} |
| if args.timeout: |
| limits['timeout'] = args.timeout |
| if args.memory : |
| limits['memory'] = args.memory |
| return limits |
| |
| # creates one of: |
| # { kind: "nodejs", code: "js code", initializer: "base64 encoded string" } |
| # where initializer is optional, or: |
| # { kind: "blackbox", image: "docker image" }, or: |
| # { kind: "swift", code: "swift code" }, or: |
| # { kind: "python", code: "python code" }, or: |
| # { kind: "java", jar: "base64-encoded JAR", main: "FQN of main class" } |
| def getExec(self, args, props): |
| exe = {} |
| if args.docker: |
| exe['kind'] = 'blackbox' |
| exe['image'] = args.artifact |
| elif args.copy: |
| existingAction = args.artifact |
| exe = self.getActionExec(args, props, existingAction) |
| elif args.sequence: |
| pipeAction = '/whisk.system/system/pipe' |
| exe = self.getActionExec(args, props, pipeAction) |
| elif args.artifact is not None and os.path.isfile(args.artifact): |
| contents = open(args.artifact, 'rb').read() |
| if args.artifact.endswith('.swift'): |
| exe['kind'] = 'swift' |
| exe['code'] = contents |
| elif args.artifact.endswith('.py'): |
| exe['kind'] = 'python' |
| exe['code'] = contents |
| elif args.artifact.endswith('.jar'): |
| exe['kind'] = 'java' |
| exe['jar'] = base64.b64encode(contents) |
| exe['main'] = self.findMainClass(args.artifact) |
| else: |
| exe['kind'] = 'nodejs' |
| exe['code'] = contents |
| if args.lib: |
| exe['initializer'] = base64.b64encode(args.lib.read()) |
| return exe |
| |
| def findMainClass(self, jarPath): |
| signature = """public static com.google.gson.JsonObject main(com.google.gson.JsonObject);""" |
| def run(cmd): |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| (o,e) = proc.communicate() |
| if proc.returncode != 0: |
| msg = "An error occurred while executing %s." % " ".join(cmd) |
| if e.strip() != "": |
| msg = msg + "\n" + e |
| raise Exception(msg) |
| return o |
| |
| jarLines = run(["jar", "-tf", jarPath]).split("\n") |
| classes = filter(lambda x: x.endswith(".class"), jarLines) |
| classes = map(lambda x: x[:-6].replace("/", "."), classes) |
| |
| for c in classes: |
| javapLines = run(["javap", "-cp", jarPath, c]) |
| if signature in javapLines: |
| return c |
| |
| raise Exception("Couldn't find 'main' method in %s." % jarPath) |
| |
| |
| def getActionExec(self, args, props, name): |
| res = self.httpGet(args, props, name) |
| if res.status == httplib.OK: |
| execField = json.loads(res.read())['exec'] |
| else: |
| execField = None |
| return execField |
| |
| def csvToList(self, csv): |
| return csv.split(',') |