| # |
| # 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. |
| # |
| |
| from __future__ import print_function |
| import sys |
| import os |
| import json |
| import base64 |
| import httplib |
| import argparse |
| import urllib |
| import subprocess |
| from wskitem import Item |
| from wskutil import (addAuthenticatedCommand, 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('--kind', help='the kind of the action runtime (example: swift:3)', type=str) |
| 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.add_argument('-l', '--logsize', help='the logsize 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('--kind', help='the kind of the action runtime (example: swift:3)', type=str) |
| 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.add_argument('-l', '--logsize', help='the logsize 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', action='append', |
| help='parameters', nargs=2) |
| subcmd.add_argument('-b', '--blocking', action='store_true', |
| help='blocking invoke') |
| subcmd.add_argument('-r', '--result', action='store_true', |
| help=('show only activation result if a blocking ' |
| 'activation (unless there is a failure)')) |
| |
| 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 csvToQualifiedActions(self, props, csv): |
| ns = props['namespace'] |
| actions = self.csvToList(csv) |
| return [getQName(a, ns) for a in actions] |
| |
| def create(self, args, props, update): |
| exe = self.getExec(args, props) |
| validExe = exe is not None and 'kind' in exe |
| fileok = args.artifact is None or os.path.isfile(args.artifact) |
| # if create action, then exe must be valid |
| if (update and fileok) or validExe: |
| 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 is not None or args.memory is not None or |
| args.logsize is not None): |
| 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, |
| file=sys.stderr) |
| else: |
| print('the action "%s" does not exit, is malformed, or your ' |
| 'are not entitled to it.' % args.artifact, |
| file=sys.stderr) |
| return 2 |
| |
| def invoke(self, args, props): |
| res = self.doInvoke(args, props) |
| try: |
| result = json.loads(res.read()) |
| # if args.result is true, there is no activation id |
| if 'activationId' in result: |
| fmt = 'ok: invoked %(name)s with id %(id)s' |
| print(fmt % {'name': args.name, 'id': result['activationId']}) |
| if res.status == httplib.OK: # true iff args.blocking is true |
| # prints the activation or just the result if args.result |
| print(getPrettyJson(result)) |
| return 0 |
| elif res.status == httplib.ACCEPTED: |
| return 0 if not args.blocking else res.status |
| elif res.status == httplib.BAD_GATEWAY: |
| return responseError(res, prefix='', flatten=False) |
| elif (res.status == httplib.INTERNAL_SERVER_ERROR and |
| 'code' not in result): |
| return responseError(res, prefix='', flatten=False) |
| else: |
| return responseError(res) |
| except: |
| return responseError(res) |
| |
| # invokes the action and returns HTTP response |
| def doInvoke(self, args, props): |
| namespace, pname = parseQName(args.name, props) |
| url = ('%(apibase)s/namespaces/%(namespace)s/actions/%(name)s?' |
| 'blocking=%(blocking)s&result=%(result)s') % { |
| 'apibase': apiBase(props), |
| 'namespace': urllib.quote(namespace), |
| 'name': self.getSafeName(pname), |
| 'blocking': 'true' if args.blocking else 'false', |
| 'result': 'true' if 'result' in args and args.result 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 |
| |
| # create { timeout: msecs, memory: megabytes } action timeout/memory limits |
| def getLimits(self, args): |
| limits = {} |
| if args.timeout is not None: |
| limits['timeout'] = args.timeout |
| if args.memory is not None: |
| limits['memory'] = args.memory |
| if args.logsize is not None: |
| limits['logs'] = args.logsize |
| return limits |
| |
| # creates one of: |
| # {kind: "nodejs", code: "js code", initializer: "base64 encoded string"} |
| # where initializer is optional |
| # {kind: "nodejs6", code: "js6 code", initializer: "base64 encoded string"} |
| # where initializer is optional |
| # {kind: "python", code: "python code"} |
| # {kind: "swift", code: "swift code"} |
| # {kind: "swift3", code: "swift3 code"} |
| # {kind: "java", jar: "base64-encoded JAR", main: "FQN of main class"} |
| # {kind: "blackbox", image: "docker image"} |
| # {kind: "sequence", components: list of fully qualified actions} |
| def getExec(self, args, props): |
| exe = {} |
| if args.docker: |
| exe['kind'] = 'blackbox' |
| exe['image'] = args.artifact |
| if args.lib: |
| exe['code'] = base64.b64encode(args.lib.read()) |
| elif args.copy: |
| existingAction = args.artifact |
| exe = self.getActionExec(args, props, existingAction) |
| elif args.sequence: |
| exe['kind'] = 'sequence' |
| exe['components'] = self.csvToQualifiedActions(props, |
| args.artifact) |
| elif args.artifact is not None and os.path.isfile(args.artifact): |
| contents = open(args.artifact, 'rb').read() |
| if args.kind in ['swift:3', 'swift:3.0', 'swift:3.0.0']: |
| exe['kind'] = 'swift:3' |
| exe['code'] = contents |
| elif 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) |
| elif args.kind in ['nodejs']: |
| exe['kind'] = 'nodejs' |
| exe['code'] = contents |
| elif args.kind in ['nodejs:6', 'nodejs:6.0', 'nodejs:6.0.0']: |
| exe['kind'] = 'nodejs:6' |
| exe['code'] = contents |
| else: |
| exe['kind'] = 'nodejs:default' |
| exe['code'] = contents |
| 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 += "\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: |
| return json.loads(res.read())['exec'] |
| return None |
| |
| def csvToList(self, csv): |
| return csv.split(',') |