Statistics
| Branch: | Tag: | Revision:

root / kamaki / kamaki.py @ 76f01c50

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

    
73

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

    
79

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

    
101

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

    
109

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

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

    
210

    
211
# Server Group
212

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

    
226

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

    
237

    
238
class CreateServer(Command):
239
    group = 'server'
240
    name = 'create'
241
    syntax = '<server name>'
242
    description = 'create server'
243

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

    
273

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

    
283

    
284
class DeleteServer(Command):
285
    group = 'server'
286
    name = 'delete'
287
    syntax = '<server id>'
288
    description = 'delete server'
289
    
290
    def main(self, server_id):
291
        self.client.delete_server(int(server_id))
292

    
293

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

    
307

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

    
318

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

    
329

    
330
class ServerConsole(Command):
331
    api = 'synnefo'
332
    group = 'server'
333
    name = 'console'
334
    syntax = '<server id>'
335
    description = 'get VNC console'
336

    
337
    def main(self, server_id):
338
        reply = self.client.get_server_console(int(server_id))
339
        print_dict(reply)
340

    
341

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

    
352

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

    
364

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

    
375

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

    
386

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

    
397

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

    
407

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

    
419

    
420
# Flavor Group
421

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

    
431
    def main(self):
432
        flavors = self.client.list_flavors(self.detail)
433
        print_items(flavors, self.detail)
434

    
435

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

    
446

    
447
class ListImages(Command):
448
    group = 'image'
449
    name = 'list'
450
    description = 'list images'
451

    
452
    def add_options(self, parser):
453
        parser.add_option('-l', action='store_true', dest='detail',
454
                            default=False, help='show detailed output')
455

    
456
    def main(self):
457
        images = self.client.list_images(self.detail)
458
        print_items(images, self.detail)
459

    
460

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

    
471

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

    
482

    
483
class DeleteImage(Command):
484
    group = 'image'
485
    name = 'delete'
486
    syntax = '<image id>'
487
    description = 'delete image'
488
    
489
    def main(self, image_id):
490
        self.client.delete_image(int(image_id))
491

    
492

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

    
503

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

    
514

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

    
525

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

    
535

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

    
550

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

    
562

    
563
class GetNetworkDetails(Command):
564
    api = 'synnefo'
565
    group = 'network'
566
    name = 'info'
567
    syntax = '<network id>'
568
    description = 'get network details'
569

    
570
    def main(self, network_id):
571
        network = self.client.get_network_details(network_id)
572
        print_dict(network)
573

    
574

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

    
585

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

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

    
606

    
607
class DisconnectServer(Command):
608
    api = 'synnefo'
609
    group = 'network'
610
    name = 'disconnect'
611
    syntax = '<server id> <network id>'
612
    description = 'disconnect a server from a network'
613

    
614
    def main(self, server_id, network_id):
615
        self.client.disconnect_server(server_id, network_id)
616

    
617

    
618

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

    
637

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

    
669

    
670
if __name__ == '__main__':
671
    main()