Fix some spelling and typoes in docs
[kamaki] / kamaki / clients / cyclades / __init__.py
1 # Copyright 2011-2013 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 from sys import stdout
35 from time import sleep
36
37 from kamaki.clients.cyclades.rest_api import CycladesRestClient
38 from kamaki.clients import ClientError
39
40
41 class CycladesClient(CycladesRestClient):
42     """Synnefo Cyclades Compute API client"""
43
44     def create_server(
45             self, name, flavor_id, image_id,
46             metadata=None, personality=None):
47         """Submit request to create a new server
48
49         :param name: (str)
50
51         :param flavor_id: integer id denoting a preset hardware configuration
52
53         :param image_id: (str) id denoting the OS image to run on the VM
54
55         :param metadata: (dict) vm metadata updated by os/users image metadata
56
57         :param personality: a list of (file path, file contents) tuples,
58             describing files to be injected into VM upon creation.
59
60         :returns: a dict with the new VMs details
61
62         :raises ClientError: wraps request errors
63         """
64         image = self.get_image_details(image_id)
65         metadata = metadata or dict()
66         for key in ('os', 'users'):
67             try:
68                 metadata[key] = image['metadata'][key]
69             except KeyError:
70                 pass
71
72         return super(CycladesClient, self).create_server(
73             name, flavor_id, image_id,
74             metadata=metadata, personality=personality)
75
76     def start_server(self, server_id):
77         """Submit a startup request
78
79         :param server_id: integer (str or int)
80
81         :returns: (dict) response headers
82         """
83         req = {'start': {}}
84         r = self.servers_action_post(server_id, json_data=req, success=202)
85         return r.headers
86
87     def shutdown_server(self, server_id):
88         """Submit a shutdown request
89
90         :param server_id: integer (str or int)
91
92         :returns: (dict) response headers
93         """
94         req = {'shutdown': {}}
95         r = self.servers_action_post(server_id, json_data=req, success=202)
96         return r.headers
97
98     def get_server_console(self, server_id):
99         """
100         :param server_id: integer (str or int)
101
102         :returns: (dict) info to set a VNC connection to VM
103         """
104         req = {'console': {'type': 'vnc'}}
105         r = self.servers_action_post(server_id, json_data=req, success=200)
106         return r.json['console']
107
108     def get_firewall_profile(self, server_id):
109         """
110         :param server_id: integer (str or int)
111
112         :returns: (str) ENABLED | DISABLED | PROTECTED
113
114         :raises ClientError: 520 No Firewall Profile
115         """
116         r = self.get_server_details(server_id)
117         try:
118             return r['attachments'][0]['firewallProfile']
119         except KeyError:
120             raise ClientError(
121                 'No Firewall Profile',
122                 details='Server %s is missing a firewall profile' % server_id)
123
124     def set_firewall_profile(self, server_id, profile):
125         """Set the firewall profile for the public interface of a server
126
127         :param server_id: integer (str or int)
128
129         :param profile: (str) ENABLED | DISABLED | PROTECTED
130
131         :returns: (dict) response headers
132         """
133         req = {'firewallProfile': {'profile': profile}}
134         r = self.servers_action_post(server_id, json_data=req, success=202)
135         return r.headers
136
137     def list_server_nics(self, server_id):
138         """
139         :param server_id: integer (str or int)
140
141         :returns: (dict) network interface connections
142         """
143         r = self.servers_ips_get(server_id)
144         return r.json['attachments']
145
146     def get_server_stats(self, server_id):
147         """
148         :param server_id: integer (str or int)
149
150         :returns: (dict) auto-generated graphs of statistics (urls)
151         """
152         r = self.servers_stats_get(server_id)
153         return r.json['stats']
154
155     def list_networks(self, detail=False):
156         """
157         :param detail: (bool)
158
159         :returns: (list) id,name if not detail else full info per network
160         """
161         detail = 'detail' if detail else ''
162         r = self.networks_get(command=detail)
163         return r.json['networks']
164
165     def list_network_nics(self, network_id):
166         """
167         :param network_id: integer (str or int)
168
169         :returns: (list)
170         """
171         r = self.networks_get(network_id=network_id)
172         return r.json['network']['attachments']
173
174     def create_network(
175             self, name,
176             cidr=None, gateway=None, type=None, dhcp=False):
177         """
178         :param name: (str)
179
180         :param cidr: (str)
181
182         :param geteway: (str)
183
184         :param type: (str) if None, will use MAC_FILTERED as default
185             Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
186
187         :param dhcp: (bool)
188
189         :returns: (dict) network detailed info
190         """
191         net = dict(name=name)
192         if cidr:
193             net['cidr'] = cidr
194         if gateway:
195             net['gateway'] = gateway
196         net['type'] = type or 'MAC_FILTERED'
197         net['dhcp'] = True if dhcp else False
198         req = dict(network=net)
199         r = self.networks_post(json_data=req, success=202)
200         return r.json['network']
201
202     def get_network_details(self, network_id):
203         """
204         :param network_id: integer (str or int)
205
206         :returns: (dict)
207         """
208         r = self.networks_get(network_id=network_id)
209         return r.json['network']
210
211     def update_network_name(self, network_id, new_name):
212         """
213         :param network_id: integer (str or int)
214
215         :param new_name: (str)
216
217         :returns: (dict) response headers
218         """
219         req = {'network': {'name': new_name}}
220         r = self.networks_put(network_id=network_id, json_data=req)
221         return r.headers
222
223     def delete_network(self, network_id):
224         """
225         :param network_id: integer (str or int)
226
227         :returns: (dict) response headers
228
229         :raises ClientError: 421 Network in use
230         """
231         try:
232             r = self.networks_delete(network_id)
233             return r.headers
234         except ClientError as err:
235             if err.status == 421:
236                 err.details = [
237                     'Network may be still connected to at least one server']
238             raise
239
240     def connect_server(self, server_id, network_id):
241         """ Connect a server to a network
242
243         :param server_id: integer (str or int)
244
245         :param network_id: integer (str or int)
246
247         :returns: (dict) response headers
248         """
249         req = {'add': {'serverRef': server_id}}
250         r = self.networks_post(network_id, 'action', json_data=req)
251         return r.headers
252
253     def disconnect_server(self, server_id, nic_id):
254         """
255         :param server_id: integer (str or int)
256
257         :param nic_id: (str)
258
259         :returns: (int) the number of nics disconnected
260         """
261         vm_nets = self.list_server_nics(server_id)
262         num_of_disconnections = 0
263         for (nic_id, network_id) in [(
264                 net['id'],
265                 net['network_id']) for net in vm_nets if nic_id == net['id']]:
266             req = {'remove': {'attachment': '%s' % nic_id}}
267             self.networks_post(network_id, 'action', json_data=req)
268             num_of_disconnections += 1
269         return num_of_disconnections
270
271     def disconnect_network_nics(self, netid):
272         """
273         :param netid: integer (str or int)
274         """
275         for nic in self.list_network_nics(netid):
276             req = dict(remove=dict(attachment=nic))
277             self.networks_post(netid, 'action', json_data=req)
278
279     def _wait(
280             self, item_id, current_status, get_status,
281             delay=1, max_wait=100, wait_cb=None):
282         """Wait for item while its status is current_status
283
284         :param server_id: integer (str or int)
285
286         :param current_status: (str)
287
288         :param get_status: (method(self, item_id)) if called, returns
289             (status, progress %) If no way to tell progress, return None
290
291         :param delay: time interval between retries
292
293         :param wait_cb: if set a progress bar is used to show progress
294
295         :returns: (str) the new mode if successful, (bool) False if timed out
296         """
297         status, progress = get_status(self, item_id)
298         if status != current_status:
299             return status
300         old_wait = total_wait = 0
301
302         if wait_cb:
303             wait_gen = wait_cb(1 + max_wait // delay)
304             wait_gen.next()
305
306         while status == current_status and total_wait <= max_wait:
307             if wait_cb:
308                 try:
309                     for i in range(total_wait - old_wait):
310                         wait_gen.next()
311                 except Exception:
312                     break
313             else:
314                 stdout.write('.')
315                 stdout.flush()
316             old_wait = total_wait
317             total_wait = progress or (total_wait + 1)
318             sleep(delay)
319             status, progress = get_status(self, item_id)
320
321         if total_wait < max_wait:
322             if wait_cb:
323                 try:
324                     for i in range(max_wait):
325                         wait_gen.next()
326                 except:
327                     pass
328         return status if status != current_status else False
329
330     def wait_server(
331             self, server_id,
332             current_status='BUILD',
333             delay=1, max_wait=100, wait_cb=None):
334         """Wait for server while its status is current_status
335
336         :param server_id: integer (str or int)
337
338         :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
339
340         :param delay: time interval between retries
341
342         :param wait_cb: if set a progressbar is used to show progress
343
344         :returns: (str) the new mode if succesfull, (bool) False if timed out
345         """
346
347         def get_status(self, server_id):
348             r = self.get_server_details(server_id)
349             return r['status'], (r.get('progress', None) if (
350                             current_status in ('BUILD', )) else None)
351
352         return self._wait(
353             server_id, current_status, get_status, delay, max_wait, wait_cb)
354
355     def wait_network(
356             self, net_id,
357             current_status='LALA', delay=1, max_wait=100, wait_cb=None):
358         """Wait for network while its status is current_status
359
360         :param net_id: integer (str or int)
361
362         :param current_status: (str) PENDING | ACTIVE | DELETED
363
364         :param delay: time interval between retries
365
366         :param wait_cb: if set a progressbar is used to show progress
367
368         :returns: (str) the new mode if succesfull, (bool) False if timed out
369         """
370
371         def get_status(self, net_id):
372             r = self.get_network_details(net_id)
373             return r['status'], None
374
375         return self._wait(
376             net_id, current_status, get_status, delay, max_wait, wait_cb)
377
378     def get_floating_ip_pools(self):
379         """
380         :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
381         """
382         r = self.floating_ip_pools_get()
383         return r.json
384
385     def get_floating_ips(self):
386         """
387         :returns: (dict) {floating_ips:[
388             {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},
389             ... ]}
390         """
391         r = self.floating_ips_get()
392         return r.json
393
394     def alloc_floating_ip(self, pool=None, address=None):
395         """
396         :param pool: (str) pool of ips to allocate from
397
398         :param address: (str) ip address to request
399
400         :returns: (dict) {
401             fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
402         """
403         json_data = dict()
404         if pool:
405             json_data['pool'] = pool
406             if address:
407                 json_data['address'] = address
408         r = self.floating_ips_post(json_data)
409         return r.json['floating_ip']
410
411     def get_floating_ip(self, fip_id):
412         """
413         :param fip_id: (str) floating ip id
414
415         :returns: (dict)
416             {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},
417
418         :raises AssertionError: if fip_id is emtpy
419         """
420         assert fip_id, 'floating ip id is needed for get_floating_ip'
421         r = self.floating_ips_get(fip_id)
422         return r.json['floating_ip']
423
424     def delete_floating_ip(self, fip_id=None):
425         """
426         :param fip_id: (str) floating ip id (if None, all ips are deleted)
427
428         :returns: (dict) request headers
429
430         :raises AssertionError: if fip_id is emtpy
431         """
432         assert fip_id, 'floating ip id is needed for delete_floating_ip'
433         r = self.floating_ips_delete(fip_id)
434         return r.headers
435
436     def attach_floating_ip(self, server_id, address):
437         """Associate the address ip to server with server_id
438
439         :param server_id: (int)
440
441         :param address: (str) the ip address to assign to server (vm)
442
443         :returns: (dict) request headers
444
445         :raises ValueError: if server_id cannot be converted to int
446
447         :raises ValueError: if server_id is not of a int-convertable type
448
449         :raises AssertionError: if address is emtpy
450         """
451         server_id = int(server_id)
452         assert address, 'address is needed for attach_floating_ip'
453         req = dict(addFloatingIp=dict(address=address))
454         r = self.servers_action_post(server_id, json_data=req)
455         return r.headers
456
457     def detach_floating_ip(self, server_id, address):
458         """Disassociate an address ip from the server with server_id
459
460         :param server_id: (int)
461
462         :param address: (str) the ip address to assign to server (vm)
463
464         :returns: (dict) request headers
465
466         :raises ValueError: if server_id cannot be converted to int
467
468         :raises ValueError: if server_id is not of a int-convertable type
469
470         :raises AssertionError: if address is emtpy
471         """
472         server_id = int(server_id)
473         assert address, 'address is needed for detach_floating_ip'
474         req = dict(removeFloatingIp=dict(address=address))
475         r = self.servers_action_post(server_id, json_data=req)
476         return r.headers