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):
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 :returns: a dict with the new virtual server details
63 :raises ClientError: wraps request errors
65 image = self.get_image_details(image_id)
66 metadata = metadata or dict()
67 for key in ('os', 'users'):
69 metadata[key] = image['metadata'][key]
73 return super(CycladesClient, self).create_server(
74 name, flavor_id, image_id,
75 metadata=metadata, personality=personality)
77 def start_server(self, server_id):
78 """Submit a startup request
80 :param server_id: integer (str or int)
82 :returns: (dict) response headers
85 r = self.servers_action_post(server_id, json_data=req, success=202)
88 def shutdown_server(self, server_id):
89 """Submit a shutdown request
91 :param server_id: integer (str or int)
93 :returns: (dict) response headers
95 req = {'shutdown': {}}
96 r = self.servers_action_post(server_id, json_data=req, success=202)
99 def get_server_console(self, server_id):
101 :param server_id: integer (str or int)
103 :returns: (dict) info to set a VNC connection to virtual server
105 req = {'console': {'type': 'vnc'}}
106 r = self.servers_action_post(server_id, json_data=req, success=200)
107 return r.json['console']
109 def get_firewall_profile(self, server_id):
111 :param server_id: integer (str or int)
113 :returns: (str) ENABLED | DISABLED | PROTECTED
115 :raises ClientError: 520 No Firewall Profile
117 r = self.get_server_details(server_id)
119 return r['attachments'][0]['firewallProfile']
122 'No Firewall Profile',
123 details='Server %s is missing a firewall profile' % server_id)
125 def set_firewall_profile(self, server_id, profile):
126 """Set the firewall profile for the public interface of a server
128 :param server_id: integer (str or int)
130 :param profile: (str) ENABLED | DISABLED | PROTECTED
132 :returns: (dict) response headers
134 req = {'firewallProfile': {'profile': profile}}
135 r = self.servers_action_post(server_id, json_data=req, success=202)
138 def list_server_nics(self, server_id):
140 :param server_id: integer (str or int)
142 :returns: (dict) network interface connections
144 r = self.servers_ips_get(server_id)
145 return r.json['attachments']
147 def get_server_stats(self, server_id):
149 :param server_id: integer (str or int)
151 :returns: (dict) auto-generated graphs of statistics (urls)
153 r = self.servers_stats_get(server_id)
154 return r.json['stats']
156 def list_networks(self, detail=False):
158 :param detail: (bool)
160 :returns: (list) id,name if not detail else full info per network
162 detail = 'detail' if detail else ''
163 r = self.networks_get(command=detail)
164 return r.json['networks']
166 def list_network_nics(self, network_id):
168 :param network_id: integer (str or int)
172 r = self.networks_get(network_id=network_id)
173 return r.json['network']['attachments']
177 cidr=None, gateway=None, type=None, dhcp=False):
183 :param geteway: (str)
185 :param type: (str) if None, will use MAC_FILTERED as default
186 Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
190 :returns: (dict) network detailed info
192 net = dict(name=name)
196 net['gateway'] = gateway
197 net['type'] = type or 'MAC_FILTERED'
198 net['dhcp'] = True if dhcp else False
199 req = dict(network=net)
200 r = self.networks_post(json_data=req, success=202)
201 return r.json['network']
203 def get_network_details(self, network_id):
205 :param network_id: integer (str or int)
209 r = self.networks_get(network_id=network_id)
210 return r.json['network']
212 def update_network_name(self, network_id, new_name):
214 :param network_id: integer (str or int)
216 :param new_name: (str)
218 :returns: (dict) response headers
220 req = {'network': {'name': new_name}}
221 r = self.networks_put(network_id=network_id, json_data=req)
224 def delete_network(self, network_id):
226 :param network_id: integer (str or int)
228 :returns: (dict) response headers
230 :raises ClientError: 421 Network in use
233 r = self.networks_delete(network_id)
235 except ClientError as err:
236 if err.status == 421:
238 'Network may be still connected to at least one server']
241 def connect_server(self, server_id, network_id):
242 """ Connect a server to a network
244 :param server_id: integer (str or int)
246 :param network_id: integer (str or int)
248 :returns: (dict) response headers
250 req = {'add': {'serverRef': server_id}}
251 r = self.networks_post(network_id, 'action', json_data=req)
254 def disconnect_server(self, server_id, nic_id):
256 :param server_id: integer (str or int)
260 :returns: (int) the number of nics disconnected
262 vm_nets = self.list_server_nics(server_id)
263 num_of_disconnections = 0
264 for (nic_id, network_id) in [(
266 net['network_id']) for net in vm_nets if nic_id == net['id']]:
267 req = {'remove': {'attachment': '%s' % nic_id}}
268 self.networks_post(network_id, 'action', json_data=req)
269 num_of_disconnections += 1
270 return num_of_disconnections
272 def disconnect_network_nics(self, netid):
274 :param netid: integer (str or int)
276 for nic in self.list_network_nics(netid):
277 req = dict(remove=dict(attachment=nic))
278 self.networks_post(netid, 'action', json_data=req)
281 self, item_id, current_status, get_status,
282 delay=1, max_wait=100, wait_cb=None):
283 """Wait for item while its status is current_status
285 :param server_id: integer (str or int)
287 :param current_status: (str)
289 :param get_status: (method(self, item_id)) if called, returns
290 (status, progress %) If no way to tell progress, return None
292 :param delay: time interval between retries
294 :param wait_cb: if set a progress bar is used to show progress
296 :returns: (str) the new mode if successful, (bool) False if timed out
298 status, progress = get_status(self, item_id)
301 wait_gen = wait_cb(max_wait // delay)
304 if status != current_status:
311 old_wait = total_wait = 0
313 while status == current_status and total_wait <= max_wait:
316 for i in range(total_wait - old_wait):
320 old_wait = total_wait
321 total_wait = progress or total_wait + 1
323 status, progress = get_status(self, item_id)
325 if total_wait < max_wait:
328 for i in range(max_wait):
332 return status if status != current_status else False
336 current_status='BUILD',
337 delay=1, max_wait=100, wait_cb=None):
338 """Wait for server while its status is current_status
340 :param server_id: integer (str or int)
342 :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
344 :param delay: time interval between retries
346 :max_wait: (int) timeout in secconds
348 :param wait_cb: if set a progressbar is used to show progress
350 :returns: (str) the new mode if succesfull, (bool) False if timed out
353 def get_status(self, server_id):
354 r = self.get_server_details(server_id)
355 return r['status'], (r.get('progress', None) if (
356 current_status in ('BUILD', )) else None)
359 server_id, current_status, get_status, delay, max_wait, wait_cb)
363 current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
364 """Wait for network while its status is current_status
366 :param net_id: integer (str or int)
368 :param current_status: (str) PENDING | ACTIVE | DELETED
370 :param delay: time interval between retries
372 :max_wait: (int) timeout in secconds
374 :param wait_cb: if set a progressbar is used to show progress
376 :returns: (str) the new mode if succesfull, (bool) False if timed out
379 def get_status(self, net_id):
380 r = self.get_network_details(net_id)
381 return r['status'], None
384 net_id, current_status, get_status, delay, max_wait, wait_cb)
388 current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
389 """Wait while the public network firewall status is current_status
391 :param server_id: integer (str or int)
393 :param current_status: (str) DISABLED | ENABLED | PROTECTED
395 :param delay: time interval between retries
397 :max_wait: (int) timeout in secconds
399 :param wait_cb: if set a progressbar is used to show progress
401 :returns: (str) the new mode if succesfull, (bool) False if timed out
404 def get_status(self, server_id):
405 return self.get_firewall_profile(server_id), None
408 server_id, current_status, get_status, delay, max_wait, wait_cb)
410 def get_floating_ip_pools(self):
412 :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
414 r = self.floating_ip_pools_get()
417 def get_floating_ips(self):
419 :returns: (dict) {floating_ips: [fixed_ip: , id: , ip: , pool: ]}
421 r = self.floating_ips_get()
424 def alloc_floating_ip(self, pool=None, address=None):
426 :param pool: (str) pool of ips to allocate from
428 :param address: (str) ip address to request
431 fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
435 json_data['pool'] = pool
437 json_data['address'] = address
438 r = self.floating_ips_post(json_data)
439 return r.json['floating_ip']
441 def get_floating_ip(self, fip_id):
443 :param fip_id: (str) floating ip id
446 {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},
448 :raises AssertionError: if fip_id is emtpy
450 assert fip_id, 'floating ip id is needed for get_floating_ip'
451 r = self.floating_ips_get(fip_id)
452 return r.json['floating_ip']
454 def delete_floating_ip(self, fip_id=None):
456 :param fip_id: (str) floating ip id (if None, all ips are deleted)
458 :returns: (dict) request headers
460 :raises AssertionError: if fip_id is emtpy
462 assert fip_id, 'floating ip id is needed for delete_floating_ip'
463 r = self.floating_ips_delete(fip_id)
466 def attach_floating_ip(self, server_id, address):
467 """Associate the address ip to server with server_id
469 :param server_id: (int)
471 :param address: (str) the ip address to assign to server (vm)
473 :returns: (dict) request headers
475 :raises ValueError: if server_id cannot be converted to int
477 :raises ValueError: if server_id is not of a int-convertable type
479 :raises AssertionError: if address is emtpy
481 server_id = int(server_id)
482 assert address, 'address is needed for attach_floating_ip'
483 req = dict(addFloatingIp=dict(address=address))
484 r = self.servers_action_post(server_id, json_data=req)
487 def detach_floating_ip(self, server_id, address):
488 """Disassociate an address ip from the server with server_id
490 :param server_id: (int)
492 :param address: (str) the ip address to assign to server (vm)
494 :returns: (dict) request headers
496 :raises ValueError: if server_id cannot be converted to int
498 :raises ValueError: if server_id is not of a int-convertable type
500 :raises AssertionError: if address is emtpy
502 server_id = int(server_id)
503 assert address, 'address is needed for detach_floating_ip'
504 req = dict(removeFloatingIp=dict(address=address))
505 r = self.servers_action_post(server_id, json_data=req)
509 class CycladesNetworkClient(NetworkClient):
510 """Cyclades Network API extentions"""
513 'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
515 def list_networks(self, detail=None):
516 path = path4url('networks', 'detail' if detail else '')
517 r = self.get(path, success=200)
518 return r.json['networks']
520 def create_network(self, type, name=None, shared=None):
521 req = dict(network=dict(type=type, admin_state_up=True))
523 req['network']['name'] = name
524 if shared not in (None, ):
525 req['network']['shared'] = bool(shared)
526 r = self.networks_post(json_data=req, success=201)
527 return r.json['network']
530 self, network_id, device_id, security_groups=None, name=None):
531 port = dict(network_id=network_id, device_id=device_id)
533 port['security_groups'] = security_groups
536 r = self.ports_post(json_data=dict(port=port), success=201)
537 return r.json['port']