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
                    'To see all cached session users:', '  kamaki user list'])
344 345
        if self.astakos.token != first_token:
            self.astakos.token = first_token
346
            name = self.astakos.user_info()['name'] or '<USER>'
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
            name = self.astakos.user_info()['name'] or '<USER>'
360
            self.error('User %s is already the selected session user' % name)
361

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


367
@command(user_cmds)
368
class user_delete(_AstakosInit):
369 370 371 372 373 374 375 376 377
    """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', )
378

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

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


411 412
#  command admin

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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


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

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

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


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

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

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


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

534 535
    @errors.Generic.all
    @errors.Astakos.astakosclient
536 537 538 539 540 541 542 543 544
    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)


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

549 550
    @errors.Generic.all
    @errors.Astakos.astakosclient
551 552 553 554 555 556 557 558 559
    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)


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

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

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

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


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

    arguments = dict(
591 592 593 594
        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'),
595 596 597
        force=FlagArgument('Force commission', '--force'),
        accept=FlagArgument('Do not wait for verification', '--accept')
    )
598
    required = ('uuid', 'source', 'file_path')
599

600 601
    @errors.Generic.all
    @errors.Astakos.astakosclient
602 603 604 605 606 607 608 609
    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])
610
        self.print_(self.client.issue_one_commission(
611 612
            self['uuid'], self['source'], provisions,
            self['description'] or '', self['force'], self['accept']))
613

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


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

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

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


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

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

639 640
    @errors.Generic.all
    @errors.Astakos.astakosclient
641
    def _run(self):
642 643 644 645
        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']))
646
        self.print_(r)
647 648 649 650 651 652

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


653 654
#  command project

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


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


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

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

692 693
    @errors.Generic.all
    @errors.Astakos.astakosclient
694
    def _run(self):
695 696 697 698 699 700 701
        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]
702
        self.print_(r)
703 704 705 706 707 708

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


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

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

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


724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
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(
739 740
                    'Invalid value for %s' % self.lvalue,
                    details=['Valid values: %s' % ', '.join(self.policies)])
741 742 743 744


class ProjectResourceArgument(KeyValueArgument):
    """"A <resource>=<member_capacity>,<project_capacity> argument  e.g.,
745 746
    --resource cyclades.cpu=5,1
    """
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
    @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=[
766 767 768 769 770 771
                            'Usage:',
                            '  %s %s=<member_capacity>,<project_capacity>' % (
                                self.lvalue, key),
                            'where both capacities are integers',
                            'and member_capacity <= project_capacity', '',
                            '(%s)' % e])
772 773 774 775 776
                self._value[key] = dict(
                    member_capacity=member_capacity,
                    project_capacity=project_capacity)


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

781
    __doc__ += _project_specs
782 783
    arguments = dict(
        specs_path=ValueArgument(
784 785 786 787 788
            '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'),
789
        max_members=IntArgument('Maximum subscribers', '--max-members'),
790 791
        private=BooleanArgument(
            'True for private, False (default) for public', '--private'),
792 793 794 795 796 797 798 799 800 801 802 803 804 805
        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')
806
    )
807
    required = ['specs_path', 'project_name', 'end_date']
808

809 810
    @errors.Generic.all
    @errors.Astakos.astakosclient
811 812
    @apply_notification
    def _run(self):
813 814 815 816 817 818
        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']),
819 820
                ('end_date', self.arguments['end_date'].isoformat),
                ('start_date', self.arguments['start_date'].isoformat),
821 822 823
                ('owner', self['owner_uuid']),
                ('homepage', self['homepage_url']),
                ('description', self['description']),
824 825
                ('max_members', self['max_members']),
                ('private', self['private']),
826 827 828 829 830
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
831
        self.print_(self.client.create_project(specs), self.print_dict)
832 833 834

    def main(self):
        super(self.__class__, self)._run()
835 836 837 838 839 840 841 842
        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'])
843 844 845
        self._run()


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

    __doc__ += _project_specs
    arguments = dict(
        specs_path=ValueArgument(
853 854 855 856 857
            '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'),
858
        max_members=IntArgument('Maximum subscribers', '--max-members'),
859 860
        private=FlagArgument('Make the project private', '--private'),
        public=FlagArgument('Make the project public', '--public'),
861 862 863 864 865 866 867 868 869 870 871 872 873 874
        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')
875
    )
876
    required = [
877 878 879
        'specs_path', 'owner_uuid', 'homepage_url', 'description', 'public',
        'private', 'project_name', 'start_date', 'end_date', 'join_policy',
        'leave_policy', 'resource_capacities', 'max_members']
880

881 882
    @errors.Generic.all
    @errors.Astakos.astakosclient
883
    @errors.Astakos.project_id
884 885
    @apply_notification
    def _run(self, project_id):
886 887 888 889 890 891 892 893 894
        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']),
895
                ('max_members', self['max_members']),
896 897
                ('start_date', self.arguments['start_date'].isoformat),
                ('end_date', self.arguments['end_date'].isoformat),
898 899 900 901 902
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
903 904 905
        private = self['private'] or (False if self['public'] else None)
        if private is not None:
            self['private'] = private
906

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

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


921
class _ProjectAction(_AstakosInit):
922