Revision eb3ca8ca

b/kamaki/__init__.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
__version__ = '0.3'
b/kamaki/client.py
129 129
    
130 130
    def _delete(self, path, success=204):
131 131
        return self._cmd('DELETE', path, None, success)
132
    
133
    
132

  
133

  
134
class ComputeClient(Client):
134 135
    # Servers
135 136
    
136 137
    def list_servers(self, detail=False):
......
350 351
        path = '/networks/%s/action' % network_id
351 352
        body = json.dumps({'remove': {'serverRef': server_id}})
352 353
        self._post(path, body)
354

  
355

  
356
class ImagesClient(Client):
357
    def list_public(self, detail=False):
358
        path = '/images/detail' if detail else '/images/'
359
        return self._get(path)
b/kamaki/config.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import json
35
import logging
36
import os
37

  
38

  
39
log = logging.getLogger('kamaki.config')
40

  
41

  
42
class ConfigError(Exception):
43
    pass
44

  
45

  
46
class Config(object):
47
    def __init__(self, path, env, defaults):
48
        self.path = os.environ.get(env, path)
49
        self.defaults = defaults
50
        
51
        d = self.read()
52
        for key, val in d.items():
53
            if key not in defaults:
54
                log.warning('Ignoring unknown config key "%s".', key)
55
        
56
        self.d = d
57
        self.overrides = {}
58
    
59
    def read(self):
60
        if not os.path.exists(self.path):
61
            return {}
62
        
63
        with open(self.path) as f:
64
            data = f.read()
65
        
66
        try:
67
            d = json.loads(data)
68
            assert isinstance(d, dict)
69
            return d
70
        except (ValueError, AssertionError):
71
            msg = '"%s" does not look like a kamaki config file.' % self.path
72
            raise ConfigError(msg)
73
    
74
    def write(self):
75
        self.read()     # Make sure we don't overwrite anything wrong
76
        with open(self.path, 'w') as f:
77
            data = json.dumps(self.d, indent=True)
78
            f.write(data)
79
    
80
    def items(self):
81
        for key, val in self.defaults.items():
82
            yield key, self.get(key)
83
    
84
    def get(self, key):
85
        if key in self.overrides:
86
            return self.overrides[key]
87
        if key in self.d:
88
            return self.d[key]
89
        return self.defaults.get(key)
90
    
91
    def set(self, key, val):
92
        if key not in self.defaults:
93
            log.warning('Ignoring unknown config key "%s".', key)
94
            return
95
        self.d[key] = val
96
        self.write()
97
    
98
    def delete(self, key):
99
        if key not in self.defaults:
100
            log.warning('Ignoring unknown config key "%s".', key)
101
            return
102
        self.d.pop(key, None)
103
        self.write()
104
    
105
    def override(self, key, val):
106
        assert key in self.defaults
107
        if val is not None:
108
            self.overrides[key] = val
b/kamaki/kamaki.py
33 33
# interpreted as representing official policies, either expressed
34 34
# or implied, of GRNET S.A.
35 35

  
36
"""
37
To add a command create a new class and add a 'command' decorator. The class
38
must have a 'main' method which will contain the code to be executed.
39
Optionally a command can implement an 'update_parser' class method in order
40
to add command line arguments, or modify the OptionParser in any way.
41

  
42
The name of the class is important and it will determine the name and grouping
43
of the command. This behavior can be overriden with the 'group' and 'name'
44
decorator arguments:
45

  
46
    @command(api='nova')
47
    class server_list(object):
48
        # This command will be named 'list' under group 'server'
49
        ...
50

  
51
    @command(api='nova', name='ls')
52
    class server_list(object):
53
        # This command will be named 'ls' under group 'server'
54
        ...
55

  
56
The docstring of a command class will be used as the command description in
57
help messages, unless overriden with the 'description' decorator argument.
58

  
59
The syntax of a command will be generated dynamically based on the signature
60
of the 'main' method, unless overriden with the 'syntax' decorator argument:
61

  
62
    def main(self, server_id, network=None):
63
        # This syntax of this command will be: '<server id> [network]'
64
        ...
65

  
66
The order of commands is important, it will be preserved in the help output.
67
"""
68

  
36 69
import inspect
37 70
import logging
38 71
import os
39 72
import sys
40 73

  
41 74
from base64 import b64encode
42
from collections import defaultdict
43 75
from grp import getgrgid
44 76
from optparse import OptionParser
45 77
from pwd import getpwuid
46 78

  
47
from client import Client, ClientError
79
from client import ComputeClient, ImagesClient, ClientError
80
from config import Config, ConfigError
81
from utils import OrderedDict, print_addresses, print_dict, print_items
48 82

  
49 83

  
50
API_ENV = 'KAMAKI_API'
51
URL_ENV = 'KAMAKI_URL'
52
TOKEN_ENV = 'KAMAKI_TOKEN'
53
RCFILE = '.kamakirc'
84
# Path to the file that stores the configuration
85
CONFIG_PATH = os.path.expanduser('~/.kamakirc')
54 86

  
87
# Name of a shell variable to bypass the CONFIG_PATH value
88
CONFIG_ENV = 'KAMAKI_CONFIG'
89

  
90
# The defaults also determine the allowed keys
91
CONFIG_DEFAULTS = {
92
    'apis': 'nova synnefo glance plankton',
93
    'token': '',
94
    'compute_url': 'https://okeanos.grnet.gr/api/v1',
95
    'images_url': 'https://okeanos.grnet.gr/plankton',
96
}
55 97

  
56 98
log = logging.getLogger('kamaki')
57 99

  
100
_commands = OrderedDict()
101

  
58 102

  
59
def print_addresses(addresses, margin):
60
    for address in addresses:
61
        if address['id'] == 'public':
62
            net = 'public'
63
        else:
64
            net = '%s/%s' % (address['id'], address['name'])
65
        print '%s:' % net.rjust(margin + 4)
103
def command(api=None, group=None, name=None, description=None, syntax=None):
104
    """Class decorator that registers a class as a CLI command."""
105
    
106
    def decorator(cls):
107
        grp, sep, cmd = cls.__name__.partition('_')
108
        if not sep:
109
            grp, cmd = None, cls.__name__
66 110
        
67
        ether = address.get('mac', None)
68
        if ether:
69
            print '%s: %s' % ('ether'.rjust(margin + 8), ether)
111
        cls.api = api
112
        cls.group = group or grp
113
        cls.name = name or cmd
114
        cls.description = description or cls.__doc__
115
        cls.syntax = syntax
70 116
        
71
        firewall = address.get('firewallProfile', None)
72
        if firewall:
73
            print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
117
        if cls.syntax is None:
118
            # Generate a syntax string based on main's arguments
119
            spec = inspect.getargspec(cls.main.im_func)
120
            args = spec.args[1:]
121
            n = len(args) - len(spec.defaults or ())
122
            required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n])
123
            optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:])
124
            cls.syntax = ' '.join(x for x in [required, optional] if x)
74 125
        
75
        for ip in address.get('values', []):
76
            key = 'inet' if ip['version'] == 4 else 'inet6'
77
            print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
126
        if cls.group not in _commands:
127
            _commands[cls.group] = OrderedDict()
128
        _commands[cls.group][cls.name] = cls
129
        return cls
130
    return decorator
78 131

  
79 132

  
80
def print_metadata(metadata, margin):
81
    print '%s:' % 'metadata'.rjust(margin)
82
    for key, val in metadata.get('values', {}).items():
83
        print '%s: %s' % (key.rjust(margin + 4), val)
133
@command()
134
class config_list(object):
135
    """list configuration options"""
136
    
137
    def main(self):
138
        for key, val in sorted(self.config.items()):
139
            print '%s=%s' % (key, val)
84 140

  
85 141

  
86
def print_dict(d, exclude=()):
87
    if not d:
88
        return
89
    margin = max(len(key) for key in d) + 1
142
@command()
143
class config_get(object):
144
    """get a configuration option"""
90 145
    
91
    for key, val in sorted(d.items()):
92
        if key in exclude:
93
            continue
94
        
95
        if key == 'addresses':
96
            print '%s:' % 'addresses'.rjust(margin)
97
            print_addresses(val.get('values', []), margin)
98
            continue
99
        elif key == 'metadata':
100
            print_metadata(val, margin)
101
            continue
102
        elif key == 'servers':
103
            val = ', '.join(str(x) for x in val['values'])
104
        
105
        print '%s: %s' % (key.rjust(margin), val)
106

  
107

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

  
115

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

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

  
209

  
210
# Server Group
211

  
212
class ListServers(Command):
213
    group = 'server'
214
    name = 'list'
215
    description = 'list servers'
216
    
217
    def add_options(self, parser):
218
        parser.add_option('-l', action='store_true', dest='detail',
219
                            default=False, help='show detailed output')
146
    def main(self, key):
147
        val = self.config.get(key)
148
        if val is not None:
149
            print val
150

  
151

  
152
@command()
153
class config_set(object):
154
    """set a configuration option"""
155
    
156
    def main(self, key, val):
157
        self.config.set(key, val)
158

  
159

  
160
@command()
161
class config_del(object):
162
    """delete a configuration option"""
163
    
164
    def main(self, key):
165
        self.config.delete(key)
166

  
167

  
168
@command(api='nova')
169
class server_list(object):
170
    """list servers"""
171
    
172
    @classmethod
173
    def update_parser(cls, parser):
174
        parser.add_option('-l', dest='detail', action='store_true',
175
                default=False, help='show detailed output')
220 176
    
221 177
    def main(self):
222
        servers = self.client.list_servers(self.detail)
223
        print_items(servers, self.detail)
178
        servers = self.client.list_servers(self.options.detail)
179
        print_items(servers, self.options.detail)
224 180

  
225 181

  
226
class GetServerDetails(Command):
227
    group = 'server'
228
    name = 'info'
229
    syntax = '<server id>'
230
    description = 'get server details'
182
@command(api='nova')
183
class server_info(object):
184
    """get server details"""
231 185
    
232 186
    def main(self, server_id):
233 187
        server = self.client.get_server_details(int(server_id))
234 188
        print_dict(server)
235 189

  
236 190

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

  
243
    def add_options(self, parser):
244
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
245
                        help='use flavor FLAVOR_ID')
246
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
247
                        help='use image IMAGE_ID')
248
        parser.add_option('--personality',
249
                        dest='personalities',
250
                        action='append',
251
                        default=[],
252
                        metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
253
                        help='add a personality file')
191
@command(api='nova')
192
class server_create(object):
193
    """create server"""
254 194
    
255
    def main(self, name):
256
        flavor_id = int(self.flavor)
257
        image_id = int(self.image)
195
    @classmethod
196
    def update_parser(cls, parser):
197
        parser.add_option('--personality', dest='personalities',
198
                action='append', default=[],
199
                metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
200
                help='add a personality file')
201
        parser.epilog = "If missing, optional personality values will be " \
202
                "filled based on the file at PATH if missing."
203
    
204
    def main(self, name, flavor_id, image_id):
258 205
        personalities = []
259
        for personality in self.personalities:
206
        for personality in self.options.personalities:
260 207
            p = personality.split(',')
261 208
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
262 209
            
......
264 211
            
265 212
            if not path:
266 213
                log.error("Invalid personality argument '%s'", p)
267
                return
214
                return 1
268 215
            if not os.path.exists(path):
269 216
                log.error("File %s does not exist", path)
270
                return
217
                return 1
271 218
            
272 219
            with open(path) as f:
273 220
                contents = b64encode(f.read())
......
280 227
                'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
281 228
                'contents': contents})
282 229
        
283
        reply = self.client.create_server(name, flavor_id, image_id,
284
                                            personalities)
230
        reply = self.client.create_server(name, int(flavor_id), image_id,
231
                personalities)
285 232
        print_dict(reply)
286 233

  
287 234

  
288
class UpdateServerName(Command):
289
    group = 'server'
290
    name = 'rename'
291
    syntax = '<server id> <new name>'
292
    description = 'update server name'
235
@command(api='nova')
236
class server_rename(object):
237
    """update server name"""
293 238
    
294 239
    def main(self, server_id, new_name):
295 240
        self.client.update_server_name(int(server_id), new_name)
296 241

  
297 242

  
298
class DeleteServer(Command):
299
    group = 'server'
300
    name = 'delete'
301
    syntax = '<server id>'
302
    description = 'delete server'
243
@command(api='nova')
244
class server_delete(object):
245
    """delete server"""
303 246
    
304 247
    def main(self, server_id):
305 248
        self.client.delete_server(int(server_id))
306 249

  
307 250

  
308
class RebootServer(Command):
309
    group = 'server'
310
    name = 'reboot'
311
    syntax = '<server id>'
312
    description = 'reboot server'
251
@command(api='nova')
252
class server_reboot(object):
253
    """reboot server"""
313 254
    
314
    def add_options(self, parser):
315
        parser.add_option('-f', action='store_true', dest='hard',
316
                            default=False, help='perform a hard reboot')
255
    @classmethod
256
    def update_parser(cls, parser):
257
        parser.add_option('-f', dest='hard', action='store_true',
258
                default=False, help='perform a hard reboot')
317 259
    
318 260
    def main(self, server_id):
319
        self.client.reboot_server(int(server_id), self.hard)
261
        self.client.reboot_server(int(server_id), self.options.hard)
320 262

  
321 263

  
322
class StartServer(Command):
323
    api = 'synnefo'
324
    group = 'server'
325
    name = 'start'
326
    syntax = '<server id>'
327
    description = 'start server'
264
@command(api='synnefo')
265
class server_start(object):
266
    """start server"""
328 267
    
329 268
    def main(self, server_id):
330 269
        self.client.start_server(int(server_id))
331 270

  
332 271

  
333
class StartServer(Command):
334
    api = 'synnefo'
335
    group = 'server'
336
    name = 'shutdown'
337
    syntax = '<server id>'
338
    description = 'shutdown server'
272
@command(api='synnefo')
273
class server_shutdown(object):
274
    """shutdown server"""
339 275
    
340 276
    def main(self, server_id):
341 277
        self.client.shutdown_server(int(server_id))
342 278

  
343 279

  
344
class ServerConsole(Command):
345
    api = 'synnefo'
346
    group = 'server'
347
    name = 'console'
348
    syntax = '<server id>'
349
    description = 'get VNC console'
350

  
280
@command(api='synnefo')
281
class server_console(object):
282
    """get a VNC console"""
283
    
351 284
    def main(self, server_id):
352 285
        reply = self.client.get_server_console(int(server_id))
353 286
        print_dict(reply)
354 287

  
355 288

  
356
class SetFirewallProfile(Command):
357
    api = 'synnefo'
358
    group = 'server'
359
    name = 'firewall'
360
    syntax = '<server id> <profile>'
361
    description = 'set the firewall profile'
289
@command(api='synnefo')
290
class server_firewall(object):
291
    """set the firewall profile"""
362 292
    
363 293
    def main(self, server_id, profile):
364 294
        self.client.set_firewall_profile(int(server_id), profile)
365 295

  
366 296

  
367
class ListAddresses(Command):
368
    group = 'server'
369
    name = 'addr'
370
    syntax = '<server id> [network]'
371
    description = 'list server addresses'
297
@command(api='synnefo')
298
class server_addr(object):
299
    """list server addresses"""
372 300
    
373 301
    def main(self, server_id, network=None):
374 302
        reply = self.client.list_server_addresses(int(server_id), network)
......
376 304
        print_addresses(reply, margin)
377 305

  
378 306

  
379
class GetServerMeta(Command):
380
    group = 'server'
381
    name = 'meta'
382
    syntax = '<server id> [key]'
383
    description = 'get server metadata'
307
@command(api='nova')
308
class server_meta(object):
309
    """get server metadata"""
384 310
    
385 311
    def main(self, server_id, key=None):
386 312
        reply = self.client.get_server_metadata(int(server_id), key)
387 313
        print_dict(reply)
388 314

  
389 315

  
390
class CreateServerMetadata(Command):
391
    group = 'server'
392
    name = 'addmeta'
393
    syntax = '<server id> <key> <val>'
394
    description = 'add server metadata'
316
@command(api='nova')
317
class server_addmeta(object):
318
    """add server metadata"""
395 319
    
396 320
    def main(self, server_id, key, val):
397 321
        reply = self.client.create_server_metadata(int(server_id), key, val)
398 322
        print_dict(reply)
399 323

  
400 324

  
401
class UpdateServerMetadata(Command):
402
    group = 'server'
403
    name = 'setmeta'
404
    syntax = '<server id> <key> <val>'
405
    description = 'update server metadata'
325
@command(api='nova')
326
class server_setmeta(object):
327
    """update server metadata"""
406 328
    
407 329
    def main(self, server_id, key, val):
408 330
        metadata = {key: val}
......
410 332
        print_dict(reply)
411 333

  
412 334

  
413
class DeleteServerMetadata(Command):
414
    group = 'server'
415
    name = 'delmeta'
416
    syntax = '<server id> <key>'
417
    description = 'delete server metadata'
335
@command(api='nova')
336
class server_delmeta(object):
337
    """delete server metadata"""
418 338
    
419 339
    def main(self, server_id, key):
420 340
        self.client.delete_server_metadata(int(server_id), key)
421 341

  
422 342

  
423
class GetServerStats(Command):
424
    api = 'synnefo'
425
    group = 'server'
426
    name = 'stats'
427
    syntax = '<server id>'
428
    description = 'get server statistics'
343
@command(api='synnefo')
344
class server_stats(object):
345
    """get server statistics"""
429 346
    
430 347
    def main(self, server_id):
431 348
        reply = self.client.get_server_stats(int(server_id))
432 349
        print_dict(reply, exclude=('serverRef',))
433 350

  
434 351

  
435
# Flavor Group
436

  
437
class ListFlavors(Command):
438
    group = 'flavor'
439
    name = 'list'
440
    description = 'list flavors'
352
@command(api='nova')
353
class flavor_list(object):
354
    """list flavors"""
355
    
356
    @classmethod
357
    def update_parser(cls, parser):
358
        parser.add_option('-l', dest='detail', action='store_true',
359
                default=False, help='show detailed output')
441 360
    
442
    def add_options(self, parser):
443
        parser.add_option('-l', action='store_true', dest='detail',
444
                            default=False, help='show detailed output')
445

  
446 361
    def main(self):
447
        flavors = self.client.list_flavors(self.detail)
448
        print_items(flavors, self.detail)
362
        flavors = self.client.list_flavors(self.options.detail)
363
        print_items(flavors, self.options.detail)
449 364

  
450 365

  
451
class GetFlavorDetails(Command):
452
    group = 'flavor'
453
    name = 'info'
454
    syntax = '<flavor id>'
455
    description = 'get flavor details'
366
@command(api='nova')
367
class flavor_info(object):
368
    """get flavor details"""
456 369
    
457 370
    def main(self, flavor_id):
458 371
        flavor = self.client.get_flavor_details(int(flavor_id))
459 372
        print_dict(flavor)
460 373

  
461 374

  
462
class ListImages(Command):
463
    group = 'image'
464
    name = 'list'
465
    description = 'list images'
466

  
467
    def add_options(self, parser):
468
        parser.add_option('-l', action='store_true', dest='detail',
469
                            default=False, help='show detailed output')
470

  
375
@command(api='nova')
376
class image_list(object):
377
    """list images"""
378
    
379
    @classmethod
380
    def update_parser(cls, parser):
381
        parser.add_option('-l', dest='detail', action='store_true',
382
                default=False, help='show detailed output')
383
    
471 384
    def main(self):
472
        images = self.client.list_images(self.detail)
473
        print_items(images, self.detail)
385
        images = self.client.list_images(self.options.detail)
386
        print_items(images, self.options.detail)
474 387

  
475 388

  
476
class GetImageDetails(Command):
477
    group = 'image'
478
    name = 'info'
479
    syntax = '<image id>'
480
    description = 'get image details'
389
@command(api='nova')
390
class image_info(object):
391
    """get image details"""
481 392
    
482 393
    def main(self, image_id):
483
        image = self.client.get_image_details(int(image_id))
394
        image = self.client.get_image_details(image_id)
484 395
        print_dict(image)
485 396

  
486 397

  
487
class CreateImage(Command):
488
    group = 'image'
489
    name = 'create'
490
    syntax = '<server id> <image name>'
491
    description = 'create image'
398
@command(api='nova')
399
class image_create(object):
400
    """create image"""
492 401
    
493 402
    def main(self, server_id, name):
494 403
        reply = self.client.create_image(int(server_id), name)
495 404
        print_dict(reply)
496 405

  
497 406

  
498
class DeleteImage(Command):
499
    group = 'image'
500
    name = 'delete'
501
    syntax = '<image id>'
502
    description = 'delete image'
407
@command(api='nova')
408
class image_delete(object):
409
    """delete image"""
503 410
    
504 411
    def main(self, image_id):
505
        self.client.delete_image(int(image_id))
412
        self.client.delete_image(image_id)
506 413

  
507 414

  
508
class GetImageMetadata(Command):
509
    group = 'image'
510
    name = 'meta'
511
    syntax = '<image id> [key]'
512
    description = 'get image metadata'
415
@command(api='nova')
416
class image_meta(object):
417
    """get image metadata"""
513 418
    
514 419
    def main(self, image_id, key=None):
515
        reply = self.client.get_image_metadata(int(image_id), key)
420
        reply = self.client.get_image_metadata(image_id, key)
516 421
        print_dict(reply)
517 422

  
518 423

  
519
class CreateImageMetadata(Command):
520
    group = 'image'
521
    name = 'addmeta'
522
    syntax = '<image id> <key> <val>'
523
    description = 'add image metadata'
424
@command(api='nova')
425
class image_addmeta(object):
426
    """add image metadata"""
524 427
    
525 428
    def main(self, image_id, key, val):
526
        reply = self.client.create_image_metadata(int(image_id), key, val)
429
        reply = self.client.create_image_metadata(image_id, key, val)
527 430
        print_dict(reply)
528 431

  
529 432

  
530
class UpdateImageMetadata(Command):
531
    group = 'image'
532
    name = 'setmeta'
533
    syntax = '<image id> <key> <val>'
534
    description = 'update image metadata'
433
@command(api='nova')
434
class image_setmeta(object):
435
    """update image metadata"""
535 436
    
536 437
    def main(self, image_id, key, val):
537 438
        metadata = {key: val}
538
        reply = self.client.update_image_metadata(int(image_id), **metadata)
439
        reply = self.client.update_image_metadata(image_id, **metadata)
539 440
        print_dict(reply)
540 441

  
541 442

  
542
class DeleteImageMetadata(Command):
543
    group = 'image'
544
    name = 'delmeta'
545
    syntax = '<image id> <key>'
546
    description = 'delete image metadata'
443
@command(api='nova')
444
class image_delmeta(object):
445
    """delete image metadata"""
547 446
    
548 447
    def main(self, image_id, key):
549
        self.client.delete_image_metadata(int(image_id), key)
448
        self.client.delete_image_metadata(image_id, key)
550 449

  
551 450

  
552
class ListNetworks(Command):
553
    api = 'synnefo'
554
    group = 'network'
555
    name = 'list'
556
    description = 'list networks'
451
@command(api='synnefo')
452
class network_list(object):
453
    """list networks"""
557 454
    
558
    def add_options(self, parser):
559
        parser.add_option('-l', action='store_true', dest='detail',
560
                            default=False, help='show detailed output')
455
    @classmethod
456
    def update_parser(cls, parser):
457
        parser.add_option('-l', dest='detail', action='store_true',
458
                default=False, help='show detailed output')
561 459
    
562 460
    def main(self):
563
        networks = self.client.list_networks(self.detail)
564
        print_items(networks, self.detail)
461
        networks = self.client.list_networks(self.options.detail)
462
        print_items(networks, self.options.detail)
565 463

  
566 464

  
567
class CreateNetwork(Command):
568
    api = 'synnefo'
569
    group = 'network'
570
    name = 'create'
571
    syntax = '<network name>'
572
    description = 'create a network'
465
@command(api='synnefo')
466
class network_create(object):
467
    """create a network"""
573 468
    
574 469
    def main(self, name):
575 470
        reply = self.client.create_network(name)
576 471
        print_dict(reply)
577 472

  
578 473

  
579
class GetNetworkDetails(Command):
580
    api = 'synnefo'
581
    group = 'network'
582
    name = 'info'
583
    syntax = '<network id>'
584
    description = 'get network details'
585

  
474
@command(api='synnefo')
475
class network_info(object):
476
    """get network details"""
477
    
586 478
    def main(self, network_id):
587 479
        network = self.client.get_network_details(network_id)
588 480
        print_dict(network)
589 481

  
590 482

  
591
class RenameNetwork(Command):
592
    api = 'synnefo'
593
    group = 'network'
594
    name = 'rename'
595
    syntax = '<network id> <new name>'
596
    description = 'update network name'
483
@command(api='synnefo')
484
class network_rename(object):
485
    """update network name"""
597 486
    
598
    def main(self, network_id, name):
599
        self.client.update_network_name(network_id, name)
487
    def main(self, network_id, new_name):
488
        self.client.update_network_name(network_id, new_name)
600 489

  
601 490

  
602
class DeleteNetwork(Command):
603
    api = 'synnefo'
604
    group = 'network'
605
    name = 'delete'
606
    syntax = '<network id>'
607
    description = 'delete a network'
491
@command(api='synnefo')
492
class network_delete(object):
493
    """delete a network"""
608 494
    
609 495
    def main(self, network_id):
610 496
        self.client.delete_network(network_id)
611 497

  
612
class ConnectServer(Command):
613
    api = 'synnefo'
614
    group = 'network'
615
    name = 'connect'
616
    syntax = '<server id> <network id>'
617
    description = 'connect a server to a network'
498

  
499
@command(api='synnefo')
500
class network_connect(object):
501
    """connect a server to a network"""
618 502
    
619 503
    def main(self, server_id, network_id):
620 504
        self.client.connect_server(server_id, network_id)
621 505

  
622 506

  
623
class DisconnectServer(Command):
624
    api = 'synnefo'
625
    group = 'network'
626
    name = 'disconnect'
627
    syntax = '<server id> <network id>'
628
    description = 'disconnect a server from a network'
629

  
507
@command(api='synnefo')
508
class network_disconnect(object):
509
    """disconnect a server from a network"""
510
    
630 511
    def main(self, server_id, network_id):
631 512
        self.client.disconnect_server(server_id, network_id)
632 513

  
633 514

  
634

  
635
def print_usage(exe, groups, group=None):
636
    nop = Command([])
637
    nop.parser.print_help()
515
@command(api='glance')
516
class glance_list(object):
517
    """list images"""
638 518
    
519
    def main(self):
520
        images = self.client.list_public()
521
        print images
522

  
523

  
524
def print_groups(groups):
525
    print
526
    print 'Groups:'
527
    for group in groups:
528
        print '  %s' % group
529

  
530

  
531
def print_commands(group, commands):
639 532
    print
640 533
    print 'Commands:'
641
    
642
    if group:
643
        items = [(group, groups[group])]
644
    else:
645
        items = sorted(groups.items())
646
    
647
    for group, commands in items:
648
        for command, cls in sorted(commands.items()):
649
            name = '  %s %s' % (group, command)
650
            print '%s %s' % (name.ljust(22), cls.description)
651
        print
534
    for name, cls in _commands[group].items():
535
        if name in commands:
536
            print '  %s %s' % (name.ljust(10), cls.description)
652 537

  
653 538

  
654 539
def main():
655
    nop = Command([])
656
    groups = defaultdict(dict)
657
    module = sys.modules[__name__]
658
    for name, cls in inspect.getmembers(module, inspect.isclass):
659
        if issubclass(cls, Command) and cls != Command:
660
            if nop.api == 'openstack' and nop.api != cls.api:
661
                continue    # Ignore synnefo commands
662
            groups[cls.group][cls.name] = cls
540
    parser = OptionParser(add_help_option=False)
541
    parser.usage = '%prog <group> <command> [options]'
542
    parser.add_option('--help', dest='help', action='store_true',
543
            default=False, help='show this help message and exit')
544
    parser.add_option('--api', dest='apis', metavar='API', action='append',
545
            help='API to use (can be used multiple times)')
546
    parser.add_option('--compute-url', dest='compute_url', metavar='URL',
547
            help='URL for the compute API')
548
    parser.add_option('--images-url', dest='images_url', metavar='URL',
549
            help='URL for the images API')
550
    parser.add_option('--token', dest='token', metavar='TOKEN',
551
            help='use token TOKEN')
552
    parser.add_option('-v', dest='verbose', action='store_true', default=False,
553
            help='use verbose output')
554
    parser.add_option('-d', dest='debug', action='store_true', default=False,
555
            help='use debug output')
556
    
557
    # Do a preliminary parsing, ignore any errors since we will print help
558
    # anyway if we don't reach the main parsing.
559
    _error = parser.error
560
    parser.error = lambda msg: None
561
    options, args = parser.parse_args(sys.argv)
562
    parser.error = _error
563
    
564
    if options.debug:
565
        log.setLevel(logging.DEBUG)
566
    elif options.verbose:
567
        log.setLevel(logging.INFO)
568
    else:
569
        log.setLevel(logging.WARNING)
663 570
    
664
    argv = list(sys.argv)
665
    exe = os.path.basename(argv.pop(0))
666
    group = argv.pop(0) if argv else None
667
    command = argv.pop(0) if argv else None
571
    try:
572
        config = Config(CONFIG_PATH, CONFIG_ENV, CONFIG_DEFAULTS)
573
    except ConfigError, e:
574
        log.error('%s', e.args[0])
575
        return 1
576
    
577
    for key in CONFIG_DEFAULTS:
578
        config.override(key, getattr(options, key))
579
    
580
    apis = config.get('apis').split()
581
    
582
    # Find available groups based on the given APIs
583
    available_groups = []
584
    for group, group_commands in _commands.items():
585
        for name, cls in group_commands.items():
586
            if cls.api is None or cls.api in apis:
587
                available_groups.append(group)
588
                break
589
    
590
    if len(args) < 2:
591
        parser.print_help()
592
        print_groups(available_groups)
593
        return 0
594
    
595
    group = args[1]
596
    
597
    if group not in available_groups:
598
        parser.print_help()
599
        print_groups(available_groups)
600
        return 1
601
    
602
    # Find available commands based on the given APIs
603
    available_commands = []
604
    for name, cls in _commands[group].items():
605
        if cls.api is None or cls.api in apis:
606
            available_commands.append(name)
607
            continue
608
    
609
    parser.usage = '%%prog %s <command> [options]' % group
610
    
611
    if len(args) < 3:
612
        parser.print_help()
613
        print_commands(group, available_commands)
614
        return 0
615
    
616
    name = args[2]
668 617
    
669
    if group not in groups:
670
        group = None
618
    if name not in available_commands:
619
        parser.print_help()
620
        print_commands(group, available_commands)
621
        return 1
671 622
    
672
    if not group or command not in groups[group]:
673
        print_usage(exe, groups, group)
674
        sys.exit(1)
623
    cls = _commands[group][name]
624
    cls.config = config
675 625
    
676
    cls = groups[group][command]
626
    syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
627
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
628
    parser.epilog = ''
629
    if hasattr(cls, 'update_parser'):
630
        cls.update_parser(parser)
631
    
632
    options, args = parser.parse_args(sys.argv)
633
    if options.help:
634
        parser.print_help()
635
        return 0
636
    
637
    cmd = cls()
638
    cmd.config = config
639
    cmd.options = options
640
    
641
    if cmd.api in ('nova', 'synnefo'):
642
        url = config.get('compute_url')
643
        token = config.get('token')
644
        cmd.client = ComputeClient(url, token)
645
    elif cmd.api in ('glance', 'plankton'):
646
        url = config.get('images_url')
647
        token = config.get('token')
648
        cmd.client = ImagesClient(url, token)
677 649
    
678 650
    try:
679
        cmd = cls(argv)
680
        cmd.execute()
651
        return cmd.main(*args[3:])
652
    except TypeError:
653
        parser.print_help()
654
        return 1
681 655
    except ClientError, err:
682 656
        log.error('%s', err.message)
683 657
        log.info('%s', err.details)
658
        return 2
684 659

  
685 660

  
686 661
if __name__ == '__main__':
687 662
    ch = logging.StreamHandler()
688 663
    ch.setFormatter(logging.Formatter('%(message)s'))
689 664
    log.addHandler(ch)
690
    
691
    main()
665
    err = main() or 0
666
    sys.exit(err)
b/kamaki/utils.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
class OrderedDict(dict):
35
    """An ordered dict implementation for Python versions prior to 2.7"""
36
    
37
    def __init__(self):
38
        dict.__init__(self)
39
        self._keys = []
40
    
41
    def __delitem__(self, key):
42
        dict.__delitem__(self, key)
43
        self._keys.remove(key)
44
    
45
    def __iter__(self):
46
        return iter(self._keys)
47
    
48
    def __repr__(self):
49
        return repr(self.items())
50
    
51
    def __setitem__(self, key, value):
52
        if key not in self:
53
            self._keys.append(key)
54
        dict.__setitem__(self, key, value)
55
    
56
    def keys(self):
57
        return self._keys
58
    
59
    def iteritems(self):
60
        for key in self._keys:
61
            yield key, self[key]
62
    
63
    def items(self):
64
        return list(self.iteritems())
65

  
66

  
67
def print_addresses(addresses, margin):
68
    for address in addresses:
69
        if address['id'] == 'public':
70
            net = 'public'
71
        else:
72
            net = '%s/%s' % (address['id'], address['name'])
73
        print '%s:' % net.rjust(margin + 4)
74

  
75
        ether = address.get('mac', None)
76
        if ether:
77
            print '%s: %s' % ('ether'.rjust(margin + 8), ether)
78

  
79
        firewall = address.get('firewallProfile', None)
80
        if firewall:
81
            print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
82

  
83
        for ip in address.get('values', []):
84
            key = 'inet' if ip['version'] == 4 else 'inet6'
85
            print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
86

  
87

  
88
def print_metadata(metadata, margin):
89
    print '%s:' % 'metadata'.rjust(margin)
90
    for key, val in metadata.get('values', {}).items():
91
        print '%s: %s' % (key.rjust(margin + 4), val)
92

  
93

  
94
def print_dict(d, exclude=()):
95
    if not d:
96
        return
97
    margin = max(len(key) for key in d) + 1
98

  
99
    for key, val in sorted(d.items()):
100
        if key in exclude:
101
            continue
102

  
103
        if key == 'addresses':
104
            print '%s:' % 'addresses'.rjust(margin)
105
            print_addresses(val.get('values', []), margin)
106
            continue
107
        elif key == 'metadata':
108
            print_metadata(val, margin)
109
            continue
110
        elif key == 'servers':
111
            val = ', '.join(str(x) for x in val['values'])
112

  
113
        print '%s: %s' % (key.rjust(margin), val)
114

  
115

  
116
def print_items(items, detail=False):
117
    for item in items:
118
        print '%s %s' % (item['id'], item.get('name', ''))
119
        if detail:
120
            print_dict(item, exclude=('id', 'name'))
121
            print

Also available in: Unified diff