Statistics
| Branch: | Tag: | Revision:

root / tools / cloud @ 5039a44f

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
MARGIN = 14
49

    
50
commands = {}
51

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

    
58

    
59
def print_addresses(networks):
60
    for i, net in enumerate(networks):
61
        key = 'addresses:'.rjust(MARGIN + 1) if i == 0 else ' ' * (MARGIN + 1)
62
        addr = ''
63
        if 'values' in net:
64
            addr = '[%s]' % ' '.join(ip['addr'] for ip in net['values'])
65
        
66
        val = '%s/%s %s %s' % (net['id'], net['name'], net['mac'], addr)
67
        if 'firewallProfile' in net:
68
            val += ' - %s' % net['firewallProfile']
69
        print '%s %s' % (key, val)
70

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

    
83

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

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

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

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

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

    
151
        return reply
152

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

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

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

    
165

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

    
181
        reply = self.http_get(path)
182

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

    
193

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

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

    
230

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

    
241

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

    
251

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

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

    
278

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

    
289

    
290
@command_name('console')
291
class ServerConsole(Command):
292
    description = 'get VNC console'
293
    syntax = '<server id>'
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('profile')
303
class SetFirewallProfile(Command):
304
    description = 'set the firewall profile'
305
    syntax = '<server id> <profile>'
306
    
307
    def execute(self, server_id, profile):
308
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
309
        body = json.dumps({'firewallProfile': {'profile': profile}})
310
        self.http_cmd('POST', path, body, 202)
311

    
312

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

    
327

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

    
348

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

    
364

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

    
389

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

    
402

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
520

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

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

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

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

    
545

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

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

    
558

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

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

    
571

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

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

    
582

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

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

    
592

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

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

    
603

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

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

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

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

    
626

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

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

    
653

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