1 # Copyright 2011 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
37 from base64 import b64encode
38 from httplib import HTTPConnection, HTTPSConnection
39 from urlparse import urlparse
42 log = logging.getLogger('kamaki.client')
45 class ClientError(Exception):
46 def __init__(self, message, status=0, details=''):
47 self.message = message
49 self.details = details
52 return int(self.status)
57 r += "\nHTTP Status: %d" % self.status
59 r += "\nDetails: \n%s" % self.details
64 def __init__(self, url, token=''):
68 def _cmd(self, method, path, body=None, success=200):
69 p = urlparse(self.url)
71 if p.scheme == 'http':
72 conn = HTTPConnection(p.netloc)
73 elif p.scheme == 'https':
74 conn = HTTPSConnection(p.netloc)
76 raise ClientError('Unknown URL scheme')
78 headers = {'X-Auth-Token': self.token}
80 headers['Content-Type'] = 'application/json'
81 headers['Content-Length'] = len(body)
83 log.debug('%s', '>' * 40)
84 log.debug('%s %s', method, path)
86 for key, val in headers.items():
87 log.debug('%s: %s', key, val)
93 conn.request(method, path, body, headers)
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)
104 reply = json.loads(buf) if buf else {}
106 raise ClientError('Did not receive valid JSON reply',
109 if resp.status != success:
111 key = reply.keys()[0]
113 message = '%s: %s' % (key, val.get('message', ''))
114 details = val.get('details', '')
115 raise ClientError(message, resp.status, details)
117 raise ClientError('Invalid response from the server')
121 def _get(self, path, success=200):
122 return self._cmd('GET', path, None, success)
124 def _post(self, path, body, success=202):
125 return self._cmd('POST', path, body, success)
127 def _put(self, path, body, success=204):
128 return self._cmd('PUT', path, body, success)
130 def _delete(self, path, success=204):
131 return self._cmd('DELETE', path, None, success)
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']
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']
148 def create_server(self, name, flavor_id, image_id, personality=None):
149 """Submit request to create a new server
151 The flavor_id specifies the hardware configuration to use,
152 the image_id specifies the OS Image to be deployed inside the new
155 The personality argument is a list of (file path, file contents)
156 tuples, describing files to be injected into the server upon creation.
158 The call returns a dictionary describing the newly created server.
162 req = {'name': name, 'flavorRef': flavor_id, 'imageRef': image_id}
165 for path, data in personality:
166 contents = b64encode(data)
167 p.append({'path': path, 'contents': contents})
168 req['personality'] = p
170 body = json.dumps({'server': req})
171 reply = self._post('/servers', body)
172 return reply['server']
174 def update_server_name(self, server_id, new_name):
175 """Update the name of the server as reported by the API.
177 This call does not modify the hostname actually used by the server
181 path = '/servers/%d' % server_id
182 body = json.dumps({'server': {'name': new_name}})
183 self._put(path, body)
185 def delete_server(self, server_id):
186 """Submit a deletion request for a server specified by id"""
187 path = '/servers/%d' % server_id
190 def reboot_server(self, server_id, hard=False):
191 """Submit a reboot request for a server specified by id"""
192 path = '/servers/%d/action' % server_id
193 type = 'HARD' if hard else 'SOFT'
194 body = json.dumps({'reboot': {'type': type}})
195 self._post(path, body)
197 def start_server(self, server_id):
198 """Submit a startup request for a server specified by id"""
199 path = '/servers/%d/action' % server_id
200 body = json.dumps({'start': {}})
201 self._post(path, body)
203 def shutdown_server(self, server_id):
204 """Submit a shutdown request for a server specified by id"""
205 path = '/servers/%d/action' % server_id
206 body = json.dumps({'shutdown': {}})
207 self._post(path, body)
209 def get_server_console(self, server_id):
210 """Get a VNC connection to the console of a server specified by id"""
211 path = '/servers/%d/action' % server_id
212 body = json.dumps({'console': {'type': 'vnc'}})
213 reply = self._cmd('POST', path, body, 200)
214 return reply['console']
216 def set_firewall_profile(self, server_id, profile):
217 """Set the firewall profile for the public interface of a server
219 The server is specified by id, the profile argument
220 is one of (ENABLED, DISABLED, PROTECTED).
223 path = '/servers/%d/action' % server_id
224 body = json.dumps({'firewallProfile': {'profile': profile}})
225 self._cmd('POST', path, body, 202)
227 def list_server_addresses(self, server_id, network=None):
228 path = '/servers/%d/ips' % server_id
230 path += '/%s' % network
231 reply = self._get(path)
232 return [reply['network']] if network else reply['addresses']['values']
234 def get_server_metadata(self, server_id, key=None):
235 path = '/servers/%d/meta' % server_id
238 reply = self._get(path)
239 return reply['meta'] if key else reply['metadata']['values']
241 def create_server_metadata(self, server_id, key, val):
242 path = '/servers/%d/meta/%s' % (server_id, key)
243 body = json.dumps({'meta': {key: val}})
244 reply = self._put(path, body, 201)
247 def update_server_metadata(self, server_id, key, val):
248 path = '/servers/%d/meta' % server_id
249 body = json.dumps({'metadata': {key: val}})
250 reply = self._post(path, body, 201)
251 return reply['metadata']
253 def delete_server_metadata(self, server_id, key):
254 path = '/servers/%d/meta/%s' % (server_id, key)
255 reply = self._delete(path)
257 def get_server_stats(self, server_id):
258 path = '/servers/%d/stats' % server_id
259 reply = self._get(path)
260 return reply['stats']
265 def list_flavors(self, detail=False):
266 path = '/flavors/detail' if detail else '/flavors'
267 reply = self._get(path)
268 return reply['flavors']['values']
270 def get_flavor_details(self, flavor_id):
271 path = '/flavors/%d' % flavor_id
272 reply = self._get(path)
273 return reply['flavor']
278 def list_images(self, detail=False):
279 path = '/images/detail' if detail else '/images'
280 reply = self._get(path)
281 return reply['images']['values']
283 def get_image_details(self, image_id):
284 path = '/images/%d' % image_id
285 reply = self._get(path)
286 return reply['image']
288 def create_image(self, server_id, name):
289 req = {'name': name, 'serverRef': server_id}
290 body = json.dumps({'image': req})
291 reply = self._post('/images', body)
292 return reply['image']
294 def delete_image(self, image_id):
295 path = '/images/%d' % image_id
298 def get_image_metadata(self, image_id, key=None):
299 path = '/images/%d/meta' % image_id
302 reply = self._get(path)
303 return reply['meta'] if key else reply['metadata']['values']
305 def create_image_metadata(self, image_id, key, val):
306 path = '/images/%d/meta/%s' % (image_id, key)
307 body = json.dumps({'meta': {key: val}})
308 reply = self._put(path, body, 201)
311 def update_image_metadata(self, image_id, key, val):
312 path = '/images/%d/meta' % image_id
313 body = json.dumps({'metadata': {key: val}})
314 reply = self._post(path, body, 201)
315 return reply['metadata']
317 def delete_image_metadata(self, image_id, key):
318 path = '/images/%d/meta/%s' % (image_id, key)
319 reply = self._delete(path)
324 def list_networks(self, detail=False):
325 path = '/networks/detail' if detail else '/networks'
326 reply = self._get(path)
327 return reply['networks']['values']
329 def create_network(self, name):
330 body = json.dumps({'network': {'name': name}})
331 reply = self._post('/networks', body)
332 return reply['network']
334 def get_network_details(self, network_id):
335 path = '/networks/%s' % network_id
336 reply = self._get(path)
337 return reply['network']
339 def update_network_name(self, network_id, new_name):
340 path = '/networks/%s' % network_id
341 body = json.dumps({'network': {'name': new_name}})
342 self._put(path, body)
344 def delete_network(self, network_id):
345 path = '/networks/%s' % network_id
348 def connect_server(self, server_id, network_id):
349 path = '/networks/%s/action' % network_id
350 body = json.dumps({'add': {'serverRef': server_id}})
351 self._post(path, body)
353 def disconnect_server(self, server_id, network_id):
354 path = '/networks/%s/action' % network_id
355 body = json.dumps({'remove': {'serverRef': server_id}})
356 self._post(path, body)