Statistics
| Branch: | Tag: | Revision:

root / kamaki / client.py @ 57b8dd5a

History | View | Annotate | Download (12 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
    # Servers
135
    
136
    def list_servers(self, detail=False):
137
        """List servers, returned detailed output if detailed is True"""
138
        path = '/servers/detail' if detail else '/servers'
139
        reply = self._get(path)
140
        return reply['servers']['values']
141
    
142
    def get_server_details(self, server_id):
143
        """Return detailed output on a server specified by its id"""
144
        path = '/servers/%d' % server_id
145
        reply = self._get(path)
146
        return reply['server']
147
    
148
    def create_server(self, name, flavor_id, image_id, personality=None):
149
        """Submit request to create a new server
150

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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