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