Revision 5d1d131b

b/.gitignore
1
*.pyc
2
.DS_Store
3
bin/
b/build
1
#!/usr/bin/env python
2

  
3
import os
4
import stat
5

  
6
SRCDIR = 'kamaki'
7
SRC = 'kamaki.py'
8
CLIENT = 'client.py'
9
DSTDIR = 'bin'
10
DST = 'kamaki'
11

  
12

  
13
def main():
14
    if not os.path.exists(DSTDIR):
15
        os.makedirs(DSTDIR)
16
    dstpath = os.path.join(DSTDIR, DST)
17
    dst = open(dstpath, 'w')
18
    
19
    srcpath = os.path.join(SRCDIR, SRC)
20
    clientpath = os.path.join(SRCDIR, CLIENT)
21

  
22
    for line in open(srcpath):
23
        if line.startswith('from client import'):
24
            for l in open(clientpath):
25
                if l.startswith('#'):
26
                    continue    # Skip comments
27
                dst.write(l)
28
        else:
29
            dst.write(line)
30
    
31
    dst.close()
32
    
33
    # Make file executable
34
    mode = stat.S_IMODE(os.stat(dstpath).st_mode)
35
    mode |= 0111
36
    os.chmod(dstpath, mode)
37

  
38

  
39
if __name__ == '__main__':
40
    main()
b/kamaki/client.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

  
37
from httplib import HTTPConnection, HTTPSConnection
38
from urlparse import urlparse
39

  
40

  
41
class ClientError(Exception):
42
    def __init__(self, message, details=''):
43
        self.message = message
44
        self.details = details
45

  
46

  
47
class Client(object):
48
    def __init__(self, url, token=''):
49
        self.url = url
50
        self.token = token
51
    
52
    def _cmd(self, method, path, body=None, success=200):
53
        p = urlparse(self.url)
54
        path = p.path + path
55
        if p.scheme == 'http':
56
            conn = HTTPConnection(p.netloc)
57
        elif p.scheme == 'https':
58
            conn = HTTPSConnection(p.netloc)
59
        else:
60
            raise ClientError("Unknown URL scheme")
61
        
62
        headers = {'X-Auth-Token': self.token}
63
        if body:
64
            headers['Content-Type'] = 'application/json'
65
            headers['Content-Length'] = len(body)
66
        
67
        logging.debug('%s', '>' * 40)
68
        logging.debug('%s %s', method, path)
69

  
70
        for key, val in headers.items():
71
            logging.debug('%s: %s', key, val)
72
        logging.debug('')
73
        if body:
74
            logging.debug(body)
75
            logging.debug('')
76
        
77
        conn.request(method, path, body, headers)
78

  
79
        resp = conn.getresponse()
80
        logging.debug('%s', '<' * 40)
81
        logging.info('%d %s', resp.status, resp.reason)
82
        for key, val in resp.getheaders():
83
            logging.info('%s: %s', key.capitalize(), val)
84
        logging.info('')
85
        
86
        buf = resp.read()
87
        try:
88
            reply = json.loads(buf) if buf else {}
89
        except ValueError:
90
            raise ClientError('Invalid response from the server', buf)
91
        
92
        if resp.status != success:
93
            if len(reply) == 1:
94
                key = reply.keys()[0]
95
                val = reply[key]
96
                message = '%s: %s' % (key, val.get('message', ''))
97
                details = val.get('details', '')
98
                raise ClientError(message, details)
99
            else:
100
                raise ClientError('Invalid response from the server')
101

  
102
        return reply
103
    
104
    def _get(self, path, success=200):
105
        return self._cmd('GET', path, None, success)
106
    
107
    def _post(self, path, body, success=202):
108
        return self._cmd('POST', path, body, success)
109
    
110
    def _put(self, path, body, success=204):
111
        return self._cmd('PUT', path, body, success)
112
    
113
    def _delete(self, path, success=204):
114
        return self._cmd('DELETE', path, None, success)
115
    
116
    
117
    # Servers
118
    
119
    def list_servers(self, detail=False):
120
        path = '/servers/detail' if detail else '/servers'
121
        reply = self._get(path)
122
        return reply['servers']['values']
123
    
124
    def get_server_details(self, server_id):
125
        path = '/servers/%d' % server_id
126
        reply = self._get(path)
127
        return reply['server']
128
    
129
    def create_server(self, name, flavor, image):
130
        req = {'name': name, 'flavorRef': flavor, 'imageRef': image}
131
        body = json.dumps({'server': req})
132
        reply = self._post('/servers', body)
133
        return reply['server']
134
    
135
    def update_server_name(self, server_id, new_name):
136
        path = '/servers/%d' % server_id
137
        body = json.dumps({'server': {'name': new_name}})
138
        self._put(path, body)
139
    
140
    def delete_server(self, server_id):
141
        path = '/servers/%d' % server_id
142
        self._delete(path)
143
    
144
    def reboot_server(self, server_id, hard=False):
145
        path = '/servers/%d/action' % server_id
146
        type = 'HARD' if hard else 'SOFT'
147
        body = json.dumps({'reboot': {'type': type}})
148
        self._post(path, body)
149
    
150
    def start_server(self, server_id):
151
        path = '/servers/%d/action' % server_id
152
        body = json.dumps({'start': {}})
153
        self._post(path, body)
154
    
155
    def shutdown_server(self, server_id):
156
        path = '/servers/%d/action' % server_id
157
        body = json.dumps({'shutdown': {}})
158
        self._post(path, body)
159
    
160
    def get_server_console(self, server_id):
161
        path = '/servers/%d/action' % server_id
162
        body = json.dumps({'console': {'type': 'vnc'}})
163
        reply = self._cmd('POST', path, body, 200)
164
        return reply['console']
165
    
166
    def set_firewall_profile(self, server_id, profile):
167
        path = '/servers/%d/action' % server_id
168
        body = json.dumps({'firewallProfile': {'profile': profile}})
169
        self._cmd('POST', path, body, 202)
170
    
171
    def list_server_addresses(self, server_id, network=None):
172
        path = '/servers/%d/ips' % server_id
173
        if network:
174
            path += '/%s' % network
175
        reply = self._get(path)
176
        return [reply['network']] if network else reply['addresses']['values']
177
    
178
    def get_server_metadata(self, server_id, key=None):
179
        path = '/servers/%d/meta' % server_id
180
        if key:
181
            path += '/%s' % key
182
        reply = self._get(path)
183
        return reply['meta'] if key else reply['metadata']['values']
184
    
185
    def create_server_metadata(self, server_id, key, val):
186
        path = '/servers/%d/meta/%s' % (server_id, key)
187
        body = json.dumps({'meta': {key: val}})
188
        reply = self._put(path, body, 201)
189
        return reply['meta']
190
    
191
    def update_server_metadata(self, server_id, key, val):
192
        path = '/servers/%d/meta' % server_id
193
        body = json.dumps({'metadata': {key: val}})
194
        reply = self._post(path, body, 201)
195
        return reply['metadata']
196
    
197
    def delete_server_metadata(self, server_id, key):
198
        path = '/servers/%d/meta/%s' % (server_id, key)
199
        reply = self._delete(path)
200
    
201
    def get_server_stats(self, server_id):
202
        path = '/servers/%d/stats' % server_id
203
        reply = self._get(path)
204
        return reply['stats']
205
    
206
    
207
    # Flavors
208
    
209
    def list_flavors(self, detail=False):
210
        path = '/flavors/detail' if detail else '/flavors'
211
        reply = self._get(path)
212
        return reply['flavors']['values']
213

  
214
    def get_flavor_details(self, flavor_id):
215
        path = '/flavors/%d' % flavor_id
216
        reply = self._get(path)
217
        return reply['flavor']
218
    
219
    
220
    # Images
221
    
222
    def list_images(self, detail=False):
223
        path = '/images/detail' if detail else '/images'
224
        reply = self._get(path)
225
        return reply['images']['values']
226

  
227
    def get_image_details(self, image_id):
228
        path = '/images/%d' % image_id
229
        reply = self._get(path)
230
        return reply['image']
231

  
232
    def create_image(self, server_id, name):
233
        req = {'name': name, 'serverRef': server_id}
234
        body = json.dumps({'image': req})
235
        reply = self._post('/images', body)
236
        return reply['image']
237

  
238
    def delete_image(self, image_id):
239
        path = '/images/%d' % image_id
240
        self._delete(path)
241

  
242
    def get_image_metadata(self, image_id, key=None):
243
        path = '/images/%d/meta' % image_id
244
        if key:
245
            path += '/%s' % key
246
        reply = self._get(path)
247
        return reply['meta'] if key else reply['metadata']['values']
248
    
249
    def create_image_metadata(self, image_id, key, val):
250
        path = '/images/%d/meta/%s' % (image_id, key)
251
        body = json.dumps({'meta': {key: val}})
252
        reply = self._put(path, body, 201)
253
        reply['meta']
254

  
255
    def update_image_metadata(self, image_id, key, val):
256
        path = '/images/%d/meta' % image_id
257
        body = json.dumps({'metadata': {key: val}})
258
        reply = self._post(path, body, 201)
259
        return reply['metadata']
260

  
261
    def delete_image_metadata(self, image_id, key):
262
        path = '/images/%d/meta/%s' % (image_id, key)
263
        reply = self._delete(path)
264
    
265
    
266
    # Networks
267
    
268
    def list_networks(self, detail=False):
269
        path = '/networks/detail' if detail else '/networks'
270
        reply = self._get(path)
271
        return reply['networks']['values']
272
    
273
    def create_network(self, name):
274
        body = json.dumps({'network': {'name': name}})
275
        reply = self._post('/networks', body)
276
        return reply['network']
277
    
278
    def get_network_details(self, network_id):
279
        path = '/networks/%s' % network_id
280
        reply = self._get(path)
281
        return reply['network']
282
    
283
    def update_network_name(self, network_id, new_name):
284
        path = '/networks/%s' % network_id
285
        body = json.dumps({'network': {'name': new_name}})
286
        self._put(path, body)
287
    
288
    def delete_network(self, network_id):
289
        path = '/networks/%s' % network_id
290
        self._delete(path)
291

  
292
    def connect_server(self, server_id, network_id):
293
        path = '/networks/%s/action' % network_id
294
        body = json.dumps({'add': {'serverRef': server_id}})
295
        self._post(path, body)
296
    
297
    def disconnect_server(self, server_id, network_id):
298
        path = '/networks/%s/action' % network_id
299
        body = json.dumps({'remove': {'serverRef': server_id}})
300
        self._post(path, body)
b/kamaki/kamaki.py
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

  
250
    def main(self, name):
251
        flavor_id = int(self.flavor)
252
        image_id = int(self.image)
253
        reply = self.client.create_server(name, flavor_id, image_id)
254
        print_dict(reply)
255

  
256

  
257
class UpdateServerName(Command):
258
    group = 'server'
259
    name = 'rename'
260
    syntax = '<server id> <new name>'
261
    description = 'update server name'
262
    
263
    def main(self, server_id, new_name):
264
        self.client.update_server_name(int(server_id), new_name)
265

  
266

  
267
class DeleteServer(Command):
268
    group = 'server'
269
    name = 'delete'
270
    syntax = '<server id>'
271
    description = 'delete server'
272
    
273
    def main(self, server_id):
274
        self.client.delete_server(int(server_id))
275

  
276

  
277
class RebootServer(Command):
278
    group = 'server'
279
    name = 'reboot'
280
    syntax = '<server id>'
281
    description = 'reboot server'
282
    
283
    def add_options(self, parser):
284
        parser.add_option('-f', action='store_true', dest='hard',
285
                            default=False, help='perform a hard reboot')
286
    
287
    def main(self, server_id):
288
        self.client.reboot_server(int(server_id), self.hard)
289

  
290

  
291
class StartServer(Command):
292
    api = 'synnefo'
293
    group = 'server'
294
    name = 'start'
295
    syntax = '<server id>'
296
    description = 'start server'
297
    
298
    def main(self, server_id):
299
        self.client.start_server(int(server_id))
300

  
301

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

  
312

  
313
class ServerConsole(Command):
314
    api = 'synnefo'
315
    group = 'server'
316
    name = 'console'
317
    syntax = '<server id>'
318
    description = 'get VNC console'
319

  
320
    def main(self, server_id):
321
        reply = self.client.get_server_console(int(server_id))
322
        print_dict(reply)
323

  
324

  
325
class SetFirewallProfile(Command):
326
    api = 'synnefo'
327
    group = 'server'
328
    name = 'firewall'
329
    syntax = '<server id> <profile>'
330
    description = 'set the firewall profile'
331
    
332
    def main(self, server_id, profile):
333
        self.client.set_firewall_profile(int(server_id), profile)
334

  
335

  
336
class ListAddresses(Command):
337
    group = 'server'
338
    name = 'addr'
339
    syntax = '<server id> [network]'
340
    description = 'list server addresses'
341
    
342
    def main(self, server_id, network=None):
343
        reply = self.client.list_server_addresses(int(server_id), network)
344
        margin = max(len(x['name']) for x in reply)
345
        print_addresses(reply, margin)
346

  
347

  
348
class GetServerMeta(Command):
349
    group = 'server'
350
    name = 'meta'
351
    syntax = '<server id> [key]'
352
    description = 'get server metadata'
353
    
354
    def main(self, server_id, key=None):
355
        reply = self.client.get_server_metadata(int(server_id), key)
356
        print_dict(reply)
357

  
358

  
359
class CreateServerMetadata(Command):
360
    group = 'server'
361
    name = 'addmeta'
362
    syntax = '<server id> <key> <val>'
363
    description = 'add server metadata'
364
    
365
    def main(self, server_id, key, val):
366
        reply = self.client.create_server_metadata(int(server_id), key, val)
367
        print_dict(reply)
368

  
369

  
370
class UpdateServerMetadata(Command):
371
    group = 'server'
372
    name = 'setmeta'
373
    syntax = '<server id> <key> <val>'
374
    description = 'update server metadata'
375
    
376
    def main(self, server_id, key, val):
377
        reply = self.client.update_server_metadata(int(server_id), key, val)
378
        print_dict(reply)
379

  
380

  
381
class DeleteServerMetadata(Command):
382
    group = 'server'
383
    name = 'delmeta'
384
    syntax = '<server id> <key>'
385
    description = 'delete server metadata'
386
    
387
    def main(self, server_id, key):
388
        self.client.delete_server_metadata(int(server_id), key)
389

  
390

  
391
class GetServerStats(Command):
392
    api = 'synnefo'
393
    group = 'server'
394
    name = 'stats'
395
    syntax = '<server id>'
396
    description = 'get server statistics'
397
    
398
    def main(self, server_id):
399
        reply = self.client.get_server_stats(int(server_id))
400
        print_dict(reply, exclude=('serverRef',))
401

  
402

  
403
# Flavor Group
404

  
405
class ListFlavors(Command):
406
    group = 'flavor'
407
    name = 'list'
408
    description = 'list flavors'
409
    
410
    def add_options(self, parser):
411
        parser.add_option('-l', action='store_true', dest='detail',
412
                            default=False, help='show detailed output')
413

  
414
    def main(self):
415
        flavors = self.client.list_flavors(self.detail)
416
        print_items(flavors, self.detail)
417

  
418

  
419
class GetFlavorDetails(Command):
420
    group = 'flavor'
421
    name = 'info'
422
    syntax = '<flavor id>'
423
    description = 'get flavor details'
424
    
425
    def main(self, flavor_id):
426
        flavor = self.client.get_flavor_details(int(flavor_id))
427
        print_dict(flavor)
428

  
429

  
430
class ListImages(Command):
431
    group = 'image'
432
    name = 'list'
433
    description = 'list images'
434

  
435
    def add_options(self, parser):
436
        parser.add_option('-l', action='store_true', dest='detail',
437
                            default=False, help='show detailed output')
438

  
439
    def main(self):
440
        images = self.client.list_images(self.detail)
441
        print_items(images, self.detail)
442

  
443

  
444
class GetImageDetails(Command):
445
    group = 'image'
446
    name = 'info'
447
    syntax = '<image id>'
448
    description = 'get image details'
449
    
450
    def main(self, image_id):
451
        image = self.client.get_image_details(int(image_id))
452
        print_dict(image)
453

  
454

  
455
class CreateImage(Command):
456
    group = 'image'
457
    name = 'create'
458
    syntax = '<server id> <image name>'
459
    description = 'create image'
460
    
461
    def main(self, server_id, name):
462
        reply = self.client.create_image(int(server_id), name)
463
        print_dict(reply)
464

  
465

  
466
class DeleteImage(Command):
467
    group = 'image'
468
    name = 'delete'
469
    syntax = '<image id>'
470
    description = 'delete image'
471
    
472
    def main(self, image_id):
473
        self.client.delete_image(int(image_id))
474

  
475

  
476
class GetImageMetadata(Command):
477
    group = 'image'
478
    name = 'meta'
479
    syntax = '<image id> [key]'
480
    description = 'get image metadata'
481
    
482
    def main(self, image_id, key=None):
483
        reply = self.client.get_image_metadata(int(image_id), key)
484
        print_dict(reply)
485

  
486

  
487
class CreateImageMetadata(Command):
488
    group = 'image'
489
    name = 'addmeta'
490
    syntax = '<image id> <key> <val>'
491
    description = 'add image metadata'
492
    
493
    def main(self, image_id, key, val):
494
        reply = self.client.create_image_metadata(int(image_id), key, val)
495
        print_dict(reply)
496

  
497

  
498
class UpdateImageMetadata(Command):
499
    group = 'image'
500
    name = 'setmeta'
501
    syntax = '<image id> <key> <val>'
502
    description = 'update image metadata'
503
    
504
    def main(self, image_id, key, val):
505
        reply = self.client.update_image_metadata(int(image_id), key, val)
506
        print_dict(reply)
507

  
508

  
509
class DeleteImageMetadata(Command):
510
    group = 'image'
511
    name = 'delmeta'
512
    syntax = '<image id> <key>'
513
    description = 'delete image metadata'
514
    
515
    def main(self, image_id, key):
516
        self.client.delete_image_metadata(int(image_id), key)
517

  
518

  
519
class ListNetworks(Command):
520
    api = 'synnefo'
521
    group = 'network'
522
    name = 'list'
523
    description = 'list networks'
524
    
525
    def add_options(self, parser):
526
        parser.add_option('-l', action='store_true', dest='detail',
527
                            default=False, help='show detailed output')
528
    
529
    def main(self):
530
        networks = self.client.list_networks(self.detail)
531
        print_items(networks, self.detail)
532

  
533

  
534
class CreateNetwork(Command):
535
    api = 'synnefo'
536
    group = 'network'
537
    name = 'create'
538
    syntax = '<network name>'
539
    description = 'create a network'
540
    
541
    def main(self, name):
542
        reply = self.client.create_network(name)
543
        print_dict(reply)
544

  
545

  
546
class GetNetworkDetails(Command):
547
    api = 'synnefo'
548
    group = 'network'
549
    name = 'info'
550
    syntax = '<network id>'
551
    description = 'get network details'
552

  
553
    def main(self, network_id):
554
        network = self.client.get_network_details(network_id)
555
        print_dict(network)
556

  
557

  
558
class RenameNetwork(Command):
559
    api = 'synnefo'
560
    group = 'network'
561
    name = 'rename'
562
    syntax = '<network id> <new name>'
563
    description = 'update network name'
564
    
565
    def main(self, network_id, name):
566
        self.client.update_network_name(network_id, name)
567

  
568

  
569
class DeleteNetwork(Command):
570
    api = 'synnefo'
571
    group = 'network'
572
    name = 'delete'
573
    syntax = '<network id>'
574
    description = 'delete a network'
575
    
576
    def main(self, network_id):
577
        self.client.delete_network(network_id)
578

  
579
class ConnectServer(Command):
580
    api = 'synnefo'
581
    group = 'network'
582
    name = 'connect'
583
    syntax = '<server id> <network id>'
584
    description = 'connect a server to a network'
585
    
586
    def main(self, server_id, network_id):
587
        self.client.connect_server(server_id, network_id)
588

  
589

  
590
class DisconnectServer(Command):
591
    api = 'synnefo'
592
    group = 'network'
593
    name = 'disconnect'
594
    syntax = '<server id> <network id>'
595
    description = 'disconnect a server from a network'
596

  
597
    def main(self, server_id, network_id):
598
        self.client.disconnect_server(server_id, network_id)
599

  
600

  
601

  
602
def print_usage(exe, groups, group=None):
603
    nop = Command([])
604
    nop.parser.print_help()
605
    
606
    print
607
    print 'Commands:'
608
    
609
    if group:
610
        items = [(group, groups[group])]
611
    else:
612
        items = sorted(groups.items())
613
    
614
    for group, commands in items:
615
        for command, cls in sorted(commands.items()):
616
            name = '  %s %s' % (group, command)
617
            print '%s %s' % (name.ljust(22), cls.description)
618
        print
619

  
620

  
621
def main():
622
    nop = Command([])
623
    groups = defaultdict(dict)
624
    module = sys.modules[__name__]
625
    for name, cls in inspect.getmembers(module, inspect.isclass):
626
        if issubclass(cls, Command) and cls != Command:
627
            if nop.api == 'openstack' and nop.api != cls.api:
628
                continue    # Ignore synnefo commands
629
            groups[cls.group][cls.name] = cls
630
    
631
    argv = list(sys.argv)
632
    exe = os.path.basename(argv.pop(0))
633
    group = argv.pop(0) if argv else None
634
    command = argv.pop(0) if argv else None
635
    
636
    if group not in groups:
637
        group = None
638
    
639
    if not group or command not in groups[group]:
640
        print_usage(exe, groups, group)
641
        sys.exit(1)
642
    
643
    cls = groups[group][command]
644
    
645
    try:
646
        cmd = cls(argv)
647
        cmd.execute()
648
    except ClientError, err:
649
        logging.error('%s', err.message)
650
        logging.info('%s', err.details)
651

  
652

  
653
if __name__ == '__main__':
654
    main()

Also available in: Unified diff