Statistics
| Branch: | Tag: | Revision:

root / snf-tools / snf-admin @ 7cc3c7d9

History | View | Annotate | Download (19.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
        servers = models.VirtualMachine.objects.order_by('id')
153
        if server_id:
154
            servers = servers.filter(id=server_id)
155
        if not self.show_deleted:
156
            servers = servers.filter(deleted=False)
157
        if self.uid:
158
            user = get_user(self.uid)
159
            if user:
160
                servers = servers.filter(owner=user)
161
            else:
162
                print 'Unknown user id'
163
                return
164
        
165
        print_items(servers, self.detail)
166

    
167

    
168
# User commands
169

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

    
195

    
196
class DeleteUser(Command):
197
    group = 'user'
198
    name = 'delete'
199
    syntax = '<user id>'
200
    description = 'delete a user'
201
    hidden = True
202
    
203
    def add_options(self, parser):
204
        parser.add_option('--force', action='store_true', dest='force',
205
                        default=False, help='force deletion (use with care)')
206
    
207
    def main(self, user_id):
208
        if self.force:
209
            user = get_user(user_id)
210
            users.delete_user(user)
211
        else:
212
            print "WARNING: Deleting a user is a very destructive operation."
213
            print "Any objects with foreign keys pointing to this user will" \
214
                    " be deleted as well."
215
            print "Use --force to force deletion of this user."
216
            print "You have been warned!"
217

    
218

    
219
class InviteUser(Command):
220
    group = 'user'
221
    name = 'invite'
222
    syntax = '<inviter id> <invitee name> <invitee email>'
223
    description = 'invite a user'
224
    
225
    def main(self, inviter_id, name, email):
226
        name = name.decode('utf8')
227
        inviter = get_user(inviter_id)
228
        inv = add_invitation(inviter, name, email)
229
        send_invitation(inv)
230

    
231

    
232
class ListUsers(Command):
233
    group = 'user'
234
    name = 'list'
235
    syntax = '[user id]'
236
    description = 'list users'
237
    
238
    def add_options(self, parser):
239
        parser.add_option('-l', action='store_true', dest='detail',
240
                        default=False, help='show detailed output')
241
    
242
    def main(self, user_id=None):
243
        if user_id:
244
            users = [models.SynnefoUser.objects.get(id=user_id)]
245
        else:
246
            users = models.SynnefoUser.objects.order_by('id')
247
        print_items(users, self.detail, keys=('id', 'name', 'uniq'))
248

    
249

    
250
class ModifyUser(Command):
251
    group = 'user'
252
    name = 'modify'
253
    syntax = '<user id>'
254
    description = 'modify a user'
255
    
256
    def add_options(self, parser):
257
        types = ', '.join(x[0] for x in models.SynnefoUser.ACCOUNT_TYPE)
258
        
259
        parser.add_option('--credit', dest='credit', metavar='VALUE',
260
                            help='set user credits')
261
        parser.add_option('--invitations', dest='invitations',
262
                            metavar='VALUE', help='set max invitations')
263
        parser.add_option('--realname', dest='realname', metavar='NAME',
264
                            help='set real name of user')
265
        parser.add_option('--type', dest='type', metavar='TYPE',
266
                            help='set user type (%s)' % types)
267
        parser.add_option('--uniq', dest='uniq', metavar='ID',
268
                            help='set external unique ID')
269
        parser.add_option('--username', dest='username', metavar='NAME',
270
                            help='set username')
271
    
272
    def main(self, user_id):
273
        user = get_user(user_id)
274
        
275
        if self.credit:
276
            user.credit = self.credit
277
        if self.invitations:
278
            user.max_invitations = self.invitations
279
        if self.realname:
280
            user.realname = self.realname
281
        if self.type:
282
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]
283
            if self.type not in allowed:
284
                valid = ', '.join(allowed)
285
                print 'Invalid type. Must be one of:', valid
286
                return
287
            user.type = self.type
288
        if self.uniq:
289
            user.uniq = self.uniq
290
        if self.username:
291
            user.name = self.username
292
        
293
        user.save()
294
        print_item(user)
295

    
296

    
297
# Image commands
298

    
299
class ListImages(Command):
300
    group = 'image'
301
    name = 'list'
302
    syntax = '[image id]'
303
    description = 'list images'
304
    
305
    def add_options(self, parser):
306
        parser.add_option('-l', action='store_true', dest='detail',
307
                        default=False, help='show detailed output')
308
    
309
    def main(self, image_id=None):
310
        if image_id:
311
            images = [models.Image.objects.get(id=image_id)]
312
        else:
313
            images = models.Image.objects.order_by('id')
314
        print_items(images, self.detail)
315

    
316

    
317
class RegisterImage(Command):
318
    group = 'image'
319
    name = 'register'
320
    syntax = '<name> <backend id> <format>'
321
    description = 'register an image'
322
    
323
    def add_options(self, parser):
324
        parser.add_option('--public', action='store_true', dest='public',
325
                            default=False, help='make image public')
326
        parser.add_option('-u', dest='uid', metavar='UID',
327
                            help='assign image to user with id UID')
328
    
329
    def main(self, name, backend_id, format):
330
        formats = [x[0] for x in models.Image.FORMATS]
331
        if format not in formats:
332
            valid = ', '.join(formats)
333
            print 'Invalid format. Must be one of:', valid
334
            return
335
        
336
        user = None
337
        if self.uid:
338
            user = get_user(self.uid)
339
            if not user:
340
                print 'Unknown user id'
341
                return
342
        
343
        image = models.Image.objects.create(
344
            name=name,
345
            state='ACTIVE',
346
            owner=user,
347
            backend_id=backend_id,
348
            format=format,
349
            public=self.public)
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 main(self, cpu, ram, disk):
469
        cpus = cpu.split(',')
470
        rams = ram.split(',')
471
        disks = disk.split(',')
472
        
473
        flavors = []
474
        for cpu, ram, disk in product(cpus, rams, disks):
475
            try:
476
                flavors.append((int(cpu), int(ram), int(disk)))
477
            except ValueError:
478
                print 'Invalid values'
479
                return
480
        
481
        created = []
482
        for cpu, ram, disk in flavors:
483
            flavor = models.Flavor.objects.create(cpu=cpu, ram=ram, disk=disk)
484
            created.append(flavor)
485
        
486
        print_items(created, detail=True)
487

    
488

    
489
class DeleteFlavor(Command):
490
    group = 'flavor'
491
    name = 'delete'
492
    syntax = '<flavor id> [<flavor id>] [...]'
493
    description = 'delete one or more flavors'
494
    
495
    def main(self, *args):
496
        if not args:
497
            raise TypeError
498
        for flavor_id in args:
499
            flavor = models.Flavor.objects.get(id=int(flavor_id))
500
            flavor.delete()
501

    
502

    
503
class ListFlavors(Command):
504
    group = 'flavor'
505
    name = 'list'
506
    syntax = '[flavor id]'
507
    description = 'list images'
508
    
509
    def add_options(self, parser):
510
        parser.add_option('-l', action='store_true', dest='detail',
511
                        default=False, help='show detailed output')
512
    
513
    def main(self, flavor_id=None):
514
        if flavor_id:
515
            flavors = [models.Flavor.objects.get(id=flavor_id)]
516
        else:
517
            flavors = models.Flavor.objects.order_by('id')
518
        print_items(flavors, self.detail)
519

    
520

    
521
class ShowStats(Command):
522
    group = 'stats'
523
    name = None
524
    description = 'show statistics'
525

    
526
    def main(self):
527
        stats = {}
528
        stats['Users'] = models.SynnefoUser.objects.count()
529
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
530
        stats['Flavors'] = models.Flavor.objects.count()
531
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
532
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
533
        stats['Invitations'] = models.Invitations.objects.count()
534
        
535
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
536
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
537
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
538
        
539
        print_dict(stats)
540

    
541

    
542
class ListInvitations(Command):
543
    group = 'invitation'
544
    name = 'list'
545
    syntax = '[invitation id]'
546
    description = 'list invitations'
547
    
548
    def main(self, invitation_id=None):
549
        if invitation_id:
550
            invitations = [models.Invitations.objects.get(id=invitation_id)]
551
        else:
552
            invitations = models.Invitations.objects.order_by('id')
553
        print_items(invitations, detail=True, keys=('id',))
554

    
555

    
556
class ResendInviation(Command):
557
    group = 'invitation'
558
    name = 'resend'
559
    syntax = '<invitation id>'
560
    description = 'resend an invitation'
561

    
562
    def main(self, invitation_id):
563
        invitation = models.Invitations.objects.get(id=invitation_id)
564
        send_invitation(invitation)
565

    
566

    
567
def print_usage(exe, groups, group=None, shortcut=False):
568
    nop = Command(exe, [])
569
    nop.parser.print_help()
570
    if group:
571
        groups = {group: groups[group]}
572

    
573
    print
574
    print 'Commands:'
575
    
576
    for group, commands in sorted(groups.items()):
577
        for command, cls in sorted(commands.items()):
578
            if cls.hidden:
579
                continue
580
            name = '  %s %s' % (group, command or '')
581
            print '%s %s' % (name.ljust(22), cls.description)
582
        print
583

    
584

    
585
def main():
586
    groups = defaultdict(dict)
587
    module = sys.modules[__name__]
588
    for name, cls in inspect.getmembers(module, inspect.isclass):
589
        if not issubclass(cls, Command) or cls == Command:
590
            continue
591
        groups[cls.group][cls.name] = cls
592
    
593
    argv = list(sys.argv)
594
    exe = basename(argv.pop(0))
595
    prefix, sep, suffix = exe.partition('-')
596
    if sep and prefix == 'snf' and suffix in groups:
597
        # Allow shortcut aliases like snf-image, snf-server, etc
598
        group = suffix
599
    else:
600
        group = argv.pop(0) if argv else None
601
        if group in groups:
602
            exe = '%s %s' % (exe, group)
603
        else:
604
            exe = '%s <group>' % exe
605
            group = None
606
    
607
    command = argv.pop(0) if argv else None
608
    
609
    if group not in groups or command not in groups[group]:
610
        print_usage(exe, groups, group)
611
        sys.exit(1)
612
    
613
    cls = groups[group][command]
614
    cmd = cls(exe, argv)
615
    cmd.execute()
616

    
617

    
618
if __name__ == '__main__':
619
    main()