Statistics
| Branch: | Tag: | Revision:

root / tools / cloud @ adee02b8

History | View | Annotate | Download (19.5 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 httplib import HTTPConnection
37
from optparse import OptionParser
38
from os.path import basename
39
from sys import argv, exit
40

    
41
import json
42

    
43
DEFAULT_HOST = '127.0.0.1:8000'
44
DEFAULT_API = 'v1.1'
45

    
46
TOKEN = '46e427d657b20defe352804f0eb6f8a2'
47

    
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(13) if i == 0 else ' ' * 13
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
        print '%s %s' % (key, val)
67

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

    
80

    
81
class Command(object):
82
    def __init__(self, argv):
83
        parser = OptionParser()
84
        parser.add_option('--host', dest='host', metavar='HOST', default=DEFAULT_HOST,
85
                            help='use server HOST')
86
        parser.add_option('--api', dest='api', metavar='API', default=DEFAULT_API,
87
                            help='use api API')
88
        parser.add_option('-v', action='store_true', dest='verbose', default=False,
89
                            help='use verbose output')
90
        self.add_options(parser)
91
        options, args = parser.parse_args(argv)
92
        
93
        # Add options to self
94
        for opt in parser.option_list:
95
            key = opt.dest
96
            if key:
97
                val = getattr(options, key)
98
                setattr(self, key, val)
99
        
100
        self.execute(*args)
101
    
102
    def add_options(self, parser):
103
        pass
104
    
105
    def execute(self, *args):
106
        pass
107
    
108
    def http_cmd(self, method, path, body=None, expected_status=200):
109
        conn = HTTPConnection(self.host)
110

    
111
        kwargs = {}
112
        kwargs['headers'] = {'X-Auth-Token': TOKEN}
113
        if body:
114
            kwargs['headers']['Content-Type'] = 'application/json'
115
            kwargs['body'] = body
116
        conn.request(method, path, **kwargs)
117

    
118
        resp = conn.getresponse()
119
        if self.verbose:
120
            print '%d %s' % (resp.status, resp.reason)
121
            for key, val in resp.getheaders():
122
                print '%s: %s' % (key.capitalize(), val)
123
            print
124

    
125
        buf = resp.read() or '{}'
126
        try:
127
            reply = json.loads(buf)
128
        except ValueError:
129
            print 'Invalid response from the server.'
130
            if self.verbose:
131
                print buf
132
            exit(1)
133

    
134
        # If the response status is not the expected one,
135
        # assume an error has occured and treat the body
136
        # as a cloudfault.
137
        if resp.status != expected_status:
138
            if len(reply) == 1:
139
                key = reply.keys()[0]
140
                val = reply[key]
141
                print '%s: %s' % (key, val.get('message', ''))
142
                if self.verbose:
143
                    print val.get('details', '')
144
            else:
145
                print 'Invalid response from the server.'
146
            exit(1)
147

    
148
        return reply
149

    
150
    def http_get(self, path, expected_status=200):
151
        return self.http_cmd('GET', path, None, expected_status)
152
    
153
    def http_post(self, path, body, expected_status=202):
154
        return self.http_cmd('POST', path, body, expected_status)
155

    
156
    def http_put(self, path, body, expected_status=204):
157
        return self.http_cmd('PUT', path, body, expected_status)
158

    
159
    def http_delete(self, path, expected_status=204):
160
        return self.http_cmd('DELETE', path, None, expected_status)
161

    
162

    
163
@command_name('ls')
164
class ListServers(Command):
165
    description = 'list servers'
166
    
167
    def add_options(self, parser):
168
        parser.add_option('-l', action='store_true', dest='detail', default=False,
169
                            help='show detailed output')
170
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
171
                            help='include empty values')
172
    
173
    def execute(self):
174
        path = '/api/%s/servers' % self.api
175
        if self.detail:
176
            path += '/detail'
177

    
178
        reply = self.http_get(path)
179

    
180
        for server in reply['servers']['values']:
181
            id = server.pop('id')
182
            name = server.pop('name')
183
            if self.detail:
184
                print '%d %s' % (id, name)
185
                print_dict(server, self.show_empty)
186
                print
187
            else:
188
                print '%3d %s' % (id, name)
189

    
190

    
191
@command_name('info')
192
class GetServerDetails(Command):
193
    description = 'get server details'
194
    syntax = '<server id>'
195
    
196
    def add_options(self, parser):
197
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
198
                            help='include empty values')
199
    
200
    def execute(self, server_id):
201
        path = '/api/%s/servers/%d' % (self.api, int(server_id))
202
        reply = self.http_get(path)
203
        server = reply['server']
204
        server.pop('id')
205
        print_dict(server, self.show_empty)
206
        
207

    
208
@command_name('create')
209
class CreateServer(Command):
210
    description = 'create server'
211
    syntax = '<server name>'
212
    
213
    def add_options(self, parser):
214
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
215
                            help='use flavor FLAVOR_ID')
216
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
217
                            help='use image IMAGE_ID')
218
    
219
    def execute(self, name):
220
        path = '/api/%s/servers' % self.api
221
        server = {'name': name, 'flavorRef': self.flavor, 'imageRef': self.image}
222
        body = json.dumps({'server': server})
223
        reply = self.http_post(path, body)
224
        server = reply['server']
225
        print_dict(server)
226

    
227

    
228
@command_name('rename')
229
class UpdateServerName(Command):
230
    description = 'update server name'
231
    syntax = '<server id> <new name>'
232
    
233
    def execute(self, server_id, name):
234
        path = '/api/%s/servers/%d' % (self.api, int(server_id))
235
        body = json.dumps({'server': {'name': name}})
236
        self.http_put(path, body)
237

    
238

    
239
@command_name('delete')
240
class DeleteServer(Command):
241
    description = 'delete server'
242
    syntax = '<server id>'
243
    
244
    def execute(self, server_id):
245
        path = '/api/%s/servers/%d' % (self.api, int(server_id))
246
        self.http_delete(path)
247

    
248

    
249
@command_name('reboot')
250
class RebootServer(Command):
251
    description = 'reboot server'
252
    syntax = '<server id>'
253
    
254
    def add_options(self, parser):
255
        parser.add_option('-f', action='store_true', dest='hard', default=False,
256
                            help='perform a hard reboot')
257
    
258
    def execute(self, server_id):
259
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
260
        type = 'HARD' if self.hard else 'SOFT'
261
        body = json.dumps({'reboot': {'type': type}})
262
        self.http_post(path, body)
263
    
264

    
265
@command_name('start')
266
class StartServer(Command):
267
    description = 'start server'
268
    syntax = '<server id>'
269
    
270
    def execute(self, server_id):
271
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
272
        body = json.dumps({'start': {}})
273
        self.http_post(path, body)
274

    
275

    
276
@command_name('shutdown')
277
class StartServer(Command):
278
    description = 'shutdown server'
279
    syntax = '<server id>'
280
    
281
    def execute(self, server_id):
282
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
283
        body = json.dumps({'shutdown': {}})
284
        self.http_post(path, body)
285

    
286

    
287
@command_name('console')
288
class ServerConsole(Command):
289
    description = 'get VNC console'
290
    syntax = '<server id>'
291
    
292
    def add_options(self, parser):
293
    	pass
294
    
295
    def execute(self, server_id):
296
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
297
        body = json.dumps({'console': {'type': 'vnc'}})
298
        reply = self.http_cmd('POST', path, body, 200)
299
        print_dict(reply['console'])
300

    
301

    
302
@command_name('lsaddr')
303
class ListAddresses(Command):
304
    description = 'list server addresses'
305
    syntax = '<server id> [network]'
306
    
307
    def execute(self, server_id, network=None):
308
        path = '/api/%s/servers/%d/ips' % (self.api, int(server_id))
309
        if network:
310
            path += '/%s' % network
311
        reply = self.http_get(path)
312
        
313
        addresses = [reply['network']] if network else reply['addresses']['values']
314
        print_addresses(addresses)
315

    
316

    
317
@command_name('lsflv')
318
class ListFlavors(Command):
319
    description = 'list flavors'
320
    
321
    def add_options(self, parser):
322
        parser.add_option('-l', action='store_true', dest='detail', default=False,
323
                            help='show detailed output')
324
    
325
    def execute(self):
326
        path = '/api/%s/flavors' % self.api
327
        if self.detail:
328
            path += '/detail'
329
        reply = self.http_get(path)
330
        
331
        for flavor in reply['flavors']['values']:
332
            id = flavor.pop('id')
333
            name = flavor.pop('name')
334
            details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
335
            print '%3d %s %s' % (id, name, details)
336

    
337

    
338
@command_name('flvinfo')
339
class GetFlavorDetails(Command):
340
    description = 'get flavor details'
341
    syntax = '<flavor id>'
342
    
343
    def execute(self, flavor_id):
344
        path = '/api/%s/flavors/%d' % (self.api, int(flavor_id))
345
        reply = self.http_get(path)
346
        
347
        flavor = reply['flavor']
348
        id = flavor.pop('id')
349
        name = flavor.pop('name')
350
        details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
351
        print '%3d %s %s' % (id, name, details)
352

    
353

    
354
@command_name('lsimg')
355
class ListImages(Command):
356
    description = 'list images'
357
    
358
    def add_options(self, parser):
359
        parser.add_option('-l', action='store_true', dest='detail', default=False,
360
                            help='show detailed output')
361
    
362
    def execute(self):
363
        path = '/api/%s/images' % self.api
364
        if self.detail:
365
            path += '/detail'
366
        reply = self.http_get(path)
367
        
368
        for image in reply['images']['values']:
369
            id = image.pop('id')
370
            name = image.pop('name')
371
            if self.detail:
372
                print '%d %s' % (id, name)
373
                print_dict(image)
374
                print
375
            else:
376
                print '%3d %s' % (id, name)
377

    
378

    
379
@command_name('imginfo')
380
class GetImageDetails(Command):
381
    description = 'get image details'
382
    syntax = '<image id>'
383
    
384
    def execute(self, image_id):
385
        path = '/api/%s/images/%d' % (self.api, int(image_id))
386
        reply = self.http_get(path)
387
        image = reply['image']
388
        image.pop('id')
389
        print_dict(image)
390

    
391

    
392
@command_name('createimg')
393
class CreateImage(Command):
394
    description = 'create image'
395
    syntax = '<server id> <image name>'
396
    
397
    def execute(self, server_id, name):
398
        path = '/api/%s/images' % self.api
399
        image = {'name': name, 'serverRef': int(server_id)}
400
        body = json.dumps({'image': image})
401
        reply = self.http_post(path, body)
402
        print_dict(reply['image'])
403

    
404
@command_name('deleteimg')
405
class DeleteImage(Command):
406
    description = 'delete image'
407
    syntax = '<image id>'
408
    
409
    def execute(self, image_id):
410
        path = '/api/%s/images/%d' % (self.api, int(image_id))
411
        self.http_delete(path)
412

    
413
@command_name('lsmeta')
414
class ListServerMeta(Command):
415
    description = 'list server meta'
416
    syntax = '<server id> [key]'
417

    
418
    def execute(self, server_id, key=None):
419
        path = '/api/%s/servers/%d/meta' % (self.api, int(server_id))
420
        if key:
421
            path += '/' + key
422
        reply = self.http_get(path)
423
        if key:
424
            print_dict(reply['meta'])
425
        else:
426
            print_dict(reply['metadata']['values'])
427

    
428
@command_name('setmeta')
429
class UpdateServerMeta(Command):
430
    description = 'update server meta'
431
    syntax = '<server id> <key> <val>'
432

    
433
    def execute(self, server_id, key, val):
434
        path = '/api/%s/servers/%d/meta' % (self.api, int(server_id))
435
        metadata = {key: val}
436
        body = json.dumps({'metadata': metadata})
437
        reply = self.http_post(path, body, expected_status=201)
438
        print_dict(reply['metadata'])
439

    
440
@command_name('addmeta')
441
class CreateServerMeta(Command):
442
    description = 'add server meta'
443
    syntax = '<server id> <key> <val>'
444

    
445
    def execute(self, server_id, key, val):
446
        path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key)
447
        meta = {key: val}
448
        body = json.dumps({'meta': meta})
449
        reply = self.http_put(path, body, expected_status=201)
450
        print_dict(reply['meta'])
451

    
452
@command_name('delmeta')
453
class DeleteServerMeta(Command):
454
    description = 'delete server meta'
455
    syntax = '<server id> <key>'
456

    
457
    def execute(self, server_id, key):
458
        path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key)
459
        reply = self.http_delete(path)
460

    
461
@command_name('lsimgmeta')
462
class ListImageMeta(Command):
463
    description = 'list image meta'
464
    syntax = '<image id> [key]'
465

    
466
    def execute(self, image_id, key=None):
467
        path = '/api/%s/images/%d/meta' % (self.api, int(image_id))
468
        if key:
469
            path += '/' + key
470
        reply = self.http_get(path)
471
        if key:
472
            print_dict(reply['meta'])
473
        else:
474
            print_dict(reply['metadata']['values'])
475

    
476
@command_name('setimgmeta')
477
class UpdateImageMeta(Command):
478
    description = 'update image meta'
479
    syntax = '<image id> <key> <val>'
480

    
481
    def execute(self, image_id, key, val):
482
        path = '/api/%s/images/%d/meta' % (self.api, int(image_id))
483
        metadata = {key: val}
484
        body = json.dumps({'metadata': metadata})
485
        reply = self.http_post(path, body, expected_status=201)
486
        print_dict(reply['metadata'])
487

    
488
@command_name('addimgmeta')
489
class CreateImageMeta(Command):
490
    description = 'add image meta'
491
    syntax = '<image id> <key> <val>'
492

    
493
    def execute(self, image_id, key, val):
494
        path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key)
495
        meta = {key: val}
496
        body = json.dumps({'meta': meta})
497
        reply = self.http_put(path, body, expected_status=201)
498
        print_dict(reply['meta'])
499

    
500
@command_name('delimgmeta')
501
class DeleteImageMeta(Command):
502
    description = 'delete image meta'
503
    syntax = '<image id> <key>'
504

    
505
    def execute(self, image_id, key):
506
        path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key)
507
        reply = self.http_delete(path)
508

    
509

    
510
@command_name('lsnet')
511
class ListNetworks(Command):
512
    description = 'list networks'
513

    
514
    def add_options(self, parser):
515
        parser.add_option('-l', action='store_true', dest='detail', default=False,
516
                            help='show detailed output')
517

    
518
    def execute(self):
519
        path = '/api/%s/networks' % self.api
520
        if self.detail:
521
            path += '/detail'
522
        reply = self.http_get(path)
523

    
524
        for network in reply['networks']['values']:
525
            id = network.pop('id')
526
            name = network.pop('name')
527
            if self.detail:
528
                print '%s %s' % (id, name)
529
                print_dict(network)
530
                print
531
            else:
532
                print '%3s %s' % (id, name)
533

    
534

    
535
@command_name('createnet')
536
class CreateNetwork(Command):
537
    description = 'create network'
538
    syntax = '<network name>'
539

    
540
    def execute(self, name):
541
        path = '/api/%s/networks' % self.api
542
        network = {'name': name}
543
        body = json.dumps({'network': network})
544
        reply = self.http_post(path, body)
545
        print_dict(reply['network'])
546

    
547

    
548
@command_name('netinfo')
549
class GetNetworkDetails(Command):
550
    description = 'get network details'
551
    syntax = '<network id>'
552

    
553
    def execute(self, network_id):
554
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
555
        reply = self.http_get(path)
556
        net = reply['network']
557
        name = net.pop('id')
558
        print_dict(net)
559

    
560

    
561
@command_name('renamenet')
562
class UpdateNetworkName(Command):
563
    description = 'update network name'
564
    syntax = '<network_id> <new name>'
565

    
566
    def execute(self, network_id, name):
567
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
568
        body = json.dumps({'network': {'name': name}})
569
        self.http_put(path, body)
570

    
571

    
572
@command_name('deletenet')
573
class DeleteNetwork(Command):
574
    description = 'delete network'
575
    syntax = '<network id>'
576

    
577
    def execute(self, network_id):
578
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
579
        self.http_delete(path)
580

    
581

    
582
@command_name('connect')
583
class AddNetwork(Command):
584
    description = 'connect a server to a network'
585
    syntax = '<server id> <network id>'
586

    
587
    def execute(self, server_id, network_id):
588
        path = '/api/%s/networks/%d/action' % (self.api, int(network_id))
589
        body = json.dumps({'add': {'serverRef': server_id}})
590
        self.http_post(path, body, expected_status=202)
591

    
592

    
593
@command_name('disconnect')
594
class RemoveNetwork(Command):
595
    description = 'disconnect a server from a network'
596
    syntax = '<server id> <network id>'
597

    
598
    def execute(self, server_id, network_id):
599
        path = '/api/%s/networks/%s/action' % (self.api, int(network_id))
600
        body = json.dumps({'remove': {'serverRef': server_id}})
601
        self.http_post(path, body, expected_status=202)
602

    
603

    
604
def print_usage():
605
    print 'Usage: %s <command>' % basename(argv[0])
606
    print
607
    print 'Commands:'
608
    for name, cls in sorted(commands.items()):
609
        description = getattr(cls, 'description', '')
610
        print '  %s %s' % (name.ljust(12), description)
611

    
612
def main():
613
    try:
614
        name = argv[1]    
615
        cls = commands[name]
616
    except (IndexError, KeyError):
617
        print_usage()
618
        exit(1)
619
    
620
    try:
621
        cls(argv[2:])
622
    except TypeError:
623
        syntax = getattr(cls, 'syntax', '')
624
        if syntax:
625
            print 'Syntax: %s %s' % (name, syntax)
626
        else:
627
            print 'Invalid syntax'
628
        exit(1)
629

    
630

    
631
if __name__ == '__main__':
632
    main()