Statistics
| Branch: | Tag: | Revision:

root / synnefo / tools / admin.py @ ed02e7e1

History | View | Annotate | Download (20.7 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.util.dictconfig import dictConfig
56

    
57

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

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

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

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

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

    
93

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

    
127

    
128
# Server commands
129

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

    
161

    
162
# User commands
163

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

    
189

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

    
202

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

    
224

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

    
281

    
282
# Image commands
283

    
284
class ListImages(Command):
285
    group = 'image'
286
    name = 'list'
287
    syntax = '[image id]'
288
    description = 'list images'
289
    
290
    def add_options(self, parser):
291
        parser.add_option('-a', action='store_true', dest='show_deleted',
292
                        default=False, help='also list deleted images')
293
        parser.add_option('-l', action='store_true', dest='detail',
294
                        default=False, help='show detailed output')
295
    
296
    def main(self, image_id=None):
297
        if image_id:
298
            images = [models.Image.objects.get(id=image_id)]
299
        else:
300
            images = models.Image.objects.order_by('id')
301
            if not self.show_deleted:
302
                images = images.exclude(state='DELETED')
303
        print_items(images, self.detail)
304

    
305

    
306
class RegisterImage(Command):
307
    group = 'image'
308
    name = 'register'
309
    syntax = '<name> <backend id> <format>'
310
    description = 'register an image'
311
    
312
    def add_options(self, parser):
313
        parser.add_option('--meta', dest='meta', action='append',
314
                            metavar='KEY=VAL',
315
                            help='add metadata (can be used multiple times)')
316
        parser.add_option('--public', action='store_true', dest='public',
317
                            default=False, help='make image public')
318
        parser.add_option('-u', dest='uid', metavar='UID',
319
                            help='assign image to user with id UID')
320
    
321
    def main(self, name, backend_id, format):
322
        formats = [x[0] for x in models.Image.FORMATS]
323
        if format not in formats:
324
            valid = ', '.join(formats)
325
            print 'Invalid format. Must be one of:', valid
326
            return
327
        
328
        user = None
329
        if self.uid:
330
            user = get_user(self.uid)
331
            if not user:
332
                print 'Unknown user id'
333
                return
334
        
335
        image = models.Image.objects.create(
336
            name=name,
337
            state='ACTIVE',
338
            owner=user,
339
            backend_id=backend_id,
340
            format=format,
341
            public=self.public)
342
        
343
        if self.meta:
344
            for m in self.meta:
345
                key, sep, val = m.partition('=')
346
                if key and val:
347
                    image.metadata.create(meta_key=key, meta_value=val)
348
                else:
349
                    print 'WARNING: Ignoring meta', m
350
        
351
        print_item(image)
352

    
353

    
354
class ModifyImage(Command):
355
    group = 'image'
356
    name = 'modify'
357
    syntax = '<image id>'
358
    description = 'modify an image'
359
    
360
    def add_options(self, parser):
361
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
362
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
363

    
364
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
365
                            help='set image backend id')
366
        parser.add_option('-f', dest='format', metavar='FORMAT',
367
                            help='set image format (%s)' % formats)
368
        parser.add_option('-n', dest='name', metavar='NAME',
369
                            help='set image name')
370
        parser.add_option('--public', action='store_true', dest='public',
371
                            default=False, help='make image public')
372
        parser.add_option('--nopublic', action='store_true', dest='private',
373
                            default=False, help='make image private')
374
        parser.add_option('-s', dest='state', metavar='STATE', default=False,
375
                            help='set image state (%s)' % states)
376
        parser.add_option('-u', dest='uid', metavar='UID',
377
                            help='assign image to user with id UID')
378
    
379
    def main(self, image_id):
380
        try:
381
            image = models.Image.objects.get(id=image_id)
382
        except:
383
            print 'Image not found'
384
            return
385
        
386
        if self.backend_id:
387
            image.backend_id = self.backend_id
388
        if self.format:
389
            allowed = [x[0] for x in models.Image.FORMATS]
390
            if self.format not in allowed:
391
                valid = ', '.join(allowed)
392
                print 'Invalid format. Must be one of:', valid
393
                return
394
            image.format = self.format
395
        if self.name:
396
            image.name = self.name
397
        if self.public:
398
            image.public = True
399
        if self.private:
400
            image.public = False
401
        if self.state:
402
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
403
            if self.state not in allowed:
404
                valid = ', '.join(allowed)
405
                print 'Invalid state. Must be one of:', valid
406
                return
407
            image.state = self.state
408
        if self.uid:
409
            image.owner = get_user(self.uid)
410
        
411
        image.save()
412
        print_item(image)
413

    
414

    
415
class ModifyImageMeta(Command):
416
    group = 'image'
417
    name = 'meta'
418
    syntax = '<image id> [key[=val]]'
419
    description = 'get and manipulate image metadata'
420
    
421
    def main(self, image_id, arg=''):
422
        try:
423
            image = models.Image.objects.get(id=image_id)
424
        except:
425
            print 'Image not found'
426
            return
427
        
428
        key, sep, val = arg.partition('=')
429
        if not sep:
430
            val = None
431
        
432
        if not key:
433
            metadata = {}
434
            for meta in image.metadata.order_by('meta_key'):
435
                metadata[meta.meta_key] = meta.meta_value
436
            print_dict(metadata)
437
            return
438
        
439
        try:
440
            meta = image.metadata.get(meta_key=key)
441
        except models.ImageMetadata.DoesNotExist:
442
            meta = None
443
        
444
        if val is None:
445
            if meta:
446
                print_dict({key: meta.meta_value})
447
            return
448
        
449
        if val:
450
            if not meta:
451
                meta = image.metadata.create(meta_key=key)
452
            meta.meta_value = val
453
            meta.save()
454
        else:
455
            # Delete if val is empty
456
            if meta:
457
                meta.delete()
458

    
459

    
460
# Flavor commands
461

    
462
class CreateFlavor(Command):
463
    group = 'flavor'
464
    name = 'create'
465
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
466
    description = 'create one or more flavors'
467
    
468
    def add_options(self, parser):
469
        disk_templates = ', '.join(t for t in settings.GANETI_DISK_TEMPLATES)
470
        parser.add_option('--disk-template',
471
            dest='disk_template',
472
            metavar='TEMPLATE',
473
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE,
474
            help='available disk templates: %s' % disk_templates)
475
    
476
    def main(self, cpu, ram, disk):
477
        cpus = cpu.split(',')
478
        rams = ram.split(',')
479
        disks = disk.split(',')
480
        
481
        flavors = []
482
        for cpu, ram, disk in product(cpus, rams, disks):
483
            try:
484
                flavors.append((int(cpu), int(ram), int(disk)))
485
            except ValueError:
486
                print 'Invalid values'
487
                return
488
        
489
        created = []
490
        
491
        for cpu, ram, disk in flavors:
492
            flavor = models.Flavor.objects.create(
493
                cpu=cpu,
494
                ram=ram,
495
                disk=disk,
496
                disk_template=self.disk_template)
497
            created.append(flavor)
498
        
499
        print_items(created, detail=True)
500

    
501

    
502
class DeleteFlavor(Command):
503
    group = 'flavor'
504
    name = 'delete'
505
    syntax = '<flavor id> [<flavor id>] [...]'
506
    description = 'delete one or more flavors'
507
    
508
    def main(self, *args):
509
        if not args:
510
            raise TypeError
511
        for flavor_id in args:
512
            flavor = models.Flavor.objects.get(id=int(flavor_id))
513
            flavor.deleted = True
514
            flavor.save()
515

    
516

    
517
class ListFlavors(Command):
518
    group = 'flavor'
519
    name = 'list'
520
    syntax = '[flavor id]'
521
    description = 'list images'
522
    
523
    def add_options(self, parser):
524
        parser.add_option('-a', action='store_true', dest='show_deleted',
525
                default=False, help='also list deleted flavors')
526
        parser.add_option('-l', action='store_true', dest='detail',
527
                        default=False, help='show detailed output')
528
    
529
    def main(self, flavor_id=None):
530
        if flavor_id:
531
            flavors = [models.Flavor.objects.get(id=flavor_id)]
532
        else:
533
            flavors = models.Flavor.objects.order_by('id')
534
            if not self.show_deleted:
535
                flavors = flavors.exclude(deleted=True)
536
        print_items(flavors, self.detail)
537

    
538

    
539
class ShowStats(Command):
540
    group = 'stats'
541
    name = None
542
    description = 'show statistics'
543

    
544
    def main(self):
545
        stats = {}
546
        stats['Users'] = models.SynnefoUser.objects.count()
547
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
548
        stats['Flavors'] = models.Flavor.objects.count()
549
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
550
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
551
        stats['Invitations'] = models.Invitations.objects.count()
552
        
553
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
554
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
555
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
556
        
557
        print_dict(stats)
558

    
559

    
560
class ListInvitations(Command):
561
    group = 'invitation'
562
    name = 'list'
563
    syntax = '[invitation id]'
564
    description = 'list invitations'
565
    
566
    def main(self, invitation_id=None):
567
        if invitation_id:
568
            invitations = [models.Invitations.objects.get(id=invitation_id)]
569
        else:
570
            invitations = models.Invitations.objects.order_by('id')
571
        print_items(invitations, detail=True, keys=('id',))
572

    
573

    
574
class ResendInviation(Command):
575
    group = 'invitation'
576
    name = 'resend'
577
    syntax = '<invitation id>'
578
    description = 'resend an invitation'
579

    
580
    def main(self, invitation_id):
581
        invitation = models.Invitations.objects.get(id=invitation_id)
582
        send_invitation(invitation)
583

    
584

    
585
def print_usage(exe, groups, group=None, shortcut=False):
586
    nop = Command(exe, [])
587
    nop.parser.print_help()
588
    if group:
589
        groups = {group: groups[group]}
590

    
591
    print
592
    print 'Commands:'
593
    
594
    for group, commands in sorted(groups.items()):
595
        for command, cls in sorted(commands.items()):
596
            if cls.hidden:
597
                continue
598
            name = '  %s %s' % (group, command or '')
599
            print '%s %s' % (name.ljust(22), cls.description)
600
        print
601

    
602

    
603
def main():
604
    groups = defaultdict(dict)
605
    module = sys.modules[__name__]
606
    for name, cls in inspect.getmembers(module, inspect.isclass):
607
        if not issubclass(cls, Command) or cls == Command:
608
            continue
609
        groups[cls.group][cls.name] = cls
610
    
611
    argv = list(sys.argv)
612
    exe = basename(argv.pop(0))
613
    prefix, sep, suffix = exe.partition('-')
614
    if sep and prefix == 'snf' and suffix in groups:
615
        # Allow shortcut aliases like snf-image, snf-server, etc
616
        group = suffix
617
    else:
618
        group = argv.pop(0) if argv else None
619
        if group in groups:
620
            exe = '%s %s' % (exe, group)
621
        else:
622
            exe = '%s <group>' % exe
623
            group = None
624
    
625
    command = argv.pop(0) if argv else None
626
    
627
    if group not in groups or command not in groups[group]:
628
        print_usage(exe, groups, group)
629
        sys.exit(1)
630
    
631
    cls = groups[group][command]
632
    cmd = cls(exe, argv)
633
    cmd.execute()
634

    
635

    
636
if __name__ == '__main__':
637
    dictConfig(settings.SNFADMIN_LOGGING)
638
    main()