Statistics
| Branch: | Tag: | Revision:

root / kamaki / client.py @ eb3ca8ca

History | View | Annotate | Download (12.1 kB)

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
log = logging.getLogger('kamaki.client')
42

    
43

    
44
class ClientError(Exception):
45
    def __init__(self, message, status=0, details=''):
46
        self.message = message
47
        self.status = status
48
        self.details = details
49

    
50
    def __int__(self):
51
        return int(self.status)
52

    
53
    def __str__(self):
54
        r = self.message
55
        if self.status:
56
            r += "\nHTTP Status: %d" % self.status
57
        if self.details:
58
            r += "\nDetails: \n%s" % self.details
59
        return r
60

    
61

    
62
class Client(object):
63
    def __init__(self, url, token=''):
64
        self.url = url
65
        self.token = token
66
    
67
    def _cmd(self, method, path, body=None, success=200):
68
        p = urlparse(self.url)
69
        path = p.path + path
70
        if p.scheme == 'http':
71
            conn = HTTPConnection(p.netloc)
72
        elif p.scheme == 'https':
73
            conn = HTTPSConnection(p.netloc)
74
        else:
75
            raise ClientError('Unknown URL scheme')
76
        
77
        headers = {'X-Auth-Token': self.token}
78
        if body:
79
            headers['Content-Type'] = 'application/json'
80
            headers['Content-Length'] = len(body)
81
        
82
        log.debug('>' * 50)
83
        log.debug('%s %s', method, path)
84
        for key, val in headers.items():
85
            log.debug('%s: %s', key, val)
86
        if body:
87
            log.debug('')
88
            log.debug(body)
89
        
90
        conn.request(method, path, body, headers)
91
        
92
        resp = conn.getresponse()
93
        buf = resp.read()
94
        
95
        log.debug('<' * 50)
96
        log.info('%d %s', resp.status, resp.reason)
97
        for key, val in resp.getheaders():
98
            log.info('%s: %s', key.capitalize(), val)
99
        log.info('')
100
        log.debug(buf)
101
        log.debug('-' * 50)
102
        
103
        try:
104
            reply = json.loads(buf) if buf else {}
105
        except ValueError:
106
            raise ClientError('Did not receive valid JSON reply',
107
                              resp.status, buf)
108
        
109
        if resp.status != success:
110
            if len(reply) == 1:
111
                key = reply.keys()[0]
112
                val = reply[key]
113
                message = '%s: %s' % (key, val.get('message', ''))
114
                details = val.get('details', '')
115
                raise ClientError(message, resp.status, details)
116
            else:
117
                raise ClientError('Invalid response from the server')
118

    
119
        return reply
120
    
121
    def _get(self, path, success=200):
122
        return self._cmd('GET', path, None, success)
123
    
124
    def _post(self, path, body, success=202):
125
        return self._cmd('POST', path, body, success)
126
    
127
    def _put(self, path, body, success=204):
128
        return self._cmd('PUT', path, body, success)
129
    
130
    def _delete(self, path, success=204):
131
        return self._cmd('DELETE', path, None, success)
132

    
133

    
134
class ComputeClient(Client):
135
    # Servers
136
    
137
    def list_servers(self, detail=False):
138
        """List servers, returned detailed output if detailed is True"""
139
        path = '/servers/detail' if detail else '/servers'
140
        reply = self._get(path)
141
        return reply['servers']['values']
142
    
143
    def get_server_details(self, server_id):
144
        """Return detailed output on a server specified by its id"""
145
        path = '/servers/%d' % server_id
146
        reply = self._get(path)
147
        return reply['server']
148
    
149
    def create_server(self, name, flavor_id, image_id, personality=None):
150
        """Submit request to create a new server
151

152
        The flavor_id specifies the hardware configuration to use,
153
        the image_id specifies the OS Image to be deployed inside the new
154
        server.
155

156
        The personality argument is a list of (file path, file contents)
157
        tuples, describing files to be injected into the server upon creation.
158

159
        The call returns a dictionary describing the newly created server.
160

161
        """
162
        
163
        req = {'name': name, 'flavorRef': flavor_id, 'imageRef': image_id}
164
        if personality:
165
            req['personality'] = personality
166
        
167
        body = json.dumps({'server': req})
168
        reply = self._post('/servers', body)
169
        return reply['server']
170
    
171
    def update_server_name(self, server_id, new_name):
172
        """Update the name of the server as reported by the API.
173

174
        This call does not modify the hostname actually used by the server
175
        internally.
176

177
        """
178
        path = '/servers/%d' % server_id
179
        body = json.dumps({'server': {'name': new_name}})
180
        self._put(path, body)
181
    
182
    def delete_server(self, server_id):
183
        """Submit a deletion request for a server specified by id"""
184
        path = '/servers/%d' % server_id
185
        self._delete(path)
186
    
187
    def reboot_server(self, server_id, hard=False):
188
        """Submit a reboot request for a server specified by id"""
189
        path = '/servers/%d/action' % server_id
190
        type = 'HARD' if hard else 'SOFT'
191
        body = json.dumps({'reboot': {'type': type}})
192
        self._post(path, body)
193
    
194
    def start_server(self, server_id):
195
        """Submit a startup request for a server specified by id"""
196
        path = '/servers/%d/action' % server_id
197
        body = json.dumps({'start': {}})
198
        self._post(path, body)
199
    
200
    def shutdown_server(self, server_id):
201
        """Submit a shutdown request for a server specified by id"""
202
        path = '/servers/%d/action' % server_id
203
        body = json.dumps({'shutdown': {}})
204
        self._post(path, body)
205
    
206
    def get_server_console(self, server_id):
207
        """Get a VNC connection to the console of a server specified by id"""
208
        path = '/servers/%d/action' % server_id
209
        body = json.dumps({'console': {'type': 'vnc'}})
210
        reply = self._post(path, body, 200)
211
        return reply['console']
212
    
213
    def set_firewall_profile(self, server_id, profile):
214
        """Set the firewall profile for the public interface of a server
215

216
        The server is specified by id, the profile argument
217
        is one of (ENABLED, DISABLED, PROTECTED).
218

219
        """
220
        path = '/servers/%d/action' % server_id
221
        body = json.dumps({'firewallProfile': {'profile': profile}})
222
        self._post(path, body, 202)
223
    
224
    def list_server_addresses(self, server_id, network=None):
225
        path = '/servers/%d/ips' % server_id
226
        if network:
227
            path += '/%s' % network
228
        reply = self._get(path)
229
        return [reply['network']] if network else reply['addresses']['values']
230
    
231
    def get_server_metadata(self, server_id, key=None):
232
        path = '/servers/%d/meta' % server_id
233
        if key:
234
            path += '/%s' % key
235
        reply = self._get(path)
236
        return reply['meta'] if key else reply['metadata']['values']
237
    
238
    def create_server_metadata(self, server_id, key, val):
239
        path = '/servers/%d/meta/%s' % (server_id, key)
240
        body = json.dumps({'meta': {key: val}})
241
        reply = self._put(path, body, 201)
242
        return reply['meta']
243
    
244
    def update_server_metadata(self, server_id, **metadata):
245
        path = '/servers/%d/meta' % server_id
246
        body = json.dumps({'metadata': metadata})
247
        reply = self._post(path, body, 201)
248
        return reply['metadata']
249
    
250
    def delete_server_metadata(self, server_id, key):
251
        path = '/servers/%d/meta/%s' % (server_id, key)
252
        reply = self._delete(path)
253
    
254
    def get_server_stats(self, server_id):
255
        path = '/servers/%d/stats' % server_id
256
        reply = self._get(path)
257
        return reply['stats']
258
    
259
    
260
    # Flavors
261
    
262
    def list_flavors(self, detail=False):
263
        path = '/flavors/detail' if detail else '/flavors'
264
        reply = self._get(path)
265
        return reply['flavors']['values']
266

    
267
    def get_flavor_details(self, flavor_id):
268
        path = '/flavors/%d' % flavor_id
269
        reply = self._get(path)
270
        return reply['flavor']
271
    
272
    
273
    # Images
274
    
275
    def list_images(self, detail=False):
276
        path = '/images/detail' if detail else '/images'
277
        reply = self._get(path)
278
        return reply['images']['values']
279

    
280
    def get_image_details(self, image_id):
281
        path = '/images/%d' % image_id
282
        reply = self._get(path)
283
        return reply['image']
284

    
285
    def create_image(self, server_id, name):
286
        req = {'name': name, 'serverRef': server_id}
287
        body = json.dumps({'image': req})
288
        reply = self._post('/images', body)
289
        return reply['image']
290

    
291
    def delete_image(self, image_id):
292
        path = '/images/%d' % image_id
293
        self._delete(path)
294

    
295
    def get_image_metadata(self, image_id, key=None):
296
        path = '/images/%d/meta' % image_id
297
        if key:
298
            path += '/%s' % key
299
        reply = self._get(path)
300
        return reply['meta'] if key else reply['metadata']['values']
301
    
302
    def create_image_metadata(self, image_id, key, val):
303
        path = '/images/%d/meta/%s' % (image_id, key)
304
        body = json.dumps({'meta': {key: val}})
305
        reply = self._put(path, body, 201)
306
        reply['meta']
307

    
308
    def update_image_metadata(self, image_id, **metadata):
309
        path = '/images/%d/meta' % image_id
310
        body = json.dumps({'metadata': metadata})
311
        reply = self._post(path, body, 201)
312
        return reply['metadata']
313

    
314
    def delete_image_metadata(self, image_id, key):
315
        path = '/images/%d/meta/%s' % (image_id, key)
316
        reply = self._delete(path)
317
    
318
    
319
    # Networks
320
    
321
    def list_networks(self, detail=False):
322
        path = '/networks/detail' if detail else '/networks'
323
        reply = self._get(path)
324
        return reply['networks']['values']
325
    
326
    def create_network(self, name):
327
        body = json.dumps({'network': {'name': name}})
328
        reply = self._post('/networks', body)
329
        return reply['network']
330
    
331
    def get_network_details(self, network_id):
332
        path = '/networks/%s' % network_id
333
        reply = self._get(path)
334
        return reply['network']
335
    
336
    def update_network_name(self, network_id, new_name):
337
        path = '/networks/%s' % network_id
338
        body = json.dumps({'network': {'name': new_name}})
339
        self._put(path, body)
340
    
341
    def delete_network(self, network_id):
342
        path = '/networks/%s' % network_id
343
        self._delete(path)
344

    
345
    def connect_server(self, server_id, network_id):
346
        path = '/networks/%s/action' % network_id
347
        body = json.dumps({'add': {'serverRef': server_id}})
348
        self._post(path, body)
349
    
350
    def disconnect_server(self, server_id, network_id):
351
        path = '/networks/%s/action' % network_id
352
        body = json.dumps({'remove': {'serverRef': server_id}})
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)