Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / tools / admin.py @ 9c0ac5af

History | View | Annotate | Download (27.5 kB)

1
#!/usr/bin/env python
2

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

    
36
from django.core.management import setup_environ
37
try:
38
    from synnefo import settings
39
except ImportError:
40
    raise Exception("Cannot import settings, make sure PYTHONPATH contains "
41
                    "the parent directory of the Synnefo Django project.")
42
setup_environ(settings)
43

    
44
import inspect
45
import sys
46

    
47
from collections import defaultdict
48
from itertools import product
49
from optparse import OptionParser
50
from os.path import basename
51

    
52
from synnefo.db import models
53
from synnefo.invitations.invitations import add_invitation, send_invitation
54
from synnefo.logic import backend, users
55
from synnefo.plankton.backend import ImageBackend
56
from synnefo.util.dictconfig import dictConfig
57

    
58

    
59
def get_user(uid):
60
    try:
61
        uid = int(uid)
62
        return models.SynnefoUser.objects.get(id=uid)
63
    except ValueError:
64
        return None
65
    except models.SynnefoUser.DoesNotExist:
66
        return None
67

    
68
def print_dict(d, exclude=()):
69
    if not d:
70
        return
71
    margin = max(len(key) for key in d) + 1
72

    
73
    for key, val in sorted(d.items()):
74
        if key in exclude or key.startswith('_'):
75
            continue
76
        print '%s: %s' % (key.rjust(margin), val)
77

    
78
def print_item(item):
79
    name = getattr(item, 'name', '')
80
    print '%d %s' % (item.id, name)
81
    print_dict(item.__dict__, exclude=('id', 'name'))
82

    
83
def print_items(items, detail=False, keys=None):
84
    keys = keys or ('id', 'name')
85
    for item in items:
86
        for key in keys:
87
            print getattr(item, key),
88
        print
89
        
90
        if detail:
91
            print_dict(item.__dict__, exclude=keys)
92
            print
93

    
94

    
95
class Command(object):
96
    group = '<group>'
97
    name = '<command>'
98
    syntax = ''
99
    description = ''
100
    hidden = False
101
    
102
    def __init__(self, exe, argv):
103
        parser = OptionParser()
104
        syntax = '%s [options]' % self.syntax if self.syntax else '[options]'
105
        parser.usage = '%s %s %s' % (exe, self.name, syntax)
106
        parser.description = self.description
107
        self.add_options(parser)
108
        options, self.args = parser.parse_args(argv)
109
        
110
        # Add options to self
111
        for opt in parser.option_list:
112
            key = opt.dest
113
            if key:
114
                val = getattr(options, key)
115
                setattr(self, key, val)
116
        
117
        self.parser = parser
118
    
119
    def add_options(self, parser):
120
        pass
121
    
122
    def execute(self):
123
        try:
124
            self.main(*self.args)
125
        except TypeError:
126
            self.parser.print_help()
127

    
128

    
129
# Server commands
130

    
131
class ListServers(Command):
132
    group = 'server'
133
    name = 'list'
134
    syntax = '[server id]'
135
    description = 'list servers'
136
    
137
    def add_options(self, parser):
138
        parser.add_option('-a', action='store_true', dest='show_deleted',
139
                        default=False, help='also list deleted servers')
140
        parser.add_option('-l', action='store_true', dest='detail',
141
                        default=False, help='show detailed output')
142
        parser.add_option('-u', dest='uid', metavar='UID',
143
                            help='show servers of user with id UID')
144
    
145
    def main(self, server_id=None):
146
        if server_id:
147
            servers = [models.VirtualMachine.objects.get(id=server_id)]
148
        else:
149
            servers = models.VirtualMachine.objects.order_by('id')
150
            if not self.show_deleted:
151
                servers = servers.exclude(deleted=True)
152
            if self.uid:
153
                user = get_user(self.uid)
154
                if user:
155
                    servers = servers.filter(owner=user)
156
                else:
157
                    print 'Unknown user id'
158
                    return
159
        
160
        print_items(servers, self.detail)
161

    
162

    
163
# User commands
164

    
165
class CreateUser(Command):
166
    group = 'user'
167
    name = 'create'
168
    syntax = '<username> <email>'
169
    description = 'create a user'
170
    
171
    def add_options(self, parser):
172
        parser.add_option('--realname', dest='realname', metavar='NAME',
173
                            help='set real name of user')
174
        parser.add_option('--type', dest='type', metavar='TYPE',
175
                            help='set user type')
176
    
177
    def main(self, username, email):
178
        username = username.decode('utf8')
179
        realname = self.realname or username
180
        type = self.type or 'USER'
181
        types = [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]
182
        if type not in types:
183
            valid = ', '.join(types)
184
            print 'Invalid type. Must be one of:', valid
185
            return
186
        
187
        user = users._register_user(realname, username, email, type)
188
        print_item(user)
189

    
190

    
191
class InviteUser(Command):
192
    group = 'user'
193
    name = 'invite'
194
    syntax = '<inviter id> <invitee name> <invitee email>'
195
    description = 'invite a user'
196
    
197
    def main(self, inviter_id, name, email):
198
        name = name.decode('utf8')
199
        inviter = get_user(inviter_id)
200
        inv = add_invitation(inviter, name, email)
201
        send_invitation(inv)
202

    
203

    
204
class ListUsers(Command):
205
    group = 'user'
206
    name = 'list'
207
    syntax = '[user id]'
208
    description = 'list users'
209
    
210
    def add_options(self, parser):
211
        parser.add_option('-a', action='store_true', dest='show_deleted',
212
                        default=False, help='also list deleted users')
213
        parser.add_option('-l', action='store_true', dest='detail',
214
                        default=False, help='show detailed output')
215
    
216
    def main(self, user_id=None):
217
        if user_id:
218
            users = [models.SynnefoUser.objects.get(id=user_id)]
219
        else:
220
            users = models.SynnefoUser.objects.order_by('id')
221
            if not self.show_deleted:
222
                users = users.exclude(state='DELETED')
223
        print_items(users, self.detail, keys=('id', 'name', 'uniq'))
224

    
225

    
226
class ModifyUser(Command):
227
    group = 'user'
228
    name = 'modify'
229
    syntax = '<user id>'
230
    description = 'modify a user'
231
    
232
    def add_options(self, parser):
233
        types = ', '.join(x[0] for x in models.SynnefoUser.ACCOUNT_TYPE)
234
        states = ', '.join(x[0] for x in models.SynnefoUser.ACCOUNT_STATE)
235
        
236
        parser.add_option('--credit', dest='credit', metavar='VALUE',
237
                            help='set user credits')
238
        parser.add_option('--invitations', dest='invitations',
239
                            metavar='VALUE', help='set max invitations')
240
        parser.add_option('--realname', dest='realname', metavar='NAME',
241
                            help='set real name of user')
242
        parser.add_option('--type', dest='type', metavar='TYPE',
243
                            help='set user type (%s)' % types)
244
        parser.add_option('--state', dest='state', metavar='STATE',
245
                            help='set user state (%s)' % states)
246
        parser.add_option('--uniq', dest='uniq', metavar='ID',
247
                            help='set external unique ID')
248
        parser.add_option('--username', dest='username', metavar='NAME',
249
                            help='set username')
250
    
251
    def main(self, user_id):
252
        user = get_user(user_id)
253
        
254
        if self.credit:
255
            user.credit = self.credit
256
        if self.invitations:
257
            user.max_invitations = self.invitations
258
        if self.realname:
259
            user.realname = self.realname
260
        if self.type:
261
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]
262
            if self.type not in allowed:
263
                valid = ', '.join(allowed)
264
                print 'Invalid type. Must be one of:', valid
265
                return
266
            user.type = self.type
267
        if self.state:
268
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_STATE]
269
            if self.state not in allowed:
270
                valid = ', '.join(allowed)
271
                print 'Invalid state. Must be one of:', valid
272
                return
273
            user.state = self.state
274
        if self.uniq:
275
            user.uniq = self.uniq
276
        if self.username:
277
            user.name = self.username
278
        
279
        user.save()
280
        print_item(user)
281

    
282

    
283
# Image commands
284

    
285
class ListImages(Command):
286
    group = 'image'
287
    name = 'list'
288
    syntax = '[image id]'
289
    description = 'list images'
290
    
291
    def add_options(self, parser):
292
        parser.add_option('-a', action='store_true', dest='show_deleted',
293
                default=False, help='also list deleted images')
294
        parser.add_option('-l', action='store_true', dest='detail',
295
                default=False, help='show detailed output')
296
        parser.add_option('-p', action='store_true', dest='pithos',
297
                default=False, help='show images stored in Pithos')
298
        parser.add_option('--user', dest='user',
299
                default=settings.SYSTEM_IMAGES_OWNER,
300
                metavar='USER',
301
                help='list images accessible to USER')
302
    
303
    def main(self, image_id=None):
304
        if self.pithos:
305
            return self.main_pithos(image_id)
306
        
307
        if image_id:
308
            images = [models.Image.objects.get(id=image_id)]
309
        else:
310
            images = models.Image.objects.order_by('id')
311
            if not self.show_deleted:
312
                images = images.exclude(state='DELETED')
313
        print_items(images, self.detail)
314
    
315
    def main_pithos(self, image_id=None):
316
        backend = ImageBackend(self.user)
317
        if image_id:
318
            images = [backend.get_meta(image_id)]
319
        else:
320
            images = backend.iter_shared()
321
        
322
        for image in images:
323
            print image['id'], image['name']
324
            if self.detail:
325
                print_dict(image, exclude=('id',))
326
                print
327
        
328
        backend.close()
329

    
330

    
331
class RegisterImage(Command):
332
    group = 'image'
333
    name = 'register'
334
    syntax = '<name> <Backend ID or Pithos URL> <disk format>'
335
    description = 'register an image'
336
    
337
    def add_options(self, parser):
338
        parser.add_option('--meta', dest='meta', action='append',
339
                metavar='KEY=VAL',
340
                help='add metadata (can be used multiple times)')
341
        parser.add_option('--public', action='store_true', dest='public',
342
                default=False, help='make image public')
343
        parser.add_option('-u', dest='uid', metavar='UID',
344
                help='assign image to user with id UID')
345
    
346
    def main(self, name, backend_id, format):
347
        if backend_id.startswith('pithos://'):
348
            return self.main_pithos(name, backend_id, format)
349
        
350
        formats = [x[0] for x in models.Image.FORMATS]
351
        if format not in formats:
352
            valid = ', '.join(formats)
353
            print 'Invalid format. Must be one of:', valid
354
            return
355
        
356
        user = None
357
        if self.uid:
358
            user = get_user(self.uid)
359
            if not user:
360
                print 'Unknown user id'
361
                return
362
        
363
        image = models.Image.objects.create(
364
            name=name,
365
            state='ACTIVE',
366
            owner=user,
367
            backend_id=backend_id,
368
            format=format,
369
            public=self.public)
370
        
371
        if self.meta:
372
            for m in self.meta:
373
                key, sep, val = m.partition('=')
374
                if key and val:
375
                    image.metadata.create(meta_key=key, meta_value=val)
376
                else:
377
                    print 'WARNING: Ignoring meta', m
378
        
379
        print_item(image)
380
    
381
    def main_pithos(self, name, url, disk_format):
382
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
383
            print 'Invalid disk format'
384
            return
385
        
386
        params = {
387
            'disk_format': disk_format,
388
            'is_public': self.public,
389
            'properties': {}}
390
        
391
        if self.meta:
392
            for m in self.meta:
393
                key, sep, val = m.partition('=')
394
                if key and val:
395
                    params['properties'][key] = val
396
                else:
397
                    print 'WARNING: Ignoring meta', m
398
        
399
        backend = ImageBackend(self.uid or settings.SYSTEM_IMAGES_OWNER)
400
        backend.register(name, url, params)
401
        backend.close()
402

    
403

    
404
class UploadImage(Command):
405
    group = 'image'
406
    name = 'upload'
407
    syntax = '<name> <path>'
408
    description = 'upload an image'
409
    
410
    def add_options(self, parser):
411
        container_formats = ', '.join(settings.ALLOWED_CONTAINER_FORMATS)
412
        disk_formats = ', '.join(settings.ALLOWED_DISK_FORMATS)
413
        
414
        parser.add_option('--container-format', dest='container_format',
415
                default=settings.DEFAULT_CONTAINER_FORMAT,
416
                metavar='FORMAT',
417
                help='set container format (%s)' % container_formats)
418
        parser.add_option('--disk-format', dest='disk_format',
419
                default=settings.DEFAULT_DISK_FORMAT,
420
                metavar='FORMAT',
421
                help='set disk format (%s)' % disk_formats)
422
        parser.add_option('--meta', dest='meta', action='append',
423
                metavar='KEY=VAL',
424
                help='add metadata (can be used multiple times)')
425
        parser.add_option('--owner', dest='owner',
426
                default=settings.SYSTEM_IMAGES_OWNER,
427
                metavar='USER',
428
                help='set owner to USER')
429
        parser.add_option('--public', action='store_true', dest='public',
430
                default=False,
431
                help='make image public')
432
    
433
    def main(self, name, path):
434
        backend = ImageBackend(self.owner)
435
        
436
        params = {
437
            'container_format': self.container_format,
438
            'disk_format': self.disk_format,
439
            'is_public': self.public,
440
            'filename': basename(path),
441
            'properties': {}}
442
        
443
        if self.meta:
444
            for m in self.meta:
445
                key, sep, val = m.partition('=')
446
                if key and val:
447
                    params['properties'][key] = val
448
                else:
449
                    print 'WARNING: Ignoring meta', m
450
        
451
        with open(path) as f:
452
            backend.put(name, f, params)
453
        
454
        backend.close()
455

    
456

    
457
class UpdateImage(Command):
458
    group = 'image'
459
    name = 'update'
460
    syntax = '<image id>'
461
    description = 'update an image stored in Pithos'
462
    
463
    def add_options(self, parser):
464
        container_formats = ', '.join(settings.ALLOWED_CONTAINER_FORMATS)
465
        disk_formats = ', '.join(settings.ALLOWED_DISK_FORMATS)
466
        
467
        parser.add_option('--container-format', dest='container_format',
468
                metavar='FORMAT',
469
                help='set container format (%s)' % container_formats)
470
        parser.add_option('--disk-format', dest='disk_format',
471
                metavar='FORMAT',
472
                help='set disk format (%s)' % disk_formats)
473
        parser.add_option('--name', dest='name',
474
                metavar='NAME',
475
                help='set name to NAME')
476
        parser.add_option('--private', action='store_true', dest='private',
477
                help='make image private')
478
        parser.add_option('--public', action='store_true', dest='public',
479
                help='make image public')
480
        parser.add_option('--user', dest='user',
481
                default=settings.SYSTEM_IMAGES_OWNER,
482
                metavar='USER',
483
                help='connect as USER')
484
    
485
    def main(self, image_id):
486
        backend = ImageBackend(self.user)
487
        
488
        image = backend.get_meta(image_id)
489
        if not image:
490
            print 'Image not found'
491
            return
492
        
493
        params = {}
494
        
495
        if self.container_format:
496
            if self.container_format not in settings.ALLOWED_CONTAINER_FORMATS:
497
                print 'Invalid container format'
498
                return
499
            params['container_format'] = self.container_format
500
        if self.disk_format:
501
            if self.disk_format not in settings.ALLOWED_DISK_FORMATS:
502
                print 'Invalid disk format'
503
                return
504
            params['disk_format'] = self.disk_format
505
        if self.name:
506
            params['name'] = self.name
507
        if self.private:
508
            params['is_public'] = False
509
        if self.public:
510
            params['is_public'] = True
511
        
512
        backend.update(image_id, params)
513
        backend.close()
514

    
515

    
516
class ModifyImage(Command):
517
    group = 'image'
518
    name = 'modify'
519
    syntax = '<image id>'
520
    description = 'modify an image'
521
    
522
    def add_options(self, parser):
523
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
524
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
525

    
526
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
527
                help='set image backend id')
528
        parser.add_option('-f', dest='format', metavar='FORMAT',
529
                help='set image format (%s)' % formats)
530
        parser.add_option('-n', dest='name', metavar='NAME',
531
                help='set image name')
532
        parser.add_option('--public', action='store_true', dest='public',
533
                help='make image public')
534
        parser.add_option('--nopublic', action='store_true', dest='private',
535
                help='make image private')
536
        parser.add_option('-s', dest='state', metavar='STATE',
537
                help='set image state (%s)' % states)
538
        parser.add_option('-u', dest='uid', metavar='UID',
539
                help='assign image to user with id UID')
540
    
541
    def main(self, image_id):
542
        try:
543
            image = models.Image.objects.get(id=image_id)
544
        except:
545
            print 'Image not found'
546
            return
547
        
548
        if self.backend_id:
549
            image.backend_id = self.backend_id
550
        if self.format:
551
            allowed = [x[0] for x in models.Image.FORMATS]
552
            if self.format not in allowed:
553
                valid = ', '.join(allowed)
554
                print 'Invalid format. Must be one of:', valid
555
                return
556
            image.format = self.format
557
        if self.name:
558
            image.name = self.name
559
        if self.public:
560
            image.public = True
561
        if self.private:
562
            image.public = False
563
        if self.state:
564
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
565
            if self.state not in allowed:
566
                valid = ', '.join(allowed)
567
                print 'Invalid state. Must be one of:', valid
568
                return
569
            image.state = self.state
570
        if self.uid:
571
            image.owner = get_user(self.uid)
572
        
573
        image.save()
574
        print_item(image)
575

    
576

    
577
class ModifyImageMeta(Command):
578
    group = 'image'
579
    name = 'meta'
580
    syntax = '<image id> [key[=val]]'
581
    description = 'get and manipulate image metadata'
582
    
583
    def add_options(self, parser):
584
        parser.add_option('--user', dest='user',
585
                default=settings.SYSTEM_IMAGES_OWNER,
586
                metavar='USER',
587
                help='connect as USER')
588

    
589
    def main(self, image_id, arg=''):
590
        if not image_id.isdigit():
591
            return self.main_pithos(image_id, arg)
592
        
593
        try:
594
            image = models.Image.objects.get(id=image_id)
595
        except:
596
            print 'Image not found'
597
            return
598
        
599
        key, sep, val = arg.partition('=')
600
        if not sep:
601
            val = None
602
        
603
        if not key:
604
            metadata = {}
605
            for meta in image.metadata.order_by('meta_key'):
606
                metadata[meta.meta_key] = meta.meta_value
607
            print_dict(metadata)
608
            return
609
        
610
        try:
611
            meta = image.metadata.get(meta_key=key)
612
        except models.ImageMetadata.DoesNotExist:
613
            meta = None
614
        
615
        if val is None:
616
            if meta:
617
                print_dict({key: meta.meta_value})
618
            return
619
        
620
        if val:
621
            if not meta:
622
                meta = image.metadata.create(meta_key=key)
623
            meta.meta_value = val
624
            meta.save()
625
        else:
626
            # Delete if val is empty
627
            if meta:
628
                meta.delete()
629
    
630
    def main_pithos(self, image_id, arg=''):
631
        backend = ImageBackend(self.user)
632
                
633
        try:
634
            image = backend.get_meta(image_id)
635
            if not image:
636
                print 'Image not found'
637
                return
638
            
639
            key, sep, val = arg.partition('=')
640
            if not sep:
641
                val = None
642
            
643
            properties = image.get('properties', {})
644
            
645
            if not key:
646
                print_dict(properties)
647
                return
648
            
649
            if val is None:
650
                if key in properties:
651
                    print_dict({key: properties[key]})
652
                return
653
            
654
            if val:
655
                properties[key] = val        
656
                params = {'properties': properties}
657
                backend.update(image_id, params)
658
        finally:
659
            backend.close()
660

    
661

    
662
# Flavor commands
663

    
664
class CreateFlavor(Command):
665
    group = 'flavor'
666
    name = 'create'
667
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
668
    description = 'create one or more flavors'
669
    
670
    def add_options(self, parser):
671
        disk_templates = ', '.join(t for t in settings.GANETI_DISK_TEMPLATES)
672
        parser.add_option('--disk-template',
673
            dest='disk_template',
674
            metavar='TEMPLATE',
675
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE,
676
            help='available disk templates: %s' % disk_templates)
677
    
678
    def main(self, cpu, ram, disk):
679
        cpus = cpu.split(',')
680
        rams = ram.split(',')
681
        disks = disk.split(',')
682
        
683
        flavors = []
684
        for cpu, ram, disk in product(cpus, rams, disks):
685
            try:
686
                flavors.append((int(cpu), int(ram), int(disk)))
687
            except ValueError:
688
                print 'Invalid values'
689
                return
690
        
691
        created = []
692
        
693
        for cpu, ram, disk in flavors:
694
            flavor = models.Flavor.objects.create(
695
                cpu=cpu,
696
                ram=ram,
697
                disk=disk,
698
                disk_template=self.disk_template)
699
            created.append(flavor)
700
        
701
        print_items(created, detail=True)
702

    
703

    
704
class DeleteFlavor(Command):
705
    group = 'flavor'
706
    name = 'delete'
707
    syntax = '<flavor id> [<flavor id>] [...]'
708
    description = 'delete one or more flavors'
709
    
710
    def main(self, *args):
711
        if not args:
712
            raise TypeError
713
        for flavor_id in args:
714
            flavor = models.Flavor.objects.get(id=int(flavor_id))
715
            flavor.deleted = True
716
            flavor.save()
717

    
718

    
719
class ListFlavors(Command):
720
    group = 'flavor'
721
    name = 'list'
722
    syntax = '[flavor id]'
723
    description = 'list images'
724
    
725
    def add_options(self, parser):
726
        parser.add_option('-a', action='store_true', dest='show_deleted',
727
                default=False, help='also list deleted flavors')
728
        parser.add_option('-l', action='store_true', dest='detail',
729
                        default=False, help='show detailed output')
730
    
731
    def main(self, flavor_id=None):
732
        if flavor_id:
733
            flavors = [models.Flavor.objects.get(id=flavor_id)]
734
        else:
735
            flavors = models.Flavor.objects.order_by('id')
736
            if not self.show_deleted:
737
                flavors = flavors.exclude(deleted=True)
738
        print_items(flavors, self.detail)
739

    
740

    
741
class ShowStats(Command):
742
    group = 'stats'
743
    name = None
744
    description = 'show statistics'
745

    
746
    def main(self):
747
        stats = {}
748
        stats['Users'] = models.SynnefoUser.objects.count()
749
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
750
        stats['Flavors'] = models.Flavor.objects.count()
751
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
752
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
753
        stats['Invitations'] = models.Invitations.objects.count()
754
        
755
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
756
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
757
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
758
        
759
        print_dict(stats)
760

    
761

    
762
class ListInvitations(Command):
763
    group = 'invitation'
764
    name = 'list'
765
    syntax = '[invitation id]'
766
    description = 'list invitations'
767
    
768
    def main(self, invitation_id=None):
769
        if invitation_id:
770
            invitations = [models.Invitations.objects.get(id=invitation_id)]
771
        else:
772
            invitations = models.Invitations.objects.order_by('id')
773
        print_items(invitations, detail=True, keys=('id',))
774

    
775

    
776
class ResendInviation(Command):
777
    group = 'invitation'
778
    name = 'resend'
779
    syntax = '<invitation id>'
780
    description = 'resend an invitation'
781

    
782
    def main(self, invitation_id):
783
        invitation = models.Invitations.objects.get(id=invitation_id)
784
        send_invitation(invitation)
785

    
786

    
787
def print_usage(exe, groups, group=None, shortcut=False):
788
    nop = Command(exe, [])
789
    nop.parser.print_help()
790
    if group:
791
        groups = {group: groups[group]}
792

    
793
    print
794
    print 'Commands:'
795
    
796
    for group, commands in sorted(groups.items()):
797
        for command, cls in sorted(commands.items()):
798
            if cls.hidden:
799
                continue
800
            name = '  %s %s' % (group, command or '')
801
            print '%s %s' % (name.ljust(22), cls.description)
802
        print
803

    
804

    
805
def main():
806
    groups = defaultdict(dict)
807
    module = sys.modules[__name__]
808
    for name, cls in inspect.getmembers(module, inspect.isclass):
809
        if not issubclass(cls, Command) or cls == Command:
810
            continue
811
        groups[cls.group][cls.name] = cls
812
    
813
    argv = list(sys.argv)
814
    exe = basename(argv.pop(0))
815
    prefix, sep, suffix = exe.partition('-')
816
    if sep and prefix == 'snf' and suffix in groups:
817
        # Allow shortcut aliases like snf-image, snf-server, etc
818
        group = suffix
819
    else:
820
        group = argv.pop(0) if argv else None
821
        if group in groups:
822
            exe = '%s %s' % (exe, group)
823
        else:
824
            exe = '%s <group>' % exe
825
            group = None
826
    
827
    command = argv.pop(0) if argv else None
828
    
829
    if group not in groups or command not in groups[group]:
830
        print_usage(exe, groups, group)
831
        sys.exit(1)
832
    
833
    cls = groups[group][command]
834
    cmd = cls(exe, argv)
835
    cmd.execute()
836

    
837

    
838
if __name__ == '__main__':
839
    dictConfig(settings.SNFADMIN_LOGGING)
840
    main()