Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / tools / admin.py @ 65462ca9

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 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
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 as e:
116
            if e.args and e.args[0].startswith('main()'):
117
                self.parser.print_help()
118
            else:
119
                raise
120

    
121

    
122
# Server commands
123

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

    
150

    
151
# Image commands
152

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

    
198

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

    
264

    
265
class UpdateImage(Command):
266
    group = 'image'
267
    name = 'update'
268
    syntax = '<image id>'
269
    description = 'update an image stored in Pithos'
270
    
271
    def add_options(self, parser):
272
        container_formats = ', '.join(settings.ALLOWED_CONTAINER_FORMATS)
273
        disk_formats = ', '.join(settings.ALLOWED_DISK_FORMATS)
274
        
275
        parser.add_option('--container-format', dest='container_format',
276
                metavar='FORMAT',
277
                help='set container format (%s)' % container_formats)
278
        parser.add_option('--disk-format', dest='disk_format',
279
                metavar='FORMAT',
280
                help='set disk format (%s)' % disk_formats)
281
        parser.add_option('--name', dest='name',
282
                metavar='NAME',
283
                help='set name to NAME')
284
        parser.add_option('--private', action='store_true', dest='private',
285
                help='make image private')
286
        parser.add_option('--public', action='store_true', dest='public',
287
                help='make image public')
288
        parser.add_option('--user', dest='user',
289
                default=settings.SYSTEM_IMAGES_OWNER,
290
                metavar='USER',
291
                help='connect as USER')
292
    
293
    def main(self, image_id):
294
        backend = ImageBackend(self.user)
295
        
296
        image = backend.get_image(image_id)
297
        if not image:
298
            print 'Image not found'
299
            return
300
        
301
        params = {}
302
        
303
        if self.container_format:
304
            if self.container_format not in settings.ALLOWED_CONTAINER_FORMATS:
305
                print 'Invalid container format'
306
                return
307
            params['container_format'] = self.container_format
308
        if self.disk_format:
309
            if self.disk_format not in settings.ALLOWED_DISK_FORMATS:
310
                print 'Invalid disk format'
311
                return
312
            params['disk_format'] = self.disk_format
313
        if self.name:
314
            params['name'] = self.name
315
        if self.private:
316
            params['is_public'] = False
317
        if self.public:
318
            params['is_public'] = True
319
        
320
        backend.update(image_id, params)
321
        backend.close()
322

    
323

    
324
class ModifyImage(Command):
325
    group = 'image'
326
    name = 'modify'
327
    syntax = '<image id>'
328
    description = 'modify an image'
329
    
330
    def add_options(self, parser):
331
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
332
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
333

    
334
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
335
                help='set image backend id')
336
        parser.add_option('-f', dest='format', metavar='FORMAT',
337
                help='set image format (%s)' % formats)
338
        parser.add_option('-n', dest='name', metavar='NAME',
339
                help='set image name')
340
        parser.add_option('--public', action='store_true', dest='public',
341
                help='make image public')
342
        parser.add_option('--nopublic', action='store_true', dest='private',
343
                help='make image private')
344
        parser.add_option('-s', dest='state', metavar='STATE',
345
                help='set image state (%s)' % states)
346
        parser.add_option('-u', dest='uid', metavar='UID',
347
                help='assign image to user with id UID')
348
    
349
    def main(self, image_id):
350
        try:
351
            image = models.Image.objects.get(id=image_id)
352
        except:
353
            print 'Image not found'
354
            return
355
        
356
        if self.backend_id:
357
            image.backend_id = self.backend_id
358
        if self.format:
359
            allowed = [x[0] for x in models.Image.FORMATS]
360
            if self.format not in allowed:
361
                valid = ', '.join(allowed)
362
                print 'Invalid format. Must be one of:', valid
363
                return
364
            image.format = self.format
365
        if self.name:
366
            image.name = self.name
367
        if self.public:
368
            image.public = True
369
        if self.private:
370
            image.public = False
371
        if self.state:
372
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
373
            if self.state not in allowed:
374
                valid = ', '.join(allowed)
375
                print 'Invalid state. Must be one of:', valid
376
                return
377
            image.state = self.state
378
        
379
        image.userid = self.uid
380
        
381
        image.save()
382
        print_item(image)
383

    
384

    
385
class ModifyImageMeta(Command):
386
    group = 'image'
387
    name = 'meta'
388
    syntax = '<image id> [key[=val]]'
389
    description = 'get and manipulate image metadata'
390
    
391
    def add_options(self, parser):
392
        parser.add_option('--user', dest='user',
393
                default=settings.SYSTEM_IMAGES_OWNER,
394
                metavar='USER',
395
                help='connect as USER')
396

    
397
    def main(self, image_id, arg=''):
398
        if not image_id.isdigit():
399
            return self.main_pithos(image_id, arg)
400
        
401
        try:
402
            image = models.Image.objects.get(id=image_id)
403
        except:
404
            print 'Image not found'
405
            return
406
        
407
        key, sep, val = arg.partition('=')
408
        if not sep:
409
            val = None
410
        
411
        if not key:
412
            metadata = {}
413
            for meta in image.metadata.order_by('meta_key'):
414
                metadata[meta.meta_key] = meta.meta_value
415
            print_dict(metadata)
416
            return
417
        
418
        try:
419
            meta = image.metadata.get(meta_key=key)
420
        except models.ImageMetadata.DoesNotExist:
421
            meta = None
422
        
423
        if val is None:
424
            if meta:
425
                print_dict({key: meta.meta_value})
426
            return
427
        
428
        if val:
429
            if not meta:
430
                meta = image.metadata.create(meta_key=key)
431
            meta.meta_value = val
432
            meta.save()
433
        else:
434
            # Delete if val is empty
435
            if meta:
436
                meta.delete()
437
    
438
    def main_pithos(self, image_id, arg=''):
439
        backend = ImageBackend(self.user)
440
                
441
        try:
442
            image = backend.get_image(image_id)
443
            if not image:
444
                print 'Image not found'
445
                return
446
            
447
            key, sep, val = arg.partition('=')
448
            if not sep:
449
                val = None
450
            
451
            properties = image.get('properties', {})
452
            
453
            if not key:
454
                print_dict(properties)
455
                return
456
            
457
            if val is None:
458
                if key in properties:
459
                    print_dict({key: properties[key]})
460
                return
461
            
462
            if val:
463
                properties[key] = val        
464
                params = {'properties': properties}
465
                backend.update(image_id, params)
466
        finally:
467
            backend.close()
468

    
469

    
470
# Flavor commands
471

    
472
class CreateFlavor(Command):
473
    group = 'flavor'
474
    name = 'create'
475
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
476
    description = 'create one or more flavors'
477
    
478
    def add_options(self, parser):
479
        disk_templates = ', '.join(t for t in settings.GANETI_DISK_TEMPLATES)
480
        parser.add_option('--disk-template',
481
            dest='disk_template',
482
            metavar='TEMPLATE',
483
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE,
484
            help='available disk templates: %s' % disk_templates)
485
    
486
    def main(self, cpu, ram, disk):
487
        cpus = cpu.split(',')
488
        rams = ram.split(',')
489
        disks = disk.split(',')
490
        
491
        flavors = []
492
        for cpu, ram, disk in product(cpus, rams, disks):
493
            try:
494
                flavors.append((int(cpu), int(ram), int(disk)))
495
            except ValueError:
496
                print 'Invalid values'
497
                return
498
        
499
        created = []
500
        
501
        for cpu, ram, disk in flavors:
502
            flavor = models.Flavor.objects.create(
503
                cpu=cpu,
504
                ram=ram,
505
                disk=disk,
506
                disk_template=self.disk_template)
507
            created.append(flavor)
508
        
509
        print_items(created, detail=True)
510

    
511

    
512
class DeleteFlavor(Command):
513
    group = 'flavor'
514
    name = 'delete'
515
    syntax = '<flavor id> [<flavor id>] [...]'
516
    description = 'delete one or more flavors'
517
    
518
    def main(self, *args):
519
        if not args:
520
            raise TypeError
521
        for flavor_id in args:
522
            flavor = models.Flavor.objects.get(id=int(flavor_id))
523
            flavor.deleted = True
524
            flavor.save()
525

    
526

    
527
class ListFlavors(Command):
528
    group = 'flavor'
529
    name = 'list'
530
    syntax = '[flavor id]'
531
    description = 'list images'
532
    
533
    def add_options(self, parser):
534
        parser.add_option('-a', action='store_true', dest='show_deleted',
535
                default=False, help='also list deleted flavors')
536
        parser.add_option('-l', action='store_true', dest='detail',
537
                        default=False, help='show detailed output')
538
    
539
    def main(self, flavor_id=None):
540
        if flavor_id:
541
            flavors = [models.Flavor.objects.get(id=flavor_id)]
542
        else:
543
            flavors = models.Flavor.objects.order_by('id')
544
            if not self.show_deleted:
545
                flavors = flavors.exclude(deleted=True)
546
        print_items(flavors, self.detail)
547

    
548

    
549
class ShowStats(Command):
550
    group = 'stats'
551
    name = None
552
    description = 'show statistics'
553

    
554
    def main(self):
555
        stats = {}
556
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
557
        stats['Flavors'] = models.Flavor.objects.count()
558
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
559
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
560
        
561
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
562
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
563
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
564
        
565
        print_dict(stats)
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
    dictConfig(settings.SNFADMIN_LOGGING)
588
    groups = defaultdict(dict)
589
    module = sys.modules[__name__]
590
    for name, cls in inspect.getmembers(module, inspect.isclass):
591
        if not issubclass(cls, Command) or cls == Command:
592
            continue
593
        groups[cls.group][cls.name] = cls
594
    
595
    argv = list(sys.argv)
596
    exe = basename(argv.pop(0))
597
    prefix, sep, suffix = exe.partition('-')
598
    if sep and prefix == 'snf' and suffix in groups:
599
        # Allow shortcut aliases like snf-image, snf-server, etc
600
        group = suffix
601
    else:
602
        group = argv.pop(0) if argv else None
603
        if group in groups:
604
            exe = '%s %s' % (exe, group)
605
        else:
606
            exe = '%s <group>' % exe
607
            group = None
608
    
609
    command = argv.pop(0) if argv else None
610
    
611
    if group not in groups or command not in groups[group]:
612
        print_usage(exe, groups, group)
613
        sys.exit(1)
614
    
615
    cls = groups[group][command]
616
    cmd = cls(exe, argv)
617
    cmd.execute()
618

    
619

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