Implement network wait
[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 kamaki.clients.cyclades.rest_api import CycladesRestClient
35 from kamaki.clients.network import NetworkClient
36 from kamaki.clients.utils import path4url
37 from kamaki.clients import ClientError, Waiter
38
39
40 class CycladesClient(CycladesRestClient, Waiter):
41     """Synnefo Cyclades Compute API client"""
42
43     def create_server(
44             self, name, flavor_id, image_id,
45             metadata=None, personality=None, networks=None):
46         """Submit request to create a new server
47
48         :param name: (str)
49
50         :param flavor_id: integer id denoting a preset hardware configuration
51
52         :param image_id: (str) id denoting the OS image to run on virt. server
53
54         :param metadata: (dict) vm metadata updated by os/users image metadata
55
56         :param personality: a list of (file path, file contents) tuples,
57             describing files to be injected into virtual server upon creation
58
59         :param networks: (list of dicts) Networks to connect to, list this:
60             "networks": [
61                 {"network": <network_uuid>},
62                 {"network": <network_uuid>, "fixed_ip": address},
63                 {"port": <port_id>}, ...]
64
65         :returns: a dict with the new virtual server details
66
67         :raises ClientError: wraps request errors
68         """
69         image = self.get_image_details(image_id)
70         metadata = metadata or dict()
71         for key in ('os', 'users'):
72             try:
73                 metadata[key] = image['metadata'][key]
74             except KeyError:
75                 pass
76
77         return super(CycladesClient, self).create_server(
78             name, flavor_id, image_id,
79             metadata=metadata, personality=personality)
80
81     def start_server(self, server_id):
82         """Submit a startup request
83
84         :param server_id: integer (str or int)
85
86         :returns: (dict) response headers
87         """
88         req = {'start': {}}
89         r = self.servers_action_post(server_id, json_data=req, success=202)
90         return r.headers
91
92     def shutdown_server(self, server_id):
93         """Submit a shutdown request
94
95         :param server_id: integer (str or int)
96
97         :returns: (dict) response headers
98         """
99         req = {'shutdown': {}}
100         r = self.servers_action_post(server_id, json_data=req, success=202)
101         return r.headers
102
103     def get_server_console(self, server_id):
104         """
105         :param server_id: integer (str or int)
106
107         :returns: (dict) info to set a VNC connection to virtual server
108         """
109         req = {'console': {'type': 'vnc'}}
110         r = self.servers_action_post(server_id, json_data=req, success=200)
111         return r.json['console']
112
113     def get_firewall_profile(self, server_id):
114         """
115         :param server_id: integer (str or int)
116
117         :returns: (str) ENABLED | DISABLED | PROTECTED
118
119         :raises ClientError: 520 No Firewall Profile
120         """
121         r = self.get_server_details(server_id)
122         try:
123             return r['attachments'][0]['firewallProfile']
124         except KeyError:
125             raise ClientError(
126                 'No Firewall Profile',
127                 details='Server %s is missing a firewall profile' % server_id)
128
129     def set_firewall_profile(self, server_id, profile):
130         """Set the firewall profile for the public interface of a server
131
132         :param server_id: integer (str or int)
133
134         :param profile: (str) ENABLED | DISABLED | PROTECTED
135
136         :returns: (dict) response headers
137         """
138         req = {'firewallProfile': {'profile': profile}}
139         r = self.servers_action_post(server_id, json_data=req, success=202)
140         return r.headers
141
142     def list_server_nics(self, server_id):
143         """
144         :param server_id: integer (str or int)
145
146         :returns: (dict) network interface connections
147         """
148         r = self.servers_ips_get(server_id)
149         return r.json['attachments']
150
151     def get_server_stats(self, server_id):
152         """
153         :param server_id: integer (str or int)
154
155         :returns: (dict) auto-generated graphs of statistics (urls)
156         """
157         r = self.servers_stats_get(server_id)
158         return r.json['stats']
159
160     def list_networks(self, detail=False):
161         """
162         :param detail: (bool)
163
164         :returns: (list) id,name if not detail else full info per network
165         """
166         detail = 'detail' if detail else ''
167         r = self.networks_get(command=detail)
168         return r.json['networks']
169
170     def list_network_nics(self, network_id):
171         """
172         :param network_id: integer (str or int)
173
174         :returns: (list)
175         """
176         r = self.networks_get(network_id=network_id)
177         return r.json['network']['attachments']
178
179     def create_network(
180             self, name,
181             cidr=None, gateway=None, type=None, dhcp=False):
182         """
183         :param name: (str)
184
185         :param cidr: (str)
186
187         :param geteway: (str)
188
189         :param type: (str) if None, will use MAC_FILTERED as default
190             Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
191
192         :param dhcp: (bool)
193
194         :returns: (dict) network detailed info
195         """
196         net = dict(name=name)
197         if cidr:
198             net['cidr'] = cidr
199         if gateway:
200             net['gateway'] = gateway
201         net['type'] = type or 'MAC_FILTERED'
202         net['dhcp'] = True if dhcp else False
203         req = dict(network=net)
204         r = self.networks_post(json_data=req, success=202)
205         return r.json['network']
206
207     def get_network_details(self, network_id):
208         """
209         :param network_id: integer (str or int)
210
211         :returns: (dict)
212         """
213         r = self.networks_get(network_id=network_id)
214         return r.json['network']
215
216     def update_network_name(self, network_id, new_name):
217         """
218         :param network_id: integer (str or int)
219
220         :param new_name: (str)
221
222         :returns: (dict) response headers
223         """
224         req = {'network': {'name': new_name}}
225         r = self.networks_put(network_id=network_id, json_data=req)
226         return r.headers
227
228     def delete_network(self, network_id):
229         """
230         :param network_id: integer (str or int)
231
232         :returns: (dict) response headers
233
234         :raises ClientError: 421 Network in use
235         """
236         try:
237             r = self.networks_delete(network_id)
238             return r.headers
239         except ClientError as err:
240             if err.status == 421:
241                 err.details = [
242                     'Network may be still connected to at least one server']
243             raise
244
245     def connect_server(self, server_id, network_id):
246         """ Connect a server to a network
247
248         :param server_id: integer (str or int)
249
250         :param network_id: integer (str or int)
251
252         :returns: (dict) response headers
253         """
254         req = {'add': {'serverRef': server_id}}
255         r = self.networks_post(network_id, 'action', json_data=req)
256         return r.headers
257
258     def disconnect_server(self, server_id, nic_id):
259         """
260         :param server_id: integer (str or int)
261
262         :param nic_id: (str)
263
264         :returns: (int) the number of nics disconnected
265         """
266         vm_nets = self.list_server_nics(server_id)
267         num_of_disconnections = 0
268         for (nic_id, network_id) in [(
269                 net['id'],
270                 net['network_id']) for net in vm_nets if nic_id == net['id']]:
271             req = {'remove': {'attachment': '%s' % nic_id}}
272             self.networks_post(network_id, 'action', json_data=req)
273             num_of_disconnections += 1
274         return num_of_disconnections
275
276     def disconnect_network_nics(self, netid):
277         """
278         :param netid: integer (str or int)
279         """
280         for nic in self.list_network_nics(netid):
281             req = dict(remove=dict(attachment=nic))
282             self.networks_post(netid, 'action', json_data=req)
283
284     def wait_server(
285             self, server_id,
286             current_status='BUILD',
287             delay=1, max_wait=100, wait_cb=None):
288         """Wait for server while its status is current_status
289
290         :param server_id: integer (str or int)
291
292         :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
293
294         :param delay: time interval between retries
295
296         :max_wait: (int) timeout in secconds
297
298         :param wait_cb: if set a progressbar is used to show progress
299
300         :returns: (str) the new mode if succesfull, (bool) False if timed out
301         """
302
303         def get_status(self, server_id):
304             r = self.get_server_details(server_id)
305             return r['status'], (r.get('progress', None) if (
306                             current_status in ('BUILD', )) else None)
307
308         return self._wait(
309             server_id, current_status, get_status, delay, max_wait, wait_cb)
310
311     def wait_network(
312             self, net_id,
313             current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
314         """Wait for network while its status is current_status
315
316         :param net_id: integer (str or int)
317
318         :param current_status: (str) PENDING | ACTIVE | DELETED
319
320         :param delay: time interval between retries
321
322         :max_wait: (int) timeout in secconds
323
324         :param wait_cb: if set a progressbar is used to show progress
325
326         :returns: (str) the new mode if succesfull, (bool) False if timed out
327         """
328
329         def get_status(self, net_id):
330             r = self.get_network_details(net_id)
331             return r['status'], None
332
333         return self._wait(
334             net_id, current_status, get_status, delay, max_wait, wait_cb)
335
336     def wait_firewall(
337             self, server_id,
338             current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
339         """Wait while the public network firewall status is current_status
340
341         :param server_id: integer (str or int)
342
343         :param current_status: (str) DISABLED | ENABLED | PROTECTED
344
345         :param delay: time interval between retries
346
347         :max_wait: (int) timeout in secconds
348
349         :param wait_cb: if set a progressbar is used to show progress
350
351         :returns: (str) the new mode if succesfull, (bool) False if timed out
352         """
353
354         def get_status(self, server_id):
355             return self.get_firewall_profile(server_id), None
356
357         return self._wait(
358             server_id, current_status, get_status, delay, max_wait, wait_cb)
359
360
361 class CycladesNetworkClient(NetworkClient, Waiter):
362     """Cyclades Network API extentions"""
363
364     network_types = (
365         'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
366
367     def list_networks(self, detail=None):
368         path = path4url('networks', 'detail' if detail else '')
369         r = self.get(path, success=200)
370         return r.json['networks']
371
372     def create_network(self, type, name=None, shared=None):
373         req = dict(network=dict(type=type, admin_state_up=True))
374         if name:
375             req['network']['name'] = name
376         if shared not in (None, ):
377             req['network']['shared'] = bool(shared)
378         r = self.networks_post(json_data=req, success=201)
379         return r.json['network']
380
381     def create_port(
382             self, network_id, device_id,
383             security_groups=None, name=None, fixed_ips=None):
384         port = dict(network_id=network_id, device_id=device_id)
385         if security_groups:
386             port['security_groups'] = security_groups
387         if name:
388             port['name'] = name
389         for fixed_ip in fixed_ips:
390             diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
391             if diff:
392                 raise ValueError(
393                     'Invalid format for "fixed_ips", %s missing' % diff)
394         if fixed_ips:
395             port['fixed_ips'] = fixed_ips
396         r = self.ports_post(json_data=dict(port=port), success=201)
397         return r.json['port']
398
399     def wait_network(
400             self, net_id,
401             current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
402         """Wait for network while its status is current_status
403
404         :param net_id: integer (str or int)
405
406         :param current_status: (str) PENDING | ACTIVE | DELETED
407
408         :param delay: time interval between retries
409
410         :max_wait: (int) timeout in secconds
411
412         :param wait_cb: if set a progressbar is used to show progress
413
414         :returns: (str) the new mode if succesfull, (bool) False if timed out
415         """
416
417         def get_status(self, net_id):
418             r = self.get_network_details(net_id)
419             return r['status'], None
420
421         return self._wait(
422             net_id, current_status, get_status, delay, max_wait, wait_cb)