Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / astakos.py @ 4573f225

History | View | Annotate | Download (35 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 category in quotas.values():
181
                for service in self._to_format.intersection(category):
182
                    for attr, v in category[service].items():
183
                        category[service][attr] = format_size(v)
184
        self.print_dict(quotas, *args, **kwargs)
185

    
186

    
187
@command(quota_commands)
188
class quota_info(_quota):
189
    """Get quota for a service (cyclades, pithos, astakos)"""
190

    
191
    @errors.generic.all
192
    @errors.user.astakosclient
193
    def _run(self, service):
194
        r = dict()
195
        for k, v in self.client.get_quotas()['system'].items():
196
            if (k.startswith(service)):
197
                r[k] = v
198
        self._print({'%s*' % service: r}, self._print_quotas)
199

    
200
    def main(self, service):
201
        super(self.__class__, self)._run()
202
        self._run(service)
203

    
204

    
205
@command(quota_commands)
206
class quota_list(_quota):
207
    """Get user quotas"""
208

    
209
    @errors.generic.all
210
    @errors.user.astakosclient
211
    def _run(self):
212
        self._print(self.client.get_quotas(), self._print_quotas)
213

    
214
    def main(self):
215
        super(self.__class__, self)._run()
216
        self._run()
217

    
218

    
219
#  command user session
220

    
221

    
222
@command(user_commands)
223
class user_info(_init_synnefo_astakosclient, _optional_json):
224
    """Get info for (current) session user"""
225

    
226
    arguments = dict(
227
        uuid=ValueArgument('Query user with uuid', '--uuid'),
228
        name=ValueArgument('Query user with username/email', '--username')
229
    )
230

    
231
    @errors.generic.all
232
    @errors.user.astakosclient
233
    def _run(self):
234
        if self['uuid'] and self['name']:
235
            raise CLISyntaxError(
236
                'Arguments uuid and username are mutually exclusive',
237
                details=['Use either uuid OR username OR none, not both'])
238
        uuid = self['uuid'] or (self._username2uuid(self['name']) if (
239
            self['name']) else None)
240
        try:
241
            token = self.auth_base.get_token(uuid) if uuid else None
242
        except KeyError:
243
            msg = ('id %s' % self['uuid']) if (
244
                self['uuid']) else 'username %s' % self['name']
245
            raise CLIError(
246
                'No user with %s in the cached session list' % msg, details=[
247
                    'To see all cached session users',
248
                    '  /user list',
249
                    'To authenticate and add a new user in the session list',
250
                    '  /user add <new token>'])
251
        self._print(self.auth_base.user_info(token), self.print_dict)
252

    
253

    
254
@command(user_commands)
255
class user_add(_init_synnefo_astakosclient, _optional_json):
256
    """Authenticate a user by token and add to kamaki session (cache)"""
257

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

    
273
    def main(self, new_token=None):
274
        super(self.__class__, self)._run()
275
        self._run(token=new_token)
276

    
277

    
278
@command(user_commands)
279
class user_list(_init_synnefo_astakosclient, _optional_json):
280
    """List (cached) session users"""
281

    
282
    arguments = dict(
283
        detail=FlagArgument('Detailed listing', ('-l', '--detail'))
284
    )
285

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

    
292
    def main(self):
293
        super(self.__class__, self)._run()
294
        self._run()
295

    
296

    
297
@command(user_commands)
298
class user_select(_init_synnefo_astakosclient):
299
    """Select a user from the (cached) list as the current session user"""
300

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

    
331
    def main(self, user_uuid):
332
        super(self.__class__, self)._run()
333
        self._run(uuid=user_uuid)
334

    
335

    
336
@command(user_commands)
337
class user_delete(_init_synnefo_astakosclient):
338
    """Delete a user (token) from the (cached) list of session users"""
339

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

    
367
    def main(self, user_uuid):
368
        super(self.__class__, self)._run()
369
        self._run(uuid=user_uuid)
370

    
371

    
372
#  command admin
373

    
374
@command(service_commands)
375
class service_list(_init_synnefo_astakosclient, _optional_json):
376
    """List available services"""
377

    
378
    @errors.generic.all
379
    @errors.user.astakosclient
380
    def _run(self):
381
        self._print(self.client.get_services())
382

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

    
387

    
388
@command(service_commands)
389
class service_uuid2username(_init_synnefo_astakosclient, _optional_json):
390
    """Get service username(s) from uuid(s)"""
391

    
392
    @errors.generic.all
393
    @errors.user.astakosclient
394
    @with_temp_token
395
    def _run(self, uuids):
396
        if 1 == len(uuids):
397
            self._print(self.client.service_get_username(uuids[0]))
398
        else:
399
            self._print(
400
                self.client.service_get_usernames(uuids),
401
                self.print_dict)
402

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

    
407

    
408
@command(service_commands)
409
class service_username2uuid(_init_synnefo_astakosclient, _optional_json):
410
    """Get service uuid(s) from username(s)"""
411

    
412
    @errors.generic.all
413
    @errors.user.astakosclient
414
    @with_temp_token
415
    def _run(self, usernames):
416
        if 1 == len(usernames):
417
            self._print(self.client.service_get_uuid(usernames[0]))
418
        else:
419
            self._print(
420
                self.client.service_get_uuids(usernames),
421
                self.print_dict)
422

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

    
427

    
428
@command(service_commands)
429
class service_quotas(_init_synnefo_astakosclient, _optional_json):
430
    """Get service quotas"""
431

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

    
436
    @errors.generic.all
437
    @errors.user.astakosclient
438
    @with_temp_token
439
    def _run(self):
440
        self._print(self.client.service_get_quotas(self['uuid']))
441

    
442
    def main(self, service_token):
443
        super(self.__class__, self)._run()
444
        self._run(token=service_token)
445

    
446

    
447
@command(commission_commands)
448
class commission_pending(_init_synnefo_astakosclient, _optional_json):
449
    """List pending commissions (special privileges required)"""
450

    
451
    @errors.generic.all
452
    @errors.user.astakosclient
453
    def _run(self):
454
        self._print(self.client.get_pending_commissions())
455

    
456
    def main(self):
457
        super(self.__class__, self)._run()
458
        self._run()
459

    
460

    
461
@command(commission_commands)
462
class commission_info(_init_synnefo_astakosclient, _optional_json):
463
    """Get commission info (special privileges required)"""
464

    
465
    @errors.generic.all
466
    @errors.user.astakosclient
467
    def _run(self, commission_id):
468
        commission_id = int(commission_id)
469
        self._print(
470
            self.client.get_commission_info(commission_id), self.print_dict)
471

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

    
476

    
477
@command(commission_commands)
478
class commission_accept(_init_synnefo_astakosclient):
479
    """Accept a pending commission  (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.client.accept_commission(commission_id)
486

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

    
491

    
492
@command(commission_commands)
493
class commission_reject(_init_synnefo_astakosclient):
494
    """Reject a pending commission (special privileges required)"""
495

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

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

    
506

    
507
@command(commission_commands)
508
class commission_resolve(_init_synnefo_astakosclient, _optional_json):
509
    """Resolve multiple commissions (special privileges required)"""
510

    
511
    arguments = dict(
512
        accept=CommaSeparatedListArgument(
513
            'commission ids to accept (e.g., --accept=11,12,13,...',
514
            '--accept'),
515
        reject=CommaSeparatedListArgument(
516
            'commission ids to reject (e.g., --reject=11,12,13,...',
517
            '--reject')
518
    )
519

    
520
    @errors.generic.all
521
    @errors.user.astakosclient
522
    def _run(self):
523
        self.writeln('accepted ', self['accept'])
524
        self.writeln('rejected ', self['reject'])
525
        self._print(
526
            self.client.resolve_commissions(self['accept'], self['reject']),
527
            self.print_dict)
528

    
529
    def main(self):
530
        super(self.__class__, self)._run()
531
        self._run()
532

    
533

    
534
@command(commission_commands)
535
class commission_issue(_init_synnefo_astakosclient, _optional_json):
536
    """Issue commissions as a json string (special privileges required)
537
    Parameters:
538
    holder      -- user's id (string)
539
    source      -- commission's source (ex system) (string)
540
    provisions  -- resources with their quantity (json-dict from string to int)
541
    name        -- description of the commission (string)
542
    """
543

    
544
    arguments = dict(
545
        force=FlagArgument('Force commission', '--force'),
546
        accept=FlagArgument('Do not wait for verification', '--accept')
547
    )
548

    
549
    @errors.generic.all
550
    @errors.user.astakosclient
551
    def _run(self, holder, source, provisions, name=''):
552
        provisions = loads(provisions)
553
        self._print(self.client.issue_one_commission(
554
            holder, source, provisions, name,
555
            self['force'], self['accept']))
556

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

    
561

    
562
@command(resource_commands)
563
class resource_list(_init_synnefo_astakosclient, _optional_json):
564
    """List user resources"""
565

    
566
    @errors.generic.all
567
    @errors.user.astakosclient
568
    def _run(self):
569
        self._print(self.client.get_resources(), self.print_dict)
570

    
571
    def main(self):
572
        super(self.__class__, self)._run()
573
        self._run()
574

    
575

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

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

    
583
    @errors.generic.all
584
    @errors.user.astakosclient
585
    def _run(self):
586
        r = self.client.get_endpoints()['access']['serviceCatalog']
587
        r = self._filter_by_name(r)
588
        if self['endpoint_type']:
589
            r = filter_dicts_by_dict(r, dict(type=self['endpoint_type']))
590
        self._print(r)
591

    
592
    def main(self):
593
        super(self.__class__, self)._run()
594
        self._run()
595

    
596

    
597
#  command project
598

    
599

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

    
616

    
617
def apply_notification(func):
618
    def wrap(self, *args, **kwargs):
619
        r = func(self, *args, **kwargs)
620
        self.writeln('Application is submitted successfully')
621
        return r
622
    return wrap
623

    
624

    
625
@command(project_commands)
626
class project_list(_init_synnefo_astakosclient, _optional_json):
627
    """List all projects"""
628

    
629
    arguments = dict(
630
        details=FlagArgument('Show details', ('-l', '--details')),
631
        name=ValueArgument('Filter by name', ('--with-name', )),
632
        state=ValueArgument('Filter by state', ('--with-state', )),
633
        owner=ValueArgument('Filter by owner', ('--with-owner', ))
634
    )
635

    
636
    @errors.generic.all
637
    @errors.user.astakosclient
638
    def _run(self):
639
        r = self.client.get_projects(
640
            self['name'], self['state'], self['owner'])
641
        if not (self['details'] or self['output_format']):
642
            r = [dict(
643
                id=i['id'],
644
                name=i['name'],
645
                description=i['description']) for i in r]
646
        self._print(r)
647

    
648
    def main(self):
649
        super(self.__class__, self)._run()
650
        self._run()
651

    
652

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

    
657
    @errors.generic.all
658
    @errors.user.astakosclient
659
    def _run(self, project_id):
660
        self._print(
661
            self.client.get_project(project_id), self.print_dict)
662

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

    
667

    
668
class PolicyArgument(ValueArgument):
669
    """A Policy argument"""
670
    policies = ('auto', 'moderated', 'closed')
671

    
672
    @property
673
    def value(self):
674
        return getattr(self, '_value', None)
675

    
676
    @value.setter
677
    def value(self, new_policy):
678
        if new_policy:
679
            if new_policy.lower() in self.policies:
680
                self._value = new_policy.lower()
681
            else:
682
                raise CLIInvalidArgument(
683
                    'Invalid value for %s' % self.lvalue, details=[
684
                    'Valid values: %s' % ', '.join(self.policies)])
685

    
686

    
687
class ProjectResourceArgument(KeyValueArgument):
688
    """"A <resource>=<member_capacity>,<project_capacity> argument  e.g.,
689
    --resource cyclades.cpu=5,1
690
    """
691
    @property
692
    def value(self):
693
        return super(ProjectResourceArgument, self).value
694

    
695
    @value.setter
696
    def value(self, key_value_pairs):
697
        if key_value_pairs:
698
            super(ProjectResourceArgument, self.__class__).value.fset(
699
                self, key_value_pairs)
700
            d = dict(self._value)
701
            for key, value in d.items():
702
                try:
703
                    member_capacity, project_capacity = value.split(',')
704
                    member_capacity = int(member_capacity)
705
                    project_capacity = int(project_capacity)
706
                    assert member_capacity <= project_capacity
707
                except Exception as e:
708
                    raise CLIInvalidArgument(
709
                        'Invalid resource value %s' % value, details=[
710
                        'Usage:',
711
                        '  %s %s=<member_capacity>,<project_capacity>' % (
712
                            self.lvalue, key),
713
                        'where both capacities are integers',
714
                        'and member_capacity <= project_capacity', '',
715
                        '(%s)' % e])
716
                self._value[key] = dict(
717
                    member_capacity=member_capacity,
718
                    project_capacity=project_capacity)
719

    
720

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

    
725
    arguments = dict(
726
        specs_path=ValueArgument(
727
            'Specification file (contents in json)', '--spec-file'),
728
        project_name=ValueArgument('Name the project', '--name'),
729
        owner_uuid=ValueArgument('Project owner', '--owner'),
730
        homepage_url=ValueArgument('Project homepage', '--homepage'),
731
        description=ValueArgument('Describe the project', '--description'),
732
        max_members=IntArgument('Maximum subscribers', '--max-members'),
733
        private=FlagArgument('Set if the project is private', '--private'),
734
        start_date=DateArgument('When to start the project', '--start-date'),
735
        end_date=DateArgument('When to end the project', '--end-date'),
736
        join_policy=PolicyArgument(
737
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
738
            '--join-policy'),
739
        leave_policy=PolicyArgument(
740
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
741
            '--leave-policy'),
742
        resource_capacities=ProjectResourceArgument(
743
            'Set the member and project capacities for resources (repeatable) '
744
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
745
            'most 1 cpu but the project will have at most 5"       To see all '
746
            'resources:   kamaki resource list',
747
            '--resource')
748
    )
749
    required = ['specs_path', 'project_name', 'end_date']
750

    
751
    @errors.generic.all
752
    @errors.user.astakosclient
753
    @apply_notification
754
    def _run(self):
755
        specs = dict()
756
        if self['specs_path']:
757
            with open(abspath(self['specs_path'])) as f:
758
                specs = load(f)
759
        for key, arg in (
760
                ('name', self['project_name']),
761
                ('owner', self['owner_uuid']),
762
                ('homepage', self['homepage_url']),
763
                ('description', self['description']),
764
                ('max_members', self['max_members']),
765
                ('private', self['private']),
766
                ('start_date', self['start_date']),
767
                ('end_date', self['end_date']),
768
                ('join_policy', self['join_policy']),
769
                ('leave_policy', self['leave_policy']),
770
                ('resources', self['resource_capacities'])):
771
            if arg:
772
                specs[key] = arg
773

    
774
        self._print(self.client.create_project(specs), self.print_dict)
775

    
776
    def main(self):
777
        super(self.__class__, self)._run()
778
        self._req2 = [arg for arg in self.required if arg != 'specs_path']
779
        if not (self['specs_path'] or all(self[arg] for arg in self._req2)):
780
            raise CLIInvalidArgument('Insufficient arguments', details=[
781
                'Both of the following arguments are needed:',
782
                ', '.join([self.arguments[arg].lvalue for arg in self._req2]),
783
                'OR provide a spec file (json) with %s' % self.arguments[
784
                    'specs_path'].lvalue,
785
                'OR combine arguments (higher priority) with a file'])
786
        self._run()
787

    
788

    
789
@command(project_commands)
790
class project_modify(_init_synnefo_astakosclient, _optional_json):
791
    """Modify properties of a project"""
792

    
793
    __doc__ += _project_specs
794

    
795
    arguments = dict(
796
        specs_path=ValueArgument(
797
            'Specification file (contents in json)', '--spec-file'),
798
        project_name=ValueArgument('Name the project', '--name'),
799
        owner_uuid=ValueArgument('Project owner', '--owner'),
800
        homepage_url=ValueArgument('Project homepage', '--homepage'),
801
        description=ValueArgument('Describe the project', '--description'),
802
        max_members=IntArgument('Maximum subscribers', '--max-members'),
803
        private=FlagArgument('Set if the project is private', '--private'),
804
        start_date=DateArgument('When to start the project', '--start-date'),
805
        end_date=DateArgument('When to end the project', '--end-date'),
806
        join_policy=PolicyArgument(
807
            'Set join policy (%s)' % ', '.join(PolicyArgument.policies),
808
            '--join-policy'),
809
        leave_policy=PolicyArgument(
810
            'Set leave policy (%s)' % ', '.join(PolicyArgument.policies),
811
            '--leave-policy'),
812
        resource_capacities=ProjectResourceArgument(
813
            'Set the member and project capacities for resources (repeatable) '
814
            'e.g., --resource cyclades.cpu=1,5    means "members will have at '
815
            'most 1 cpu but the project will have at most 5"       To see all '
816
            'resources:   kamaki resource list',
817
            '--resource')
818
    )
819
    required = [
820
        'specs_path', 'owner_uuid', 'homepage_url', 'description',
821
        'project_name', 'start_date', 'end_date', 'join_policy',
822
        'leave_policy', 'resource_capacities', 'max_members', 'private']
823

    
824
    @errors.generic.all
825
    @errors.user.astakosclient
826
    @apply_notification
827
    def _run(self, project_id):
828
        specs = dict()
829
        if self['specs_path']:
830
            with open(abspath(self['specs_path'])) as f:
831
                specs = load(f)
832
        for key, arg in (
833
                ('name', self['project_name']),
834
                ('owner', self['owner_uuid']),
835
                ('homepage', self['homepage_url']),
836
                ('description', self['description']),
837
                ('max_members', self['max_members']),
838
                ('private', self['private']),
839
                ('start_date', self['start_date']),
840
                ('end_date', self['end_date']),
841
                ('join_policy', self['join_policy']),
842
                ('leave_policy', self['leave_policy']),
843
                ('resources', self['resource_capacities'])):
844
            if arg:
845
                specs[key] = arg
846

    
847
        self._print(
848
            self.client.modify_project(project_id, specs), self.print_dict)
849

    
850
    def main(self, project_id):
851
        super(self.__class__, self)._run()
852
        self._run(project_id)
853

    
854

    
855
class _project_action(_init_synnefo_astakosclient):
856

    
857
    action = ''
858

    
859
    arguments = dict(
860
        reason=ValueArgument('Quote a reason for this action', '--reason'),
861
    )
862

    
863
    @errors.generic.all
864
    @errors.user.astakosclient
865
    def _run(self, project_id, quote_a_reason):
866
        self.client.project_action(project_id, self.action, quote_a_reason)
867

    
868
    def main(self, project_id):
869
        super(_project_action, self)._run()
870
        self._run(project_id, self['reason'] or '')
871

    
872

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

    
878

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

    
884

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

    
890

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

    
896

    
897
class _application_action(_init_synnefo_astakosclient):
898

    
899
    action = ''
900

    
901
    arguments = dict(
902
        app_id=ValueArgument('The application ID', '--app-id'),
903
        reason=ValueArgument('Quote a reason for this action', '--reason'),
904
    )
905
    required = ('app_id', )
906

    
907
    @errors.generic.all
908
    @errors.user.astakosclient
909
    def _run(self, project_id, app_id, quote_a_reason):
910
        self.client.application_action(
911
            project_id, app_id, self.action, quote_a_reason)
912

    
913
    def main(self, project_id):
914
        super(_application_action, self)._run()
915
        self._run(project_id, self['app_id'], self['reason'] or '')
916

    
917

    
918
@command(project_commands)
919
class project_approve(_application_action):
920
    """Approve an application (special privileges needed)"""
921
    action = 'approve'
922

    
923

    
924
@command(project_commands)
925
class project_deny(_application_action):
926
    """Deny an application (special privileges needed)"""
927
    action = 'deny'
928

    
929

    
930
@command(project_commands)
931
class project_dismiss(_application_action):
932
    """Dismiss your denied application"""
933
    action = 'dismiss'
934

    
935

    
936
@command(project_commands)
937
class project_cancel(_application_action):
938
    """Cancel your application"""
939
    action = 'cancel'
940

    
941

    
942
@command(membership_commands)
943
class membership(_init_synnefo_astakosclient):
944
    """Project membership management commands"""
945

    
946

    
947
@command(membership_commands)
948
class membership_list(_init_synnefo_astakosclient, _optional_json):
949
    """List all memberships"""
950

    
951
    arguments = dict(
952
        project=IntArgument('Filter by project id', '--project-id')
953
    )
954

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

    
960
    def main(self):
961
        super(self.__class__, self)._run()
962
        self._run()
963

    
964

    
965
@command(membership_commands)
966
class membership_info(_init_synnefo_astakosclient, _optional_json):
967
    """Details on a membership"""
968

    
969
    @errors.generic.all
970
    @errors.user.astakosclient
971
    def _run(self, memb_id):
972
        self._print(
973
            self.client.get_membership(memb_id), self.print_dict)
974

    
975
    def main(self, membership_id):
976
        super(self.__class__, self)._run()
977
        self._run(memb_id=membership_id)
978

    
979

    
980
class _membership_action(_init_synnefo_astakosclient, _optional_json):
981

    
982
    action = ''
983
    arguments = dict(reason=ValueArgument('Reason for the action', '--reason'))
984

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

    
991
    def main(self, membership_id):
992
        super(_membership_action, self)._run()
993
        self._run(membership_id, self['reason'] or '')
994

    
995

    
996
@command(membership_commands)
997
class membership_leave(_membership_action):
998
    """Leave a project you have membership to"""
999
    action = 'leave'
1000

    
1001

    
1002
@command(membership_commands)
1003
class membership_cancel(_membership_action):
1004
    """Cancel your (probably pending) membership to a project"""
1005
    action = 'cancel'
1006

    
1007

    
1008
@command(membership_commands)
1009
class membership_accept(_membership_action):
1010
    """Accept a membership for a project you manage"""
1011
    action = 'accept'
1012

    
1013

    
1014
@command(membership_commands)
1015
class membership_reject(_membership_action):
1016
    """Reject a membership for a project you manage"""
1017
    action = 'reject'
1018

    
1019

    
1020
@command(membership_commands)
1021
class membership_remove(_membership_action):
1022
    """Remove a membership for a project you manage"""
1023
    action = 'remove'
1024

    
1025

    
1026
@command(project_commands)
1027
class project_join(_init_synnefo_astakosclient):
1028
    """Join a project"""
1029

    
1030
    @errors.generic.all
1031
    @errors.user.astakosclient
1032
    def _run(self, project_id):
1033
        self.writeln(self.client.join_project(project_id))
1034

    
1035
    def main(self, project_id):
1036
        super(project_join, self)._run()
1037
        self._run(project_id)
1038

    
1039

    
1040
@command(project_commands)
1041
class project_enroll(_init_synnefo_astakosclient):
1042
    """Enroll a user to a project"""
1043

    
1044
    arguments = dict(email=ValueArgument('User e-mail', '--email'))
1045
    required = ('email', )
1046

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

    
1052
    def main(self, project_id):
1053
        super(project_enroll, self)._run()
1054
        self._run(project_id, self['email'])