Statistics
| Branch: | Tag: | Revision:

root / kamaki / client.py @ 054e60c0

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 base64 import b64encode
38
from httplib import HTTPConnection, HTTPSConnection
39
from urlparse import urlparse
40

    
41

    
42
log = logging.getLogger('kamaki.client')
43

    
44

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

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

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

    
62

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

    
120
        return reply
121
    
122
    def _get(self, path, success=200):
123
        return self._cmd('GET', path, None, success)
124
    
125
    def _post(self, path, body, success=202):
126
        return self._cmd('POST', path, body, success)
127
    
128
    def _put(self, path, body, success=204):
129
        return self._cmd('PUT', path, body, success)
130
    
131
    def _delete(self, path, success=204):
132
        return self._cmd('DELETE', path, None, success)
133
    
134
    
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
            p = []
166
            for path, data in personality:
167
                contents = b64encode(data)
168
                p.append({'path': path, 'contents': contents})
169
            req['personality'] = p
170
        
171
        body = json.dumps({'server': req})
172
        reply = self._post('/servers', body)
173
        return reply['server']
174
    
175
    def update_server_name(self, server_id, new_name):
176
        """Update the name of the server as reported by the API.
177

178
        This call does not modify the hostname actually used by the server
179
        internally.
180

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

220
        The server is specified by id, the profile argument
221
        is one of (ENABLED, DISABLED, PROTECTED).
222

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

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

    
284
    def get_image_details(self, image_id):
285
        path = '/images/%d' % image_id
286
        reply = self._get(path)
287
        return reply['image']
288

    
289
    def create_image(self, server_id, name):
290
        req = {'name': name, 'serverRef': server_id}
291
        body = json.dumps({'image': req})
292
        reply = self._post('/images', body)
293
        return reply['image']
294

    
295
    def delete_image(self, image_id):
296
        path = '/images/%d' % image_id
297
        self._delete(path)
298

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

    
312
    def update_image_metadata(self, image_id, key, val):
313
        path = '/images/%d/meta' % image_id
314
        body = json.dumps({'metadata': {key: val}})
315
        reply = self._post(path, body, 201)
316
        return reply['metadata']
317

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

    
349
    def connect_server(self, server_id, network_id):
350
        path = '/networks/%s/action' % network_id
351
        body = json.dumps({'add': {'serverRef': server_id}})
352
        self._post(path, body)
353
    
354
    def disconnect_server(self, server_id, network_id):
355
        path = '/networks/%s/action' % network_id
356
        body = json.dumps({'remove': {'serverRef': server_id}})
357
        self._post(path, body)