Statistics
| Branch: | Tag: | Revision:

root / snf-tools / snf-admin @ 330c7d80

History | View | Annotate | Download (20.4 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 logging
46
import sys
47

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

    
53
from synnefo.db import models
54
from synnefo.invitations.invitations import add_invitation, send_invitation
55
from synnefo.logic import backend, users
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
        ch = logging.StreamHandler()
117
        ch.setFormatter(logging.Formatter('%(message)s'))
118
        logger = logging.getLogger()
119
        logger.addHandler(ch)
120
        level = logging.WARNING
121
        logger.setLevel(level)
122
        
123
        self.parser = parser
124
    
125
    def add_options(self, parser):
126
        pass
127
    
128
    def execute(self):
129
        try:
130
            self.main(*self.args)
131
        except TypeError:
132
            self.parser.print_help()
133

    
134

    
135
# Server commands
136

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

    
168

    
169
# User commands
170

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

    
196

    
197
class InviteUser(Command):
198
    group = 'user'
199
    name = 'invite'
200
    syntax = '<inviter id> <invitee name> <invitee email>'
201
    description = 'invite a user'
202
    
203
    def main(self, inviter_id, name, email):
204
        name = name.decode('utf8')
205
        inviter = get_user(inviter_id)
206
        inv = add_invitation(inviter, name, email)
207
        send_invitation(inv)
208

    
209

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

    
231

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

    
288

    
289
# Image commands
290

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

    
312

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

    
360

    
361
class ModifyImage(Command):
362
    group = 'image'
363
    name = 'modify'
364
    syntax = '<image id>'
365
    description = 'modify an image'
366
    
367
    def add_options(self, parser):
368
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
369
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
370

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

    
421

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

    
466

    
467
# Flavor commands
468

    
469
class CreateFlavor(Command):
470
    group = 'flavor'
471
    name = 'create'
472
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
473
    description = 'create one or more flavors'
474
    
475
    def main(self, cpu, ram, disk):
476
        cpus = cpu.split(',')
477
        rams = ram.split(',')
478
        disks = disk.split(',')
479
        
480
        flavors = []
481
        for cpu, ram, disk in product(cpus, rams, disks):
482
            try:
483
                flavors.append((int(cpu), int(ram), int(disk)))
484
            except ValueError:
485
                print 'Invalid values'
486
                return
487
        
488
        created = []
489
        for cpu, ram, disk in flavors:
490
            flavor = models.Flavor.objects.create(cpu=cpu, ram=ram, disk=disk)
491
            created.append(flavor)
492
        
493
        print_items(created, detail=True)
494

    
495

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

    
510

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

    
532

    
533
class ShowStats(Command):
534
    group = 'stats'
535
    name = None
536
    description = 'show statistics'
537

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

    
553

    
554
class ListInvitations(Command):
555
    group = 'invitation'
556
    name = 'list'
557
    syntax = '[invitation id]'
558
    description = 'list invitations'
559
    
560
    def main(self, invitation_id=None):
561
        if invitation_id:
562
            invitations = [models.Invitations.objects.get(id=invitation_id)]
563
        else:
564
            invitations = models.Invitations.objects.order_by('id')
565
        print_items(invitations, detail=True, keys=('id',))
566

    
567

    
568
class ResendInviation(Command):
569
    group = 'invitation'
570
    name = 'resend'
571
    syntax = '<invitation id>'
572
    description = 'resend an invitation'
573

    
574
    def main(self, invitation_id):
575
        invitation = models.Invitations.objects.get(id=invitation_id)
576
        send_invitation(invitation)
577

    
578

    
579
def print_usage(exe, groups, group=None, shortcut=False):
580
    nop = Command(exe, [])
581
    nop.parser.print_help()
582
    if group:
583
        groups = {group: groups[group]}
584

    
585
    print
586
    print 'Commands:'
587
    
588
    for group, commands in sorted(groups.items()):
589
        for command, cls in sorted(commands.items()):
590
            if cls.hidden:
591
                continue
592
            name = '  %s %s' % (group, command or '')
593
            print '%s %s' % (name.ljust(22), cls.description)
594
        print
595

    
596

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

    
629

    
630
if __name__ == '__main__':
631
    main()