Statistics
| Branch: | Tag: | Revision:

root / tools / cloud @ c738c935

History | View | Annotate | Download (20.3 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
        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(12), val)
81

    
82

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

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

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

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

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

    
150
        return reply
151

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

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

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

    
164

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

    
180
        reply = self.http_get(path)
181

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

    
192

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

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

    
229

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

    
240

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

    
250

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

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

    
277

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

    
288

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

    
300

    
301
@command_name('profile')
302
class SetFirewallProfile(Command):
303
    description = 'set the firewall profile'
304
    syntax = '<server id> <profile>'
305
    
306
    def execute(self, server_id, profile):
307
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
308
        body = json.dumps({'firewallProfile': {'profile': profile}})
309
        self.http_cmd('POST', path, body, 202)
310

    
311

    
312
@command_name('lsaddr')
313
class ListAddresses(Command):
314
    description = 'list server addresses'
315
    syntax = '<server id> [network]'
316
    
317
    def execute(self, server_id, network=None):
318
        path = '/api/%s/servers/%d/ips' % (self.api, int(server_id))
319
        if network:
320
            path += '/%s' % network
321
        reply = self.http_get(path)
322
        
323
        addresses = [reply['network']] if network else reply['addresses']['values']
324
        print_addresses(addresses)
325

    
326

    
327
@command_name('lsflv')
328
class ListFlavors(Command):
329
    description = 'list flavors'
330
    
331
    def add_options(self, parser):
332
        parser.add_option('-l', action='store_true', dest='detail', default=False,
333
                            help='show detailed output')
334
    
335
    def execute(self):
336
        path = '/api/%s/flavors' % self.api
337
        if self.detail:
338
            path += '/detail'
339
        reply = self.http_get(path)
340
        
341
        for flavor in reply['flavors']['values']:
342
            id = flavor.pop('id')
343
            name = flavor.pop('name')
344
            details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
345
            print '%3d %s %s' % (id, name, details)
346

    
347

    
348
@command_name('flvinfo')
349
class GetFlavorDetails(Command):
350
    description = 'get flavor details'
351
    syntax = '<flavor id>'
352
    
353
    def execute(self, flavor_id):
354
        path = '/api/%s/flavors/%d' % (self.api, int(flavor_id))
355
        reply = self.http_get(path)
356
        
357
        flavor = reply['flavor']
358
        id = flavor.pop('id')
359
        name = flavor.pop('name')
360
        details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
361
        print '%3d %s %s' % (id, name, details)
362

    
363

    
364
@command_name('lsimg')
365
class ListImages(Command):
366
    description = 'list images'
367
    
368
    def add_options(self, parser):
369
        parser.add_option('-l', action='store_true', dest='detail', default=False,
370
                            help='show detailed output')
371
    
372
    def execute(self):
373
        path = '/api/%s/images' % self.api
374
        if self.detail:
375
            path += '/detail'
376
        reply = self.http_get(path)
377
        
378
        for image in reply['images']['values']:
379
            id = image.pop('id')
380
            name = image.pop('name')
381
            if self.detail:
382
                print '%d %s' % (id, name)
383
                print_dict(image)
384
                print
385
            else:
386
                print '%3d %s' % (id, name)
387

    
388

    
389
@command_name('imginfo')
390
class GetImageDetails(Command):
391
    description = 'get image details'
392
    syntax = '<image id>'
393
    
394
    def execute(self, image_id):
395
        path = '/api/%s/images/%d' % (self.api, int(image_id))
396
        reply = self.http_get(path)
397
        image = reply['image']
398
        image.pop('id')
399
        print_dict(image)
400

    
401

    
402
@command_name('createimg')
403
class CreateImage(Command):
404
    description = 'create image'
405
    syntax = '<server id> <image name>'
406
    
407
    def execute(self, server_id, name):
408
        path = '/api/%s/images' % self.api
409
        image = {'name': name, 'serverRef': int(server_id)}
410
        body = json.dumps({'image': image})
411
        reply = self.http_post(path, body)
412
        print_dict(reply['image'])
413

    
414
@command_name('deleteimg')
415
class DeleteImage(Command):
416
    description = 'delete image'
417
    syntax = '<image id>'
418
    
419
    def execute(self, image_id):
420
        path = '/api/%s/images/%d' % (self.api, int(image_id))
421
        self.http_delete(path)
422

    
423
@command_name('lsmeta')
424
class ListServerMeta(Command):
425
    description = 'list server meta'
426
    syntax = '<server id> [key]'
427

    
428
    def execute(self, server_id, key=None):
429
        path = '/api/%s/servers/%d/meta' % (self.api, int(server_id))
430
        if key:
431
            path += '/' + key
432
        reply = self.http_get(path)
433
        if key:
434
            print_dict(reply['meta'])
435
        else:
436
            print_dict(reply['metadata']['values'])
437

    
438
@command_name('setmeta')
439
class UpdateServerMeta(Command):
440
    description = 'update server meta'
441
    syntax = '<server id> <key> <val>'
442

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

    
450
@command_name('addmeta')
451
class CreateServerMeta(Command):
452
    description = 'add server meta'
453
    syntax = '<server id> <key> <val>'
454

    
455
    def execute(self, server_id, key, val):
456
        path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key)
457
        meta = {key: val}
458
        body = json.dumps({'meta': meta})
459
        reply = self.http_put(path, body, expected_status=201)
460
        print_dict(reply['meta'])
461

    
462
@command_name('delmeta')
463
class DeleteServerMeta(Command):
464
    description = 'delete server meta'
465
    syntax = '<server id> <key>'
466

    
467
    def execute(self, server_id, key):
468
        path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key)
469
        reply = self.http_delete(path)
470

    
471
@command_name('lsimgmeta')
472
class ListImageMeta(Command):
473
    description = 'list image meta'
474
    syntax = '<image id> [key]'
475

    
476
    def execute(self, image_id, key=None):
477
        path = '/api/%s/images/%d/meta' % (self.api, int(image_id))
478
        if key:
479
            path += '/' + key
480
        reply = self.http_get(path)
481
        if key:
482
            print_dict(reply['meta'])
483
        else:
484
            print_dict(reply['metadata']['values'])
485

    
486
@command_name('setimgmeta')
487
class UpdateImageMeta(Command):
488
    description = 'update image meta'
489
    syntax = '<image id> <key> <val>'
490

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

    
498
@command_name('addimgmeta')
499
class CreateImageMeta(Command):
500
    description = 'add image meta'
501
    syntax = '<image id> <key> <val>'
502

    
503
    def execute(self, image_id, key, val):
504
        path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key)
505
        meta = {key: val}
506
        body = json.dumps({'meta': meta})
507
        reply = self.http_put(path, body, expected_status=201)
508
        print_dict(reply['meta'])
509

    
510
@command_name('delimgmeta')
511
class DeleteImageMeta(Command):
512
    description = 'delete image meta'
513
    syntax = '<image id> <key>'
514

    
515
    def execute(self, image_id, key):
516
        path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key)
517
        reply = self.http_delete(path)
518

    
519

    
520
@command_name('lsnet')
521
class ListNetworks(Command):
522
    description = 'list networks'
523

    
524
    def add_options(self, parser):
525
        parser.add_option('-l', action='store_true', dest='detail', default=False,
526
                            help='show detailed output')
527

    
528
    def execute(self):
529
        path = '/api/%s/networks' % self.api
530
        if self.detail:
531
            path += '/detail'
532
        reply = self.http_get(path)
533

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

    
544

    
545
@command_name('createnet')
546
class CreateNetwork(Command):
547
    description = 'create network'
548
    syntax = '<network name>'
549

    
550
    def execute(self, name):
551
        path = '/api/%s/networks' % self.api
552
        network = {'name': name}
553
        body = json.dumps({'network': network})
554
        reply = self.http_post(path, body)
555
        print_dict(reply['network'])
556

    
557

    
558
@command_name('netinfo')
559
class GetNetworkDetails(Command):
560
    description = 'get network details'
561
    syntax = '<network id>'
562

    
563
    def execute(self, network_id):
564
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
565
        reply = self.http_get(path)
566
        net = reply['network']
567
        name = net.pop('id')
568
        print_dict(net)
569

    
570

    
571
@command_name('renamenet')
572
class UpdateNetworkName(Command):
573
    description = 'update network name'
574
    syntax = '<network_id> <new name>'
575

    
576
    def execute(self, network_id, name):
577
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
578
        body = json.dumps({'network': {'name': name}})
579
        self.http_put(path, body)
580

    
581

    
582
@command_name('deletenet')
583
class DeleteNetwork(Command):
584
    description = 'delete network'
585
    syntax = '<network id>'
586

    
587
    def execute(self, network_id):
588
        path = '/api/%s/networks/%d' % (self.api, int(network_id))
589
        self.http_delete(path)
590

    
591

    
592
@command_name('connect')
593
class AddNetwork(Command):
594
    description = 'connect a server to a network'
595
    syntax = '<server id> <network id>'
596

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

    
602

    
603
@command_name('disconnect')
604
class RemoveNetwork(Command):
605
    description = 'disconnect a server from a network'
606
    syntax = '<server id> <network id>'
607

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

    
613
@command_name('stats')
614
class ServerStats(Command):
615
    description = 'get server stats'
616
    syntax = '<server id>'
617

    
618
    def execute(self, server_id):
619
        path = '/api/%s/servers/%d/stats' % (self.api, int(server_id))
620
        reply = self.http_get(path)
621
        stats = reply['stats']
622
        stats.pop('serverRef')
623
        print_dict(stats)
624

    
625

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

    
634
def main():
635
    try:
636
        name = argv[1]    
637
        cls = commands[name]
638
    except (IndexError, KeyError):
639
        print_usage()
640
        exit(1)
641
    
642
    try:
643
        cls(argv[2:])
644
    except TypeError:
645
        syntax = getattr(cls, 'syntax', '')
646
        if syntax:
647
            print 'Syntax: %s %s' % (name, syntax)
648
        else:
649
            print 'Invalid syntax'
650
        exit(1)
651

    
652

    
653
if __name__ == '__main__':
654
    main()