Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / astakos.py @ 1d89fbd0

History | View | Annotate | Download (28.7 kB)

1
# Copyright 2011-2013 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 CLIBaseUrlError, CLISyntaxError, CLIError
43
from kamaki.cli.argument import (
44
    FlagArgument, ValueArgument, IntArgument, CommaSeparatedListArgument)
45
from kamaki.cli.utils import format_size, filter_dicts_by_dict
46

    
47
#  Mandatory
48

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

    
58

    
59
#  Optional
60

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

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

    
72

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

    
88

    
89
class _init_synnefo_astakosclient(_command_init):
90

    
91
    @errors.generic.all
92
    @errors.user.load
93
    @errors.user.astakosclient
94
    @addLogSettings
95
    def _run(self):
96
        if getattr(self, 'cloud', None):
97
            base_url = self._custom_url('astakos')
98
            if base_url:
99
                token = self._custom_token(
100
                    'astakos') or self.config.get_cloud(
101
                    self.cloud, 'token')
102
                token = token.split()[0] if ' ' in token else token
103
                self.client = LoggedAstakosClient(base_url, token)
104
                return
105
        else:
106
            self.cloud = 'default'
107
        if getattr(self, 'auth_base', None):
108
            self.client = self.auth_base.get_client()
109
            return
110
        raise CLIBaseUrlError(service='astakos')
111

    
112
    def main(self):
113
        self._run()
114

    
115

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

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

    
127
    def main(self, token=None):
128
        super(self.__class__, self)._run()
129
        self._run(token=token)
130

    
131

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

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

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

    
149

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

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

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

    
167

    
168
class _quota(_init_synnefo_astakosclient, _optional_json):
169

    
170
    _to_format = set(['cyclades.disk', 'pithos.diskspace', 'cyclades.ram'])
171

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

    
176
    def _print_quotas(self, quotas, *args, **kwargs):
177
        if not self['bytes']:
178
            for category in quotas.values():
179
                for service in self._to_format.intersection(category):
180
                    for attr, v in category[service].items():
181
                        category[service][attr] = format_size(v)
182
        self.print_dict(quotas, *args, **kwargs)
183

    
184

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

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

    
198
    def main(self, service):
199
        super(self.__class__, self)._run()
200
        self._run(service)
201

    
202

    
203
@command(quota_commands)
204
class quota_list(_quota):
205
    """Get user quotas"""
206

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

    
212
    def main(self):
213
        super(self.__class__, self)._run()
214
        self._run()
215

    
216

    
217
#  command user session
218

    
219

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

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

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

    
251

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

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

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

    
275

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

    
280
    arguments = dict(
281
        detail=FlagArgument('Detailed listing', ('-l', '--detail'))
282
    )
283

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

    
290
    def main(self):
291
        super(self.__class__, self)._run()
292
        self._run()
293

    
294

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

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

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

    
333

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

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

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

    
369

    
370
#  command admin
371

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

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

    
381
    def main(self):
382
        super(self.__class__, self)._run()
383
        self._run()
384

    
385

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

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

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

    
405

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

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

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

    
425

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

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

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

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

    
444

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

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

    
454
    def main(self):
455
        super(self.__class__, self)._run()
456
        self._run()
457

    
458

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

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

    
470
    def main(self, commission_id):
471
        super(self.__class__, self)._run()
472
        self._run(commission_id)
473

    
474

    
475
@command(commission_commands)
476
class commission_accept(_init_synnefo_astakosclient):
477
    """Accept a pending commission  (special privileges required)"""
478

    
479
    @errors.generic.all
480
    @errors.user.astakosclient
481
    def _run(self, commission_id):
482
        commission_id = int(commission_id)
483
        self.client.accept_commission(commission_id)
484

    
485
    def main(self, commission_id):
486
        super(self.__class__, self)._run()
487
        self._run(commission_id)
488

    
489

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

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

    
500
    def main(self, commission_id):
501
        super(self.__class__, self)._run()
502
        self._run(commission_id)
503

    
504

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

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

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

    
527
    def main(self):
528
        super(self.__class__, self)._run()
529
        self._run()
530

    
531

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

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

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

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

    
559

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

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

    
569
    def main(self):
570
        super(self.__class__, self)._run()
571
        self._run()
572

    
573

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

    
579
    arguments = dict(endpoint_type=ValueArgument('Filter by type', '--type'))
580

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

    
590
    def main(self):
591
        super(self.__class__, self)._run()
592
        self._run()
593

    
594

    
595
#  command project
596

    
597

    
598
_project_specs = """{
599
    "name": name,
600
    "owner": uuid,
601
    "homepage": homepage,         # optional
602
    "description": description,   # optional
603
    "comments": comments,         # optional
604
    "start_date": date,           # optional
605
    "end_date": date,
606
    "join_policy": "auto" | "moderated" | "closed",  # default: "moderated"
607
    "leave_policy": "auto" | "moderated" | "closed", # default: "auto"
608
    "resources": {"cyclades.vm": {
609
    "project_capacity": int or null,
610
    "member_capacity": int
611
    }}}
612

613
"""
614

    
615

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

    
623

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

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

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

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

    
651

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

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

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

    
666

    
667
@command(project_commands)
668
class project_create(_init_synnefo_astakosclient, _optional_json):
669
    """Apply for a new project (enter data though standard input or file path)
670

671
    Project details must be provided as a json-formated dict from the
672
    standard input, or through a file
673
    """
674
    __doc__ += _project_specs
675

    
676
    arguments = dict(
677
        specs_path=ValueArgument(
678
            'Specification file path (content must be in json)', '--spec-file')
679
    )
680

    
681
    @errors.generic.all
682
    @errors.user.astakosclient
683
    @apply_notification
684
    def _run(self):
685
        input_stream = open(abspath(self['specs_path'])) if (
686
            self['specs_path']) else self._in
687
        specs = load(input_stream)
688
        self._print(
689
            self.client.create_project(specs), self.print_dict)
690

    
691
    def main(self):
692
        super(self.__class__, self)._run()
693
        self._run()
694

    
695

    
696
@command(project_commands)
697
class project_modify(_init_synnefo_astakosclient, _optional_json):
698
    """Modify a project (input a json-dict)
699
    Project details must be provided as a json-formated dict from the standard
700
    input, or through a file
701
    """
702

    
703
    __doc__ += _project_specs
704

    
705
    arguments = dict(
706
        specs_path=ValueArgument(
707
            'Specification file path (content must be in json)', '--spec-file')
708
    )
709

    
710
    @errors.generic.all
711
    @errors.user.astakosclient
712
    @apply_notification
713
    def _run(self, project_id):
714
        input_stream = open(abspath(self['specs_path'])) if (
715
            self['specs_path']) else self._in
716
        specs = load(input_stream)
717
        self._print(
718
            self.client.modify_project(project_id, specs),
719
            self.print_dict)
720

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

    
725

    
726
class _project_action(_init_synnefo_astakosclient):
727

    
728
    action = ''
729

    
730
    arguments = dict(
731
        reason=ValueArgument('Quote a reason for this action', '--reason'),
732
    )
733

    
734
    @errors.generic.all
735
    @errors.user.astakosclient
736
    def _run(self, project_id, quote_a_reason):
737
        self.client.project_action(project_id, self.action, quote_a_reason)
738

    
739
    def main(self, project_id):
740
        super(_project_action, self)._run()
741
        self._run(project_id, self['reason'] or '')
742

    
743

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

    
749

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

    
755

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

    
761

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

    
767

    
768
class _application_action(_init_synnefo_astakosclient):
769

    
770
    action = ''
771

    
772
    arguments = dict(
773
        app_id=ValueArgument('The application ID', '--app-id'),
774
        reason=ValueArgument('Quote a reason for this action', '--reason'),
775
    )
776
    required = ('app_id', )
777

    
778
    @errors.generic.all
779
    @errors.user.astakosclient
780
    def _run(self, project_id, app_id, quote_a_reason):
781
        self.client.application_action(
782
            project_id, app_id, self.action, quote_a_reason)
783

    
784
    def main(self, project_id):
785
        super(_application_action, self)._run()
786
        self._run(self['app_id'], self['reason'] or '')
787

    
788

    
789
@command(project_commands)
790
class project_approve(_application_action):
791
    """Approve an application (special privileges needed)"""
792
    action = 'approve'
793

    
794

    
795
@command(project_commands)
796
class project_deny(_application_action):
797
    """Deny an application (special privileges needed)"""
798
    action = 'deny'
799

    
800

    
801
@command(project_commands)
802
class project_dismiss(_application_action):
803
    """Dismiss your denied application"""
804
    action = 'dismiss'
805

    
806

    
807
@command(project_commands)
808
class project_cancel(_application_action):
809
    """Cancel your application"""
810
    action = 'cancel'
811

    
812

    
813
@command(membership_commands)
814
class membership(_init_synnefo_astakosclient):
815
    """Project membership management commands"""
816

    
817

    
818
@command(membership_commands)
819
class membership_list(_init_synnefo_astakosclient, _optional_json):
820
    """List all memberships"""
821

    
822
    arguments = dict(
823
        project=IntArgument('Filter by project id', '--with-project-id')
824
    )
825

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

    
831
    def main(self):
832
        super(self.__class__, self)._run()
833
        self._run()
834

    
835

    
836
@command(membership_commands)
837
class membership_info(_init_synnefo_astakosclient, _optional_json):
838
    """Details on a membership"""
839

    
840
    @errors.generic.all
841
    @errors.user.astakosclient
842
    def _run(self, memb_id):
843
        self._print(
844
            self.client.get_membership(memb_id), self.print_dict)
845

    
846
    def main(self, membership_id):
847
        super(self.__class__, self)._run()
848
        self._run(membership_id)
849

    
850

    
851
class _membership_action(_init_synnefo_astakosclient, _optional_json):
852

    
853
    action = ''
854

    
855
    @errors.generic.all
856
    @errors.user.astakosclient
857
    def _run(self, memb_id, quote_a_reason):
858
        self._print(self.client.membership_action(
859
            memb_id, self.action, quote_a_reason))
860

    
861
    def main(self, membership_id, quote_a_reason=''):
862
        super(_membership_action, self)._run()
863
        self._run(membership_id, quote_a_reason)
864

    
865

    
866
@command(membership_commands)
867
class membership_leave(_membership_action):
868
    """Leave a project you have membership to"""
869
    action = 'leave'
870

    
871

    
872
@command(membership_commands)
873
class membership_cancel(_membership_action):
874
    """Cancel your (probably pending) membership to a project"""
875
    action = 'cancel'
876

    
877

    
878
@command(membership_commands)
879
class membership_accept(_membership_action):
880
    """Accept a membership for a project you manage"""
881
    action = 'accept'
882

    
883

    
884
@command(membership_commands)
885
class membership_reject(_membership_action):
886
    """Reject a membership for a project you manage"""
887
    action = 'reject'
888

    
889

    
890
@command(membership_commands)
891
class membership_remove(_membership_action):
892
    """Remove a membership for a project you manage"""
893
    action = 'remove'
894

    
895

    
896
@command(membership_commands)
897
class membership_join(_init_synnefo_astakosclient):
898
    """Join a project"""
899

    
900
    @errors.generic.all
901
    @errors.user.astakosclient
902
    def _run(self, project_id):
903
        self.writeln(self.client.join_project(project_id))
904

    
905
    def main(self, project_id):
906
        super(membership_join, self)._run()
907
        self._run(project_id)
908

    
909

    
910
@command(membership_commands)
911
class membership_enroll(_init_synnefo_astakosclient):
912
    """Enroll somebody to a project you manage"""
913

    
914
    @errors.generic.all
915
    @errors.user.astakosclient
916
    def _run(self, project_id, email):
917
        self.writeln(self.client.enroll_member(project_id, email))
918

    
919
    def main(self, project_id, email):
920
        super(membership_enroll, self)._run()
921
        self._run(project_id, email)