entrypoint.py 12.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# Copyright (c) 2016, Daniele Venzano
#
# 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.

"""
This module contains the entrypoint for the commandline Zoe client
"""

20
from datetime import datetime, timezone
21 22 23
import json
import logging
import os
Daniele Venzano's avatar
Daniele Venzano committed
24
import pprint
25
import sys
26
import time
27
from argparse import ArgumentParser, Namespace, FileType, RawDescriptionHelpFormatter
Daniele Venzano's avatar
Daniele Venzano committed
28
from typing import Tuple
29

30 31
from tabulate import tabulate

32
from zoe_cmd import utils
33
from zoe_cmd.api_lib import ZoeAPI
34
from zoe_lib.exceptions import ZoeAPIException
35 36


37
def _log_stream_stdout(service_id, timestamps, api: ZoeAPI):
38
    try:
39
        for line in api.services.get_logs(service_id):
40 41 42 43 44 45 46 47 48 49
            if timestamps:
                print(line[0], line[1])
            else:
                print(line[1])
    except KeyboardInterrupt:
        print('CTRL-C detected, exiting...')
        return 'interrupt'
    return 'stream_end'


50
def info_cmd(api: ZoeAPI, args_):
51
    """Queries the info endpoint."""
52
    info = api.info.info()
53 54 55 56 57 58
    print("Zoe version: ", info['version'])
    print("Zoe API version: ", info['api_version'])
    print("ZApp format version: ", info['application_format_version'])
    print("Deployment name: ", info['deployment_name'])


59
def app_get_cmd(api: ZoeAPI, args):
60
    """Extract an application description from an execution."""
61
    execution = api.executions.get(args.id)
62
    if execution is None:
63
        print("no such execution")
64
    else:
65
        json.dump(execution['description'], sys.stdout, sort_keys=True, indent=4)
66 67


68
def exec_list_cmd(api: ZoeAPI, args):
69
    """List executions"""
70
    print(api.auth_user)
Daniele Venzano's avatar
Daniele Venzano committed
71 72 73 74 75 76 77 78 79 80 81
    filter_names = [
        'status',
        'name',
        'limit',
        'earlier_than_submit',
        'earlier_than_start',
        'earlier_than_end',
        'later_than_submit',
        'later_than_start',
        'later_than_end'
    ]
82
    filters = {
83
        'user_id': api.auth_user['id']
84
    }
Daniele Venzano's avatar
Daniele Venzano committed
85 86 87
    for key, value in vars(args).items():
        if key in filter_names:
            filters[key] = value
88
    data = api.executions.list(**filters)
89 90
    if len(data) == 0:
        return
91
    tabular_data = [[e['id'], e['name'], e['user_id'], e['status']] for e in sorted(data, key=lambda x: x['id'])]
92 93
    headers = ['ID', 'Name', 'User ID', 'Status']
    print(tabulate(tabular_data, headers))
94 95


96
def exec_start_cmd(api: ZoeAPI, args):
97
    """Submit an execution."""
98
    app_descr = json.load(args.jsonfile)
99
    exec_id = api.executions.start(args.name, app_descr)
100
    if not args.synchronous:
101
        print("Execution created successfully with ID {}, use the exec-get command to check its status".format(exec_id))
102
    else:
103
        print("Execution created successfully with ID {}, waiting for status change".format(exec_id))
104 105
        old_status = 'submitted'
        while True:
106
            execution = api.executions.get(exec_id)
107 108 109 110 111 112
            current_status = execution['status']
            if old_status != current_status:
                print('Execution is now {}'.format(current_status))
                old_status = current_status
            if current_status == 'running':
                break
113 114 115 116
            time.sleep(0.2)
        if args.running:
            print('Execution running')
            exit(0)
117 118
        monitor_service_id = None
        for service_id in execution['services']:
119
            service = api.services.get(service_id)
120 121 122 123 124
            if service['description']['monitor']:
                monitor_service_id = service['id']
                break

        print('\n>------ start of log streaming -------<\n')
125
        why_stop = _log_stream_stdout(monitor_service_id, False, api)
126 127 128 129 130 131 132
        print('\n>------ end of log streaming -------<\n')
        if why_stop == 'stream_end':
            print('Execution finished')
            exit(0)
        elif why_stop == 'interrupt':
            print('Do not worry, your execution ({}) is still running.'.format(exec_id))
            exit(1)
133 134


135
def exec_get_cmd(api: ZoeAPI, args):
136
    """Gather information about an execution."""
137
    execution = api.executions.get(args.id)
138 139 140 141
    if execution is None:
        print('Execution not found')
    else:
        print('Execution {} (ID: {})'.format(execution['name'], execution['id']))
142
        print('Application name: {}'.format(execution['description']['name']))
143
        print('Status: {}'.format(execution['status']))
144
        if execution['status'] == 'error':
145
            print('Last error: {}'.format(execution['error_message']))
146
        print()
147
        print('Time submit: {}'.format(datetime.fromtimestamp(execution['time_submit'], timezone.utc).astimezone()))
148 149 150 151

        if execution['time_start'] is None:
            print('Time start: {}'.format('not yet'))
        else:
152
            print('Time start: {}'.format(datetime.fromtimestamp(execution['time_start'], timezone.utc).astimezone()))
153 154 155 156

        if execution['time_end'] is None:
            print('Time end: {}'.format('not yet'))
        else:
157
            print('Time end: {}'.format(datetime.fromtimestamp(execution['time_end'], timezone.utc).astimezone()))
158
        print()
159

160
        endpoints = api.executions.endpoints(execution['id'])
161
        if endpoints is not None and len(endpoints) > 0:
162
            print('Exposed endpoints:')
163 164
            for endpoint in endpoints:
                print(' - {}: {}'.format(endpoint[0], endpoint[1]))
165 166 167 168
        else:
            print('This ZApp does not expose any endpoint')

        print()
169
        tabular_data = []
170
        for c_id in execution['services']:
171
            service = api.services.get(c_id)
172
            service_data = [service['id'], service['name'], 'true' if service['essential'] else 'false', service['status'], service['backend_status'], service['error_message'] if service['error_message'] is not None else '']
173
            tabular_data.append(service_data)
174
        headers = ['ID', 'Name', 'Essential', 'Zoe status', 'Backend status', 'Error message']
175
        print(tabulate(tabular_data, headers))
176 177


178
def exec_kill_cmd(api: ZoeAPI, args):
179
    """Kill an execution."""
180
    api.executions.terminate(args.id)
181 182
    if args.synchronous:
        while True:
Daniele Venzano's avatar
Typo  
Daniele Venzano committed
183
            execution = api.executions.get(args.id)
184 185 186 187 188 189 190 191
            current_status = execution['status']
            if old_status != current_status:
                print('Execution is now {}'.format(current_status))
                old_status = current_status
            if current_status == 'terminated':
                break
            time.sleep(0.2)
        print('Execution terminated')
192 193


194
def logs_cmd(api: ZoeAPI, args):
195
    """Retrieves and streams the logs of a service."""
196
    _log_stream_stdout(args.service_id, args.timestamps, api)
197 198


199
def stats_cmd(api: ZoeAPI, args_):
200
    """Prints statistics on Zoe internals."""
201
    sched = api.statistics.scheduler()
Daniele Venzano's avatar
Daniele Venzano committed
202
    pprint.pprint(sched)
203

204

205
ENV_HELP_TEXT = '''To authenticate with Zoe you need to define three environment variables:
206 207
ZOE_URL: point to the URL of the Zoe Scheduler (ex.: http://localhost:5000/
ZOE_USER: the username used for authentication
208 209
ZOE_PASS: the password used for authentication

210
or create a ~/.zoerc file (another location can be specified with --api-file) like this:
211 212 213 214 215 216
url = xxx
user = yyy
pass = zzz

Environment variable will override the values specified in the configuration file.
'''
217 218


Daniele Venzano's avatar
Daniele Venzano committed
219
def process_arguments() -> Tuple[ArgumentParser, Namespace]:
220
    """Parse command line arguments."""
221 222
    parser = ArgumentParser(description="Zoe command-line client", epilog=ENV_HELP_TEXT, formatter_class=RawDescriptionHelpFormatter)
    parser.add_argument('--debug', action='store_true', help='Enable debug output')
223
    parser.add_argument('--auth-file', type=str, help='Enable debug output', default=os.path.join(os.getenv('HOME', ''), '.zoerc'))
224 225 226

    subparser = parser.add_subparsers()

227
    # info
228 229 230
    argparser_info = subparser.add_parser('info', help="Queries the API for supported versions")
    argparser_info.set_defaults(func=info_cmd)

Francesco Pace's avatar
Francesco Pace committed
231
    argparser_exec_start = subparser.add_parser('start', help="Start an application")
232
    argparser_exec_start.add_argument('-s', '--synchronous', action='store_true', help="Do not detach, wait for execution to finish, print main service log")
233
    argparser_exec_start.add_argument('-r', '--running', action='store_true', help="Do not detach, wait for execution to start (state: running)")
234
    argparser_exec_start.add_argument('name', help="Name of the execution")
235
    argparser_exec_start.add_argument('jsonfile', type=FileType("r"), help='Application description')
236 237
    argparser_exec_start.set_defaults(func=exec_start_cmd)

238
    argparser_app_list = subparser.add_parser('exec-ls', help="List all executions for the calling user")
Daniele Venzano's avatar
Daniele Venzano committed
239 240
    argparser_app_list.add_argument('--limit', type=int, help='Limit the number of executions')
    argparser_app_list.add_argument('--name', help='Show only executions with this name')
241
    argparser_app_list.add_argument('--status', choices=["submitted", "queued", "starting", "error", "running", "cleaning up", "terminated"], help='Show only executions with this status')
Daniele Venzano's avatar
Daniele Venzano committed
242
    argparser_app_list.add_argument('--earlier-than-submit', help='Show only executions submitted earlier than this timestamp (seconds since UTC epoch)')
243 244 245 246 247
    argparser_app_list.add_argument('--earlier-than-start', help='Show only executions started earlier than this timestamp (seconds since UTC epoch)')
    argparser_app_list.add_argument('--earlier-than-end', help='Show only executions ended earlier than this timestamp (seconds since UTC epoch)')
    argparser_app_list.add_argument('--later-than-submit', help='Show only executions submitted later than this timestamp (seconds since UTC epoch)')
    argparser_app_list.add_argument('--later-than-start', help='Show only executions started later than this timestamp (seconds since UTC epoch)')
    argparser_app_list.add_argument('--later-than-end', help='Show only executions ended later than this timestamp (seconds since UTC epoch)')
248 249
    argparser_app_list.set_defaults(func=exec_list_cmd)

250 251 252 253
    argparser_execution_get = subparser.add_parser('exec-get', help="Get execution status")
    argparser_execution_get.add_argument('id', type=int, help="Execution id")
    argparser_execution_get.set_defaults(func=exec_get_cmd)

254
    argparser_app_get = subparser.add_parser('exec-app-get', help="Retrieve an already defined application description")
255
    argparser_app_get.add_argument('id', help='The ID of the application')
256
    argparser_app_get.set_defaults(func=app_get_cmd)
257 258 259

    argparser_execution_kill = subparser.add_parser('terminate', help="Terminates an execution")
    argparser_execution_kill.add_argument('id', type=int, help="Execution id")
260
    argparser_execution_kill.add_argument('-s', '--synchronous', action='store_true', help="Do not detach, wait for execution to terminate")
261 262
    argparser_execution_kill.set_defaults(func=exec_kill_cmd)

263 264 265 266 267
    argparser_logs = subparser.add_parser('logs', help="Streams the service logs")
    argparser_logs.add_argument('service_id', type=int, help="Service id")
    argparser_logs.add_argument('-t', '--timestamps', action='store_true', help="Prefix timestamps for each line")
    argparser_logs.set_defaults(func=logs_cmd)

268 269 270
    argparser_stats = subparser.add_parser('stats', help="Prints all available statistics")
    argparser_stats.set_defaults(func=stats_cmd)

271 272 273 274
    return parser, parser.parse_args()


def zoe():
275
    """Main entrypoint."""
276 277 278 279 280 281 282 283 284 285 286
    parser, args = process_arguments()
    if args.debug:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO)
    logging.getLogger("requests").setLevel(logging.WARNING)

    if not hasattr(args, "func"):
        parser.print_help()
        return

287 288 289
    auth = utils.read_auth(args)
    if auth is None:
        sys.exit(1)
290 291

    try:
292 293
        api = ZoeAPI(auth['url'], auth['user'], auth['pass'])
        args.func(api, args)
294 295
    except ZoeAPIException as e:
        print(e.message)
296 297
    except KeyboardInterrupt:
        print('CTRL-C pressed, exiting...')
298
    sys.exit(0)