Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / tools / cloud.py @ 9c0ac5af

History | View | Annotate | Download (19.8 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

    
47
MARGIN = 14
48

    
49
commands = {}
50

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

    
57

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

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

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

    
82

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

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

    
110
        self.execute(*args)
111

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

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

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

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

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

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

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

    
162
        return reply
163

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

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

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

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

    
176

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

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

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

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

    
201

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

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

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

    
218

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

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

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

    
240

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

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

    
251

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

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

    
261

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

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

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

    
277

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

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

    
288

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

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

    
299

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

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

    
311

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

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

    
322

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

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

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

    
337

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

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

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

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

    
356

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

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

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

    
372

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

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

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

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

    
395

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

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

    
408

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
525

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

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

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

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

    
548

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

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

    
560

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

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

    
573

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

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

    
584

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

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

    
594

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

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

    
605

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

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

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

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

    
628

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

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

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

    
655

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