1 # Copyright 2011-2013 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.
34 from time import sleep
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
42 class CycladesClient(CycladesRestClient):
43 """Synnefo Cyclades Compute API client"""
46 self, name, flavor_id, image_id,
47 metadata=None, personality=None, networks=None):
48 """Submit request to create a new server
52 :param flavor_id: integer id denoting a preset hardware configuration
54 :param image_id: (str) id denoting the OS image to run on virt. server
56 :param metadata: (dict) vm metadata updated by os/users image metadata
58 :param personality: a list of (file path, file contents) tuples,
59 describing files to be injected into virtual server upon creation
61 :param networks: (list of dicts) Networks to connect to, list this:
63 {"network": <network_uuid>},
64 {"network": <network_uuid>, "fixed_ip": address},
65 {"port": <port_id>}, ...]
67 :returns: a dict with the new virtual server details
69 :raises ClientError: wraps request errors
71 image = self.get_image_details(image_id)
72 metadata = metadata or dict()
73 for key in ('os', 'users'):
75 metadata[key] = image['metadata'][key]
79 return super(CycladesClient, self).create_server(
80 name, flavor_id, image_id,
81 metadata=metadata, personality=personality)
83 def start_server(self, server_id):
84 """Submit a startup request
86 :param server_id: integer (str or int)
88 :returns: (dict) response headers
91 r = self.servers_action_post(server_id, json_data=req, success=202)
94 def shutdown_server(self, server_id):
95 """Submit a shutdown request
97 :param server_id: integer (str or int)
99 :returns: (dict) response headers
101 req = {'shutdown': {}}
102 r = self.servers_action_post(server_id, json_data=req, success=202)
105 def get_server_console(self, server_id):
107 :param server_id: integer (str or int)
109 :returns: (dict) info to set a VNC connection to virtual server
111 req = {'console': {'type': 'vnc'}}
112 r = self.servers_action_post(server_id, json_data=req, success=200)
113 return r.json['console']
115 def get_firewall_profile(self, server_id):
117 :param server_id: integer (str or int)
119 :returns: (str) ENABLED | DISABLED | PROTECTED
121 :raises ClientError: 520 No Firewall Profile
123 r = self.get_server_details(server_id)
125 return r['attachments'][0]['firewallProfile']
128 'No Firewall Profile',
129 details='Server %s is missing a firewall profile' % server_id)
131 def set_firewall_profile(self, server_id, profile):
132 """Set the firewall profile for the public interface of a server
134 :param server_id: integer (str or int)
136 :param profile: (str) ENABLED | DISABLED | PROTECTED
138 :returns: (dict) response headers
140 req = {'firewallProfile': {'profile': profile}}
141 r = self.servers_action_post(server_id, json_data=req, success=202)
144 def list_server_nics(self, server_id):
146 :param server_id: integer (str or int)
148 :returns: (dict) network interface connections
150 r = self.servers_ips_get(server_id)
151 return r.json['attachments']
153 def get_server_stats(self, server_id):
155 :param server_id: integer (str or int)
157 :returns: (dict) auto-generated graphs of statistics (urls)
159 r = self.servers_stats_get(server_id)
160 return r.json['stats']
162 def list_networks(self, detail=False):
164 :param detail: (bool)
166 :returns: (list) id,name if not detail else full info per network
168 detail = 'detail' if detail else ''
169 r = self.networks_get(command=detail)
170 return r.json['networks']
172 def list_network_nics(self, network_id):
174 :param network_id: integer (str or int)
178 r = self.networks_get(network_id=network_id)
179 return r.json['network']['attachments']
183 cidr=None, gateway=None, type=None, dhcp=False):
189 :param geteway: (str)
191 :param type: (str) if None, will use MAC_FILTERED as default
192 Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
196 :returns: (dict) network detailed info
198 net = dict(name=name)
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']
209 def get_network_details(self, network_id):
211 :param network_id: integer (str or int)
215 r = self.networks_get(network_id=network_id)
216 return r.json['network']
218 def update_network_name(self, network_id, new_name):
220 :param network_id: integer (str or int)
222 :param new_name: (str)
224 :returns: (dict) response headers
226 req = {'network': {'name': new_name}}
227 r = self.networks_put(network_id=network_id, json_data=req)
230 def delete_network(self, network_id):
232 :param network_id: integer (str or int)
234 :returns: (dict) response headers
236 :raises ClientError: 421 Network in use
239 r = self.networks_delete(network_id)
241 except ClientError as err:
242 if err.status == 421:
244 'Network may be still connected to at least one server']
247 def connect_server(self, server_id, network_id):
248 """ Connect a server to a network
250 :param server_id: integer (str or int)
252 :param network_id: integer (str or int)
254 :returns: (dict) response headers
256 req = {'add': {'serverRef': server_id}}
257 r = self.networks_post(network_id, 'action', json_data=req)
260 def disconnect_server(self, server_id, nic_id):
262 :param server_id: integer (str or int)
266 :returns: (int) the number of nics disconnected
268 vm_nets = self.list_server_nics(server_id)
269 num_of_disconnections = 0
270 for (nic_id, network_id) in [(
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
278 def disconnect_network_nics(self, netid):
280 :param netid: integer (str or int)
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)
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
291 :param server_id: integer (str or int)
293 :param current_status: (str)
295 :param get_status: (method(self, item_id)) if called, returns
296 (status, progress %) If no way to tell progress, return None
298 :param delay: time interval between retries
300 :param wait_cb: if set a progress bar is used to show progress
302 :returns: (str) the new mode if successful, (bool) False if timed out
304 status, progress = get_status(self, item_id)
307 wait_gen = wait_cb(max_wait // delay)
310 if status != current_status:
317 old_wait = total_wait = 0
319 while status == current_status and total_wait <= max_wait:
322 for i in range(total_wait - old_wait):
326 old_wait = total_wait
327 total_wait = progress or total_wait + 1
329 status, progress = get_status(self, item_id)
331 if total_wait < max_wait:
334 for i in range(max_wait):
338 return status if status != current_status else False
342 current_status='BUILD',
343 delay=1, max_wait=100, wait_cb=None):
344 """Wait for server while its status is current_status
346 :param server_id: integer (str or int)
348 :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
350 :param delay: time interval between retries
352 :max_wait: (int) timeout in secconds
354 :param wait_cb: if set a progressbar is used to show progress
356 :returns: (str) the new mode if succesfull, (bool) False if timed out
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)
365 server_id, current_status, get_status, delay, max_wait, wait_cb)
369 current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
370 """Wait for network while its status is current_status
372 :param net_id: integer (str or int)
374 :param current_status: (str) PENDING | ACTIVE | DELETED
376 :param delay: time interval between retries
378 :max_wait: (int) timeout in secconds
380 :param wait_cb: if set a progressbar is used to show progress
382 :returns: (str) the new mode if succesfull, (bool) False if timed out
385 def get_status(self, net_id):
386 r = self.get_network_details(net_id)
387 return r['status'], None
390 net_id, current_status, get_status, delay, max_wait, wait_cb)
394 current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
395 """Wait while the public network firewall status is current_status
397 :param server_id: integer (str or int)
399 :param current_status: (str) DISABLED | ENABLED | PROTECTED
401 :param delay: time interval between retries
403 :max_wait: (int) timeout in secconds
405 :param wait_cb: if set a progressbar is used to show progress
407 :returns: (str) the new mode if succesfull, (bool) False if timed out
410 def get_status(self, server_id):
411 return self.get_firewall_profile(server_id), None
414 server_id, current_status, get_status, delay, max_wait, wait_cb)
416 def get_floating_ip_pools(self):
418 :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
420 r = self.floating_ip_pools_get()
423 def get_floating_ips(self):
425 :returns: (dict) {floating_ips: [fixed_ip: , id: , ip: , pool: ]}
427 r = self.floating_ips_get()
430 def alloc_floating_ip(self, pool=None, address=None):
432 :param pool: (str) pool of ips to allocate from
434 :param address: (str) ip address to request
437 fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
441 json_data['pool'] = pool
443 json_data['address'] = address
444 r = self.floating_ips_post(json_data)
445 return r.json['floating_ip']
447 def get_floating_ip(self, fip_id):
449 :param fip_id: (str) floating ip id
452 {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},
454 :raises AssertionError: if fip_id is emtpy
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']
460 def delete_floating_ip(self, fip_id=None):
462 :param fip_id: (str) floating ip id (if None, all ips are deleted)
464 :returns: (dict) request headers
466 :raises AssertionError: if fip_id is emtpy
468 assert fip_id, 'floating ip id is needed for delete_floating_ip'
469 r = self.floating_ips_delete(fip_id)
472 def attach_floating_ip(self, server_id, address):
473 """Associate the address ip to server with server_id
475 :param server_id: (int)
477 :param address: (str) the ip address to assign to server (vm)
479 :returns: (dict) request headers
481 :raises ValueError: if server_id cannot be converted to int
483 :raises ValueError: if server_id is not of a int-convertable type
485 :raises AssertionError: if address is emtpy
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)
493 def detach_floating_ip(self, server_id, address):
494 """Disassociate an address ip from the server with server_id
496 :param server_id: (int)
498 :param address: (str) the ip address to assign to server (vm)
500 :returns: (dict) request headers
502 :raises ValueError: if server_id cannot be converted to int
504 :raises ValueError: if server_id is not of a int-convertable type
506 :raises AssertionError: if address is emtpy
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)
515 class CycladesNetworkClient(NetworkClient):
516 """Cyclades Network API extentions"""
519 'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
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']
526 def create_network(self, type, name=None, shared=None):
527 req = dict(network=dict(type=type, admin_state_up=True))
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']
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)
540 port['security_groups'] = security_groups
543 for fixed_ip in fixed_ips:
544 diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
547 'Invalid format for "fixed_ips", %s missing' % diff)
549 port['fixed_ips'] = fixed_ips
550 r = self.ports_post(json_data=dict(port=port), success=201)
551 return r.json['port']