Revision 998f872c

b/docs/admin-guide.rst
931 931

  
932 932
Additionally, administrative tasks can be performed via the admin web interface
933 933
located in /admin. Only users of type ADMIN can access the admin pages. To
934
change the type of a user to ADMIN, snf-admin can be used:
934
change the type of a user to ADMIN, snf-manage can be used:
935 935

  
936 936
.. code-block:: console
937 937

  
b/snf-cyclades-app/Changelog
1 1
Changelog
2 2
---------
3 3

  
4
v0.13.0
5
*******
6
  * Remove 'snf-admin' and 'snf-cloud' tools
7

  
4 8
v0.12.0
5 9
*******
6 10

  
b/snf-cyclades-app/setup.py
209 209
    entry_points = {
210 210
     'console_scripts': [
211 211
         'snf-dispatcher = synnefo.logic.dispatcher:main',
212
         'snf-admin = synnefo.tools.admin:main',
213
         'snf-cloud = synnefo.tools.cloud:main',
214 212
         ],
215 213
     'synnefo': [
216 214
         'default_settings = synnefo.app_settings.default',
......
222 220
         ]
223 221
      },
224 222
)
225

  
/dev/null
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()
/dev/null
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 httplib import HTTPConnection, HTTPSConnection
37
from optparse import OptionParser
38
from os.path import basename
39
from sys import argv, exit
40
from urlparse import urlparse
41

  
42
import json
43

  
44

  
45
DEFAULT_API_URL = 'http://127.0.0.1:8000/api/v1.1'
46

  
47
MARGIN = 14
48

  
49
commands = {}
50

  
51
def command_name(name):
52
    def decorator(cls):
53
        commands[name] = cls
54
        return cls
55
    return decorator
56

  
57

  
58
def print_addresses(networks):
59
    for i, net in enumerate(networks):
60
        key = 'addresses:'.rjust(MARGIN + 1) if i == 0 else ' ' * (MARGIN + 1)
61
        addr = ''
62
        if 'values' in net:
63
            addr = '[%s]' % ' '.join(ip['addr'] for ip in net['values'])
64

  
65
        val = '%s/%s %s %s' % (net['id'], net['name'], net['mac'], addr)
66
        if 'firewallProfile' in net:
67
            val += ' - %s' % net['firewallProfile']
68
        print '%s %s' % (key, val)
69

  
70
def print_dict(d, show_empty=True):
71
    for key, val in sorted(d.items()):
72
        if key == 'metadata':
73
            val = ', '.join('%s="%s"' % x for x in val['values'].items())
74
        elif key == 'addresses':
75
            print_addresses(val['values'])
76
            continue
77
        elif key == 'servers':
78
            val = ', '.join(str(server_id) for server_id in val['values'])
79
        if val or show_empty:
80
            print '%s: %s' % (key.rjust(MARGIN), val)
81

  
82

  
83
class Command(object):
84
    def __init__(self, argv):
85
        parser = OptionParser()
86
        parser.add_option('--apiurl',
87
                            dest='apiurl',
88
                            metavar='URL',
89
                            default=DEFAULT_API_URL,
90
                            help='use api API')
91
        parser.add_option('--token',
92
                            dest='token',
93
                            metavar='TOKEN',
94
                            help='use user token TOKEN')
95
        parser.add_option('-v',
96
                            action='store_true',
97
                            dest='verbose',
98
                            default=False,
99
                            help='use verbose output')
100
        self.add_options(parser)
101
        options, args = parser.parse_args(argv)
102

  
103
        # Add options to self
104
        for opt in parser.option_list:
105
            key = opt.dest
106
            if key:
107
                val = getattr(options, key)
108
                setattr(self, key, val)
109

  
110
        self.execute(*args)
111

  
112
    def add_options(self, parser):
113
        pass
114

  
115
    def execute(self, *args):
116
        pass
117

  
118
    def http_cmd(self, method, path, body=None, expected_status=200):
119
        p = urlparse(self.apiurl)
120
        if p.scheme == 'https':
121
            conn = HTTPSConnection(p.netloc)
122
        else:
123
            conn = HTTPConnection(p.netloc)
124

  
125
        kwargs = {}
126
        kwargs['headers'] = {'X-Auth-Token': self.token}
127
        if body:
128
            kwargs['headers']['Content-Type'] = 'application/json'
129
            kwargs['body'] = body
130
        conn.request(method, p.path + path, **kwargs)
131

  
132
        resp = conn.getresponse()
133
        if self.verbose:
134
            print '%d %s' % (resp.status, resp.reason)
135
            for key, val in resp.getheaders():
136
                print '%s: %s' % (key.capitalize(), val)
137
            print
138

  
139
        buf = resp.read() or '{}'
140
        try:
141
            reply = json.loads(buf)
142
        except ValueError:
143
            print 'Invalid response from the server.'
144
            if self.verbose:
145
                print buf
146
            exit(1)
147

  
148
        # If the response status is not the expected one,
149
        # assume an error has occured and treat the body
150
        # as a cloudfault.
151
        if resp.status != expected_status:
152
            if len(reply) == 1:
153
                key = reply.keys()[0]
154
                val = reply[key]
155
                print '%s: %s' % (key, val.get('message', ''))
156
                if self.verbose:
157
                    print val.get('details', '')
158
            else:
159
                print 'Invalid response from the server.'
160
            exit(1)
161

  
162
        return reply
163

  
164
    def http_get(self, path, expected_status=200):
165
        return self.http_cmd('GET', path, None, expected_status)
166

  
167
    def http_post(self, path, body, expected_status=202):
168
        return self.http_cmd('POST', path, body, expected_status)
169

  
170
    def http_put(self, path, body, expected_status=204):
171
        return self.http_cmd('PUT', path, body, expected_status)
172

  
173
    def http_delete(self, path, expected_status=204):
174
        return self.http_cmd('DELETE', path, None, expected_status)
175

  
176

  
177
@command_name('ls')
178
class ListServers(Command):
179
    description = 'list servers'
180

  
181
    def add_options(self, parser):
182
        parser.add_option('-l', action='store_true', dest='detail', default=False,
183
                            help='show detailed output')
184
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
185
                            help='include empty values')
186

  
187
    def execute(self):
188
        path = '/servers/detail' if self.detail else '/servers'
189
        reply = self.http_get(path)
190

  
191
        for server in reply['servers']['values']:
192
            id = server.pop('id')
193
            name = server.pop('name')
194
            if self.detail:
195
                print '%d %s' % (id, name)
196
                print_dict(server, self.show_empty)
197
                print
198
            else:
199
                print '%3d %s' % (id, name)
200

  
201

  
202
@command_name('info')
203
class GetServerDetails(Command):
204
    description = 'get server details'
205
    syntax = '<server id>'
206

  
207
    def add_options(self, parser):
208
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
209
                            help='include empty values')
210

  
211
    def execute(self, server_id):
212
        path = '/servers/%d' % int(server_id)
213
        reply = self.http_get(path)
214
        server = reply['server']
215
        server.pop('id')
216
        print_dict(server, self.show_empty)
217

  
218

  
219
@command_name('create')
220
class CreateServer(Command):
221
    description = 'create server'
222
    syntax = '<server name>'
223

  
224
    def add_options(self, parser):
225
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
226
                            help='use flavor FLAVOR_ID')
227
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
228
                            help='use image IMAGE_ID')
229

  
230
    def execute(self, name):
231
        server = {
232
            'name': name,
233
            'flavorRef': self.flavor,
234
            'imageRef': self.image}
235
        body = json.dumps({'server': server})
236
        reply = self.http_post('/servers', body)
237
        server = reply['server']
238
        print_dict(server)
239

  
240

  
241
@command_name('rename')
242
class UpdateServerName(Command):
243
    description = 'update server name'
244
    syntax = '<server id> <new name>'
245

  
246
    def execute(self, server_id, name):
247
        path = '/servers/%d' % int(server_id)
248
        body = json.dumps({'server': {'name': name}})
249
        self.http_put(path, body)
250

  
251

  
252
@command_name('delete')
253
class DeleteServer(Command):
254
    description = 'delete server'
255
    syntax = '<server id>'
256

  
257
    def execute(self, server_id):
258
        path = '/servers/%d' % int(server_id)
259
        self.http_delete(path)
260

  
261

  
262
@command_name('reboot')
263
class RebootServer(Command):
264
    description = 'reboot server'
265
    syntax = '<server id>'
266

  
267
    def add_options(self, parser):
268
        parser.add_option('-f', action='store_true', dest='hard', default=False,
269
                            help='perform a hard reboot')
270

  
271
    def execute(self, server_id):
272
        path = '/servers/%d/action' % int(server_id)
273
        type = 'HARD' if self.hard else 'SOFT'
274
        body = json.dumps({'reboot': {'type': type}})
275
        self.http_post(path, body)
276

  
277

  
278
@command_name('start')
279
class StartServer(Command):
280
    description = 'start server'
281
    syntax = '<server id>'
282

  
283
    def execute(self, server_id):
284
        path = '/servers/%d/action' % int(server_id)
285
        body = json.dumps({'start': {}})
286
        self.http_post(path, body)
287

  
288

  
289
@command_name('shutdown')
290
class StartServer(Command):
291
    description = 'shutdown server'
292
    syntax = '<server id>'
293

  
294
    def execute(self, server_id):
295
        path = '/servers/%d/action' % int(server_id)
296
        body = json.dumps({'shutdown': {}})
297
        self.http_post(path, body)
298

  
299

  
300
@command_name('console')
301
class ServerConsole(Command):
302
    description = 'get VNC console'
303
    syntax = '<server id>'
304

  
305
    def execute(self, server_id):
306
        path = '/servers/%d/action' % int(server_id)
307
        body = json.dumps({'console': {'type': 'vnc'}})
308
        reply = self.http_cmd('POST', path, body, 200)
309
        print_dict(reply['console'])
310

  
311

  
312
@command_name('profile')
313
class SetFirewallProfile(Command):
314
    description = 'set the firewall profile'
315
    syntax = '<server id> <profile>'
316

  
317
    def execute(self, server_id, profile):
318
        path = '/servers/%d/action' % int(server_id)
319
        body = json.dumps({'firewallProfile': {'profile': profile}})
320
        self.http_cmd('POST', path, body, 202)
321

  
322

  
323
@command_name('lsaddr')
324
class ListAddresses(Command):
325
    description = 'list server addresses'
326
    syntax = '<server id> [network]'
327

  
328
    def execute(self, server_id, network=None):
329
        path = '/servers/%d/ips' % int(server_id)
330
        if network:
331
            path += '/%s' % network
332
        reply = self.http_get(path)
333

  
334
        addresses = [reply['network']] if network else reply['addresses']['values']
335
        print_addresses(addresses)
336

  
337

  
338
@command_name('lsflv')
339
class ListFlavors(Command):
340
    description = 'list flavors'
341

  
342
    def add_options(self, parser):
343
        parser.add_option('-l', action='store_true', dest='detail', default=False,
344
                            help='show detailed output')
345

  
346
    def execute(self):
347
        path = '/flavors/detail' if self.detail else '/flavors'
348
        reply = self.http_get(path)
349

  
350
        for flavor in reply['flavors']['values']:
351
            id = flavor.pop('id')
352
            name = flavor.pop('name')
353
            details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
354
            print '%3d %s %s' % (id, name, details)
355

  
356

  
357
@command_name('flvinfo')
358
class GetFlavorDetails(Command):
359
    description = 'get flavor details'
360
    syntax = '<flavor id>'
361

  
362
    def execute(self, flavor_id):
363
        path = '/flavors/%d' % int(flavor_id)
364
        reply = self.http_get(path)
365

  
366
        flavor = reply['flavor']
367
        id = flavor.pop('id')
368
        name = flavor.pop('name')
369
        details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
370
        print '%3d %s %s' % (id, name, details)
371

  
372

  
373
@command_name('lsimg')
374
class ListImages(Command):
375
    description = 'list images'
376

  
377
    def add_options(self, parser):
378
        parser.add_option('-l', action='store_true', dest='detail', default=False,
379
                            help='show detailed output')
380

  
381
    def execute(self):
382
        path = '/images/detail' if self.detail else '/images'
383
        reply = self.http_get(path)
384

  
385
        for image in reply['images']['values']:
386
            id = image.pop('id')
387
            name = image.pop('name')
388
            if self.detail:
389
                print '%d %s' % (id, name)
390
                print_dict(image)
391
                print
392
            else:
393
                print '%3d %s' % (id, name)
394

  
395

  
396
@command_name('imginfo')
397
class GetImageDetails(Command):
398
    description = 'get image details'
399
    syntax = '<image id>'
400

  
401
    def execute(self, image_id):
402
        path = '/images/%d' % int(image_id)
403
        reply = self.http_get(path)
404
        image = reply['image']
405
        image.pop('id')
406
        print_dict(image)
407

  
408

  
409
@command_name('createimg')
410
class CreateImage(Command):
411
    description = 'create image'
412
    syntax = '<server id> <image name>'
413

  
414
    def execute(self, server_id, name):
415
        image = {'name': name, 'serverRef': int(server_id)}
416
        body = json.dumps({'image': image})
417
        reply = self.http_post('/images', body)
418
        print_dict(reply['image'])
419

  
420
@command_name('deleteimg')
421
class DeleteImage(Command):
422
    description = 'delete image'
423
    syntax = '<image id>'
424

  
425
    def execute(self, image_id):
426
        path = '/images/%d' % int(image_id)
427
        self.http_delete(path)
428

  
429
@command_name('lsmeta')
430
class ListServerMeta(Command):
431
    description = 'list server meta'
432
    syntax = '<server id> [key]'
433

  
434
    def execute(self, server_id, key=None):
435
        path = '/servers/%d/meta' % int(server_id)
436
        if key:
437
            path += '/' + key
438
        reply = self.http_get(path)
439
        if key:
440
            print_dict(reply['meta'])
441
        else:
442
            print_dict(reply['metadata']['values'])
443

  
444
@command_name('setmeta')
445
class UpdateServerMeta(Command):
446
    description = 'update server meta'
447
    syntax = '<server id> <key> <val>'
448

  
449
    def execute(self, server_id, key, val):
450
        path = '/servers/%d/meta' % int(server_id)
451
        metadata = {key: val}
452
        body = json.dumps({'metadata': metadata})
453
        reply = self.http_post(path, body, expected_status=201)
454
        print_dict(reply['metadata'])
455

  
456
@command_name('addmeta')
457
class CreateServerMeta(Command):
458
    description = 'add server meta'
459
    syntax = '<server id> <key> <val>'
460

  
461
    def execute(self, server_id, key, val):
462
        path = '/servers/%d/meta/%s' % (int(server_id), key)
463
        meta = {key: val}
464
        body = json.dumps({'meta': meta})
465
        reply = self.http_put(path, body, expected_status=201)
466
        print_dict(reply['meta'])
467

  
468
@command_name('delmeta')
469
class DeleteServerMeta(Command):
470
    description = 'delete server meta'
471
    syntax = '<server id> <key>'
472

  
473
    def execute(self, server_id, key):
474
        path = '/servers/%d/meta/%s' % (int(server_id), key)
475
        reply = self.http_delete(path)
476

  
477
@command_name('lsimgmeta')
478
class ListImageMeta(Command):
479
    description = 'list image meta'
480
    syntax = '<image id> [key]'
481

  
482
    def execute(self, image_id, key=None):
483
        path = '/images/%d/meta' % int(image_id)
484
        if key:
485
            path += '/' + key
486
        reply = self.http_get(path)
487
        if key:
488
            print_dict(reply['meta'])
489
        else:
490
            print_dict(reply['metadata']['values'])
491

  
492
@command_name('setimgmeta')
493
class UpdateImageMeta(Command):
494
    description = 'update image meta'
495
    syntax = '<image id> <key> <val>'
496

  
497
    def execute(self, image_id, key, val):
498
        path = '/images/%d/meta' % int(image_id)
499
        metadata = {key: val}
500
        body = json.dumps({'metadata': metadata})
501
        reply = self.http_post(path, body, expected_status=201)
502
        print_dict(reply['metadata'])
503

  
504
@command_name('addimgmeta')
505
class CreateImageMeta(Command):
506
    description = 'add image meta'
507
    syntax = '<image id> <key> <val>'
508

  
509
    def execute(self, image_id, key, val):
510
        path = '/images/%d/meta/%s' % (int(image_id), key)
511
        meta = {key: val}
512
        body = json.dumps({'meta': meta})
513
        reply = self.http_put(path, body, expected_status=201)
514
        print_dict(reply['meta'])
515

  
516
@command_name('delimgmeta')
517
class DeleteImageMeta(Command):
518
    description = 'delete image meta'
519
    syntax = '<image id> <key>'
520

  
521
    def execute(self, image_id, key):
522
        path = '/images/%d/meta/%s' % (int(image_id), key)
523
        reply = self.http_delete(path)
524

  
525

  
526
@command_name('lsnet')
527
class ListNetworks(Command):
528
    description = 'list networks'
529

  
530
    def add_options(self, parser):
531
        parser.add_option('-l', action='store_true', dest='detail', default=False,
532
                            help='show detailed output')
533

  
534
    def execute(self):
535
        path = '/networks/detail' if self.detail else '/networks'
536
        reply = self.http_get(path)
537

  
538
        for network in reply['networks']['values']:
539
            id = network.pop('id')
540
            name = network.pop('name')
541
            if self.detail:
542
                print '%s %s' % (id, name)
543
                print_dict(network)
544
                print
545
            else:
546
                print '%3s %s' % (id, name)
547

  
548

  
549
@command_name('createnet')
550
class CreateNetwork(Command):
551
    description = 'create network'
552
    syntax = '<network name>'
553

  
554
    def execute(self, name):
555
        network = {'name': name}
556
        body = json.dumps({'network': network})
557
        reply = self.http_post('/networks', body)
558
        print_dict(reply['network'])
559

  
560

  
561
@command_name('netinfo')
562
class GetNetworkDetails(Command):
563
    description = 'get network details'
564
    syntax = '<network id>'
565

  
566
    def execute(self, network_id):
567
        path = '/networks/%d' % int(network_id)
568
        reply = self.http_get(path)
569
        net = reply['network']
570
        name = net.pop('id')
571
        print_dict(net)
572

  
573

  
574
@command_name('renamenet')
575
class UpdateNetworkName(Command):
576
    description = 'update network name'
577
    syntax = '<network_id> <new name>'
578

  
579
    def execute(self, network_id, name):
580
        path = '/networks/%d' % int(network_id)
581
        body = json.dumps({'network': {'name': name}})
582
        self.http_put(path, body)
583

  
584

  
585
@command_name('deletenet')
586
class DeleteNetwork(Command):
587
    description = 'delete network'
588
    syntax = '<network id>'
589

  
590
    def execute(self, network_id):
591
        path = '/networks/%d' % int(network_id)
592
        self.http_delete(path)
593

  
594

  
595
@command_name('connect')
596
class AddNetwork(Command):
597
    description = 'connect a server to a network'
598
    syntax = '<server id> <network id>'
599

  
600
    def execute(self, server_id, network_id):
601
        path = '/networks/%d/action' % int(network_id)
602
        body = json.dumps({'add': {'serverRef': server_id}})
603
        self.http_post(path, body, expected_status=202)
604

  
605

  
606
@command_name('disconnect')
607
class RemoveNetwork(Command):
608
    description = 'disconnect a server from a network'
609
    syntax = '<server id> <network id>'
610

  
611
    def execute(self, server_id, network_id):
612
        path = '/networks/%s/action' % int(network_id)
613
        body = json.dumps({'remove': {'serverRef': server_id}})
614
        self.http_post(path, body, expected_status=202)
615

  
616
@command_name('stats')
617
class ServerStats(Command):
618
    description = 'get server stats'
619
    syntax = '<server id>'
620

  
621
    def execute(self, server_id):
622
        path = '/servers/%d/stats' % int(server_id)
623
        reply = self.http_get(path)
624
        stats = reply['stats']
625
        stats.pop('serverRef')
626
        print_dict(stats)
627

  
628

  
629
def print_usage():
630
    print 'Usage: %s <command>' % basename(argv[0])
631
    print
632
    print 'Commands:'
633
    for name, cls in sorted(commands.items()):
634
        description = getattr(cls, 'description', '')
635
        print '  %s %s' % (name.ljust(12), description)
636

  
637
def main():
638
    try:
639
        name = argv[1]
640
        cls = commands[name]
641
    except (IndexError, KeyError):
642
        print_usage()
643
        exit(1)
644

  
645
    try:
646
        cls(argv[2:])
647
    except TypeError:
648
        syntax = getattr(cls, 'syntax', '')
649
        if syntax:
650
            print 'Syntax: %s %s' % (name, syntax)
651
        else:
652
            print 'Invalid syntax'
653
        exit(1)
654

  
655

  
656
if __name__ == '__main__':
657
    main()

Also available in: Unified diff