Statistics
| Branch: | Tag: | Revision:

root / kamaki / kamaki.py @ 653b0597

History | View | Annotate | Download (18.6 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
import inspect
37
import logging
38
import os
39
import sys
40

    
41
from collections import defaultdict
42
from optparse import OptionParser
43

    
44
from client import Client, ClientError
45

    
46

    
47
API_ENV = 'KAMAKI_API'
48
URL_ENV = 'KAMAKI_URL'
49
TOKEN_ENV = 'KAMAKI_TOKEN'
50
RCFILE = '.kamakirc'
51

    
52

    
53
log = logging.getLogger('kamaki')
54

    
55

    
56
def print_addresses(addresses, margin):
57
    for address in addresses:
58
        if address['id'] == 'public':
59
            net = 'public'
60
        else:
61
            net = '%s/%s' % (address['id'], address['name'])
62
        print '%s:' % net.rjust(margin + 4)
63
        
64
        ether = address.get('mac', None)
65
        if ether:
66
            print '%s: %s' % ('ether'.rjust(margin + 8), ether)
67
        
68
        firewall = address.get('firewallProfile', None)
69
        if firewall:
70
            print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
71
        
72
        for ip in address.get('values', []):
73
            key = 'inet' if ip['version'] == 4 else 'inet6'
74
            print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
75

    
76

    
77
def print_metadata(metadata, margin):
78
    print '%s:' % 'metadata'.rjust(margin)
79
    for key, val in metadata.get('values', {}).items():
80
        print '%s: %s' % (key.rjust(margin + 4), val)
81

    
82

    
83
def print_dict(d, exclude=()):
84
    if not d:
85
        return
86
    margin = max(len(key) for key in d) + 1
87
    
88
    for key, val in sorted(d.items()):
89
        if key in exclude:
90
            continue
91
        
92
        if key == 'addresses':
93
            print '%s:' % 'addresses'.rjust(margin)
94
            print_addresses(val.get('values', []), margin)
95
            continue
96
        elif key == 'metadata':
97
            print_metadata(val, margin)
98
            continue
99
        elif key == 'servers':
100
            val = ', '.join(str(x) for x in val['values'])
101
        
102
        print '%s: %s' % (key.rjust(margin), val)
103

    
104

    
105
def print_items(items, detail=False):
106
    for item in items:
107
        print '%s %s' % (item['id'], item.get('name', ''))
108
        if detail:
109
            print_dict(item, exclude=('id', 'name'))
110
            print
111

    
112

    
113
class Command(object):
114
    """Abstract class.
115
    
116
    All commands should subclass this class.
117
    """
118
    
119
    api = 'openstack'
120
    group = '<group>'
121
    name = '<command>'
122
    syntax = ''
123
    description = ''
124
    
125
    def __init__(self, argv):
126
        self._init_parser(argv)
127
        self._init_logging()
128
        self._init_conf()
129
        if self.name != '<command>':
130
            self.client = Client(self.url, self.token)
131
    
132
    def _init_parser(self, argv):
133
        parser = OptionParser()
134
        parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name,
135
                                                        self.syntax)
136
        parser.add_option('--api', dest='api', metavar='API',
137
                            help='API can be either openstack or synnefo')
138
        parser.add_option('--url', dest='url', metavar='URL',
139
                            help='API URL')
140
        parser.add_option('--token', dest='token', metavar='TOKEN',
141
                            help='use token TOKEN')
142
        parser.add_option('-v', action='store_true', dest='verbose',
143
                            default=False, help='use verbose output')
144
        parser.add_option('-d', action='store_true', dest='debug',
145
                            default=False, help='use debug output')
146
        
147
        self.add_options(parser)
148
        
149
        options, args = parser.parse_args(argv)
150
        
151
        # Add options to self
152
        for opt in parser.option_list:
153
            key = opt.dest
154
            if key:
155
                val = getattr(options, key)
156
                setattr(self, key, val)
157
        
158
        self.args = args
159
        self.parser = parser
160
    
161
    def _init_logging(self):
162
        if self.debug:
163
            log.setLevel(logging.DEBUG)
164
        elif self.verbose:
165
            log.setLevel(logging.INFO)
166
        else:
167
            log.setLevel(logging.WARNING)
168
        
169
    def _init_conf(self):
170
        if not self.api:
171
            self.api = os.environ.get(API_ENV, None)
172
        if not self.url:
173
            self.url = os.environ.get(URL_ENV, None)
174
        if not self.token:
175
            self.token = os.environ.get(TOKEN_ENV, None)
176
        
177
        path = os.path.join(os.path.expanduser('~'), RCFILE)
178
        if not os.path.exists(path):
179
            return
180

    
181
        for line in open(path):
182
            key, sep, val = line.partition('=')
183
            if not sep:
184
                continue
185
            key = key.strip().lower()
186
            val = val.strip()
187
            if key == 'api' and not self.api:
188
                self.api = val
189
            elif key == 'url' and not self.url:
190
                self.url = val
191
            elif key == 'token' and not self.token:
192
                self.token = val
193
    
194
    def add_options(self, parser):
195
        pass
196
    
197
    def main(self, *args):
198
        pass
199
    
200
    def execute(self):
201
        try:
202
            self.main(*self.args)
203
        except TypeError:
204
            self.parser.print_help()
205

    
206

    
207
# Server Group
208

    
209
class ListServers(Command):
210
    group = 'server'
211
    name = 'list'
212
    description = 'list servers'
213
    
214
    def add_options(self, parser):
215
        parser.add_option('-l', action='store_true', dest='detail',
216
                            default=False, help='show detailed output')
217
    
218
    def main(self):
219
        servers = self.client.list_servers(self.detail)
220
        print_items(servers, self.detail)
221

    
222

    
223
class GetServerDetails(Command):
224
    group = 'server'
225
    name = 'info'
226
    syntax = '<server id>'
227
    description = 'get server details'
228
    
229
    def main(self, server_id):
230
        server = self.client.get_server_details(int(server_id))
231
        print_dict(server)
232

    
233

    
234
class CreateServer(Command):
235
    group = 'server'
236
    name = 'create'
237
    syntax = '<server name>'
238
    description = 'create server'
239

    
240
    def add_options(self, parser):
241
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
242
                            help='use flavor FLAVOR_ID')
243
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
244
                            help='use image IMAGE_ID')
245
        parser.add_option('--personality', dest='personality', action='append',
246
                            metavar='PATH,SERVERPATH',
247
                            help='add a personality file')
248
    
249
    def main(self, name):
250
        flavor_id = int(self.flavor)
251
        image_id = int(self.image)
252
        personality = []
253
        if self.personality:
254
            for p in self.personality:
255
                lpath, sep, rpath = p.partition(',')
256
                if not lpath or not rpath:
257
                    log.error("Invalid personality argument '%s'", p)
258
                    return
259
                if not os.path.exists(lpath):
260
                    log.error("File %s does not exist", lpath)
261
                    return
262
                with open(lpath) as f:
263
                    personality.append((rpath, f.read()))
264
        
265
        reply = self.client.create_server(name, flavor_id, image_id,
266
                                            personality)
267
        print_dict(reply)
268

    
269

    
270
class UpdateServerName(Command):
271
    group = 'server'
272
    name = 'rename'
273
    syntax = '<server id> <new name>'
274
    description = 'update server name'
275
    
276
    def main(self, server_id, new_name):
277
        self.client.update_server_name(int(server_id), new_name)
278

    
279

    
280
class DeleteServer(Command):
281
    group = 'server'
282
    name = 'delete'
283
    syntax = '<server id>'
284
    description = 'delete server'
285
    
286
    def main(self, server_id):
287
        self.client.delete_server(int(server_id))
288

    
289

    
290
class RebootServer(Command):
291
    group = 'server'
292
    name = 'reboot'
293
    syntax = '<server id>'
294
    description = 'reboot server'
295
    
296
    def add_options(self, parser):
297
        parser.add_option('-f', action='store_true', dest='hard',
298
                            default=False, help='perform a hard reboot')
299
    
300
    def main(self, server_id):
301
        self.client.reboot_server(int(server_id), self.hard)
302

    
303

    
304
class StartServer(Command):
305
    api = 'synnefo'
306
    group = 'server'
307
    name = 'start'
308
    syntax = '<server id>'
309
    description = 'start server'
310
    
311
    def main(self, server_id):
312
        self.client.start_server(int(server_id))
313

    
314

    
315
class StartServer(Command):
316
    api = 'synnefo'
317
    group = 'server'
318
    name = 'shutdown'
319
    syntax = '<server id>'
320
    description = 'shutdown server'
321
    
322
    def main(self, server_id):
323
        self.client.shutdown_server(int(server_id))
324

    
325

    
326
class ServerConsole(Command):
327
    api = 'synnefo'
328
    group = 'server'
329
    name = 'console'
330
    syntax = '<server id>'
331
    description = 'get VNC console'
332

    
333
    def main(self, server_id):
334
        reply = self.client.get_server_console(int(server_id))
335
        print_dict(reply)
336

    
337

    
338
class SetFirewallProfile(Command):
339
    api = 'synnefo'
340
    group = 'server'
341
    name = 'firewall'
342
    syntax = '<server id> <profile>'
343
    description = 'set the firewall profile'
344
    
345
    def main(self, server_id, profile):
346
        self.client.set_firewall_profile(int(server_id), profile)
347

    
348

    
349
class ListAddresses(Command):
350
    group = 'server'
351
    name = 'addr'
352
    syntax = '<server id> [network]'
353
    description = 'list server addresses'
354
    
355
    def main(self, server_id, network=None):
356
        reply = self.client.list_server_addresses(int(server_id), network)
357
        margin = max(len(x['name']) for x in reply)
358
        print_addresses(reply, margin)
359

    
360

    
361
class GetServerMeta(Command):
362
    group = 'server'
363
    name = 'meta'
364
    syntax = '<server id> [key]'
365
    description = 'get server metadata'
366
    
367
    def main(self, server_id, key=None):
368
        reply = self.client.get_server_metadata(int(server_id), key)
369
        print_dict(reply)
370

    
371

    
372
class CreateServerMetadata(Command):
373
    group = 'server'
374
    name = 'addmeta'
375
    syntax = '<server id> <key> <val>'
376
    description = 'add server metadata'
377
    
378
    def main(self, server_id, key, val):
379
        reply = self.client.create_server_metadata(int(server_id), key, val)
380
        print_dict(reply)
381

    
382

    
383
class UpdateServerMetadata(Command):
384
    group = 'server'
385
    name = 'setmeta'
386
    syntax = '<server id> <key> <val>'
387
    description = 'update server metadata'
388
    
389
    def main(self, server_id, key, val):
390
        reply = self.client.update_server_metadata(int(server_id), key, val)
391
        print_dict(reply)
392

    
393

    
394
class DeleteServerMetadata(Command):
395
    group = 'server'
396
    name = 'delmeta'
397
    syntax = '<server id> <key>'
398
    description = 'delete server metadata'
399
    
400
    def main(self, server_id, key):
401
        self.client.delete_server_metadata(int(server_id), key)
402

    
403

    
404
class GetServerStats(Command):
405
    api = 'synnefo'
406
    group = 'server'
407
    name = 'stats'
408
    syntax = '<server id>'
409
    description = 'get server statistics'
410
    
411
    def main(self, server_id):
412
        reply = self.client.get_server_stats(int(server_id))
413
        print_dict(reply, exclude=('serverRef',))
414

    
415

    
416
# Flavor Group
417

    
418
class ListFlavors(Command):
419
    group = 'flavor'
420
    name = 'list'
421
    description = 'list flavors'
422
    
423
    def add_options(self, parser):
424
        parser.add_option('-l', action='store_true', dest='detail',
425
                            default=False, help='show detailed output')
426

    
427
    def main(self):
428
        flavors = self.client.list_flavors(self.detail)
429
        print_items(flavors, self.detail)
430

    
431

    
432
class GetFlavorDetails(Command):
433
    group = 'flavor'
434
    name = 'info'
435
    syntax = '<flavor id>'
436
    description = 'get flavor details'
437
    
438
    def main(self, flavor_id):
439
        flavor = self.client.get_flavor_details(int(flavor_id))
440
        print_dict(flavor)
441

    
442

    
443
class ListImages(Command):
444
    group = 'image'
445
    name = 'list'
446
    description = 'list images'
447

    
448
    def add_options(self, parser):
449
        parser.add_option('-l', action='store_true', dest='detail',
450
                            default=False, help='show detailed output')
451

    
452
    def main(self):
453
        images = self.client.list_images(self.detail)
454
        print_items(images, self.detail)
455

    
456

    
457
class GetImageDetails(Command):
458
    group = 'image'
459
    name = 'info'
460
    syntax = '<image id>'
461
    description = 'get image details'
462
    
463
    def main(self, image_id):
464
        image = self.client.get_image_details(int(image_id))
465
        print_dict(image)
466

    
467

    
468
class CreateImage(Command):
469
    group = 'image'
470
    name = 'create'
471
    syntax = '<server id> <image name>'
472
    description = 'create image'
473
    
474
    def main(self, server_id, name):
475
        reply = self.client.create_image(int(server_id), name)
476
        print_dict(reply)
477

    
478

    
479
class DeleteImage(Command):
480
    group = 'image'
481
    name = 'delete'
482
    syntax = '<image id>'
483
    description = 'delete image'
484
    
485
    def main(self, image_id):
486
        self.client.delete_image(int(image_id))
487

    
488

    
489
class GetImageMetadata(Command):
490
    group = 'image'
491
    name = 'meta'
492
    syntax = '<image id> [key]'
493
    description = 'get image metadata'
494
    
495
    def main(self, image_id, key=None):
496
        reply = self.client.get_image_metadata(int(image_id), key)
497
        print_dict(reply)
498

    
499

    
500
class CreateImageMetadata(Command):
501
    group = 'image'
502
    name = 'addmeta'
503
    syntax = '<image id> <key> <val>'
504
    description = 'add image metadata'
505
    
506
    def main(self, image_id, key, val):
507
        reply = self.client.create_image_metadata(int(image_id), key, val)
508
        print_dict(reply)
509

    
510

    
511
class UpdateImageMetadata(Command):
512
    group = 'image'
513
    name = 'setmeta'
514
    syntax = '<image id> <key> <val>'
515
    description = 'update image metadata'
516
    
517
    def main(self, image_id, key, val):
518
        reply = self.client.update_image_metadata(int(image_id), key, val)
519
        print_dict(reply)
520

    
521

    
522
class DeleteImageMetadata(Command):
523
    group = 'image'
524
    name = 'delmeta'
525
    syntax = '<image id> <key>'
526
    description = 'delete image metadata'
527
    
528
    def main(self, image_id, key):
529
        self.client.delete_image_metadata(int(image_id), key)
530

    
531

    
532
class ListNetworks(Command):
533
    api = 'synnefo'
534
    group = 'network'
535
    name = 'list'
536
    description = 'list networks'
537
    
538
    def add_options(self, parser):
539
        parser.add_option('-l', action='store_true', dest='detail',
540
                            default=False, help='show detailed output')
541
    
542
    def main(self):
543
        networks = self.client.list_networks(self.detail)
544
        print_items(networks, self.detail)
545

    
546

    
547
class CreateNetwork(Command):
548
    api = 'synnefo'
549
    group = 'network'
550
    name = 'create'
551
    syntax = '<network name>'
552
    description = 'create a network'
553
    
554
    def main(self, name):
555
        reply = self.client.create_network(name)
556
        print_dict(reply)
557

    
558

    
559
class GetNetworkDetails(Command):
560
    api = 'synnefo'
561
    group = 'network'
562
    name = 'info'
563
    syntax = '<network id>'
564
    description = 'get network details'
565

    
566
    def main(self, network_id):
567
        network = self.client.get_network_details(network_id)
568
        print_dict(network)
569

    
570

    
571
class RenameNetwork(Command):
572
    api = 'synnefo'
573
    group = 'network'
574
    name = 'rename'
575
    syntax = '<network id> <new name>'
576
    description = 'update network name'
577
    
578
    def main(self, network_id, name):
579
        self.client.update_network_name(network_id, name)
580

    
581

    
582
class DeleteNetwork(Command):
583
    api = 'synnefo'
584
    group = 'network'
585
    name = 'delete'
586
    syntax = '<network id>'
587
    description = 'delete a network'
588
    
589
    def main(self, network_id):
590
        self.client.delete_network(network_id)
591

    
592
class ConnectServer(Command):
593
    api = 'synnefo'
594
    group = 'network'
595
    name = 'connect'
596
    syntax = '<server id> <network id>'
597
    description = 'connect a server to a network'
598
    
599
    def main(self, server_id, network_id):
600
        self.client.connect_server(server_id, network_id)
601

    
602

    
603
class DisconnectServer(Command):
604
    api = 'synnefo'
605
    group = 'network'
606
    name = 'disconnect'
607
    syntax = '<server id> <network id>'
608
    description = 'disconnect a server from a network'
609

    
610
    def main(self, server_id, network_id):
611
        self.client.disconnect_server(server_id, network_id)
612

    
613

    
614

    
615
def print_usage(exe, groups, group=None):
616
    nop = Command([])
617
    nop.parser.print_help()
618
    
619
    print
620
    print 'Commands:'
621
    
622
    if group:
623
        items = [(group, groups[group])]
624
    else:
625
        items = sorted(groups.items())
626
    
627
    for group, commands in items:
628
        for command, cls in sorted(commands.items()):
629
            name = '  %s %s' % (group, command)
630
            print '%s %s' % (name.ljust(22), cls.description)
631
        print
632

    
633

    
634
def main():
635
    nop = Command([])
636
    groups = defaultdict(dict)
637
    module = sys.modules[__name__]
638
    for name, cls in inspect.getmembers(module, inspect.isclass):
639
        if issubclass(cls, Command) and cls != Command:
640
            if nop.api == 'openstack' and nop.api != cls.api:
641
                continue    # Ignore synnefo commands
642
            groups[cls.group][cls.name] = cls
643
    
644
    argv = list(sys.argv)
645
    exe = os.path.basename(argv.pop(0))
646
    group = argv.pop(0) if argv else None
647
    command = argv.pop(0) if argv else None
648
    
649
    if group not in groups:
650
        group = None
651
    
652
    if not group or command not in groups[group]:
653
        print_usage(exe, groups, group)
654
        sys.exit(1)
655
    
656
    cls = groups[group][command]
657
    
658
    try:
659
        cmd = cls(argv)
660
        cmd.execute()
661
    except ClientError, err:
662
        log.error('%s', err.message)
663
        log.info('%s', err.details)
664

    
665

    
666
if __name__ == '__main__':
667
    ch = logging.StreamHandler()
668
    ch.setFormatter(logging.Formatter('%(message)s'))
669
    log.addHandler(ch)
670
    
671
    main()