Statistics
| Branch: | Tag: | Revision:

root / snf-tools / snf-admin @ 9e98ba3c

History | View | Annotate | Download (19.2 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
        servers = models.VirtualMachine.objects.order_by('id')
146
        if server_id:
147
            servers = servers.filter(id=server_id)
148
        if not self.show_deleted:
149
            servers = servers.filter(deleted=False)
150
        if self.uid:
151
            user = get_user(self.uid)
152
            if user:
153
                servers = servers.filter(owner=user)
154
            else:
155
                print 'Unknown user id'
156
                return
157
        
158
        print_items(servers, self.detail)
159

    
160

    
161
# User commands
162

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

    
188

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

    
211

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

    
224

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

    
242

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

    
289

    
290
# Image commands
291

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

    
309

    
310
class RegisterImage(Command):
311
    group = 'image'
312
    name = 'register'
313
    syntax = '<name> <backend id> <format>'
314
    description = 'register an image'
315
    
316
    def add_options(self, parser):
317
        parser.add_option('--public', action='store_true', dest='public',
318
                            default=False, help='make image public')
319
        parser.add_option('-u', dest='uid', metavar='UID',
320
                            help='assign image to user with id UID')
321
    
322
    def main(self, name, backend_id, format):
323
        formats = [x[0] for x in models.Image.FORMATS]
324
        if format not in formats:
325
            valid = ', '.join(formats)
326
            print 'Invalid format. Must be one of:', valid
327
            return
328
        
329
        user = None
330
        if self.uid:
331
            user = get_user(self.uid)
332
            if not user:
333
                print 'Unknown user id'
334
                return
335
        
336
        image = models.Image.objects.create(
337
            name=name,
338
            state='ACTIVE',
339
            owner=user,
340
            backend_id=backend_id,
341
            format=format,
342
            public=self.public)
343
        
344
        print_item(image)
345

    
346

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

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

    
407

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

    
452

    
453
# Flavor commands
454

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

    
481

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

    
495

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

    
513

    
514
class ShowStats(Command):
515
    group = 'stats'
516
    name = None
517
    description = 'show statistics'
518

    
519
    def main(self):
520
        stats = {}
521
        stats['Users'] = models.SynnefoUser.objects.count()
522
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
523
        stats['Flavors'] = models.Flavor.objects.count()
524
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
525
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
526
        stats['Invitations'] = models.Invitations.objects.count()
527
        
528
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
529
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
530
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
531
        
532
        print_dict(stats)
533

    
534

    
535
class ListInvitations(Command):
536
    group = 'invitation'
537
    name = 'list'
538
    syntax = '[invitation id]'
539
    description = 'list invitations'
540
    
541
    def main(self, invitation_id=None):
542
        if invitation_id:
543
            invitations = [models.Invitations.objects.get(id=invitation_id)]
544
        else:
545
            invitations = models.Invitations.objects.order_by('id')
546
        print_items(invitations, detail=True, keys=('id',))
547

    
548

    
549
class ResendInviation(Command):
550
    group = 'invitation'
551
    name = 'resend'
552
    syntax = '<invitation id>'
553
    description = 'resend an invitation'
554

    
555
    def main(self, invitation_id):
556
        invitation = models.Invitations.objects.get(id=invitation_id)
557
        send_invitation(invitation)
558

    
559

    
560
def print_usage(exe, groups, group=None, shortcut=False):
561
    nop = Command(exe, [])
562
    nop.parser.print_help()
563
    if group:
564
        groups = {group: groups[group]}
565

    
566
    print
567
    print 'Commands:'
568
    
569
    for group, commands in sorted(groups.items()):
570
        for command, cls in sorted(commands.items()):
571
            if cls.hidden:
572
                continue
573
            name = '  %s %s' % (group, command or '')
574
            print '%s %s' % (name.ljust(22), cls.description)
575
        print
576

    
577

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

    
610

    
611
if __name__ == '__main__':
612
    dictConfig(settings.SNFADMIN_LOGGING)
613
    main()