Statistics
| Branch: | Tag: | Revision:

root / snf-tools / snf-admin @ 79c33f4e

History | View | Annotate | Download (19.9 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('--public', action='store_true', dest='public',
321
                            default=False, help='make image public')
322
        parser.add_option('-u', dest='uid', metavar='UID',
323
                            help='assign image to user with id UID')
324
    
325
    def main(self, name, backend_id, format):
326
        formats = [x[0] for x in models.Image.FORMATS]
327
        if format not in formats:
328
            valid = ', '.join(formats)
329
            print 'Invalid format. Must be one of:', valid
330
            return
331
        
332
        user = None
333
        if self.uid:
334
            user = get_user(self.uid)
335
            if not user:
336
                print 'Unknown user id'
337
                return
338
        
339
        image = models.Image.objects.create(
340
            name=name,
341
            state='ACTIVE',
342
            owner=user,
343
            backend_id=backend_id,
344
            format=format,
345
            public=self.public)
346
        
347
        print_item(image)
348

    
349

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

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

    
410

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

    
455

    
456
# Flavor commands
457

    
458
class CreateFlavor(Command):
459
    group = 'flavor'
460
    name = 'create'
461
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
462
    description = 'create one or more flavors'
463
    
464
    def main(self, cpu, ram, disk):
465
        cpus = cpu.split(',')
466
        rams = ram.split(',')
467
        disks = disk.split(',')
468
        
469
        flavors = []
470
        for cpu, ram, disk in product(cpus, rams, disks):
471
            try:
472
                flavors.append((int(cpu), int(ram), int(disk)))
473
            except ValueError:
474
                print 'Invalid values'
475
                return
476
        
477
        created = []
478
        for cpu, ram, disk in flavors:
479
            flavor = models.Flavor.objects.create(cpu=cpu, ram=ram, disk=disk)
480
            created.append(flavor)
481
        
482
        print_items(created, detail=True)
483

    
484

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

    
499

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

    
521

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

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

    
542

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

    
556

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

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

    
567

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

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

    
585

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

    
618

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