entrypoint_admin.py 24.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Copyright (c) 2017, 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
24
25
26
27
28
29
30
import json
import logging
import os
import sys
from argparse import ArgumentParser, Namespace, FileType, RawDescriptionHelpFormatter
from typing import Tuple

from tabulate import tabulate

from zoe_cmd import utils
31
from zoe_cmd.api_lib import ZoeAPI
32
33
34
35
36
from zoe_lib.exceptions import ZoeAPIException, InvalidApplicationDescription
from zoe_lib.applications import app_validate
from zoe_lib.version import ZOE_API_VERSION


37
def _check_api_version(api: ZoeAPI):
38
    """Checks if there is a version mismatch between server and client."""
39
    info = api.info.info()
40
41
42
43
44
45
    if info['api_version'] != ZOE_API_VERSION:
        print('Warning: this client understands ZOE API v. {}, but server talks v. {}'.format(ZOE_API_VERSION, info['api_version']))
        print('Warning: certain commands may not work correctly')
        print('Warning: please upgrade or downgrade your client to match the server version')


46
def app_validate_cmd(api_: ZoeAPI, args):
47
48
49
50
51
52
53
54
55
56
    """Validate an application description."""
    app_descr = json.load(args.jsonfile)
    try:
        app_validate(app_descr)
    except InvalidApplicationDescription as e:
        print(e)
    else:
        print("Static validation OK")


57
def exec_list_cmd(api: ZoeAPI, args):
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
    """List executions"""
    filter_names = [
        'status',
        'name',
        'user_id',
        'limit',
        'earlier_than_submit',
        'earlier_than_start',
        'earlier_than_end',
        'later_than_submit',
        'later_than_start',
        'later_than_end'
    ]
    filters = {}
    for key, value in vars(args).items():
        if key in filter_names:
            filters[key] = value
75
    data = api.executions.list(**filters)
76
77
    if len(data) == 0:
        return
78
    tabular_data = [[e['id'], e['name'], e['user_id'], e['status']] for e in sorted(data, key=lambda x: x['id'])]
79
80
81
82
    headers = ['ID', 'Name', 'User ID', 'Status']
    print(tabulate(tabular_data, headers))


83
def exec_get_cmd(api: ZoeAPI, args):
84
    """Gather information about an execution."""
85
    execution = api.executions.get(args.id)
86
87
88
89
90
91
92
93
94
    if execution is None:
        print('Execution not found')
    else:
        print('Execution {} (ID: {})'.format(execution['name'], execution['id']))
        print('Application name: {}'.format(execution['description']['name']))
        print('Status: {}'.format(execution['status']))
        if execution['status'] == 'error':
            print('Last error: {}'.format(execution['error_message']))
        print()
95
        print('Time submit: {}'.format(datetime.fromtimestamp(execution['time_submit'], timezone.utc).astimezone()))
96
97
98
99

        if execution['time_start'] is None:
            print('Time start: {}'.format('not yet'))
        else:
100
            print('Time start: {}'.format(datetime.fromtimestamp(execution['time_start'], timezone.utc).astimezone()))
101
102
103
104

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

108
        endpoints = api.executions.endpoints(execution['id'])
109
110
111
112
113
114
115
116
117
118
        if endpoints is not None and len(endpoints) > 0:
            print('Exposed endpoints:')
            for endpoint in endpoints:
                print(' - {}: {}'.format(endpoint[0], endpoint[1]))
        else:
            print('This ZApp does not expose any endpoint')

        print()
        tabular_data = []
        for c_id in execution['services']:
119
            service = api.services.get(c_id)
120
            service_data = [service['id'], service['name'], 'true' if service['essential'] else 'false', service['status'], service['backend_status'], service['backend_host'], service['error_message'] if service['error_message'] is not None else '']
121
            tabular_data.append(service_data)
122
        headers = ['ID', 'Name', 'Essential', 'Zoe status', 'Backend status', 'Host', 'Error message']
123
124
125
        print(tabulate(tabular_data, headers))


126
def exec_rm_cmd(api: ZoeAPI, args):
127
    """Delete an execution and kill it if necessary."""
128
    api.executions.delete(args.id)
129
130


131
def exec_kill_user_cmd(api: ZoeAPI, args):
132
133
134
135
136
    """Terminates all executions for the given user."""
    filters = {
        'status': 'running',
        'user_id': args.user_id
    }
137
    data = api.executions.list(**filters)
138
139
    print('Terminating {} executions belonging to user {}'.format(len(data), args.user_id))
    for execution in data:
140
        api.executions.terminate(execution)
141
142
143
        print('Execution {} terminated'.format(execution))


144
145
146
147
148
149
def quota_ls_cmd(api: ZoeAPI, args):
    """List available quotas."""
    filters = {}
    if 'name' in args:
        filters['name'] = args.name
    quotas = api.quota.list(filters)
150
    tabular_data = [[q['id'], q['name'], q['concurrent_executions'], q['memory'], q['cores']] for q in sorted(quotas, key=lambda x: x['id'])]
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    headers = ['ID', 'Name', 'Conc. Executions', 'Memory', 'Cores']
    print(tabulate(tabular_data, headers))


def quota_get_cmd(api: ZoeAPI, args):
    """Get a quota by its ID."""
    quota = api.quota.get(args.id)
    tabular_data = [[quota['id'], quota['name'], quota['concurrent_executions'], quota['memory'], quota['cores']]]
    headers = ['ID', 'Name', 'Conc. Executions', 'Memory', 'Cores']
    print(tabulate(tabular_data, headers))


def quota_create_cmd(api: ZoeAPI, args):
    """Create a new quota."""
    quota = {
        'name': args.name,
        'concurrent_executions': args.concurrent_executions,
        'memory': args.memory,
        'cores': args.cores
    }
    new_id = api.quota.create(quota)
    print('New quota created with ID: {}'.format(new_id))


def quota_delete_cmd(api: ZoeAPI, args):
    """Delete a quota given its ID."""
    api.quota.delete(args.id)


def quota_update_cmd(api: ZoeAPI, args):
    """Updates an existing quota."""
    quota_update = {}
    if args.name is not None:
        quota_update['name'] = args.name
    if args.concurrent_executions is not None:
        quota_update['concurrent_executions'] = args.concurrent_executions
    if args.memory is not None:
        quota_update['memory'] = args.memory
    if args.cores is not None:
        quota_update['cores'] = args.cores
    api.quota.update(args.id, quota_update)


194
195
196
def role_ls_cmd(api: ZoeAPI, args):
    """List available roles."""
    def b2t(val):
197
        """Boolean to text."""
198
199
200
201
202
203
        if val:
            return "Yes"
        else:
            return "No"

    filters = {}
204
    if args.name is not None:
205
206
        filters['name'] = args.name
    roles = api.role.list(filters)
207
    tabular_data = [[r['id'], r['name'], b2t(r['can_see_status']), b2t(r['can_change_config']), b2t(r['can_operate_others']), b2t(r['can_delete_executions']), b2t(r['can_access_api']), b2t(r['can_customize_resources'])] for r in sorted(roles, key=lambda x: x['id'])]
208
209
210
211
212
213
214
    headers = ['ID', 'Name', 'See status', 'Change config', 'Operate others', 'Delete execs', 'API access', 'Customize resources']
    print(tabulate(tabular_data, headers))


def role_get_cmd(api: ZoeAPI, args):
    """Get a role by its ID."""
    def b2t(val):
215
        """Boolean to text."""
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
        if val:
            return "Yes"
        else:
            return "No"

    role = api.role.get(args.id)
    tabular_data = [[role['id'], role['name'], b2t(role['can_see_status']), b2t(role['can_change_config']), b2t(role['can_operate_others']), b2t(role['can_delete_executions']), b2t(role['can_access_api']), b2t(role['can_customize_resources'])]]
    headers = ['ID', 'Name', 'See status', 'Change config', 'Operate others', 'Delete execs', 'API access', 'Customize resources']
    print(tabulate(tabular_data, headers))


def role_create_cmd(api: ZoeAPI, args):
    """Create a new role."""
    role = {
        'name': args.name,
        'can_see_status': True if args.can_see_status else False,
        'can_change_config': True if args.can_change_config else False,
        'can_operate_others': True if args.can_operate_others else False,
        'can_delete_executions': True if args.can_delete_executions else False,
        'can_access_api': True if args.can_access_api else False,
236
237
        'can_customize_resources': True if args.can_customize_resources else False,
        'can_access_full_zapp_shop': True if args.can_access_full_zapp_shop else False
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
    }
    new_id = api.role.create(role)
    print('New role created with ID: {}'.format(new_id))


def role_delete_cmd(api: ZoeAPI, args):
    """Delete a role given its ID."""
    api.role.delete(args.id)


def role_update_cmd(api: ZoeAPI, args):
    """Updates an existing quota."""
    role_update = {}
    if args.name is not None:
        role_update['name'] = args.name
    if args.can_see_status is not None:
        role_update['can_see_status'] = True if args.can_see_status else False
    if args.can_change_config is not None:
        role_update['can_change_config'] = True if args.can_change_config else False
    if args.can_operate_others is not None:
        role_update['can_operate_others'] = True if args.can_operate_others else False
    if args.can_delete_executions is not None:
        role_update['can_delete_executions'] = True if args.can_delete_executions else False
    if args.can_access_api is not None:
        role_update['can_access_api'] = True if args.can_access_api else False
    if args.can_customize_resources is not None:
        role_update['can_customize_resources'] = True if args.can_customize_resources else False
265
266
    if args.can_access_full_zapp_shop is not None:
        role_update['can_access_full_zapp_shop'] = True if args.can_access_full_zapp_shop else False
267
268
269
    api.role.update(args.id, role_update)


270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def user_ls_cmd(api: ZoeAPI, args):
    """List defined users."""
    filters = {}
    if args.username is not None:
        filters['username'] = args.username
    if args.enabled is not None:
        filters['enabled'] = True if args.enabled == 1 else False
    if args.auth_source is not None:
        filters['auth_source'] = args.auth_source
    if args.role is not None:
        role = api.role.list({'name': args.role})[0]
        if role is None:
            print('Unknown role specified')
            return
        filters['role_id'] = role['id']
    if args.quota is not None:
        quota = api.quota.list({'name': args.quota})[0]
        if quota is None:
            print('Unknown quota specified')
            return
        filters['quota_id'] = quota['id']

    users = api.user.list(filters)
    tabular_data = []
    role_cache = {}
    quota_cache = {}
    for user in sorted(users, key=lambda u: u['id']):
        if user['role_id'] in role_cache:
            role = role_cache[user['role_id']]
        else:
            role = api.role.get(user['role_id'])
            role_cache[user['role_id']] = role

        if user['quota_id'] in quota_cache:
            quota = quota_cache[user['quota_id']]
        else:
            quota = api.quota.get(user['quota_id'])
            quota_cache[user['quota_id']] = quota

        tabular_data.append([user['id'], user['username'], user['email'], user['fs_uid'], user['priority'], user['enabled'], user['auth_source'], role['name'], quota['name']])

    headers = ['ID', 'Username', 'Email', 'FS UID', 'Priority', 'Enabled', 'Auth source', 'Role', 'Quota']
    print(tabulate(tabular_data, headers))


def user_get_cmd(api: ZoeAPI, args):
    """Get a user by its ID."""
    user = api.user.get(args.id)
    role = api.role.get(user['role_id'])
    quota = api.quota.get(role['quota_id'])
    tabular_data = [[user['id'], user['username'], user['email'], user['fs_uid'], user['priority'], user['enabled'], user['auth_source'], role['name'], quota['name']]]
    headers = ['ID', 'Username', 'Email', 'FS UID', 'Priority', 'Enabled', 'Auth source', 'Role', 'Quota']
    print(tabulate(tabular_data, headers))


def user_create_cmd(api: ZoeAPI, args):
    """Creates a user."""
    user = {
        'username': args.username,
        'email': args.email,
        'auth_source': args.auth_source,
331
        'fs_uid': args.fs_uid
332
    }
333
334
    quota = api.quota.list({'name': args.quota})
    if len(quota) == 0:
335
336
        print('Unknown quota')
        return
337
338
339
    user['quota_id'] = quota[0]['id']
    role = api.role.list({'name': args.role})
    if len(role) == 0:
340
341
        print('Unknown role')
        return
342
    user['role_id'] = role[0]['id']
343
344
345
346
347
348
349
350
351
352
353
    new_id = api.user.create(user)
    print('New user created with ID: {}'.format(new_id))


def user_delete_cmd(api: ZoeAPI, args):
    """Delete a user."""
    api.user.delete(args.id)


def user_update_cmd(api: ZoeAPI, args):
    """Updates a user."""
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
    user_update = {}
    if args.email is not None:
        user_update['email'] = args.email
    if args.fs_uid is not None:
        user_update['fs_uid'] = args.fs_uid
    if args.password is not None:
        user_update['password'] = args.password
    if args.enabled is not None:
        user_update['enabled'] = args.enabled
    if args.auth_source is not None:
        user_update['auth_source'] = args.auth_source
    if args.priority is not None:
        user_update['priority'] = args.priority
    if args.role_id is not None:
        user_update['role_id'] = args.role_id
    if args.quota_id is not None:
        user_update['quota_id'] = args.quota_id

    api.user.update(args.id, user_update)
373
374


375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
ENV_HELP_TEXT = '''To authenticate with Zoe you need to define three environment variables:
ZOE_URL: point to the URL of the Zoe Scheduler (ex.: http://localhost:5000/
ZOE_USER: the username used for authentication
ZOE_PASS: the password used for authentication

or create a ~/.zoerc file (another location can be specified with --auth-file) like this:
url = xxx
user = yyy
pass = zzz

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


def process_arguments() -> Tuple[ArgumentParser, Namespace]:
    """Parse command line arguments."""
    parser = ArgumentParser(description="Zoe command-line administration client", epilog=ENV_HELP_TEXT, formatter_class=RawDescriptionHelpFormatter)
    parser.add_argument('--debug', action='store_true', help='Enable debug output')
    parser.add_argument('--auth-file', type=str, help='Enable debug output', default=os.path.join(os.getenv('HOME', ''), '.zoerc'))

    subparser = parser.add_subparsers()

    # zapps
    argparser_zapp_validate = subparser.add_parser('zapp-validate', help='Validate an application description')
    argparser_zapp_validate.add_argument('jsonfile', type=FileType("r"), help='Application description')
    argparser_zapp_validate.set_defaults(func=app_validate_cmd)

    # executions
    argparser_app_list = subparser.add_parser('exec-ls', help="List all executions for the calling user")
    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')
    argparser_app_list.add_argument('--user_id', help='Show only executions belonging to this user')
    argparser_app_list.add_argument('--status', choices=["submitted", "scheduled", "starting", "error", "running", "cleaning up", "terminated"], help='Show only executions with this status')
    argparser_app_list.add_argument('--earlier-than-submit', help='Show only executions submitted earlier than this timestamp (seconds since UTC epoch)')
    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)')
    argparser_app_list.set_defaults(func=exec_list_cmd)

416
417
418
419
    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)

420
421
422
423
    argparser_execution_rm = subparser.add_parser('exec-rm', help="Deletes an execution")
    argparser_execution_rm.add_argument('id', type=int, help="Execution id")
    argparser_execution_rm.set_defaults(func=exec_rm_cmd)

424
425
426
427
    argparser_execution_kill_user = subparser.add_parser('user-terminate', help="Terminate all executions of a user")
    argparser_execution_kill_user.add_argument('user_id', help="User name")
    argparser_execution_kill_user.set_defaults(func=exec_kill_user_cmd)

428
429
430
431
432
    # Quotas
    sub_parser = subparser.add_parser('quota-ls', help="List existing quotas")
    sub_parser.add_argument('--name', help="Filter by name")
    sub_parser.set_defaults(func=quota_ls_cmd)

433
    sub_parser = subparser.add_parser('quota-get', help="Get a quota by its ID")
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
    sub_parser.add_argument('id', type=int, help="Quota ID")
    sub_parser.set_defaults(func=quota_get_cmd)

    sub_parser = subparser.add_parser('quota-create', help="Create a new quota")
    sub_parser.add_argument('name', help="Quota name")
    sub_parser.add_argument('concurrent_executions', type=int, help="Maximum number of concurrent executions")
    sub_parser.add_argument('memory', type=int, help="Maximum memory in bytes across all running executions")
    sub_parser.add_argument('cores', type=int, help="Maximum number of cores across all running executions")
    sub_parser.set_defaults(func=quota_create_cmd)

    sub_parser = subparser.add_parser('quota-delete', help="Delete a quota")
    sub_parser.add_argument('id', type=int, help="Quota ID")
    sub_parser.set_defaults(func=quota_delete_cmd)

    sub_parser = subparser.add_parser('quota-update', help="Update an existing quota")
    sub_parser.add_argument('id', type=int, help="ID of the quota to update")
    sub_parser.add_argument('--name', help="Quota name")
    sub_parser.add_argument('--concurrent_executions', type=int, help="Maximum number of concurrent executions")
    sub_parser.add_argument('--memory', type=int, help="Maximum memory in bytes across all running executions")
    sub_parser.add_argument('--cores', type=int, help="Maximum number of cores across all running executions")
    sub_parser.set_defaults(func=quota_update_cmd)

456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
    # Roles
    sub_parser = subparser.add_parser('role-ls', help="List existing roles")
    sub_parser.add_argument('--name', help="Filter by name")
    sub_parser.set_defaults(func=role_ls_cmd)

    sub_parser = subparser.add_parser('role-get', help="Get a role by its ID")
    sub_parser.add_argument('id', type=int, help="Role ID")
    sub_parser.set_defaults(func=role_get_cmd)

    sub_parser = subparser.add_parser('role-create', help="Create a new role")
    sub_parser.add_argument('name', help="Role name")
    sub_parser.add_argument('can_see_status', choices=[0, 1], type=int, help="Can access the status web page")
    sub_parser.add_argument('can_change_config', choices=[0, 1], type=int, help="Can change Zoe configuration")
    sub_parser.add_argument('can_operate_others', choices=[0, 1], type=int, help="Can operate on other users' executions")
    sub_parser.add_argument('can_delete_executions', choices=[0, 1], type=int, help="Can delete executions permanently")
    sub_parser.add_argument('can_access_api', choices=[0, 1], type=int, help="Can access the REST API")
    sub_parser.add_argument('can_customize_resources', choices=[0, 1], type=int, help="Can customize resource reservations before starting executions")
473
    sub_parser.add_argument('can_access_full_zapp_shop', choices=[0, 1], type=int, help="Can access all ZApps in the ZApp shop")
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
    sub_parser.set_defaults(func=role_create_cmd)

    sub_parser = subparser.add_parser('role-delete', help="Delete a role")
    sub_parser.add_argument('id', type=int, help="Role ID")
    sub_parser.set_defaults(func=role_delete_cmd)

    sub_parser = subparser.add_parser('role-update', help="Update an existing role")
    sub_parser.add_argument('id', type=int, help="ID of the role to update")
    sub_parser.add_argument('--name', help="Role name")
    sub_parser.add_argument('--can_see_status', choices=[0, 1], type=int, help="Can access the status web page")
    sub_parser.add_argument('--can_change_config', choices=[0, 1], type=int, help="Can change Zoe configuration")
    sub_parser.add_argument('--can_operate_others', choices=[0, 1], type=int, help="Can operate on other users' executions")
    sub_parser.add_argument('--can_delete_executions', choices=[0, 1], type=int, help="Can delete executions permanently")
    sub_parser.add_argument('--can_access_api', choices=[0, 1], type=int, help="Can access the REST API")
    sub_parser.add_argument('--can_customize_resources', choices=[0, 1], type=int, help="Can customize resource reservations before starting executions")
489
    sub_parser.add_argument('--can_access_full_zapp_shop', choices=[0, 1], type=int, help="Can access all ZApps in the ZApp shop")
490
491
    sub_parser.set_defaults(func=role_update_cmd)

492
493
494
495
    # Users
    sub_parser = subparser.add_parser('user-ls', help="List existing users")
    sub_parser.add_argument('--username', help="Filter by user name")
    sub_parser.add_argument('--enabled', type=int, choices=[0, 1], help="Filter by enabled status")
496
    sub_parser.add_argument('--auth_source', choices=['internal', 'ldap', 'ldap+ssl', 'textfile', 'pam'], help="Filter by auth source")
497
498
499
500
501
502
503
504
505
506
507
    sub_parser.add_argument('--role', help="Filter by role name")
    sub_parser.add_argument('--quota', help="Filter by quota name")
    sub_parser.set_defaults(func=user_ls_cmd)

    sub_parser = subparser.add_parser('user-get', help="Get a user by its ID")
    sub_parser.add_argument('id', type=int, help="User ID")
    sub_parser.set_defaults(func=user_get_cmd)

    sub_parser = subparser.add_parser('user-create', help="Create a new user")
    sub_parser.add_argument('username', help="Username")
    sub_parser.add_argument('email', help="Email")
508
    sub_parser.add_argument('auth_source', choices=['internal', 'ldap', 'ldap+ssl', 'textfile', 'pam'], help="Authentication method")
509
    sub_parser.add_argument('fs_uid', help="Filesystem UID", type=int)
510
511
512
513
514
515
516
517
518
519
520
521
522
523
    sub_parser.add_argument('role', help="Role name")
    sub_parser.add_argument('quota', help="Quota name")
    sub_parser.set_defaults(func=user_create_cmd)

    sub_parser = subparser.add_parser('user-delete', help="Delete a user")
    sub_parser.add_argument('id', type=int, help="User ID")
    sub_parser.set_defaults(func=user_delete_cmd)

    sub_parser = subparser.add_parser('user-update', help="Update an existing role")
    sub_parser.add_argument('id', type=int, help="ID of the user to update")
    sub_parser.add_argument('--email', help="Change the email")
    sub_parser.add_argument('--fs_uid', type=int, help="Filesystem UID")
    sub_parser.add_argument('--password', help="Change or set the password for internal authentication")
    sub_parser.add_argument('--enabled', type=int, choices=[0, 1], help="Enable or disable the user")
524
    sub_parser.add_argument('--auth_source', choices=['internal', 'ldap', 'ldap+ssl', 'textfile', 'pam'], help="Change the authentication source")
525
526
527
528
529
    sub_parser.add_argument('--priority', type=int, help="Change priority")
    sub_parser.add_argument('--role_id', help="Change role")
    sub_parser.add_argument('--quota_id', help="Change quota")
    sub_parser.set_defaults(func=user_update_cmd)

530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
    return parser, parser.parse_args()


def zoe():
    """Main entrypoint."""
    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

    auth = utils.read_auth(args)
    if auth is None:
        sys.exit(1)

    try:
551
552
        api = ZoeAPI(auth['url'], auth['user'], auth['pass'])
        args.func(api, args)
553
554
555
556
557
    except ZoeAPIException as e:
        print(e.message)
    except KeyboardInterrupt:
        print('CTRL-C pressed, exiting...')
    sys.exit(0)