Statistics
| Branch: | Tag: | Revision:

root / snf-tools / snf-admin @ ead18dbc

History | View | Annotate | Download (18.6 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
        if type not in [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]:
187
            print 'Invalid type'
188
            return
189
        
190
        user = users._register_user(realname, username, email, type)
191
        print_item(user)
192

    
193

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

    
216

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

    
229

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

    
247

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

    
291

    
292
# Image commands
293

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

    
311

    
312
class RegisterImage(Command):
313
    group = 'image'
314
    name = 'register'
315
    syntax = '<name> <backend id>'
316
    description = 'register an image'
317
    
318
    def add_options(self, parser):
319
        parser.add_option('--public', action='store_true', dest='public',
320
                            default=False, help='make image public')
321
        parser.add_option('-u', dest='uid', metavar='UID',
322
                            help='assign image to user with id UID')
323
    
324
    def main(self, name, backend_id):
325
        user = None
326
        if self.uid:
327
            user = get_user(self.uid)
328
            if not user:
329
                print 'Unknown user id'
330
                return
331
        
332
        image = models.Image.objects.create(
333
            name=name,
334
            state='ACTIVE',
335
            owner=user,
336
            backend_id=backend_id,
337
            public=self.public)
338
        
339
        print_item(image)
340

    
341

    
342
class ModifyImage(Command):
343
    group = 'image'
344
    name = 'modify'
345
    syntax = '<image id>'
346
    description = 'modify an image'
347
    
348
    def add_options(self, parser):
349
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
350
                            help='set image backend id')
351
        parser.add_option('-f', dest='format', metavar='FORMAT',
352
                            help='set image format')
353
        parser.add_option('-n', dest='name', metavar='NAME',
354
                            help='set image name')
355
        parser.add_option('--public', action='store_true', dest='public',
356
                            default=False, help='make image public')
357
        parser.add_option('--nopublic', action='store_true', dest='private',
358
                            default=False, help='make image private')
359
        parser.add_option('-s', dest='state', metavar='STATE',
360
                            default=False, help='set image state')
361
        parser.add_option('-u', dest='uid', metavar='UID',
362
                            help='assign image to user with id UID')
363
    
364
    def main(self, image_id):
365
        try:
366
            image = models.Image.objects.get(id=image_id)
367
        except:
368
            print 'Image not found'
369
            return
370
        
371
        if self.backend_id:
372
            image.backend_id = self.backend_id
373
        if self.format:
374
            allowed = [x[0] for x in models.Image.FORMATS]
375
            if self.format not in allowed:
376
                print 'Invalid format'
377
                return
378
            image.format = self.format
379
        if self.name:
380
            image.name = self.name
381
        if self.public:
382
            image.public = True
383
        if self.private:
384
            image.public = False
385
        if self.state:
386
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
387
            if self.state not in allowed:
388
                print 'Invalid state'
389
                return
390
            image.state = self.state
391
        if self.uid:
392
            image.owner = get_user(self.uid)
393
        
394
        image.save()
395
        print_item(image)
396

    
397

    
398
class ModifyImageMeta(Command):
399
    group = 'image'
400
    name = 'meta'
401
    syntax = '<image id> [key[=val]]'
402
    description = 'get and manipulate image metadata'
403
    
404
    def main(self, image_id, arg=''):
405
        try:
406
            image = models.Image.objects.get(id=image_id)
407
        except:
408
            print 'Image not found'
409
            return
410
        
411
        key, sep, val = arg.partition('=')
412
        if not sep:
413
            val = None
414
        
415
        if not key:
416
            metadata = {}
417
            for meta in image.imagemetadata_set.order_by('meta_key'):
418
                metadata[meta.meta_key] = meta.meta_value
419
            print_dict(metadata)
420
            return
421
        
422
        try:
423
            meta = image.imagemetadata_set.get(meta_key=key)
424
        except models.ImageMetadata.DoesNotExist:
425
            meta = None
426
        
427
        if val is None:
428
            if meta:
429
                print_dict({key: meta.meta_value})
430
            return
431
        
432
        if val:
433
            if not meta:
434
                meta = image.imagemetadata_set.create(meta_key=key)
435
            meta.meta_value = val
436
            meta.save()
437
        else:
438
            # Delete if val is empty
439
            if meta:
440
                meta.delete()
441

    
442

    
443
# Flavor commands
444

    
445
class CreateFlavor(Command):
446
    group = 'flavor'
447
    name = 'create'
448
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
449
    description = 'create one or more flavors'
450
    
451
    def main(self, cpu, ram, disk):
452
        cpus = cpu.split(',')
453
        rams = ram.split(',')
454
        disks = disk.split(',')
455
        
456
        flavors = []
457
        for cpu, ram, disk in product(cpus, rams, disks):
458
            try:
459
                flavors.append((int(cpu), int(ram), int(disk)))
460
            except ValueError:
461
                print 'Invalid values'
462
                return
463
        
464
        created = []
465
        for cpu, ram, disk in flavors:
466
            flavor = models.Flavor.objects.create(cpu=cpu, ram=ram, disk=disk)
467
            created.append(flavor)
468
        
469
        print_items(created, detail=True)
470

    
471

    
472
class DeleteFlavor(Command):
473
    group = 'flavor'
474
    name = 'delete'
475
    syntax = '<flavor id> [<flavor id>] [...]'
476
    description = 'delete one or more flavors'
477
    
478
    def main(self, *args):
479
        if not args:
480
            raise TypeError
481
        for flavor_id in args:
482
            flavor = models.Flavor.objects.get(id=int(flavor_id))
483
            flavor.delete()
484

    
485

    
486
class ListFlavors(Command):
487
    group = 'flavor'
488
    name = 'list'
489
    syntax = '[flavor id]'
490
    description = 'list images'
491
    
492
    def add_options(self, parser):
493
        parser.add_option('-l', action='store_true', dest='detail',
494
                        default=False, help='show detailed output')
495
    
496
    def main(self, flavor_id=None):
497
        if flavor_id:
498
            flavors = [models.Flavor.objects.get(id=flavor_id)]
499
        else:
500
            flavors = models.Flavor.objects.order_by('id')
501
        print_items(flavors, self.detail)
502

    
503

    
504
class ShowStats(Command):
505
    group = 'stats'
506
    name = None
507
    description = 'show statistics'
508

    
509
    def main(self):
510
        stats = {}
511
        stats['Users'] = models.SynnefoUser.objects.count()
512
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
513
        stats['Flavors'] = models.Flavor.objects.count()
514
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
515
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
516
        stats['Invitations'] = models.Invitations.objects.count()
517
        
518
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
519
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
520
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
521
        
522
        print_dict(stats)
523

    
524

    
525
class ListInvitations(Command):
526
    group = 'invitation'
527
    name = 'list'
528
    syntax = '[invitation id]'
529
    description = 'list invitations'
530
    
531
    def main(self, invitation_id=None):
532
        if invitation_id:
533
            invitations = [models.Invitations.objects.get(id=invitation_id)]
534
        else:
535
            invitations = models.Invitations.objects.order_by('id')
536
        print_items(invitations, detail=True, keys=('id',))
537

    
538

    
539
class ResendInviation(Command):
540
    group = 'invitation'
541
    name = 'resend'
542
    syntax = '<invitation id>'
543
    description = 'resend an invitation'
544

    
545
    def main(self, invitation_id):
546
        invitation = models.Invitations.objects.get(id=invitation_id)
547
        send_invitation(invitation)
548

    
549

    
550
def print_usage(exe, groups, group=None, shortcut=False):
551
    nop = Command(exe, [])
552
    nop.parser.print_help()
553
    if group:
554
        groups = {group: groups[group]}
555

    
556
    print
557
    print 'Commands:'
558
    
559
    for group, commands in sorted(groups.items()):
560
        for command, cls in sorted(commands.items()):
561
            if cls.hidden:
562
                continue
563
            name = '  %s %s' % (group, command or '')
564
            print '%s %s' % (name.ljust(22), cls.description)
565
        print
566

    
567

    
568
def main():
569
    groups = defaultdict(dict)
570
    module = sys.modules[__name__]
571
    for name, cls in inspect.getmembers(module, inspect.isclass):
572
        if not issubclass(cls, Command) or cls == Command:
573
            continue
574
        groups[cls.group][cls.name] = cls
575
    
576
    argv = list(sys.argv)
577
    exe = basename(argv.pop(0))
578
    prefix, sep, suffix = exe.partition('-')
579
    if sep and prefix == 'snf' and suffix in groups:
580
        # Allow shortcut aliases like snf-image, snf-server, etc
581
        group = suffix
582
    else:
583
        group = argv.pop(0) if argv else None
584
        if group in groups:
585
            exe = '%s %s' % (exe, group)
586
        else:
587
            exe = '%s <group>' % exe
588
            group = None
589
    
590
    command = argv.pop(0) if argv else None
591
    
592
    if group not in groups or command not in groups[group]:
593
        print_usage(exe, groups, group)
594
        sys.exit(1)
595
    
596
    cls = groups[group][command]
597
    cmd = cls(exe, argv)
598
    cmd.execute()
599

    
600

    
601
if __name__ == '__main__':
602
    main()