Improve logging
[kamaki] / 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 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('%s', '>' * 40)
84         log.debug('%s %s', method, path)
85
86         for key, val in headers.items():
87             log.debug('%s: %s', key, val)
88         log.debug('')
89         if body:
90             log.debug(body)
91             log.debug('')
92         
93         conn.request(method, path, body, headers)
94
95         resp = conn.getresponse()
96         log.debug('%s', '<' * 40)
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         
102         buf = resp.read()
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         path = '/servers/detail' if detail else '/servers'
138         reply = self._get(path)
139         return reply['servers']['values']
140     
141     def get_server_details(self, server_id):
142         path = '/servers/%d' % server_id
143         reply = self._get(path)
144         return reply['server']
145     
146     def create_server(self, name, flavor, image, personality=None):
147         """personality is a list of (path, data) tuples"""
148         
149         req = {'name': name, 'flavorRef': flavor, 'imageRef': image}
150         if personality:
151             p = []
152             for path, data in personality:
153                 contents = b64encode(data)
154                 p.append({'path': path, 'contents': contents})
155             req['personality'] = p
156         
157         body = json.dumps({'server': req})
158         reply = self._post('/servers', body)
159         return reply['server']
160     
161     def update_server_name(self, server_id, new_name):
162         path = '/servers/%d' % server_id
163         body = json.dumps({'server': {'name': new_name}})
164         self._put(path, body)
165     
166     def delete_server(self, server_id):
167         path = '/servers/%d' % server_id
168         self._delete(path)
169     
170     def reboot_server(self, server_id, hard=False):
171         path = '/servers/%d/action' % server_id
172         type = 'HARD' if hard else 'SOFT'
173         body = json.dumps({'reboot': {'type': type}})
174         self._post(path, body)
175     
176     def start_server(self, server_id):
177         path = '/servers/%d/action' % server_id
178         body = json.dumps({'start': {}})
179         self._post(path, body)
180     
181     def shutdown_server(self, server_id):
182         path = '/servers/%d/action' % server_id
183         body = json.dumps({'shutdown': {}})
184         self._post(path, body)
185     
186     def get_server_console(self, server_id):
187         path = '/servers/%d/action' % server_id
188         body = json.dumps({'console': {'type': 'vnc'}})
189         reply = self._cmd('POST', path, body, 200)
190         return reply['console']
191     
192     def set_firewall_profile(self, server_id, profile):
193         path = '/servers/%d/action' % server_id
194         body = json.dumps({'firewallProfile': {'profile': profile}})
195         self._cmd('POST', path, body, 202)
196     
197     def list_server_addresses(self, server_id, network=None):
198         path = '/servers/%d/ips' % server_id
199         if network:
200             path += '/%s' % network
201         reply = self._get(path)
202         return [reply['network']] if network else reply['addresses']['values']
203     
204     def get_server_metadata(self, server_id, key=None):
205         path = '/servers/%d/meta' % server_id
206         if key:
207             path += '/%s' % key
208         reply = self._get(path)
209         return reply['meta'] if key else reply['metadata']['values']
210     
211     def create_server_metadata(self, server_id, key, val):
212         path = '/servers/%d/meta/%s' % (server_id, key)
213         body = json.dumps({'meta': {key: val}})
214         reply = self._put(path, body, 201)
215         return reply['meta']
216     
217     def update_server_metadata(self, server_id, key, val):
218         path = '/servers/%d/meta' % server_id
219         body = json.dumps({'metadata': {key: val}})
220         reply = self._post(path, body, 201)
221         return reply['metadata']
222     
223     def delete_server_metadata(self, server_id, key):
224         path = '/servers/%d/meta/%s' % (server_id, key)
225         reply = self._delete(path)
226     
227     def get_server_stats(self, server_id):
228         path = '/servers/%d/stats' % server_id
229         reply = self._get(path)
230         return reply['stats']
231     
232     
233     # Flavors
234     
235     def list_flavors(self, detail=False):
236         path = '/flavors/detail' if detail else '/flavors'
237         reply = self._get(path)
238         return reply['flavors']['values']
239
240     def get_flavor_details(self, flavor_id):
241         path = '/flavors/%d' % flavor_id
242         reply = self._get(path)
243         return reply['flavor']
244     
245     
246     # Images
247     
248     def list_images(self, detail=False):
249         path = '/images/detail' if detail else '/images'
250         reply = self._get(path)
251         return reply['images']['values']
252
253     def get_image_details(self, image_id):
254         path = '/images/%d' % image_id
255         reply = self._get(path)
256         return reply['image']
257
258     def create_image(self, server_id, name):
259         req = {'name': name, 'serverRef': server_id}
260         body = json.dumps({'image': req})
261         reply = self._post('/images', body)
262         return reply['image']
263
264     def delete_image(self, image_id):
265         path = '/images/%d' % image_id
266         self._delete(path)
267
268     def get_image_metadata(self, image_id, key=None):
269         path = '/images/%d/meta' % image_id
270         if key:
271             path += '/%s' % key
272         reply = self._get(path)
273         return reply['meta'] if key else reply['metadata']['values']
274     
275     def create_image_metadata(self, image_id, key, val):
276         path = '/images/%d/meta/%s' % (image_id, key)
277         body = json.dumps({'meta': {key: val}})
278         reply = self._put(path, body, 201)
279         reply['meta']
280
281     def update_image_metadata(self, image_id, key, val):
282         path = '/images/%d/meta' % image_id
283         body = json.dumps({'metadata': {key: val}})
284         reply = self._post(path, body, 201)
285         return reply['metadata']
286
287     def delete_image_metadata(self, image_id, key):
288         path = '/images/%d/meta/%s' % (image_id, key)
289         reply = self._delete(path)
290     
291     
292     # Networks
293     
294     def list_networks(self, detail=False):
295         path = '/networks/detail' if detail else '/networks'
296         reply = self._get(path)
297         return reply['networks']['values']
298     
299     def create_network(self, name):
300         body = json.dumps({'network': {'name': name}})
301         reply = self._post('/networks', body)
302         return reply['network']
303     
304     def get_network_details(self, network_id):
305         path = '/networks/%s' % network_id
306         reply = self._get(path)
307         return reply['network']
308     
309     def update_network_name(self, network_id, new_name):
310         path = '/networks/%s' % network_id
311         body = json.dumps({'network': {'name': new_name}})
312         self._put(path, body)
313     
314     def delete_network(self, network_id):
315         path = '/networks/%s' % network_id
316         self._delete(path)
317
318     def connect_server(self, server_id, network_id):
319         path = '/networks/%s/action' % network_id
320         body = json.dumps({'add': {'serverRef': server_id}})
321         self._post(path, body)
322     
323     def disconnect_server(self, server_id, network_id):
324         path = '/networks/%s/action' % network_id
325         body = json.dumps({'remove': {'serverRef': server_id}})
326         self._post(path, body)