Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / tools / admin.py @ 6ef51e9f

History | View | Annotate | Download (21.7 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.logic import backend, users
54
from synnefo.plankton.backend import ImageBackend
55
from synnefo.util.dictconfig import dictConfig
56

    
57

    
58
def print_dict(d, exclude=()):
59
    if not d:
60
        return
61
    margin = max(len(key) for key in d) + 1
62

    
63
    for key, val in sorted(d.items()):
64
        if key in exclude or key.startswith('_'):
65
            continue
66
        print '%s: %s' % (key.rjust(margin), val)
67

    
68
def print_item(item):
69
    name = getattr(item, 'name', '')
70
    print '%d %s' % (item.id, name)
71
    print_dict(item.__dict__, exclude=('id', 'name'))
72

    
73
def print_items(items, detail=False, keys=None):
74
    keys = keys or ('id', 'name')
75
    for item in items:
76
        for key in keys:
77
            print getattr(item, key),
78
        print
79
        
80
        if detail:
81
            print_dict(item.__dict__, exclude=keys)
82
            print
83

    
84

    
85
class Command(object):
86
    group = '<group>'
87
    name = '<command>'
88
    syntax = ''
89
    description = ''
90
    hidden = False
91
    
92
    def __init__(self, exe, argv):
93
        parser = OptionParser()
94
        syntax = '%s [options]' % self.syntax if self.syntax else '[options]'
95
        parser.usage = '%s %s %s' % (exe, self.name, syntax)
96
        parser.description = self.description
97
        self.add_options(parser)
98
        options, self.args = parser.parse_args(argv)
99
        
100
        # Add options to self
101
        for opt in parser.option_list:
102
            key = opt.dest
103
            if key:
104
                val = getattr(options, key)
105
                setattr(self, key, val)
106
        
107
        self.parser = parser
108
    
109
    def add_options(self, parser):
110
        pass
111
    
112
    def execute(self):
113
        try:
114
            self.main(*self.args)
115
        except TypeError:
116
            self.parser.print_help()
117

    
118

    
119
# Server commands
120

    
121
class ListServers(Command):
122
    group = 'server'
123
    name = 'list'
124
    syntax = '[server id]'
125
    description = 'list servers'
126
    
127
    def add_options(self, parser):
128
        parser.add_option('-a', action='store_true', dest='show_deleted',
129
                        default=False, help='also list deleted servers')
130
        parser.add_option('-l', action='store_true', dest='detail',
131
                        default=False, help='show detailed output')
132
        parser.add_option('-u', dest='uid', metavar='UID',
133
                            help='show servers of user with id UID')
134
    
135
    def main(self, server_id=None):
136
        if server_id:
137
            servers = [models.VirtualMachine.objects.get(id=server_id)]
138
        else:
139
            servers = models.VirtualMachine.objects.order_by('id')
140
            if not self.show_deleted:
141
                servers = servers.exclude(deleted=True)
142
            if self.uid:
143
                servers = servers.filter(userid=self.uid)
144
        
145
        print_items(servers, self.detail)
146

    
147

    
148
# Image commands
149

    
150
class ListImages(Command):
151
    group = 'image'
152
    name = 'list'
153
    syntax = '[image id]'
154
    description = 'list images'
155
    
156
    def add_options(self, parser):
157
        parser.add_option('-a', action='store_true', dest='show_deleted',
158
                default=False, help='also list deleted images')
159
        parser.add_option('-l', action='store_true', dest='detail',
160
                default=False, help='show detailed output')
161
        parser.add_option('-p', action='store_true', dest='pithos',
162
                default=False, help='show images stored in Pithos')
163
        parser.add_option('--user', dest='user',
164
                default=settings.SYSTEM_IMAGES_OWNER,
165
                metavar='USER',
166
                help='list images accessible to USER')
167
    
168
    def main(self, image_id=None):
169
        if self.pithos:
170
            return self.main_pithos(image_id)
171
        
172
        if image_id:
173
            images = [models.Image.objects.get(id=image_id)]
174
        else:
175
            images = models.Image.objects.order_by('id')
176
            if not self.show_deleted:
177
                images = images.exclude(state='DELETED')
178
        print_items(images, self.detail)
179
    
180
    def main_pithos(self, image_id=None):
181
        backend = ImageBackend(self.user)
182
        if image_id:
183
            images = [backend.get_image(image_id)]
184
        else:
185
            images = backend.iter_shared()
186
        
187
        for image in images:
188
            print image['id'], image['name']
189
            if self.detail:
190
                print_dict(image, exclude=('id',))
191
                print
192
        
193
        backend.close()
194

    
195

    
196
class RegisterImage(Command):
197
    group = 'image'
198
    name = 'register'
199
    syntax = '<name> <Backend ID or Pithos URL> <disk format>'
200
    description = 'register an image'
201
    
202
    def add_options(self, parser):
203
        parser.add_option('--meta', dest='meta', action='append',
204
                metavar='KEY=VAL',
205
                help='add metadata (can be used multiple times)')
206
        parser.add_option('--public', action='store_true', dest='public',
207
                default=False, help='make image public')
208
        parser.add_option('-u', dest='uid', metavar='UID',
209
                help='assign image to user with id UID')
210
    
211
    def main(self, name, backend_id, format):
212
        if backend_id.startswith('pithos://'):
213
            return self.main_pithos(name, backend_id, format)
214
        
215
        formats = [x[0] for x in models.Image.FORMATS]
216
        if format not in formats:
217
            valid = ', '.join(formats)
218
            print 'Invalid format. Must be one of:', valid
219
            return
220
        
221
        image = models.Image.objects.create(
222
            name=name,
223
            state='ACTIVE',
224
            owner=self.uid,
225
            backend_id=backend_id,
226
            format=format,
227
            public=self.public)
228
        
229
        if self.meta:
230
            for m in self.meta:
231
                key, sep, val = m.partition('=')
232
                if key and val:
233
                    image.metadata.create(meta_key=key, meta_value=val)
234
                else:
235
                    print 'WARNING: Ignoring meta', m
236
        
237
        print_item(image)
238
    
239
    def main_pithos(self, name, url, disk_format):
240
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
241
            print 'Invalid disk format'
242
            return
243
        
244
        params = {
245
            'disk_format': disk_format,
246
            'is_public': self.public,
247
            'properties': {}}
248
        
249
        if self.meta:
250
            for m in self.meta:
251
                key, sep, val = m.partition('=')
252
                if key and val:
253
                    params['properties'][key] = val
254
                else:
255
                    print 'WARNING: Ignoring meta', m
256
        
257
        backend = ImageBackend(self.uid or settings.SYSTEM_IMAGES_OWNER)
258
        backend.register(name, url, params)
259
        backend.close()
260

    
261

    
262
class UploadImage(Command):
263
    group = 'image'
264
    name = 'upload'
265
    syntax = '<name> <path>'
266
    description = 'upload an image'
267
    
268
    def add_options(self, parser):
269
        container_formats = ', '.join(settings.ALLOWED_CONTAINER_FORMATS)
270
        disk_formats = ', '.join(settings.ALLOWED_DISK_FORMATS)
271
        
272
        parser.add_option('--container-format', dest='container_format',
273
                default=settings.DEFAULT_CONTAINER_FORMAT,
274
                metavar='FORMAT',
275
                help='set container format (%s)' % container_formats)
276
        parser.add_option('--disk-format', dest='disk_format',
277
                default=settings.DEFAULT_DISK_FORMAT,
278
                metavar='FORMAT',
279
                help='set disk format (%s)' % disk_formats)
280
        parser.add_option('--meta', dest='meta', action='append',
281
                metavar='KEY=VAL',
282
                help='add metadata (can be used multiple times)')
283
        parser.add_option('--owner', dest='owner',
284
                default=settings.SYSTEM_IMAGES_OWNER,
285
                metavar='USER',
286
                help='set owner to USER')
287
        parser.add_option('--public', action='store_true', dest='public',
288
                default=False,
289
                help='make image public')
290
    
291
    def main(self, name, path):
292
        backend = ImageBackend(self.owner)
293
        
294
        params = {
295
            'container_format': self.container_format,
296
            'disk_format': self.disk_format,
297
            'is_public': self.public,
298
            'filename': basename(path),
299
            'properties': {}}
300
        
301
        if self.meta:
302
            for m in self.meta:
303
                key, sep, val = m.partition('=')
304
                if key and val:
305
                    params['properties'][key] = val
306
                else:
307
                    print 'WARNING: Ignoring meta', m
308
        
309
        with open(path) as f:
310
            backend.put(name, f, params)
311
        
312
        backend.close()
313

    
314

    
315
class UpdateImage(Command):
316
    group = 'image'
317
    name = 'update'
318
    syntax = '<image id>'
319
    description = 'update an image stored in Pithos'
320
    
321
    def add_options(self, parser):
322
        container_formats = ', '.join(settings.ALLOWED_CONTAINER_FORMATS)
323
        disk_formats = ', '.join(settings.ALLOWED_DISK_FORMATS)
324
        
325
        parser.add_option('--container-format', dest='container_format',
326
                metavar='FORMAT',
327
                help='set container format (%s)' % container_formats)
328
        parser.add_option('--disk-format', dest='disk_format',
329
                metavar='FORMAT',
330
                help='set disk format (%s)' % disk_formats)
331
        parser.add_option('--name', dest='name',
332
                metavar='NAME',
333
                help='set name to NAME')
334
        parser.add_option('--private', action='store_true', dest='private',
335
                help='make image private')
336
        parser.add_option('--public', action='store_true', dest='public',
337
                help='make image public')
338
        parser.add_option('--user', dest='user',
339
                default=settings.SYSTEM_IMAGES_OWNER,
340
                metavar='USER',
341
                help='connect as USER')
342
    
343
    def main(self, image_id):
344
        backend = ImageBackend(self.user)
345
        
346
        image = backend.get_image(image_id)
347
        if not image:
348
            print 'Image not found'
349
            return
350
        
351
        params = {}
352
        
353
        if self.container_format:
354
            if self.container_format not in settings.ALLOWED_CONTAINER_FORMATS:
355
                print 'Invalid container format'
356
                return
357
            params['container_format'] = self.container_format
358
        if self.disk_format:
359
            if self.disk_format not in settings.ALLOWED_DISK_FORMATS:
360
                print 'Invalid disk format'
361
                return
362
            params['disk_format'] = self.disk_format
363
        if self.name:
364
            params['name'] = self.name
365
        if self.private:
366
            params['is_public'] = False
367
        if self.public:
368
            params['is_public'] = True
369
        
370
        backend.update(image_id, params)
371
        backend.close()
372

    
373

    
374
class ModifyImage(Command):
375
    group = 'image'
376
    name = 'modify'
377
    syntax = '<image id>'
378
    description = 'modify an image'
379
    
380
    def add_options(self, parser):
381
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
382
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
383

    
384
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
385
                help='set image backend id')
386
        parser.add_option('-f', dest='format', metavar='FORMAT',
387
                help='set image format (%s)' % formats)
388
        parser.add_option('-n', dest='name', metavar='NAME',
389
                help='set image name')
390
        parser.add_option('--public', action='store_true', dest='public',
391
                help='make image public')
392
        parser.add_option('--nopublic', action='store_true', dest='private',
393
                help='make image private')
394
        parser.add_option('-s', dest='state', metavar='STATE',
395
                help='set image state (%s)' % states)
396
        parser.add_option('-u', dest='uid', metavar='UID',
397
                help='assign image to user with id UID')
398
    
399
    def main(self, image_id):
400
        try:
401
            image = models.Image.objects.get(id=image_id)
402
        except:
403
            print 'Image not found'
404
            return
405
        
406
        if self.backend_id:
407
            image.backend_id = self.backend_id
408
        if self.format:
409
            allowed = [x[0] for x in models.Image.FORMATS]
410
            if self.format not in allowed:
411
                valid = ', '.join(allowed)
412
                print 'Invalid format. Must be one of:', valid
413
                return
414
            image.format = self.format
415
        if self.name:
416
            image.name = self.name
417
        if self.public:
418
            image.public = True
419
        if self.private:
420
            image.public = False
421
        if self.state:
422
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
423
            if self.state not in allowed:
424
                valid = ', '.join(allowed)
425
                print 'Invalid state. Must be one of:', valid
426
                return
427
            image.state = self.state
428
        
429
        image.userid = self.uid
430
        
431
        image.save()
432
        print_item(image)
433

    
434

    
435
class ModifyImageMeta(Command):
436
    group = 'image'
437
    name = 'meta'
438
    syntax = '<image id> [key[=val]]'
439
    description = 'get and manipulate image metadata'
440
    
441
    def add_options(self, parser):
442
        parser.add_option('--user', dest='user',
443
                default=settings.SYSTEM_IMAGES_OWNER,
444
                metavar='USER',
445
                help='connect as USER')
446

    
447
    def main(self, image_id, arg=''):
448
        if not image_id.isdigit():
449
            return self.main_pithos(image_id, arg)
450
        
451
        try:
452
            image = models.Image.objects.get(id=image_id)
453
        except:
454
            print 'Image not found'
455
            return
456
        
457
        key, sep, val = arg.partition('=')
458
        if not sep:
459
            val = None
460
        
461
        if not key:
462
            metadata = {}
463
            for meta in image.metadata.order_by('meta_key'):
464
                metadata[meta.meta_key] = meta.meta_value
465
            print_dict(metadata)
466
            return
467
        
468
        try:
469
            meta = image.metadata.get(meta_key=key)
470
        except models.ImageMetadata.DoesNotExist:
471
            meta = None
472
        
473
        if val is None:
474
            if meta:
475
                print_dict({key: meta.meta_value})
476
            return
477
        
478
        if val:
479
            if not meta:
480
                meta = image.metadata.create(meta_key=key)
481
            meta.meta_value = val
482
            meta.save()
483
        else:
484
            # Delete if val is empty
485
            if meta:
486
                meta.delete()
487
    
488
    def main_pithos(self, image_id, arg=''):
489
        backend = ImageBackend(self.user)
490
                
491
        try:
492
            image = backend.get_image(image_id)
493
            if not image:
494
                print 'Image not found'
495
                return
496
            
497
            key, sep, val = arg.partition('=')
498
            if not sep:
499
                val = None
500
            
501
            properties = image.get('properties', {})
502
            
503
            if not key:
504
                print_dict(properties)
505
                return
506
            
507
            if val is None:
508
                if key in properties:
509
                    print_dict({key: properties[key]})
510
                return
511
            
512
            if val:
513
                properties[key] = val        
514
                params = {'properties': properties}
515
                backend.update(image_id, params)
516
        finally:
517
            backend.close()
518

    
519

    
520
# Flavor commands
521

    
522
class CreateFlavor(Command):
523
    group = 'flavor'
524
    name = 'create'
525
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
526
    description = 'create one or more flavors'
527
    
528
    def add_options(self, parser):
529
        disk_templates = ', '.join(t for t in settings.GANETI_DISK_TEMPLATES)
530
        parser.add_option('--disk-template',
531
            dest='disk_template',
532
            metavar='TEMPLATE',
533
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE,
534
            help='available disk templates: %s' % disk_templates)
535
    
536
    def main(self, cpu, ram, disk):
537
        cpus = cpu.split(',')
538
        rams = ram.split(',')
539
        disks = disk.split(',')
540
        
541
        flavors = []
542
        for cpu, ram, disk in product(cpus, rams, disks):
543
            try:
544
                flavors.append((int(cpu), int(ram), int(disk)))
545
            except ValueError:
546
                print 'Invalid values'
547
                return
548
        
549
        created = []
550
        
551
        for cpu, ram, disk in flavors:
552
            flavor = models.Flavor.objects.create(
553
                cpu=cpu,
554
                ram=ram,
555
                disk=disk,
556
                disk_template=self.disk_template)
557
            created.append(flavor)
558
        
559
        print_items(created, detail=True)
560

    
561

    
562
class DeleteFlavor(Command):
563
    group = 'flavor'
564
    name = 'delete'
565
    syntax = '<flavor id> [<flavor id>] [...]'
566
    description = 'delete one or more flavors'
567
    
568
    def main(self, *args):
569
        if not args:
570
            raise TypeError
571
        for flavor_id in args:
572
            flavor = models.Flavor.objects.get(id=int(flavor_id))
573
            flavor.deleted = True
574
            flavor.save()
575

    
576

    
577
class ListFlavors(Command):
578
    group = 'flavor'
579
    name = 'list'
580
    syntax = '[flavor id]'
581
    description = 'list images'
582
    
583
    def add_options(self, parser):
584
        parser.add_option('-a', action='store_true', dest='show_deleted',
585
                default=False, help='also list deleted flavors')
586
        parser.add_option('-l', action='store_true', dest='detail',
587
                        default=False, help='show detailed output')
588
    
589
    def main(self, flavor_id=None):
590
        if flavor_id:
591
            flavors = [models.Flavor.objects.get(id=flavor_id)]
592
        else:
593
            flavors = models.Flavor.objects.order_by('id')
594
            if not self.show_deleted:
595
                flavors = flavors.exclude(deleted=True)
596
        print_items(flavors, self.detail)
597

    
598

    
599
class ShowStats(Command):
600
    group = 'stats'
601
    name = None
602
    description = 'show statistics'
603

    
604
    def main(self):
605
        stats = {}
606
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
607
        stats['Flavors'] = models.Flavor.objects.count()
608
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
609
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
610
        
611
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
612
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
613
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
614
        
615
        print_dict(stats)
616

    
617

    
618
def print_usage(exe, groups, group=None, shortcut=False):
619
    nop = Command(exe, [])
620
    nop.parser.print_help()
621
    if group:
622
        groups = {group: groups[group]}
623

    
624
    print
625
    print 'Commands:'
626
    
627
    for group, commands in sorted(groups.items()):
628
        for command, cls in sorted(commands.items()):
629
            if cls.hidden:
630
                continue
631
            name = '  %s %s' % (group, command or '')
632
            print '%s %s' % (name.ljust(22), cls.description)
633
        print
634

    
635

    
636
def main():
637
    groups = defaultdict(dict)
638
    module = sys.modules[__name__]
639
    for name, cls in inspect.getmembers(module, inspect.isclass):
640
        if not issubclass(cls, Command) or cls == Command:
641
            continue
642
        groups[cls.group][cls.name] = cls
643
    
644
    argv = list(sys.argv)
645
    exe = basename(argv.pop(0))
646
    prefix, sep, suffix = exe.partition('-')
647
    if sep and prefix == 'snf' and suffix in groups:
648
        # Allow shortcut aliases like snf-image, snf-server, etc
649
        group = suffix
650
    else:
651
        group = argv.pop(0) if argv else None
652
        if group in groups:
653
            exe = '%s %s' % (exe, group)
654
        else:
655
            exe = '%s <group>' % exe
656
            group = None
657
    
658
    command = argv.pop(0) if argv else None
659
    
660
    if group not in groups or command not in groups[group]:
661
        print_usage(exe, groups, group)
662
        sys.exit(1)
663
    
664
    cls = groups[group][command]
665
    cmd = cls(exe, argv)
666
    cmd.execute()
667

    
668

    
669
if __name__ == '__main__':
670
    dictConfig(settings.SNFADMIN_LOGGING)
671
    main()