astakos.py 37.7 KB
Newer Older
1
# Copyright 2011-2014 GRNET S.A. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command

34
from json import load, loads
35 36
from os.path import abspath

Stavros Sachtouris's avatar
Stavros Sachtouris committed
37
from kamaki.cli import command
38
from kamaki.clients.astakos import LoggedAstakosClient, ClientError
39
from kamaki.cli.cmds import (
40
    CommandInit, NameFilter, OptionalOutput, errors, client_log)
41
from kamaki.cli.cmdtree import CommandTree
42 43
from kamaki.cli.errors import (
    CLIBaseUrlError, CLISyntaxError, CLIError, CLIInvalidArgument)
44
from kamaki.cli.argument import (
45
    FlagArgument, ValueArgument, IntArgument, CommaSeparatedListArgument,
46 47
    KeyValueArgument, DateArgument, BooleanArgument, UserAccountArgument,
    RepeatableArgument)
48
from kamaki.cli.utils import format_size, filter_dicts_by_dict
49

50 51
#  Mandatory

52 53
user_cmds = CommandTree('user', 'Astakos/Identity API commands')
quota_cmds = CommandTree(
54
    'quota', 'Astakos/Account API commands for quotas')
55
resource_cmds = CommandTree(
56
    'resource', 'Astakos/Account API commands for resources')
57 58
project_cmds = CommandTree('project', 'Astakos project API commands')
membership_cmds = CommandTree(
59
    'membership', 'Astakos project membership API commands')
60 61 62 63


#  Optional

64
endpoint_cmds = CommandTree(
65
    'endpoint', 'Astakos/Account API commands for endpoints')
66 67
service_cmds = CommandTree('service', 'Astakos API commands for services')
commission_cmds = CommandTree(
68 69
    'commission', 'Astakos API commands for commissions')

70 71 72
namespaces = [
    user_cmds, quota_cmds, resource_cmds, project_cmds, service_cmds,
    commission_cmds, endpoint_cmds, membership_cmds]
Stavros Sachtouris's avatar
Stavros Sachtouris committed
73 74


75 76
def with_temp_token(func):
    """ Set token to self.client.token, run func, recover old token """
77 78 79 80
    def wrap(self, *args, **kwargs):
        try:
            token = kwargs.pop('token')
        except KeyError:
81
            raise CLISyntaxError('A token is needed for %s' % func)
82 83
        token_bu = self.client.token
        try:
84
            self.client.token = token or token_bu
85
            return func(self, *args, **kwargs)
86 87
        finally:
            self.client.token = token_bu
88
    wrap.__name__ = func.__name__
89 90 91
    return wrap


92
class _AstakosInit(CommandInit):
93

94 95
    @errors.Generic.all
    @errors.Astakos.astakosclient
96
    @client_log
97
    def _run(self):
98
        if getattr(self, 'cloud', None):
99 100
            endpoint_url = self._custom_url('astakos')
            if endpoint_url:
101
                token = self._custom_token(
102 103
                    'astakos') or self.config.get_cloud(
                    self.cloud, 'token')
104
                token = token.split()[0] if ' ' in token else token
105
                self.client = LoggedAstakosClient(endpoint_url, token)
106 107 108
                return
        else:
            self.cloud = 'default'
109 110
        if getattr(self, 'astakos', None):
            self.client = self.astakos.get_client()
111 112
            return
        raise CLIBaseUrlError(service='astakos')
113

114
    def main(self):
115
        self._run()
116

Stavros Sachtouris's avatar
Stavros Sachtouris committed
117

118
@command(quota_cmds)
119
class quota_list(_AstakosInit, OptionalOutput):
120
    """Show user quotas"""
121 122 123

    _to_format = set(['cyclades.disk', 'pithos.diskspace', 'cyclades.ram'])
    arguments = dict(
124 125
        resource=ValueArgument('Filter by resource', '--resource'),
        project_id=ValueArgument('Filter by project', '--project-id'),
126 127
        bytes=FlagArgument('Show data size in bytes', '--bytes')
    )
128

129 130
    def _print_quotas(self, quotas, *args, **kwargs):
        if not self['bytes']:
131 132 133 134
            for project_id, resources in quotas.items():
                for r in self._to_format.intersection(resources):
                    resources[r] = dict(
                        [(k, format_size(v)) for k, v in resources[r].items()])
135
        self.print_dict(quotas, *args, **kwargs)
136

137 138
    @errors.Generic.all
    @errors.Astakos.astakosclient
139 140 141
    def _run(self):
        quotas = self.client.get_quotas()
        if self['project_id']:
142 143 144
            try:
                resources = quotas[self['project_id']]
            except KeyError:
145 146 147 148 149 150
                raise CLIError(
                    'User not assigned to project with id "%s" ' % (
                        self['project_id']),
                    details=[
                        'See all quotas of current user:',
                        '  kamaki quota list'])
151
            quotas = {self['project_id']: resources}
152 153 154 155 156 157 158 159 160
        if self['resource']:
            d = dict()
            for project_id, resources in quotas.items():
                r = dict()
                for resource, value in resources.items():
                    if (resource.startswith(self['resource'])):
                        r[resource] = value
                if r:
                    d[project_id] = r
161 162
            if not d:
                raise CLIError('Resource "%s" not found' % self['resource'])
163
            quotas = d
164
        self.print_(quotas, self._print_quotas)
165 166

    def main(self):
167
        super(self.__class__, self)._run()
168
        self._run()
169 170


171 172
#  command user session

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
@command(user_cmds)
class user_authenticate(_AstakosInit, OptionalOutput):
    """Authenticate a user and get all authentication information"""

    @errors.Generic.all
    @errors.Astakos.astakosclient
    @with_temp_token
    def _run(self):
        try:
            self.print_(self.client.authenticate(), self.print_dict)
        except ClientError as ce:
            if ce.status in (401, ):
                raise CLIError(
                    'Token %s was not authenticated' % self.client.token,
                    details=['%s' % ce])
            raise

    def main(self, token=None):
        super(self.__class__, self)._run()
        self._run(token=token)


@command(user_cmds)
class user_uuid2name(_AstakosInit, OptionalOutput):
    """Get user name(s) from uuid(s)"""

    @errors.Generic.all
    @errors.Astakos.astakosclient
    def _run(self, uuids):
        r = self.client.get_usernames(uuids)
        self.print_(r, self.print_dict)
        unresolved = set(uuids).difference(r)
        if unresolved:
            self.error('Unresolved uuids: %s' % ', '.join(unresolved))

    def main(self, uuid, *more_uuids):
        super(self.__class__, self)._run()
        self._run(uuids=((uuid, ) + more_uuids))


@command(user_cmds)
class user_name2uuid(_AstakosInit, OptionalOutput):
    """Get user uuid(s) from name(s)"""

    @errors.Generic.all
    @errors.Astakos.astakosclient
    def _run(self, usernames):
        r = self.client.get_uuids(usernames)
        self.print_(r, self.print_dict)
        unresolved = set(usernames).difference(r)
        if unresolved:
            self.error('Unresolved usernames: %s' % ', '.join(unresolved))

    def main(self, username, *more_usernames):
        super(self.__class__, self)._run()
        self._run(usernames=((username, ) + more_usernames))

230

231
@command(user_cmds)
232
class user_info(_AstakosInit, OptionalOutput):
233 234 235 236 237 238 239
    """Get info for (current) session user"""

    arguments = dict(
        uuid=ValueArgument('Query user with uuid', '--uuid'),
        name=ValueArgument('Query user with username/email', '--username')
    )

240 241
    @errors.Generic.all
    @errors.Astakos.astakosclient
242 243 244 245
    def _run(self):
        if self['uuid'] and self['name']:
            raise CLISyntaxError(
                'Arguments uuid and username are mutually exclusive',
246
                details=['Use either uuid OR username OR none, but NOT both'])
247 248 249
        uuid = self['uuid'] or (self._username2uuid(self['name']) if (
            self['name']) else None)
        try:
250 251
            if any([self['uuid'], self['name']]) and not uuid:
                raise KeyError()
252
            token = self.astakos.get_token(uuid) if uuid else None
253 254 255 256 257
        except KeyError:
            msg = ('id %s' % self['uuid']) if (
                self['uuid']) else 'username %s' % self['name']
            raise CLIError(
                'No user with %s in the cached session list' % msg, details=[
258 259 260 261
                    'To see all cached session users:',
                    '  kamaki user list',
                    'To authenticate and add a new user in the session list:',
                    '  kamaki user add NEW_TOKEN'])
262
        self.print_(self.astakos.user_info(token), self.print_dict)
263

264 265 266 267
    def main(self):
        super(self.__class__, self)._run()
        self._run()

268

269
@command(user_cmds)
270
class user_add(_AstakosInit, OptionalOutput):
271 272 273 274
    """Authenticate a user by token and add to session user list (cache)"""

    arguments = dict(token=ValueArgument('Token of user to add', '--token'),)
    required = ('token', )
275

276 277
    @errors.Generic.all
    @errors.Astakos.astakosclient
278 279 280 281 282 283 284 285 286 287
    def _run(self):
        ask = self['token'] and self['token'] not in self.astakos._uuids
        try:
            self.print_(
                self.astakos.authenticate(self['token']), self.print_dict)
        except ClientError as ce:
            if ce.status in (401, ):
                raise CLIError(
                    'Token %s was not authenticated' % self['token'],
                    details=['%s' % ce])
288
        if ask and self.ask_user(
289 290
                'Token is temporarily stored in memory. Append it in '
                'configuration file as an alternative token?'):
291 292
            tokens = self.astakos._uuids.keys()
            tokens.remove(self.astakos.token)
293
            self['config'].set_cloud(
294
                self.cloud, 'token', ' '.join([self.astakos.token] + tokens))
295 296
            self['config'].write()

297
    def main(self):
298
        super(self.__class__, self)._run()
299
        self._run()
300 301


302
@command(user_cmds)
303
class user_list(_AstakosInit, OptionalOutput):
304
    """List (cached) session users"""
305 306 307 308 309

    arguments = dict(
        detail=FlagArgument('Detailed listing', ('-l', '--detail'))
    )

310 311
    @errors.Generic.all
    @errors.Astakos.astakosclient
312
    def _run(self):
313
        self.print_([u if self['detail'] else (dict(
314
            id=u['id'], name=u['name'])) for u in self.astakos.list_users()])
315 316 317 318 319 320

    def main(self):
        super(self.__class__, self)._run()
        self._run()


321
@command(user_cmds)
322
class user_select(_AstakosInit):
323
    """Select a user from the (cached) list as the current session user"""
324

325 326 327 328 329 330 331 332
    def __init__(self, arguments={}, astakos=None, cloud=None):
        super(_AstakosInit, self).__init__(arguments, astakos, cloud)
        self['uuid_or_username'] = UserAccountArgument(
            'User to select', ('--user'))
        self.arguments['uuid_or_username'].account_client = astakos

    required = ('uuid_or_username', )

333 334
    @errors.Generic.all
    @errors.Astakos.astakosclient
335
    def _run(self):
336
        try:
337
            uuid = self['uuid_or_username']
338
            first_token = self.astakos.get_token(uuid)
339 340 341 342
        except KeyError:
            raise CLIError(
                'No user with uuid %s in the cached session list' % uuid,
                details=[
343 344
                    'To see all cached session users:', '  kamaki user list'])
        name = self.astakos.user_info()['name'] or '<USER>'
345 346
        if self.astakos.token != first_token:
            self.astakos.token = first_token
347 348 349 350
            self.error('User %s with id %s is now the current session user' % (
                name, uuid))
            if self.ask_user(
                    'Make %s the default user for future sessions?' % name):
351 352 353
                tokens = self.astakos._uuids.keys()
                tokens.remove(self.astakos.token)
                tokens.insert(0, self.astakos.token)
354 355 356
                self['config'].set_cloud(
                    self.cloud, 'token',  ' '.join(tokens))
                self['config'].write()
357
                self.error('%s is now the default user' % name)
358
        else:
359
            self.error('User %s is already the selected session user' % name)
360

361
    def main(self):
362
        super(self.__class__, self)._run()
363
        self._run()
364 365


366
@command(user_cmds)
367
class user_delete(_AstakosInit):
368 369 370 371 372 373 374 375 376
    """Delete a user (token) from the list of session users"""

    def __init__(self, arguments={}, astakos=None, cloud=None):
        super(_AstakosInit, self).__init__(arguments, astakos, cloud)
        self['uuid_or_username'] = UserAccountArgument(
            'User to delete', ('--user'))
        self.arguments['uuid_or_username'].account_client = astakos

    required = ('uuid_or_username', )
377

378 379
    @errors.Generic.all
    @errors.Astakos.astakosclient
380 381
    def _run(self):
        uuid = self['uuid_or_username']
382
        if uuid == self.astakos.user_term('id'):
383 384
            raise CLIError('Cannot remove current session user', details=[
                'To see all cached session users',
385
                '  kamaki user list',
386
                'To see current session user',
387
                '  kamaki user info',
388
                'To select a different session user',
389
                '  kamaki user select --user=UUID_OR_USERNAME'])
390
        try:
391
            self.astakos.remove_user(uuid)
392
        except KeyError:
393 394
            raise CLIError(
                'No user with uuid %s in session list' % uuid,
395 396
                details=[
                    'To see all cached session users',
397
                    '  kamaki user list',
398
                    'To authenticate and add a new user in the session list',
399 400
                    '  kamaki user add --token=NEW_TOKEN'])
        if self.ask_user('Delete user token from config file?'):
401
            self['config'].set_cloud(
402
                self.cloud, 'token', ' '.join(self.astakos._uuids.keys()))
403 404
            self['config'].write()

405
    def main(self):
406
        super(self.__class__, self)._run()
407
        self._run()
408 409


410 411
#  command admin

412
@command(service_cmds)
413
class service_list(_AstakosInit, OptionalOutput):
414 415
    """List available services"""

416 417
    @errors.Generic.all
    @errors.Astakos.astakosclient
418
    def _run(self):
419
        self.print_(self.client.get_services())
420 421 422 423 424 425

    def main(self):
        super(self.__class__, self)._run()
        self._run()


426
@command(service_cmds)
427
class service_uuid2username(_AstakosInit, OptionalOutput):
428 429
    """Get service username(s) from uuid(s)"""

430 431 432 433 434 435
    arguments = dict(
        service_token=ValueArgument('Authenticate service', '--service-token'),
        uuid=RepeatableArgument('User UUID (can be repeated)', '--uuid')
    )
    required = ('service_token', 'uuid')

436 437
    @errors.Generic.all
    @errors.Astakos.astakosclient
438
    @with_temp_token
439 440 441
    def _run(self):
        if 1 == len(self['uuid']):
            self.print_(self.client.service_get_username(self['uuid'][0]))
442
        else:
443
            self.print_(
444
                self.client.service_get_usernames(self['uuid']),
445 446
                self.print_dict)

447
    def main(self):
448
        super(self.__class__, self)._run()
449
        self._run(token=self['service_token'])
450 451


452
@command(service_cmds)
453
class service_username2uuid(_AstakosInit, OptionalOutput):
454 455
    """Get service uuid(s) from username(s)"""

456 457 458 459 460 461
    arguments = dict(
        service_token=ValueArgument('Authenticate service', '--service-token'),
        username=RepeatableArgument('Username (can be repeated)', '--username')
    )
    required = ('service_token', 'username')

462 463
    @errors.Generic.all
    @errors.Astakos.astakosclient
464
    @with_temp_token
465 466 467
    def _run(self):
        if 1 == len(self['username']):
            self.print_(self.client.service_get_uuid(self['username'][0]))
468
        else:
469
            self.print_(
470
                self.client.service_get_uuids(self['username']),
471 472
                self.print_dict)

473
    def main(self):
474
        super(self.__class__, self)._run()
475
        self._run(token=self['service_token'])
476 477


478
@command(service_cmds)
479
class service_quotas(_AstakosInit, OptionalOutput):
480 481 482
    """Get service quotas"""

    arguments = dict(
483
        service_token=ValueArgument('Authenticate service', '--service-token'),
484 485
        uuid=ValueArgument('A user uuid to get quotas for', '--uuid')
    )
486
    required = ('service_token')
487

488 489
    @errors.Generic.all
    @errors.Astakos.astakosclient
490 491
    @with_temp_token
    def _run(self):
492
        self.print_(self.client.service_get_quotas(self['uuid']))
493

494
    def main(self):
495
        super(self.__class__, self)._run()
496
        self._run(token=self['service_token'])
497 498


499
@command(commission_cmds)
500
class commission_pending(_AstakosInit, OptionalOutput):
501 502
    """List pending commissions (special privileges required)"""

503 504
    @errors.Generic.all
    @errors.Astakos.astakosclient
505
    def _run(self):
506
        self.print_(self.client.get_pending_commissions())
507 508 509 510 511 512

    def main(self):
        super(self.__class__, self)._run()
        self._run()


513
@command(commission_cmds)
514
class commission_info(_AstakosInit, OptionalOutput):
515 516
    """Get commission info (special privileges required)"""

517 518
    @errors.Generic.all
    @errors.Astakos.astakosclient
519 520
    def _run(self, commission_id):
        commission_id = int(commission_id)
521
        self.print_(
522 523 524 525 526 527 528
            self.client.get_commission_info(commission_id), self.print_dict)

    def main(self, commission_id):
        super(self.__class__, self)._run()
        self._run(commission_id)


529
@command(commission_cmds)
530
class commission_accept(_AstakosInit):
531 532
    """Accept a pending commission  (special privileges required)"""

533 534
    @errors.Generic.all
    @errors.Astakos.astakosclient
535 536 537 538 539 540 541 542 543
    def _run(self, commission_id):
        commission_id = int(commission_id)
        self.client.accept_commission(commission_id)

    def main(self, commission_id):
        super(self.__class__, self)._run()
        self._run(commission_id)


544
@command(commission_cmds)
545
class commission_reject(_AstakosInit):
546 547
    """Reject a pending commission (special privileges required)"""

548 549
    @errors.Generic.all
    @errors.Astakos.astakosclient
550 551 552 553 554 555 556 557 558
    def _run(self, commission_id):
        commission_id = int(commission_id)
        self.client.reject_commission(commission_id)

    def main(self, commission_id):
        super(self.__class__, self)._run()
        self._run(commission_id)


559
@command(commission_cmds)
560
class commission_resolve(_AstakosInit, OptionalOutput):
561 562 563 564
    """Resolve multiple commissions (special privileges required)"""

    arguments = dict(
        accept=CommaSeparatedListArgument(
565
            'commission ids to accept (e.g., --accept=11,12,13,...)',
566 567
            '--accept'),
        reject=CommaSeparatedListArgument(
568
            'commission ids to reject (e.g., --reject=11,12,13,...)',
569 570
            '--reject')
    )
571
    required = ['accept', 'reject']
572

573 574
    @errors.Generic.all
    @errors.Astakos.astakosclient
575
    def _run(self):
576 577 578
        r = self.client.resolve_commissions(
            self['accept'] or [], self['reject'] or [])
        self.print_(r, self.print_dict)
579 580 581 582 583 584

    def main(self):
        super(self.__class__, self)._run()
        self._run()


585
@command(commission_cmds)
586
class commission_issue(_AstakosInit, OptionalOutput):
587
    """Issue commissions as a json string (special privileges required)"""
588 589

    arguments = dict(
590 591 592 593
        uuid=ValueArgument('User UUID', '--uuid'),
        source=ValueArgument('Commission source (ex system)', '--source'),
        file_path=ValueArgument('File of provisions', '--provisions-file'),
        description=ValueArgument('Commision description', '--description'),
594 595 596
        force=FlagArgument('Force commission', '--force'),
        accept=FlagArgument('Do not wait for verification', '--accept')
    )
597
    required = ('uuid', 'source', 'file_path')
598

599 600
    @errors.Generic.all
    @errors.Astakos.astakosclient
601 602 603 604 605 606 607 608
    def _run(self):
        try:
            with open(self['file_path']) as f:
                provisions = loads(f.read())
        except Exception as e:
            raise CLIError(
                'Failed load a json dict from file %s' % self['file_path'],
                importance=2, details=['%s' % e])
609
        self.print_(self.client.issue_one_commission(
610 611
            self['uuid'], self['source'], provisions,
            self['description'] or '', self['force'], self['accept']))
612

613
    def main(self):
614
        super(self.__class__, self)._run()
615
        self._run()
616 617


618
@command(resource_cmds)
619
class resource_list(_AstakosInit, OptionalOutput):
620 621
    """List user resources"""

622 623
    @errors.Generic.all
    @errors.Astakos.astakosclient
624
    def _run(self):
625
        self.print_(self.client.get_resources(), self.print_dict)
626 627 628 629 630 631

    def main(self):
        super(self.__class__, self)._run()
        self._run()


632
@command(endpoint_cmds)
633
class endpoint_list(_AstakosInit, OptionalOutput, NameFilter):
634 635
    """Get endpoints service endpoints"""

636 637
    arguments = dict(endpoint_type=ValueArgument('Filter by type', '--type'))

638 639
    @errors.Generic.all
    @errors.Astakos.astakosclient
640
    def _run(self):
641 642 643 644
        r = self.client.get_endpoints()['access']['serviceCatalog']
        r = self._filter_by_name(r)
        if self['endpoint_type']:
            r = filter_dicts_by_dict(r, dict(type=self['endpoint_type']))
645
        self.print_(r)
646 647 648 649 650 651

    def main(self):
        super(self.__class__, self)._run()
        self._run()


652 653
#  command project

654 655
_project_specs = """
    {
656
    "name": name,
657 658 659 660
    "owner": uuid,  # if omitted, request user assumed
    "homepage": homepage,  # optional
    "description": description,  # optional
    "comments": comments,  # optional
661 662
    "max_members": max_members,  # optional
    "private": true | false,  # optional
663
    "start_date": date,  # optional
664 665
    "end_date": date,
    "join_policy": "auto" | "moderated" | "closed",  # default: "moderated"
666 667 668 669
    "leave_policy": "auto" | "moderated" | "closed",  # default: "auto"
    "resources": {
    "cyclades.vm": {"project_capacity": int, "member_capacity": int
    }}}"""
670 671


672
def apply_notification(func):
673
    def wrap(self, *args, **kwargs):
674
        r = func(self, *args, **kwargs)
675
        self.error('Application is submitted successfully')
676 677 678 679
        return r
    return wrap


680
@command(project_cmds)
681
class project_list(_AstakosInit, OptionalOutput):
682 683 684
    """List all projects"""

    arguments = dict(
685
        details=FlagArgument('Show details', ('-l', '--details')),
686 687
        name=ValueArgument('Filter by name', ('--with-name', )),
        state=ValueArgument('Filter by state', ('--with-state', )),
688
        owner=ValueArgument('Filter by owner', ('--with-owner', )),
689 690
    )

691 692
    @errors.Generic.all
    @errors.Astakos.astakosclient
693
    def _run(self):
694 695 696 697 698 699 700
        r = self.client.get_projects(
            self['name'], self['state'], self['owner'])
        if not (self['details'] or self['output_format']):
            r = [dict(
                id=i['id'],
                name=i['name'],
                description=i['description']) for i in r]
701
        self.print_(r)
702 703 704 705 706 707

    def main(self):
        super(self.__class__, self)._run()
        self._run()


708
@command(project_cmds)
709
class project_info(_AstakosInit, OptionalOutput):
710 711
    """Get details for a project"""

712 713
    @errors.Generic.all
    @errors.Astakos.astakosclient
714
    @errors.Astakos.project_id
715
    def _run(self, project_id):
716
        self.print_(self.client.get_project(project_id), self.print_dict)
717 718 719

    def main(self, project_id):
        super(self.__class__, self)._run()
720
        self._run(project_id=project_id)
721 722


723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
class PolicyArgument(ValueArgument):
    """A Policy argument"""
    policies = ('auto', 'moderated', 'closed')

    @property
    def value(self):
        return getattr(self, '_value', None)

    @value.setter
    def value(self, new_policy):
        if new_policy:
            if new_policy.lower() in self.policies:
                self._value = new_policy.lower()
            else:
                raise CLIInvalidArgument(
738 739
                    'Invalid value for %s' % self.lvalue,
                    details=['Valid values: %s' % ', '.join(self.policies)])
740 741 742 743


class ProjectResourceArgument(KeyValueArgument):
    """"A <resource>=<member_capacity>,<project_capacity> argument  e.g.,
744 745
    --resource cyclades.cpu=5,1
    """
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764
    @property
    def value(self):
        return super(ProjectResourceArgument, self).value

    @value.setter
    def value(self, key_value_pairs):
        if key_value_pairs:
            super(ProjectResourceArgument, self.__class__).value.fset(
                self, key_value_pairs)
            d = dict(self._value)
            for key, value in d.items():
                try:
                    member_capacity, project_capacity = value.split(',')
                    member_capacity = int(member_capacity)
                    project_capacity = int(project_capacity)
                    assert member_capacity <= project_capacity
                except Exception as e:
                    raise CLIInvalidArgument(
                        'Invalid resource value %s' % value, details=[
765 766 767 768 769 770
                            'Usage:',
                            '  %s %s=<member_capacity>,<project_capacity>' % (
                                self.lvalue, key),
                            'where both capacities are integers',
                            'and member_capacity <= project_capacity', '',
                            '(%s)' % e])
771 772 773 774 775
                self._value[key] = dict(
                    member_capacity=member_capacity,
                    project_capacity=project_capacity)


776
@command(project_cmds)
777
class project_create(_AstakosInit, OptionalOutput):
778
    """Apply for a new project"""
779

780
    __doc__ += _project_specs
781 782
    arguments = dict(
        specs_path=ValueArgument(
783 784 785 786 787
            'Specification file (contents in json)', '--spec-file'),
        project_name=ValueArgument('Name the project', '--name'),
        owner_uuid=ValueArgument('Project owner', '--owner'),
        homepage_url=ValueArgument('Project homepage', '--homepage'),
        description=ValueArgument('Describe the project', '--description'),
788
        max_members=IntArgument('Maximum subscribers', '--max-members'),
789 790
        private=BooleanArgument(
            'True for private, False (default) for public', '--private'),
791 792 793 794 795 796 797 798 799 800 801 802 803 804
        start_date=DateArgument('When to start the project', '--start-date'),
        end_date=DateArgument('When to end the project', '--end-date'),
        join_policy=PolicyArgument(
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
            '--join-policy'),
        leave_policy=PolicyArgument(
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
            '--leave-policy'),
        resource_capacities=ProjectResourceArgument(
            'Set the member and project capacities for resources (repeatable) '
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
            'most 1 cpu but the project will have at most 5"       To see all '
            'resources:   kamaki resource list',
            '--resource')
805
    )
806
    required = ['specs_path', 'project_name', 'end_date']
807

808 809
    @errors.Generic.all
    @errors.Astakos.astakosclient
810 811
    @apply_notification
    def _run(self):
812 813 814 815 816 817
        specs = dict()
        if self['specs_path']:
            with open(abspath(self['specs_path'])) as f:
                specs = load(f)
        for key, arg in (
                ('name', self['project_name']),
818 819
                ('end_date', self.arguments['end_date'].isoformat),
                ('start_date', self.arguments['start_date'].isoformat),
820 821 822
                ('owner', self['owner_uuid']),
                ('homepage', self['homepage_url']),
                ('description', self['description']),
823 824
                ('max_members', self['max_members']),
                ('private', self['private']),
825 826 827 828 829
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
830
        self.print_(self.client.create_project(specs), self.print_dict)
831 832 833

    def main(self):
        super(self.__class__, self)._run()
834 835 836 837 838 839 840 841
        self._req2 = [arg for arg in self.required if arg != 'specs_path']
        if not (self['specs_path'] or all(self[arg] for arg in self._req2)):
            raise CLIInvalidArgument('Insufficient arguments', details=[
                'Both of the following arguments are needed:',
                ', '.join([self.arguments[arg].lvalue for arg in self._req2]),
                'OR provide a spec file (json) with %s' % self.arguments[
                    'specs_path'].lvalue,
                'OR combine arguments (higher priority) with a file'])
842 843 844
        self._run()


845
@command(project_cmds)
846
class project_modify(_AstakosInit, OptionalOutput):
847
    """Modify properties of a project"""
848 849 850 851

    __doc__ += _project_specs
    arguments = dict(
        specs_path=ValueArgument(
852 853 854 855 856
            'Specification file (contents in json)', '--spec-file'),
        project_name=ValueArgument('Name the project', '--name'),
        owner_uuid=ValueArgument('Project owner', '--owner'),
        homepage_url=ValueArgument('Project homepage', '--homepage'),
        description=ValueArgument('Describe the project', '--description'),
857
        max_members=IntArgument('Maximum subscribers', '--max-members'),
858 859
        private=FlagArgument('Make the project private', '--private'),
        public=FlagArgument('Make the project public', '--public'),
860 861 862 863 864 865 866 867 868 869 870 871 872 873
        start_date=DateArgument('When to start the project', '--start-date'),
        end_date=DateArgument('When to end the project', '--end-date'),
        join_policy=PolicyArgument(
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
            '--join-policy'),
        leave_policy=PolicyArgument(
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
            '--leave-policy'),
        resource_capacities=ProjectResourceArgument(
            'Set the member and project capacities for resources (repeatable) '
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
            'most 1 cpu but the project will have at most 5"       To see all '
            'resources:   kamaki resource list',
            '--resource')
874
    )
875
    required = [
876 877 878
        'specs_path', 'owner_uuid', 'homepage_url', 'description', 'public',
        'private', 'project_name', 'start_date', 'end_date', 'join_policy',
        'leave_policy', 'resource_capacities', 'max_members']
879

880 881
    @errors.Generic.all
    @errors.Astakos.astakosclient
882
    @errors.Astakos.project_id
883 884
    @apply_notification
    def _run(self, project_id):
885 886 887 888 889 890 891 892 893
        specs = dict()
        if self['specs_path']:
            with open(abspath(self['specs_path'])) as f:
                specs = load(f)
        for key, arg in (
                ('name', self['project_name']),
                ('owner', self['owner_uuid']),
                ('homepage', self['homepage_url']),
                ('description', self['description']),
894
                ('max_members', self['max_members']),
895 896
                ('start_date', self.arguments['start_date'].isoformat),
                ('end_date', self.arguments['end_date'].isoformat),
897 898 899 900 901
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
902 903 904
        private = self['private'] or (False if self['public'] else None)
        if private is not None:
            self['private'] = private
905

906
        self.print_(
907
            self.client.modify_project(project_id, specs), self.print_dict)
908 909 910

    def main(self, project_id):
        super(self.__class__, self)._run()
911 912 913 914
        if self['private'] and self['public']:
            a = self.arguments
            raise CLIInvalidArgument(
                'Invalid argument combination', details=[
915 916
                    'Arguments %s and %s are mutually exclussive' % (
                        a['private'].lvalue, a['public'].lvalue)])
917
        self._run(project_id=project_id)
918 919


920
class _ProjectAction(_AstakosInit):
921 922

    action = ''
923 924 925 926
    arguments = dict(
        reason=ValueArgument('Quote a reason for this action', '--reason'),
    )