Statistics
| Branch: | Tag: | Revision:

root / snf-tools / cloud @ fe29fb25

History | View | Annotate | Download (20.1 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, 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
DEFAULT_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('--apiurl',
88
                            dest='apiurl',
89
                            metavar='URL',
90
                            default=DEFAULT_API_URL,
91
                            help='use api API')
92
        parser.add_option('--token',
93
                            dest='token',
94
                            metavar='TOKEN',
95
                            default=DEFAULT_TOKEN,
96
                            help='use user token TOKEN')
97
        parser.add_option('-v',
98
                            action='store_true',
99
                            dest='verbose',
100
                            default=False,
101
                            help='use verbose output')
102
        self.add_options(parser)
103
        options, args = parser.parse_args(argv)
104
        
105
        # Add options to self
106
        for opt in parser.option_list:
107
            key = opt.dest
108
            if key:
109
                val = getattr(options, key)
110
                setattr(self, key, val)
111
        
112
        self.execute(*args)
113
    
114
    def add_options(self, parser):
115
        pass
116
    
117
    def execute(self, *args):
118
        pass
119
    
120
    def http_cmd(self, method, path, body=None, expected_status=200):
121
        p = urlparse(self.apiurl)
122
        if p.scheme == 'https':
123
            conn = HTTPSConnection(p.netloc)
124
        else:
125
            conn = HTTPConnection(p.netloc)
126

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

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

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

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

    
164
        return reply
165

    
166
    def http_get(self, path, expected_status=200):
167
        return self.http_cmd('GET', path, None, expected_status)
168
    
169
    def http_post(self, path, body, expected_status=202):
170
        return self.http_cmd('POST', path, body, expected_status)
171

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

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

    
178

    
179
@command_name('ls')
180
class ListServers(Command):
181
    description = 'list servers'
182
    
183
    def add_options(self, parser):
184
        parser.add_option('-l', action='store_true', dest='detail', default=False,
185
                            help='show detailed output')
186
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
187
                            help='include empty values')
188
    
189
    def execute(self):
190
        path = '/servers/detail' if self.detail else '/servers'
191
        reply = self.http_get(path)
192

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

    
203

    
204
@command_name('info')
205
class GetServerDetails(Command):
206
    description = 'get server details'
207
    syntax = '<server id>'
208
    
209
    def add_options(self, parser):
210
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
211
                            help='include empty values')
212
    
213
    def execute(self, server_id):
214
        path = '/servers/%d' % int(server_id)
215
        reply = self.http_get(path)
216
        server = reply['server']
217
        server.pop('id')
218
        print_dict(server, self.show_empty)
219
        
220

    
221
@command_name('create')
222
class CreateServer(Command):
223
    description = 'create server'
224
    syntax = '<server name>'
225
    
226
    def add_options(self, parser):
227
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
228
                            help='use flavor FLAVOR_ID')
229
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
230
                            help='use image IMAGE_ID')
231
    
232
    def execute(self, name):
233
        server = {
234
            'name': name,
235
            'flavorRef': self.flavor,
236
            'imageRef': self.image}
237
        body = json.dumps({'server': server})
238
        reply = self.http_post('/servers', body)
239
        server = reply['server']
240
        print_dict(server)
241

    
242

    
243
@command_name('rename')
244
class UpdateServerName(Command):
245
    description = 'update server name'
246
    syntax = '<server id> <new name>'
247
    
248
    def execute(self, server_id, name):
249
        path = '/servers/%d' % int(server_id)
250
        body = json.dumps({'server': {'name': name}})
251
        self.http_put(path, body)
252

    
253

    
254
@command_name('delete')
255
class DeleteServer(Command):
256
    description = 'delete server'
257
    syntax = '<server id>'
258
    
259
    def execute(self, server_id):
260
        path = '/servers/%d' % int(server_id)
261
        self.http_delete(path)
262

    
263

    
264
@command_name('reboot')
265
class RebootServer(Command):
266
    description = 'reboot server'
267
    syntax = '<server id>'
268
    
269
    def add_options(self, parser):
270
        parser.add_option('-f', action='store_true', dest='hard', default=False,
271
                            help='perform a hard reboot')
272
    
273
    def execute(self, server_id):
274
        path = '/servers/%d/action' % int(server_id)
275
        type = 'HARD' if self.hard else 'SOFT'
276
        body = json.dumps({'reboot': {'type': type}})
277
        self.http_post(path, body)
278
    
279

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

    
290

    
291
@command_name('shutdown')
292
class StartServer(Command):
293
    description = 'shutdown server'
294
    syntax = '<server id>'
295
    
296
    def execute(self, server_id):
297
        path = '/servers/%d/action' % int(server_id)
298
        body = json.dumps({'shutdown': {}})
299
        self.http_post(path, body)
300

    
301

    
302
@command_name('console')
303
class ServerConsole(Command):
304
    description = 'get VNC console'
305
    syntax = '<server id>'
306
    
307
    def execute(self, server_id):
308
        path = '/servers/%d/action' % int(server_id)
309
        body = json.dumps({'console': {'type': 'vnc'}})
310
        reply = self.http_cmd('POST', path, body, 200)
311
        print_dict(reply['console'])
312

    
313

    
314
@command_name('profile')
315
class SetFirewallProfile(Command):
316
    description = 'set the firewall profile'
317
    syntax = '<server id> <profile>'
318
    
319
    def execute(self, server_id, profile):
320
        path = '/servers/%d/action' % int(server_id)
321
        body = json.dumps({'firewallProfile': {'profile': profile}})
322
        self.http_cmd('POST', path, body, 202)
323

    
324

    
325
@command_name('lsaddr')
326
class ListAddresses(Command):
327
    description = 'list server addresses'
328
    syntax = '<server id> [network]'
329
    
330
    def execute(self, server_id, network=None):
331
        path = '/servers/%d/ips' % int(server_id)
332
        if network:
333
            path += '/%s' % network
334
        reply = self.http_get(path)
335
        
336
        addresses = [reply['network']] if network else reply['addresses']['values']
337
        print_addresses(addresses)
338

    
339

    
340
@command_name('lsflv')
341
class ListFlavors(Command):
342
    description = 'list flavors'
343
    
344
    def add_options(self, parser):
345
        parser.add_option('-l', action='store_true', dest='detail', default=False,
346
                            help='show detailed output')
347
    
348
    def execute(self):
349
        path = '/flavors/detail' if self.detail else '/flavors'
350
        reply = self.http_get(path)
351
        
352
        for flavor in reply['flavors']['values']:
353
            id = flavor.pop('id')
354
            name = flavor.pop('name')
355
            details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
356
            print '%3d %s %s' % (id, name, details)
357

    
358

    
359
@command_name('flvinfo')
360
class GetFlavorDetails(Command):
361
    description = 'get flavor details'
362
    syntax = '<flavor id>'
363
    
364
    def execute(self, flavor_id):
365
        path = '/flavors/%d' % int(flavor_id)
366
        reply = self.http_get(path)
367
        
368
        flavor = reply['flavor']
369
        id = flavor.pop('id')
370
        name = flavor.pop('name')
371
        details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
372
        print '%3d %s %s' % (id, name, details)
373

    
374

    
375
@command_name('lsimg')
376
class ListImages(Command):
377
    description = 'list images'
378
    
379
    def add_options(self, parser):
380
        parser.add_option('-l', action='store_true', dest='detail', default=False,
381
                            help='show detailed output')
382
    
383
    def execute(self):
384
        path = '/images/detail' if self.detail else '/images'
385
        reply = self.http_get(path)
386
        
387
        for image in reply['images']['values']:
388
            id = image.pop('id')
389
            name = image.pop('name')
390
            if self.detail:
391
                print '%d %s' % (id, name)
392
                print_dict(image)
393
                print
394
            else:
395
                print '%3d %s' % (id, name)
396

    
397

    
398
@command_name('imginfo')
399
class GetImageDetails(Command):
400
    description = 'get image details'
401
    syntax = '<image id>'
402
    
403
    def execute(self, image_id):
404
        path = '/images/%d' % int(image_id)
405
        reply = self.http_get(path)
406
        image = reply['image']
407
        image.pop('id')
408
        print_dict(image)
409

    
410

    
411
@command_name('createimg')
412
class CreateImage(Command):
413
    description = 'create image'
414
    syntax = '<server id> <image name>'
415
    
416
    def execute(self, server_id, name):
417
        image = {'name': name, 'serverRef': int(server_id)}
418
        body = json.dumps({'image': image})
419
        reply = self.http_post('/images', body)
420
        print_dict(reply['image'])
421

    
422
@command_name('deleteimg')
423
class DeleteImage(Command):
424
    description = 'delete image'
425
    syntax = '<image id>'
426
    
427
    def execute(self, image_id):
428
        path = '/images/%d' % int(image_id)
429
        self.http_delete(path)
430

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
527

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

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

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

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

    
550

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

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

    
562

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

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

    
575

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

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

    
586

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

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

    
596

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

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

    
607

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

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

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

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

    
630

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

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

    
657

    
658
if __name__ == '__main__':
659
    main()