Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / astakos.py @ 672238f1

History | View | Annotate | Download (35.6 kB)

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

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

    
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
from kamaki.cli.errors import (
43
    CLIBaseUrlError, CLISyntaxError, CLIError, CLIInvalidArgument)
44
from kamaki.cli.argument import (
45
    FlagArgument, ValueArgument, IntArgument, CommaSeparatedListArgument,
46
    KeyValueArgument, DateArgument)
47
from kamaki.cli.utils import format_size, filter_dicts_by_dict
48

    
49
#  Mandatory
50

    
51
user_commands = CommandTree('user', 'Astakos/Identity API commands')
52
quota_commands = CommandTree(
53
    'quota', 'Astakos/Account API commands for quotas')
54
resource_commands = CommandTree(
55
    'resource', 'Astakos/Account API commands for resources')
56
project_commands = CommandTree('project', 'Astakos project API commands')
57
membership_commands = CommandTree(
58
    'membership', 'Astakos project membership API commands')
59

    
60

    
61
#  Optional
62

    
63
endpoint_commands = CommandTree(
64
    'endpoint', 'Astakos/Account API commands for endpoints')
65
service_commands = CommandTree('service', 'Astakos API commands for services')
66
commission_commands = CommandTree(
67
    'commission', 'Astakos API commands for commissions')
68

    
69
_commands = [
70
    user_commands, quota_commands, resource_commands, project_commands,
71
    service_commands, commission_commands, endpoint_commands,
72
    membership_commands]
73

    
74

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

    
90

    
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
            base_url = self._custom_url('astakos')
100
            if base_url:
101
                token = self._custom_token(
102
                    'astakos') or self.config.get_cloud(
103
                    self.cloud, 'token')
104
                token = token.split()[0] if ' ' in token else token
105
                self.client = LoggedAstakosClient(base_url, token)
106
                return
107
        else:
108
            self.cloud = 'default'
109
        if getattr(self, 'auth_base', None):
110
            self.client = self.auth_base.get_client()
111
            return
112
        raise CLIBaseUrlError(service='astakos')
113

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

    
117

    
118
@command(user_commands)
119
class user_authenticate(_init_synnefo_astakosclient, _optional_json):
120
    """Authenticate a user and get all authentication information"""
121

    
122
    @errors.generic.all
123
    @errors.user.authenticate
124
    @errors.user.astakosclient
125
    @with_temp_token
126
    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
class user_uuid2name(_init_synnefo_astakosclient, _optional_json):
136
    """Get user name(s) from uuid(s)"""
137

    
138
    #@errors.generic.all
139
    #@errors.user.astakosclient
140
    def _run(self, uuids):
141
        r = self.client.get_usernames(uuids)
142
        self._print(r, self.print_dict)
143
        unresolved = set(uuids).difference(r)
144
        if unresolved:
145
            self.error('Unresolved uuids: %s' % ', '.join(unresolved))
146

    
147
    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
class user_name2uuid(_init_synnefo_astakosclient, _optional_json):
154
    """Get user uuid(s) from name(s)"""
155

    
156
    @errors.generic.all
157
    @errors.user.astakosclient
158
    def _run(self, usernames):
159
        r = self.client.get_uuids(usernames)
160
        self._print(r, self.print_dict)
161
        unresolved = set(usernames).difference(r)
162
        if unresolved:
163
            self.error('Unresolved usernames: %s' % ', '.join(unresolved))
164

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

    
169

    
170
class _quota(_init_synnefo_astakosclient, _optional_json):
171

    
172
    _to_format = set(['cyclades.disk', 'pithos.diskspace', 'cyclades.ram'])
173

    
174
    arguments = dict(
175
        bytes=FlagArgument('Show data size in bytes', '--bytes')
176
    )
177

    
178
    def _print_quotas(self, quotas, *args, **kwargs):
179
        if not self['bytes']:
180
            for project_id, resources in quotas.items():
181
                for r in self._to_format.intersection(resources):
182
                    resources[r] = dict(
183
                        [(k, format_size(v)) for k, v in resources[r].items()])
184
        self.print_dict(quotas, *args, **kwargs)
185

    
186

    
187
@command(quota_commands)
188
class quota_info(_quota):
189
    """Get quota information"""
190

    
191
    arguments = dict(
192
        resource=ValueArgument('Quota for this resource', '--resource'),
193
        project_id=ValueArgument('Quota for this project', '--project-id'),
194
        bytes=FlagArgument('Show data size in bytes', '--bytes')
195
    )
196
    required = ['resource', 'project_id']
197

    
198
    @errors.generic.all
199
    @errors.user.astakosclient
200
    def _run(self):
201
        quotas = self.client.get_quotas()
202
        if self['project_id']:
203
            quotas = {self['project_id']: quotas.get(self['project_id'])}
204
        if self['resource']:
205
            d = dict()
206
            for project_id, resources in quotas.items():
207
                r = dict()
208
                for resource, value in resources.items():
209
                    if (resource.startswith(self['resource'])):
210
                        r[resource] = value
211
                if r:
212
                    d[project_id] = r
213
            quotas = d
214
        self._print(quotas, self._print_quotas)
215

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

    
220

    
221
@command(quota_commands)
222
class quota_list(_quota):
223
    """Get user quotas"""
224

    
225
    @errors.generic.all
226
    @errors.user.astakosclient
227
    def _run(self):
228
        self._print(self.client.get_quotas(), self._print_quotas)
229

    
230
    def main(self):
231
        super(self.__class__, self)._run()
232
        self._run()
233

    
234

    
235
#  command user session
236

    
237

    
238
@command(user_commands)
239
class user_info(_init_synnefo_astakosclient, _optional_json):
240
    """Get info for (current) session user"""
241

    
242
    arguments = dict(
243
        uuid=ValueArgument('Query user with uuid', '--uuid'),
244
        name=ValueArgument('Query user with username/email', '--username')
245
    )
246

    
247
    @errors.generic.all
248
    @errors.user.astakosclient
249
    def _run(self):
250
        if self['uuid'] and self['name']:
251
            raise CLISyntaxError(
252
                'Arguments uuid and username are mutually exclusive',
253
                details=['Use either uuid OR username OR none, not both'])
254
        uuid = self['uuid'] or (self._username2uuid(self['name']) if (
255
            self['name']) else None)
256
        try:
257
            token = self.auth_base.get_token(uuid) if uuid else None
258
        except KeyError:
259
            msg = ('id %s' % self['uuid']) if (
260
                self['uuid']) else 'username %s' % self['name']
261
            raise CLIError(
262
                'No user with %s in the cached session list' % msg, details=[
263
                    'To see all cached session users',
264
                    '  /user list',
265
                    'To authenticate and add a new user in the session list',
266
                    '  /user add <new token>'])
267
        self._print(self.auth_base.user_info(token), self.print_dict)
268

    
269

    
270
@command(user_commands)
271
class user_add(_init_synnefo_astakosclient, _optional_json):
272
    """Authenticate a user by token and add to kamaki session (cache)"""
273

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

    
289
    def main(self, new_token=None):
290
        super(self.__class__, self)._run()
291
        self._run(token=new_token)
292

    
293

    
294
@command(user_commands)
295
class user_list(_init_synnefo_astakosclient, _optional_json):
296
    """List (cached) session users"""
297

    
298
    arguments = dict(
299
        detail=FlagArgument('Detailed listing', ('-l', '--detail'))
300
    )
301

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

    
308
    def main(self):
309
        super(self.__class__, self)._run()
310
        self._run()
311

    
312

    
313
@command(user_commands)
314
class user_select(_init_synnefo_astakosclient):
315
    """Select a user from the (cached) list as the current session user"""
316

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

    
347
    def main(self, user_uuid):
348
        super(self.__class__, self)._run()
349
        self._run(uuid=user_uuid)
350

    
351

    
352
@command(user_commands)
353
class user_delete(_init_synnefo_astakosclient):
354
    """Delete a user (token) from the (cached) list of session users"""
355

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

    
383
    def main(self, user_uuid):
384
        super(self.__class__, self)._run()
385
        self._run(uuid=user_uuid)
386

    
387

    
388
#  command admin
389

    
390
@command(service_commands)
391
class service_list(_init_synnefo_astakosclient, _optional_json):
392
    """List available services"""
393

    
394
    @errors.generic.all
395
    @errors.user.astakosclient
396
    def _run(self):
397
        self._print(self.client.get_services())
398

    
399
    def main(self):
400
        super(self.__class__, self)._run()
401
        self._run()
402

    
403

    
404
@command(service_commands)
405
class service_uuid2username(_init_synnefo_astakosclient, _optional_json):
406
    """Get service username(s) from uuid(s)"""
407

    
408
    @errors.generic.all
409
    @errors.user.astakosclient
410
    @with_temp_token
411
    def _run(self, uuids):
412
        if 1 == len(uuids):
413
            self._print(self.client.service_get_username(uuids[0]))
414
        else:
415
            self._print(
416
                self.client.service_get_usernames(uuids),
417
                self.print_dict)
418

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

    
423

    
424
@command(service_commands)
425
class service_username2uuid(_init_synnefo_astakosclient, _optional_json):
426
    """Get service uuid(s) from username(s)"""
427

    
428
    @errors.generic.all
429
    @errors.user.astakosclient
430
    @with_temp_token
431
    def _run(self, usernames):
432
        if 1 == len(usernames):
433
            self._print(self.client.service_get_uuid(usernames[0]))
434
        else:
435
            self._print(
436
                self.client.service_get_uuids(usernames),
437
                self.print_dict)
438

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

    
443

    
444
@command(service_commands)
445
class service_quotas(_init_synnefo_astakosclient, _optional_json):
446
    """Get service quotas"""
447

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

    
452
    @errors.generic.all
453
    @errors.user.astakosclient
454
    @with_temp_token
455
    def _run(self):
456
        self._print(self.client.service_get_quotas(self['uuid']))
457

    
458
    def main(self, service_token):
459
        super(self.__class__, self)._run()
460
        self._run(token=service_token)
461

    
462

    
463
@command(commission_commands)
464
class commission_pending(_init_synnefo_astakosclient, _optional_json):
465
    """List pending commissions (special privileges required)"""
466

    
467
    @errors.generic.all
468
    @errors.user.astakosclient
469
    def _run(self):
470
        self._print(self.client.get_pending_commissions())
471

    
472
    def main(self):
473
        super(self.__class__, self)._run()
474
        self._run()
475

    
476

    
477
@command(commission_commands)
478
class commission_info(_init_synnefo_astakosclient, _optional_json):
479
    """Get commission info (special privileges required)"""
480

    
481
    @errors.generic.all
482
    @errors.user.astakosclient
483
    def _run(self, commission_id):
484
        commission_id = int(commission_id)
485
        self._print(
486
            self.client.get_commission_info(commission_id), self.print_dict)
487

    
488
    def main(self, commission_id):
489
        super(self.__class__, self)._run()
490
        self._run(commission_id)
491

    
492

    
493
@command(commission_commands)
494
class commission_accept(_init_synnefo_astakosclient):
495
    """Accept a pending commission  (special privileges required)"""
496

    
497
    @errors.generic.all
498
    @errors.user.astakosclient
499
    def _run(self, commission_id):
500
        commission_id = int(commission_id)
501
        self.client.accept_commission(commission_id)
502

    
503
    def main(self, commission_id):
504
        super(self.__class__, self)._run()
505
        self._run(commission_id)
506

    
507

    
508
@command(commission_commands)
509
class commission_reject(_init_synnefo_astakosclient):
510
    """Reject a pending commission (special privileges required)"""
511

    
512
    @errors.generic.all
513
    @errors.user.astakosclient
514
    def _run(self, commission_id):
515
        commission_id = int(commission_id)
516
        self.client.reject_commission(commission_id)
517

    
518
    def main(self, commission_id):
519
        super(self.__class__, self)._run()
520
        self._run(commission_id)
521

    
522

    
523
@command(commission_commands)
524
class commission_resolve(_init_synnefo_astakosclient, _optional_json):
525
    """Resolve multiple commissions (special privileges required)"""
526

    
527
    arguments = dict(
528
        accept=CommaSeparatedListArgument(
529
            'commission ids to accept (e.g., --accept=11,12,13,...',
530
            '--accept'),
531
        reject=CommaSeparatedListArgument(
532
            'commission ids to reject (e.g., --reject=11,12,13,...',
533
            '--reject')
534
    )
535

    
536
    @errors.generic.all
537
    @errors.user.astakosclient
538
    def _run(self):
539
        self.writeln('accepted ', self['accept'])
540
        self.writeln('rejected ', self['reject'])
541
        self._print(
542
            self.client.resolve_commissions(self['accept'], self['reject']),
543
            self.print_dict)
544

    
545
    def main(self):
546
        super(self.__class__, self)._run()
547
        self._run()
548

    
549

    
550
@command(commission_commands)
551
class commission_issue(_init_synnefo_astakosclient, _optional_json):
552
    """Issue commissions as a json string (special privileges required)
553
    Parameters:
554
    holder      -- user's id (string)
555
    source      -- commission's source (ex system) (string)
556
    provisions  -- resources with their quantity (json-dict from string to int)
557
    name        -- description of the commission (string)
558
    """
559

    
560
    arguments = dict(
561
        force=FlagArgument('Force commission', '--force'),
562
        accept=FlagArgument('Do not wait for verification', '--accept')
563
    )
564

    
565
    @errors.generic.all
566
    @errors.user.astakosclient
567
    def _run(self, holder, source, provisions, name=''):
568
        provisions = loads(provisions)
569
        self._print(self.client.issue_one_commission(
570
            holder, source, provisions, name,
571
            self['force'], self['accept']))
572

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

    
577

    
578
@command(resource_commands)
579
class resource_list(_init_synnefo_astakosclient, _optional_json):
580
    """List user resources"""
581

    
582
    @errors.generic.all
583
    @errors.user.astakosclient
584
    def _run(self):
585
        self._print(self.client.get_resources(), self.print_dict)
586

    
587
    def main(self):
588
        super(self.__class__, self)._run()
589
        self._run()
590

    
591

    
592
@command(endpoint_commands)
593
class endpoint_list(
594
        _init_synnefo_astakosclient, _optional_json, _name_filter):
595
    """Get endpoints service endpoints"""
596

    
597
    arguments = dict(endpoint_type=ValueArgument('Filter by type', '--type'))
598

    
599
    @errors.generic.all
600
    @errors.user.astakosclient
601
    def _run(self):
602
        r = self.client.get_endpoints()['access']['serviceCatalog']
603
        r = self._filter_by_name(r)
604
        if self['endpoint_type']:
605
            r = filter_dicts_by_dict(r, dict(type=self['endpoint_type']))
606
        self._print(r)
607

    
608
    def main(self):
609
        super(self.__class__, self)._run()
610
        self._run()
611

    
612

    
613
#  command project
614

    
615

    
616
_project_specs = """{
617
    "name": name,
618
    "owner": uuid,  # if omitted, request user assumed
619
    "homepage": homepage,  # optional
620
    "description": description,  # optional
621
    "comments": comments,  # optional
622
    "max_members": max_members,  # optional
623
    "private": true | false,  # optional
624
    "start_date": date,  # optional
625
    "end_date": date,
626
    "join_policy": "auto" | "moderated" | "closed",  # default: "moderated"
627
    "leave_policy": "auto" | "moderated" | "closed",  # default: "auto"
628
    "resources": {
629
    "cyclades.vm": {"project_capacity": int, "member_capacity": int
630
    }}}"""
631

    
632

    
633
def apply_notification(func):
634
    def wrap(self, *args, **kwargs):
635
        r = func(self, *args, **kwargs)
636
        self.writeln('Application is submitted successfully')
637
        return r
638
    return wrap
639

    
640

    
641
@command(project_commands)
642
class project_list(_init_synnefo_astakosclient, _optional_json):
643
    """List all projects"""
644

    
645
    arguments = dict(
646
        details=FlagArgument('Show details', ('-l', '--details')),
647
        name=ValueArgument('Filter by name', ('--with-name', )),
648
        state=ValueArgument('Filter by state', ('--with-state', )),
649
        owner=ValueArgument('Filter by owner', ('--with-owner', ))
650
    )
651

    
652
    @errors.generic.all
653
    @errors.user.astakosclient
654
    def _run(self):
655
        r = self.client.get_projects(
656
            self['name'], self['state'], self['owner'])
657
        if not (self['details'] or self['output_format']):
658
            r = [dict(
659
                id=i['id'],
660
                name=i['name'],
661
                description=i['description']) for i in r]
662
        self._print(r)
663

    
664
    def main(self):
665
        super(self.__class__, self)._run()
666
        self._run()
667

    
668

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

    
673
    @errors.generic.all
674
    @errors.user.astakosclient
675
    def _run(self, project_id):
676
        self._print(
677
            self.client.get_project(project_id), self.print_dict)
678

    
679
    def main(self, project_id):
680
        super(self.__class__, self)._run()
681
        self._run(project_id)
682

    
683

    
684
class PolicyArgument(ValueArgument):
685
    """A Policy argument"""
686
    policies = ('auto', 'moderated', 'closed')
687

    
688
    @property
689
    def value(self):
690
        return getattr(self, '_value', None)
691

    
692
    @value.setter
693
    def value(self, new_policy):
694
        if new_policy:
695
            if new_policy.lower() in self.policies:
696
                self._value = new_policy.lower()
697
            else:
698
                raise CLIInvalidArgument(
699
                    'Invalid value for %s' % self.lvalue, details=[
700
                    'Valid values: %s' % ', '.join(self.policies)])
701

    
702

    
703
class ProjectResourceArgument(KeyValueArgument):
704
    """"A <resource>=<member_capacity>,<project_capacity> argument  e.g.,
705
    --resource cyclades.cpu=5,1
706
    """
707
    @property
708
    def value(self):
709
        return super(ProjectResourceArgument, self).value
710

    
711
    @value.setter
712
    def value(self, key_value_pairs):
713
        if key_value_pairs:
714
            super(ProjectResourceArgument, self.__class__).value.fset(
715
                self, key_value_pairs)
716
            d = dict(self._value)
717
            for key, value in d.items():
718
                try:
719
                    member_capacity, project_capacity = value.split(',')
720
                    member_capacity = int(member_capacity)
721
                    project_capacity = int(project_capacity)
722
                    assert member_capacity <= project_capacity
723
                except Exception as e:
724
                    raise CLIInvalidArgument(
725
                        'Invalid resource value %s' % value, details=[
726
                        'Usage:',
727
                        '  %s %s=<member_capacity>,<project_capacity>' % (
728
                            self.lvalue, key),
729
                        'where both capacities are integers',
730
                        'and member_capacity <= project_capacity', '',
731
                        '(%s)' % e])
732
                self._value[key] = dict(
733
                    member_capacity=member_capacity,
734
                    project_capacity=project_capacity)
735

    
736

    
737
@command(project_commands)
738
class project_create(_init_synnefo_astakosclient, _optional_json):
739
    """Apply for a new project"""
740

    
741
    arguments = dict(
742
        specs_path=ValueArgument(
743
            'Specification file (contents in json)', '--spec-file'),
744
        project_name=ValueArgument('Name the project', '--name'),
745
        owner_uuid=ValueArgument('Project owner', '--owner'),
746
        homepage_url=ValueArgument('Project homepage', '--homepage'),
747
        description=ValueArgument('Describe the project', '--description'),
748
        max_members=IntArgument('Maximum subscribers', '--max-members'),
749
        private=FlagArgument('Set if the project is private', '--private'),
750
        start_date=DateArgument('When to start the project', '--start-date'),
751
        end_date=DateArgument('When to end the project', '--end-date'),
752
        join_policy=PolicyArgument(
753
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
754
            '--join-policy'),
755
        leave_policy=PolicyArgument(
756
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
757
            '--leave-policy'),
758
        resource_capacities=ProjectResourceArgument(
759
            'Set the member and project capacities for resources (repeatable) '
760
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
761
            'most 1 cpu but the project will have at most 5"       To see all '
762
            'resources:   kamaki resource list',
763
            '--resource')
764
    )
765
    required = ['specs_path', 'project_name', 'end_date']
766

    
767
    @errors.generic.all
768
    @errors.user.astakosclient
769
    @apply_notification
770
    def _run(self):
771
        specs = dict()
772
        if self['specs_path']:
773
            with open(abspath(self['specs_path'])) as f:
774
                specs = load(f)
775
        for key, arg in (
776
                ('name', self['project_name']),
777
                ('owner', self['owner_uuid']),
778
                ('homepage', self['homepage_url']),
779
                ('description', self['description']),
780
                ('max_members', self['max_members']),
781
                ('private', self['private']),
782
                ('start_date', self['start_date']),
783
                ('end_date', self['end_date']),
784
                ('join_policy', self['join_policy']),
785
                ('leave_policy', self['leave_policy']),
786
                ('resources', self['resource_capacities'])):
787
            if arg:
788
                specs[key] = arg
789

    
790
        self._print(self.client.create_project(specs), self.print_dict)
791

    
792
    def main(self):
793
        super(self.__class__, self)._run()
794
        self._req2 = [arg for arg in self.required if arg != 'specs_path']
795
        if not (self['specs_path'] or all(self[arg] for arg in self._req2)):
796
            raise CLIInvalidArgument('Insufficient arguments', details=[
797
                'Both of the following arguments are needed:',
798
                ', '.join([self.arguments[arg].lvalue for arg in self._req2]),
799
                'OR provide a spec file (json) with %s' % self.arguments[
800
                    'specs_path'].lvalue,
801
                'OR combine arguments (higher priority) with a file'])
802
        self._run()
803

    
804

    
805
@command(project_commands)
806
class project_modify(_init_synnefo_astakosclient, _optional_json):
807
    """Modify properties of a project"""
808

    
809
    __doc__ += _project_specs
810

    
811
    arguments = dict(
812
        specs_path=ValueArgument(
813
            'Specification file (contents in json)', '--spec-file'),
814
        project_name=ValueArgument('Name the project', '--name'),
815
        owner_uuid=ValueArgument('Project owner', '--owner'),
816
        homepage_url=ValueArgument('Project homepage', '--homepage'),
817
        description=ValueArgument('Describe the project', '--description'),
818
        max_members=IntArgument('Maximum subscribers', '--max-members'),
819
        private=FlagArgument('Set if the project is private', '--private'),
820
        start_date=DateArgument('When to start the project', '--start-date'),
821
        end_date=DateArgument('When to end the project', '--end-date'),
822
        join_policy=PolicyArgument(
823
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
824
            '--join-policy'),
825
        leave_policy=PolicyArgument(
826
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
827
            '--leave-policy'),
828
        resource_capacities=ProjectResourceArgument(
829
            'Set the member and project capacities for resources (repeatable) '
830
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
831
            'most 1 cpu but the project will have at most 5"       To see all '
832
            'resources:   kamaki resource list',
833
            '--resource')
834
    )
835
    required = [
836
        'specs_path', 'owner_uuid', 'homepage_url', 'description',
837
        'project_name', 'start_date', 'end_date', 'join_policy',
838
        'leave_policy', 'resource_capacities', 'max_members', 'private']
839

    
840
    @errors.generic.all
841
    @errors.user.astakosclient
842
    @apply_notification
843
    def _run(self, project_id):
844
        specs = dict()
845
        if self['specs_path']:
846
            with open(abspath(self['specs_path'])) as f:
847
                specs = load(f)
848
        for key, arg in (
849
                ('name', self['project_name']),
850
                ('owner', self['owner_uuid']),
851
                ('homepage', self['homepage_url']),
852
                ('description', self['description']),
853
                ('max_members', self['max_members']),
854
                ('private', self['private']),
855
                ('start_date', self['start_date']),
856
                ('end_date', self['end_date']),
857
                ('join_policy', self['join_policy']),
858
                ('leave_policy', self['leave_policy']),
859
                ('resources', self['resource_capacities'])):
860
            if arg:
861
                specs[key] = arg
862

    
863
        self._print(
864
            self.client.modify_project(project_id, specs), self.print_dict)
865

    
866
    def main(self, project_id):
867
        super(self.__class__, self)._run()
868
        self._run(project_id)
869

    
870

    
871
class _project_action(_init_synnefo_astakosclient):
872

    
873
    action = ''
874

    
875
    arguments = dict(
876
        reason=ValueArgument('Quote a reason for this action', '--reason'),
877
    )
878

    
879
    @errors.generic.all
880
    @errors.user.astakosclient
881
    def _run(self, project_id, quote_a_reason):
882
        self.client.project_action(project_id, self.action, quote_a_reason)
883

    
884
    def main(self, project_id):
885
        super(_project_action, self)._run()
886
        self._run(project_id, self['reason'] or '')
887

    
888

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

    
894

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

    
900

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

    
906

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

    
912

    
913
class _application_action(_init_synnefo_astakosclient):
914

    
915
    action = ''
916

    
917
    arguments = dict(
918
        app_id=ValueArgument('The application ID', '--app-id'),
919
        reason=ValueArgument('Quote a reason for this action', '--reason'),
920
    )
921
    required = ('app_id', )
922

    
923
    @errors.generic.all
924
    @errors.user.astakosclient
925
    def _run(self, project_id, app_id, quote_a_reason):
926
        self.client.application_action(
927
            project_id, app_id, self.action, quote_a_reason)
928

    
929
    def main(self, project_id):
930
        super(_application_action, self)._run()
931
        self._run(project_id, self['app_id'], self['reason'] or '')
932

    
933

    
934
@command(project_commands)
935
class project_approve(_application_action):
936
    """Approve an application (special privileges needed)"""
937
    action = 'approve'
938

    
939

    
940
@command(project_commands)
941
class project_deny(_application_action):
942
    """Deny an application (special privileges needed)"""
943
    action = 'deny'
944

    
945

    
946
@command(project_commands)
947
class project_dismiss(_application_action):
948
    """Dismiss your denied application"""
949
    action = 'dismiss'
950

    
951

    
952
@command(project_commands)
953
class project_cancel(_application_action):
954
    """Cancel your application"""
955
    action = 'cancel'
956

    
957

    
958
@command(membership_commands)
959
class membership(_init_synnefo_astakosclient):
960
    """Project membership management commands"""
961

    
962

    
963
@command(membership_commands)
964
class membership_list(_init_synnefo_astakosclient, _optional_json):
965
    """List all memberships"""
966

    
967
    arguments = dict(
968
        project=IntArgument('Filter by project id', '--project-id')
969
    )
970

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

    
976
    def main(self):
977
        super(self.__class__, self)._run()
978
        self._run()
979

    
980

    
981
@command(membership_commands)
982
class membership_info(_init_synnefo_astakosclient, _optional_json):
983
    """Details on a membership"""
984

    
985
    @errors.generic.all
986
    @errors.user.astakosclient
987
    def _run(self, memb_id):
988
        self._print(
989
            self.client.get_membership(memb_id), self.print_dict)
990

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

    
995

    
996
class _membership_action(_init_synnefo_astakosclient, _optional_json):
997

    
998
    action = ''
999
    arguments = dict(reason=ValueArgument('Reason for the action', '--reason'))
1000

    
1001
    @errors.generic.all
1002
    @errors.user.astakosclient
1003
    def _run(self, memb_id, quote_a_reason):
1004
        self._print(self.client.membership_action(
1005
            memb_id, self.action, quote_a_reason))
1006

    
1007
    def main(self, membership_id):
1008
        super(_membership_action, self)._run()
1009
        self._run(membership_id, self['reason'] or '')
1010

    
1011

    
1012
@command(membership_commands)
1013
class membership_leave(_membership_action):
1014
    """Leave a project you have membership to"""
1015
    action = 'leave'
1016

    
1017

    
1018
@command(membership_commands)
1019
class membership_cancel(_membership_action):
1020
    """Cancel your (probably pending) membership to a project"""
1021
    action = 'cancel'
1022

    
1023

    
1024
@command(membership_commands)
1025
class membership_accept(_membership_action):
1026
    """Accept a membership for a project you manage"""
1027
    action = 'accept'
1028

    
1029

    
1030
@command(membership_commands)
1031
class membership_reject(_membership_action):
1032
    """Reject a membership for a project you manage"""
1033
    action = 'reject'
1034

    
1035

    
1036
@command(membership_commands)
1037
class membership_remove(_membership_action):
1038
    """Remove a membership for a project you manage"""
1039
    action = 'remove'
1040

    
1041

    
1042
@command(project_commands)
1043
class project_join(_init_synnefo_astakosclient):
1044
    """Join a project"""
1045

    
1046
    @errors.generic.all
1047
    @errors.user.astakosclient
1048
    def _run(self, project_id):
1049
        self.writeln(self.client.join_project(project_id))
1050

    
1051
    def main(self, project_id):
1052
        super(project_join, self)._run()
1053
        self._run(project_id)
1054

    
1055

    
1056
@command(project_commands)
1057
class project_enroll(_init_synnefo_astakosclient):
1058
    """Enroll a user to a project"""
1059

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

    
1063
    @errors.generic.all
1064
    @errors.user.astakosclient
1065
    def _run(self, project_id, email):
1066
        self.writeln(self.client.enroll_member(project_id, email))
1067

    
1068
    def main(self, project_id):
1069
        super(project_enroll, self)._run()
1070
        self._run(project_id, self['email'])