Add docstrings to a number of client methods
[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         """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             p = []
165             for path, data in personality:
166                 contents = b64encode(data)
167                 p.append({'path': path, 'contents': contents})
168             req['personality'] = p
169         
170         body = json.dumps({'server': req})
171         reply = self._post('/servers', body)
172         return reply['server']
173     
174     def update_server_name(self, server_id, new_name):
175         """Update the name of the server as reported by the API.
176
177         This call does not modify the hostname actually used by the server
178         internally.
179
180         """
181         path = '/servers/%d' % server_id
182         body = json.dumps({'server': {'name': new_name}})
183         self._put(path, body)
184     
185     def delete_server(self, server_id):
186         """Submit a deletion request for a server specified by id"""
187         path = '/servers/%d' % server_id
188         self._delete(path)
189     
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)
196     
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)
202     
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)
208     
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']
215     
216     def set_firewall_profile(self, server_id, profile):
217         """Set the firewall profile for the public interface of a server
218
219         The server is specified by id, the profile argument
220         is one of (ENABLED, DISABLED, PROTECTED).
221
222         """
223         path = '/servers/%d/action' % server_id
224         body = json.dumps({'firewallProfile': {'profile': profile}})
225         self._cmd('POST', path, body, 202)
226     
227     def list_server_addresses(self, server_id, network=None):
228         path = '/servers/%d/ips' % server_id
229         if network:
230             path += '/%s' % network
231         reply = self._get(path)
232         return [reply['network']] if network else reply['addresses']['values']
233     
234     def get_server_metadata(self, server_id, key=None):
235         path = '/servers/%d/meta' % server_id
236         if key:
237             path += '/%s' % key
238         reply = self._get(path)
239         return reply['meta'] if key else reply['metadata']['values']
240     
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)
245         return reply['meta']
246     
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']
252     
253     def delete_server_metadata(self, server_id, key):
254         path = '/servers/%d/meta/%s' % (server_id, key)
255         reply = self._delete(path)
256     
257     def get_server_stats(self, server_id):
258         path = '/servers/%d/stats' % server_id
259         reply = self._get(path)
260         return reply['stats']
261     
262     
263     # Flavors
264     
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']
269
270     def get_flavor_details(self, flavor_id):
271         path = '/flavors/%d' % flavor_id
272         reply = self._get(path)
273         return reply['flavor']
274     
275     
276     # Images
277     
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']
282
283     def get_image_details(self, image_id):
284         path = '/images/%d' % image_id
285         reply = self._get(path)
286         return reply['image']
287
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']
293
294     def delete_image(self, image_id):
295         path = '/images/%d' % image_id
296         self._delete(path)
297
298     def get_image_metadata(self, image_id, key=None):
299         path = '/images/%d/meta' % image_id
300         if key:
301             path += '/%s' % key
302         reply = self._get(path)
303         return reply['meta'] if key else reply['metadata']['values']
304     
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)
309         reply['meta']
310
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']
316
317     def delete_image_metadata(self, image_id, key):
318         path = '/images/%d/meta/%s' % (image_id, key)
319         reply = self._delete(path)
320     
321     
322     # Networks
323     
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']
328     
329     def create_network(self, name):
330         body = json.dumps({'network': {'name': name}})
331         reply = self._post('/networks', body)
332         return reply['network']
333     
334     def get_network_details(self, network_id):
335         path = '/networks/%s' % network_id
336         reply = self._get(path)
337         return reply['network']
338     
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)
343     
344     def delete_network(self, network_id):
345         path = '/networks/%s' % network_id
346         self._delete(path)
347
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)
352     
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)