astakos.py 36.1 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
39
from kamaki.cli.commands import (
40
    _command_init, errors, _optional_json, addLogSettings, _name_filter)
41
from kamaki.cli.command_tree 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
    KeyValueArgument, DateArgument, BooleanArgument)
47
from kamaki.cli.utils import format_size, filter_dicts_by_dict
48

49 50
#  Mandatory

51
user_commands = CommandTree('user', 'Astakos/Identity API commands')
52 53 54 55
quota_commands = CommandTree(
    'quota', 'Astakos/Account API commands for quotas')
resource_commands = CommandTree(
    'resource', 'Astakos/Account API commands for resources')
56
project_commands = CommandTree('project', 'Astakos project API commands')
57 58
membership_commands = CommandTree(
    'membership', 'Astakos project membership API commands')
59 60 61 62 63 64 65 66 67 68 69 70


#  Optional

endpoint_commands = CommandTree(
    'endpoint', 'Astakos/Account API commands for endpoints')
service_commands = CommandTree('service', 'Astakos API commands for services')
commission_commands = CommandTree(
    'commission', 'Astakos API commands for commissions')

_commands = [
    user_commands, quota_commands, resource_commands, project_commands,
71 72
    service_commands, commission_commands, endpoint_commands,
    membership_commands]
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 88 89 90
        finally:
            self.client.token = token_bu
    return wrap


91
class _init_synnefo_astakosclient(_command_init):
92

93
    @errors.generic.all
94
    @errors.user.load
95
    @errors.user.astakosclient
96
    @addLogSettings
97
    def _run(self):
98
        if getattr(self, 'cloud', None):
99 100
            base_url = self._custom_url('astakos')
            if base_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(base_url, token)
106 107 108
                return
        else:
            self.cloud = 'default'
109
        if getattr(self, 'auth_base', None):
110
            self.client = self.auth_base.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(user_commands)
119 120
class user_authenticate(_init_synnefo_astakosclient, _optional_json):
    """Authenticate a user and get all authentication information"""
121

122
    @errors.generic.all
123
    @errors.user.authenticate
124
    @errors.user.astakosclient
125 126
    @with_temp_token
    def _run(self):
127
        self._print(self.client.authenticate(), self.print_dict)
128

129
    def main(self, token=None):
130
        super(self.__class__, self)._run()
131
        self._run(token=token)
132 133


134
@command(user_commands)
135 136
class user_uuid2name(_init_synnefo_astakosclient, _optional_json):
    """Get user name(s) from uuid(s)"""
137

138 139
    #@errors.generic.all
    #@errors.user.astakosclient
140 141 142 143 144 145 146 147
    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):
148
        super(self.__class__, self)._run()
149
        self._run(uuids=((uuid, ) + more_uuids))
150 151


152
@command(user_commands)
153 154
class user_name2uuid(_init_synnefo_astakosclient, _optional_json):
    """Get user uuid(s) from name(s)"""
155 156

    @errors.generic.all
157 158 159 160 161 162 163 164 165
    @errors.user.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):
166
        super(self.__class__, self)._run()
167 168 169
        self._run(usernames=((username, ) + more_usernames))


170 171 172
@command(quota_commands)
class quota_list(_init_synnefo_astakosclient, _optional_json):
    """Show user quotas"""
173 174 175

    _to_format = set(['cyclades.disk', 'pithos.diskspace', 'cyclades.ram'])
    arguments = dict(
176 177
        resource=ValueArgument('Filter by resource', '--resource'),
        project_id=ValueArgument('Filter by project', '--project-id'),
178 179
        bytes=FlagArgument('Show data size in bytes', '--bytes')
    )
180

181 182
    def _print_quotas(self, quotas, *args, **kwargs):
        if not self['bytes']:
183 184 185 186
            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()])
187
        self.print_dict(quotas, *args, **kwargs)
188

189 190
    @errors.generic.all
    @errors.user.astakosclient
191 192 193
    def _run(self):
        quotas = self.client.get_quotas()
        if self['project_id']:
194 195 196 197 198 199 200
            try:
                resources = quotas[self['project_id']]
            except KeyError:
                raise CLIError('User not assigned to project with id "%s" ' % (
                    self['project_id']), details=[
                    'See all quotas of current user:', '  kamaki quota list'])
            quotas = {self['project_id']: resources}
201 202 203 204 205 206 207 208 209
        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
210 211
            if not d:
                raise CLIError('Resource "%s" not found' % self['resource'])
212 213 214 215
            quotas = d
        self._print(quotas, self._print_quotas)

    def main(self):
216
        super(self.__class__, self)._run()
217
        self._run()
218 219


220 221 222 223
#  command user session


@command(user_commands)
224
class user_info(_init_synnefo_astakosclient, _optional_json):
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
    """Get info for (current) session user"""

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

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        if self['uuid'] and self['name']:
            raise CLISyntaxError(
                'Arguments uuid and username are mutually exclusive',
                details=['Use either uuid OR username OR none, not both'])
        uuid = self['uuid'] or (self._username2uuid(self['name']) if (
            self['name']) else None)
        try:
            token = self.auth_base.get_token(uuid) if uuid else None
        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=[
                    'To see all cached session users',
249
                    '  /user list',
250
                    'To authenticate and add a new user in the session list',
251
                    '  /user add <new token>'])
252 253 254 255
        self._print(self.auth_base.user_info(token), self.print_dict)


@command(user_commands)
256 257
class user_add(_init_synnefo_astakosclient, _optional_json):
    """Authenticate a user by token and add to kamaki session (cache)"""
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, token=None):
        ask = token and token not in self.auth_base._uuids
        self._print(self.auth_base.authenticate(token), self.print_dict)
        if ask and self.ask_user(
                'Token is temporarily stored in memory. If it is stored in'
                ' kamaki configuration file, it will be available in later'
                ' sessions. Do you want to permanently store this token?'):
            tokens = self.auth_base._uuids.keys()
            tokens.remove(self.auth_base.token)
            self['config'].set_cloud(
                self.cloud, 'token', ' '.join([self.auth_base.token] + tokens))
            self['config'].write()

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


@command(user_commands)
280 281
class user_list(_init_synnefo_astakosclient, _optional_json):
    """List (cached) session users"""
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298

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

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self._print([u if self['detail'] else (dict(
            id=u['id'], name=u['name'])) for u in self.auth_base.list_users()])

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


@command(user_commands)
299 300
class user_select(_init_synnefo_astakosclient):
    """Select a user from the (cached) list as the current session user"""
301 302 303 304 305 306 307 308 309 310 311

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, uuid):
        try:
            first_token = self.auth_base.get_token(uuid)
        except KeyError:
            raise CLIError(
                'No user with uuid %s in the cached session list' % uuid,
                details=[
                    'To see all cached session users',
312
                    '  /user list',
313
                    'To authenticate and add a new user in the session list',
314
                    '  /user add <new token>'])
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
        if self.auth_base.token != first_token:
            self.auth_base.token = first_token
            msg = 'User with id %s is now the current session user.\n' % uuid
            msg += 'Do you want future sessions to also start with this user?'
            if self.ask_user(msg):
                tokens = self.auth_base._uuids.keys()
                tokens.remove(self.auth_base.token)
                tokens.insert(0, self.auth_base.token)
                self['config'].set_cloud(
                    self.cloud, 'token',  ' '.join(tokens))
                self['config'].write()
                self.error('User is selected for next sessions')
            else:
                self.error('User is not permanently selected')
        else:
            self.error('User was already the selected session user')

    def main(self, user_uuid):
        super(self.__class__, self)._run()
        self._run(uuid=user_uuid)


@command(user_commands)
338 339
class user_delete(_init_synnefo_astakosclient):
    """Delete a user (token) from the (cached) list of session users"""
340 341 342 343 344 345 346

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, uuid):
        if uuid == self.auth_base.user_term('id'):
            raise CLIError('Cannot remove current session user', details=[
                'To see all cached session users',
347
                '  /user list',
348
                'To see current session user',
349
                '  /user info',
350
                'To select a different session user',
351
                '  /user select <user uuid>'])
352 353 354 355 356 357
        try:
            self.auth_base.remove_user(uuid)
        except KeyError:
            raise CLIError('No user with uuid %s in session list' % uuid,
                details=[
                    'To see all cached session users',
358
                    '  /user list',
359
                    'To authenticate and add a new user in the session list',
360
                    '  /user add <new token>'])
361 362 363 364 365 366 367 368 369 370 371 372
        if self.ask_user(
                'User is removed from current session, but will be restored in'
                ' the next session. Remove the user from future sessions?'):
            self['config'].set_cloud(
                self.cloud, 'token', ' '.join(self.auth_base._uuids.keys()))
            self['config'].write()

    def main(self, user_uuid):
        super(self.__class__, self)._run()
        self._run(uuid=user_uuid)


373 374
#  command admin

375 376
@command(service_commands)
class service_list(_init_synnefo_astakosclient, _optional_json):
377 378 379 380 381 382 383 384 385 386 387 388
    """List available services"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self._print(self.client.get_services())

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


389 390
@command(service_commands)
class service_uuid2username(_init_synnefo_astakosclient, _optional_json):
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
    """Get service username(s) from uuid(s)"""

    @errors.generic.all
    @errors.user.astakosclient
    @with_temp_token
    def _run(self, uuids):
        if 1 == len(uuids):
            self._print(self.client.service_get_username(uuids[0]))
        else:
            self._print(
                self.client.service_get_usernames(uuids),
                self.print_dict)

    def main(self, service_token, uuid, *more_uuids):
        super(self.__class__, self)._run()
        self._run([uuid] + list(more_uuids), token=service_token)


409 410
@command(service_commands)
class service_username2uuid(_init_synnefo_astakosclient, _optional_json):
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    """Get service uuid(s) from username(s)"""

    @errors.generic.all
    @errors.user.astakosclient
    @with_temp_token
    def _run(self, usernames):
        if 1 == len(usernames):
            self._print(self.client.service_get_uuid(usernames[0]))
        else:
            self._print(
                self.client.service_get_uuids(usernames),
                self.print_dict)

    def main(self, service_token, usernames, *more_usernames):
        super(self.__class__, self)._run()
        self._run([usernames] + list(more_usernames), token=service_token)


429 430
@command(service_commands)
class service_quotas(_init_synnefo_astakosclient, _optional_json):
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
    """Get service quotas"""

    arguments = dict(
        uuid=ValueArgument('A user uuid to get quotas for', '--uuid')
    )

    @errors.generic.all
    @errors.user.astakosclient
    @with_temp_token
    def _run(self):
        self._print(self.client.service_get_quotas(self['uuid']))

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


448 449
@command(commission_commands)
class commission_pending(_init_synnefo_astakosclient, _optional_json):
450 451 452 453 454 455 456 457 458 459 460 461
    """List pending commissions (special privileges required)"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self._print(self.client.get_pending_commissions())

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


462 463
@command(commission_commands)
class commission_info(_init_synnefo_astakosclient, _optional_json):
464 465 466 467 468 469 470 471 472 473 474 475 476 477
    """Get commission info (special privileges required)"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, commission_id):
        commission_id = int(commission_id)
        self._print(
            self.client.get_commission_info(commission_id), self.print_dict)

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


478 479
@command(commission_commands)
class commission_accept(_init_synnefo_astakosclient):
480 481 482 483 484 485 486 487 488 489 490 491 492
    """Accept a pending commission  (special privileges required)"""

    @errors.generic.all
    @errors.user.astakosclient
    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)


493 494
@command(commission_commands)
class commission_reject(_init_synnefo_astakosclient):
495 496 497 498 499 500 501 502 503 504 505 506 507
    """Reject a pending commission (special privileges required)"""

    @errors.generic.all
    @errors.user.astakosclient
    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)


508 509
@command(commission_commands)
class commission_resolve(_init_synnefo_astakosclient, _optional_json):
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
    """Resolve multiple commissions (special privileges required)"""

    arguments = dict(
        accept=CommaSeparatedListArgument(
            'commission ids to accept (e.g., --accept=11,12,13,...',
            '--accept'),
        reject=CommaSeparatedListArgument(
            'commission ids to reject (e.g., --reject=11,12,13,...',
            '--reject')
    )

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self.writeln('accepted ', self['accept'])
        self.writeln('rejected ', self['reject'])
        self._print(
            self.client.resolve_commissions(self['accept'], self['reject']),
            self.print_dict)

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


535 536
@command(commission_commands)
class commission_issue(_init_synnefo_astakosclient, _optional_json):
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
    """Issue commissions as a json string (special privileges required)
    Parameters:
    holder      -- user's id (string)
    source      -- commission's source (ex system) (string)
    provisions  -- resources with their quantity (json-dict from string to int)
    name        -- description of the commission (string)
    """

    arguments = dict(
        force=FlagArgument('Force commission', '--force'),
        accept=FlagArgument('Do not wait for verification', '--accept')
    )

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, holder, source, provisions, name=''):
        provisions = loads(provisions)
        self._print(self.client.issue_one_commission(
            holder, source, provisions, name,
            self['force'], self['accept']))

    def main(self, user_uuid, source, provisions_file, name=''):
        super(self.__class__, self)._run()
        self._run(user_uuid, source, provisions_file, name)


563 564
@command(resource_commands)
class resource_list(_init_synnefo_astakosclient, _optional_json):
565 566 567 568 569 570 571 572 573 574 575 576
    """List user resources"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self._print(self.client.get_resources(), self.print_dict)

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


577
@command(endpoint_commands)
578 579
class endpoint_list(
        _init_synnefo_astakosclient, _optional_json, _name_filter):
580 581
    """Get endpoints service endpoints"""

582 583
    arguments = dict(endpoint_type=ValueArgument('Filter by type', '--type'))

584 585 586
    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
587 588 589 590 591
        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']))
        self._print(r)
592 593 594 595 596 597

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


598 599 600
#  command project


601 602
_project_specs = """
    {
603
    "name": name,
604 605 606 607
    "owner": uuid,  # if omitted, request user assumed
    "homepage": homepage,  # optional
    "description": description,  # optional
    "comments": comments,  # optional
608 609
    "max_members": max_members,  # optional
    "private": true | false,  # optional
610
    "start_date": date,  # optional
611 612
    "end_date": date,
    "join_policy": "auto" | "moderated" | "closed",  # default: "moderated"
613 614 615 616
    "leave_policy": "auto" | "moderated" | "closed",  # default: "auto"
    "resources": {
    "cyclades.vm": {"project_capacity": int, "member_capacity": int
    }}}"""
617 618


619
def apply_notification(func):
620
    def wrap(self, *args, **kwargs):
621
        r = func(self, *args, **kwargs)
622
        self.error('Application is submitted successfully')
623 624 625 626 627 628 629 630 631
        return r
    return wrap


@command(project_commands)
class project_list(_init_synnefo_astakosclient, _optional_json):
    """List all projects"""

    arguments = dict(
632
        details=FlagArgument('Show details', ('-l', '--details')),
633 634 635 636 637 638 639 640
        name=ValueArgument('Filter by name', ('--with-name', )),
        state=ValueArgument('Filter by state', ('--with-state', )),
        owner=ValueArgument('Filter by owner', ('--with-owner', ))
    )

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
641 642 643 644 645 646 647 648
        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]
        self._print(r)
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669

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


@command(project_commands)
class project_info(_init_synnefo_astakosclient, _optional_json):
    """Get details for a project"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, project_id):
        self._print(
            self.client.get_project(project_id), self.print_dict)

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


670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
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(
                    'Invalid value for %s' % self.lvalue, details=[
                    'Valid values: %s' % ', '.join(self.policies)])


class ProjectResourceArgument(KeyValueArgument):
    """"A <resource>=<member_capacity>,<project_capacity> argument  e.g.,
691 692
    --resource cyclades.cpu=5,1
    """
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
    @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=[
                        'Usage:',
                        '  %s %s=<member_capacity>,<project_capacity>' % (
                            self.lvalue, key),
                        'where both capacities are integers',
                        'and member_capacity <= project_capacity', '',
                        '(%s)' % e])
                self._value[key] = dict(
                    member_capacity=member_capacity,
                    project_capacity=project_capacity)


723 724
@command(project_commands)
class project_create(_init_synnefo_astakosclient, _optional_json):
725
    """Apply for a new project"""
726

727
    __doc__ += _project_specs
728 729
    arguments = dict(
        specs_path=ValueArgument(
730 731 732 733 734
            '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'),
735
        max_members=IntArgument('Maximum subscribers', '--max-members'),
736 737
        private=BooleanArgument(
            'True for private, False (default) for public', '--private'),
738 739 740 741 742 743 744 745 746 747 748 749 750 751
        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')
752
    )
753
    required = ['specs_path', 'project_name', 'end_date']
754 755 756 757 758

    @errors.generic.all
    @errors.user.astakosclient
    @apply_notification
    def _run(self):
759 760 761 762 763 764
        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']),
765 766
                ('end_date', self.arguments['end_date'].isoformat),
                ('start_date', self.arguments['start_date'].isoformat),
767 768 769
                ('owner', self['owner_uuid']),
                ('homepage', self['homepage_url']),
                ('description', self['description']),
770 771
                ('max_members', self['max_members']),
                ('private', self['private']),
772 773 774 775 776 777
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
        self._print(self.client.create_project(specs), self.print_dict)
778 779 780

    def main(self):
        super(self.__class__, self)._run()
781 782 783 784 785 786 787 788
        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'])
789 790 791 792 793
        self._run()


@command(project_commands)
class project_modify(_init_synnefo_astakosclient, _optional_json):
794
    """Modify properties of a project"""
795 796 797 798

    __doc__ += _project_specs
    arguments = dict(
        specs_path=ValueArgument(
799 800 801 802 803
            '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'),
804
        max_members=IntArgument('Maximum subscribers', '--max-members'),
805 806
        private=FlagArgument('Make the project private', '--private'),
        public=FlagArgument('Make the project public', '--public'),
807 808 809 810 811 812 813 814 815 816 817 818 819 820
        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')
821
    )
822
    required = [
823 824 825
        'specs_path', 'owner_uuid', 'homepage_url', 'description', 'public',
        'private', 'project_name', 'start_date', 'end_date', 'join_policy',
        'leave_policy', 'resource_capacities', 'max_members']
826 827 828 829 830

    @errors.generic.all
    @errors.user.astakosclient
    @apply_notification
    def _run(self, project_id):
831 832 833 834 835 836 837 838 839
        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']),
840
                ('max_members', self['max_members']),
841 842
                ('start_date', self.arguments['start_date'].isoformat),
                ('end_date', self.arguments['end_date'].isoformat),
843 844 845 846 847
                ('join_policy', self['join_policy']),
                ('leave_policy', self['leave_policy']),
                ('resources', self['resource_capacities'])):
            if arg:
                specs[key] = arg
848 849 850
        private = self['private'] or (False if self['public'] else None)
        if private is not None:
            self['private'] = private
851

852
        self._print(
853
            self.client.modify_project(project_id, specs), self.print_dict)
854 855 856

    def main(self, project_id):
        super(self.__class__, self)._run()
857 858 859 860 861 862
        if self['private'] and self['public']:
            a = self.arguments
            raise CLIInvalidArgument(
                'Invalid argument combination', details=[
                'Arguments %s and %s are mutually exclussive' % (
                    a['private'].lvalue, a['public'].lvalue)])
863 864 865 866 867 868 869
        self._run(project_id)


class _project_action(_init_synnefo_astakosclient):

    action = ''

870 871 872 873
    arguments = dict(
        reason=ValueArgument('Quote a reason for this action', '--reason'),
    )

874 875 876 877 878
    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, project_id, quote_a_reason):
        self.client.project_action(project_id, self.action, quote_a_reason)

879
    def main(self, project_id):
880
        super(_project_action, self)._run()
881
        self._run(project_id, self['reason'] or '')
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907


@command(project_commands)
class project_suspend(_project_action):
    """Suspend a project (special privileges needed)"""
    action = 'suspend'


@command(project_commands)
class project_unsuspend(_project_action):
    """Resume a suspended project (special privileges needed)"""
    action = 'unsuspend'


@command(project_commands)
class project_terminate(_project_action):
    """Terminate a project (special privileges needed)"""
    action = 'terminate'


@command(project_commands)
class project_reinstate(_project_action):
    """Reinstate a terminated project (special privileges needed)"""
    action = 'reinstate'


908
class _application_action(_init_synnefo_astakosclient):
909

910
    action = ''
911 912

    arguments = dict(
913 914
        app_id=ValueArgument('The application ID', '--app-id'),
        reason=ValueArgument('Quote a reason for this action', '--reason'),
915
    )
916
    required = ('app_id', )
917 918 919

    @errors.generic.all
    @errors.user.astakosclient
920 921 922
    def _run(self, project_id, app_id, quote_a_reason):
        self.client.application_action(
            project_id, app_id, self.action, quote_a_reason)
923

924
    def main(self, project_id):
925
        super(_application_action, self)._run()
926
        self._run(project_id, self['app_id'], self['reason'] or '')
927 928 929


@command(project_commands)
930
class project_approve(_application_action):
931 932 933 934 935
    """Approve an application (special privileges needed)"""
    action = 'approve'


@command(project_commands)
936
class project_deny(_application_action):
937 938 939 940 941
    """Deny an application (special privileges needed)"""
    action = 'deny'


@command(project_commands)
942
class project_dismiss(_application_action):
943 944 945 946 947
    """Dismiss your denied application"""
    action = 'dismiss'


@command(project_commands)
948
class project_cancel(_application_action):
949 950 951 952
    """Cancel your application"""
    action = 'cancel'


953 954
@command(membership_commands)
class membership(_init_synnefo_astakosclient):
955 956 957
    """Project membership management commands"""


958 959
@command(membership_commands)
class membership_list(_init_synnefo_astakosclient, _optional_json):
960 961 962
    """List all memberships"""

    arguments = dict(
963
        project=ValueArgument('Filter by project id', '--project-id')
964 965 966 967 968 969 970 971 972 973 974 975
    )

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self):
        self._print(self.client.get_memberships(self['project']))

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


976 977
@command(membership_commands)
class membership_info(_init_synnefo_astakosclient, _optional_json):
978 979 980 981 982 983 984 985 986 987
    """Details on a membership"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, memb_id):
        self._print(
            self.client.get_membership(memb_id), self.print_dict)

    def main(self, membership_id):
        super(self.__class__, self)._run()
988
        self._run(memb_id=membership_id)
989 990 991 992 993


class _membership_action(_init_synnefo_astakosclient, _optional_json):

    action = ''
994
    arguments = dict(reason=ValueArgument('Reason for the action', '--reason'))
995 996 997 998 999 1000 1001

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, memb_id, quote_a_reason):
        self._print(self.client.membership_action(
            memb_id, self.action, quote_a_reason))

1002
    def main(self, membership_id):
1003
        super(_membership_action, self)._run()
1004
        self._run(membership_id, self['reason'] or '')
1005 1006


1007 1008
@command(membership_commands)
class membership_leave(_membership_action):
1009 1010 1011 1012
    """Leave a project you have membership to"""
    action = 'leave'


1013 1014
@command(membership_commands)
class membership_cancel(_membership_action):
1015 1016 1017 1018
    """Cancel your (probably pending) membership to a project"""
    action = 'cancel'


1019 1020
@command(membership_commands)
class membership_accept(_membership_action):
1021 1022 1023 1024
    """Accept a membership for a project you manage"""
    action = 'accept'


1025 1026
@command(membership_commands)
class membership_reject(_membership_action):
1027 1028 1029 1030
    """Reject a membership for a project you manage"""
    action = 'reject'


1031 1032
@command(membership_commands)
class membership_remove(_membership_action):
1033 1034 1035 1036
    """Remove a membership for a project you manage"""
    action = 'remove'


1037 1038
@command(project_commands)
class project_join(_init_synnefo_astakosclient):
1039 1040 1041 1042 1043 1044 1045 1046
    """Join a project"""

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, project_id):
        self.writeln(self.client.join_project(project_id))

    def main(self, project_id):
1047
        super(project_join, self)._run()
1048 1049 1050
        self._run(project_id)


1051 1052 1053 1054 1055 1056
@command(project_commands)
class project_enroll(_init_synnefo_astakosclient):
    """Enroll a user to a project"""

    arguments = dict(email=ValueArgument('User e-mail', '--email'))
    required = ('email', )
1057 1058 1059 1060 1061 1062

    @errors.generic.all
    @errors.user.astakosclient
    def _run(self, project_id, email):
        self.writeln(self.client.enroll_member(project_id, email))

1063 1064 1065
    def main(self, project_id):
        super(project_enroll, self)._run()
        self._run(project_id, self['email'])