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 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
40 class CycladesClient(CycladesRestClient, Waiter):
41 """Synnefo Cyclades Compute API client"""
44 self, name, flavor_id, image_id,
45 metadata=None, personality=None, networks=None):
46 """Submit request to create a new server
50 :param flavor_id: integer id denoting a preset hardware configuration
52 :param image_id: (str) id denoting the OS image to run on virt. server
54 :param metadata: (dict) vm metadata updated by os/users image metadata
56 :param personality: a list of (file path, file contents) tuples,
57 describing files to be injected into virtual server upon creation
59 :param networks: (list of dicts) Networks to connect to, list this:
61 {"network": <network_uuid>},
62 {"network": <network_uuid>, "fixed_ip": address},
63 {"port": <port_id>}, ...]
65 :returns: a dict with the new virtual server details
67 :raises ClientError: wraps request errors
69 image = self.get_image_details(image_id)
70 metadata = metadata or dict()
71 for key in ('os', 'users'):
73 metadata[key] = image['metadata'][key]
77 return super(CycladesClient, self).create_server(
78 name, flavor_id, image_id,
79 metadata=metadata, personality=personality)
81 def start_server(self, server_id):
82 """Submit a startup request
84 :param server_id: integer (str or int)
86 :returns: (dict) response headers
89 r = self.servers_action_post(server_id, json_data=req, success=202)
92 def shutdown_server(self, server_id):
93 """Submit a shutdown request
95 :param server_id: integer (str or int)
97 :returns: (dict) response headers
99 req = {'shutdown': {}}
100 r = self.servers_action_post(server_id, json_data=req, success=202)
103 def get_server_console(self, server_id):
105 :param server_id: integer (str or int)
107 :returns: (dict) info to set a VNC connection to virtual server
109 req = {'console': {'type': 'vnc'}}
110 r = self.servers_action_post(server_id, json_data=req, success=200)
111 return r.json['console']
113 def get_firewall_profile(self, server_id):
115 :param server_id: integer (str or int)
117 :returns: (str) ENABLED | DISABLED | PROTECTED
119 :raises ClientError: 520 No Firewall Profile
121 r = self.get_server_details(server_id)
123 return r['attachments'][0]['firewallProfile']
126 'No Firewall Profile',
127 details='Server %s is missing a firewall profile' % server_id)
129 def set_firewall_profile(self, server_id, profile):
130 """Set the firewall profile for the public interface of a server
132 :param server_id: integer (str or int)
134 :param profile: (str) ENABLED | DISABLED | PROTECTED
136 :returns: (dict) response headers
138 req = {'firewallProfile': {'profile': profile}}
139 r = self.servers_action_post(server_id, json_data=req, success=202)
142 def list_server_nics(self, server_id):
144 :param server_id: integer (str or int)
146 :returns: (dict) network interface connections
148 r = self.servers_ips_get(server_id)
149 return r.json['attachments']
151 def get_server_stats(self, server_id):
153 :param server_id: integer (str or int)
155 :returns: (dict) auto-generated graphs of statistics (urls)
157 r = self.servers_stats_get(server_id)
158 return r.json['stats']
160 def list_networks(self, detail=False):
162 :param detail: (bool)
164 :returns: (list) id,name if not detail else full info per network
166 detail = 'detail' if detail else ''
167 r = self.networks_get(command=detail)
168 return r.json['networks']
170 def list_network_nics(self, network_id):
172 :param network_id: integer (str or int)
176 r = self.networks_get(network_id=network_id)
177 return r.json['network']['attachments']
181 cidr=None, gateway=None, type=None, dhcp=False):
187 :param geteway: (str)
189 :param type: (str) if None, will use MAC_FILTERED as default
190 Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
194 :returns: (dict) network detailed info
196 net = dict(name=name)
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']
207 def get_network_details(self, network_id):
209 :param network_id: integer (str or int)
213 r = self.networks_get(network_id=network_id)
214 return r.json['network']
216 def update_network_name(self, network_id, new_name):
218 :param network_id: integer (str or int)
220 :param new_name: (str)
222 :returns: (dict) response headers
224 req = {'network': {'name': new_name}}
225 r = self.networks_put(network_id=network_id, json_data=req)
228 def delete_network(self, network_id):
230 :param network_id: integer (str or int)
232 :returns: (dict) response headers
234 :raises ClientError: 421 Network in use
237 r = self.networks_delete(network_id)
239 except ClientError as err:
240 if err.status == 421:
242 'Network may be still connected to at least one server']
245 def connect_server(self, server_id, network_id):
246 """ Connect a server to a network
248 :param server_id: integer (str or int)
250 :param network_id: integer (str or int)
252 :returns: (dict) response headers
254 req = {'add': {'serverRef': server_id}}
255 r = self.networks_post(network_id, 'action', json_data=req)
258 def disconnect_server(self, server_id, nic_id):
260 :param server_id: integer (str or int)
264 :returns: (int) the number of nics disconnected
266 vm_nets = self.list_server_nics(server_id)
267 num_of_disconnections = 0
268 for (nic_id, network_id) in [(
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
276 def disconnect_network_nics(self, netid):
278 :param netid: integer (str or int)
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)
286 current_status='BUILD',
287 delay=1, max_wait=100, wait_cb=None):
288 """Wait for server while its status is current_status
290 :param server_id: integer (str or int)
292 :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
294 :param delay: time interval between retries
296 :max_wait: (int) timeout in secconds
298 :param wait_cb: if set a progressbar is used to show progress
300 :returns: (str) the new mode if succesfull, (bool) False if timed out
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)
309 server_id, current_status, get_status, delay, max_wait, wait_cb)
313 current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
314 """Wait for network while its status is current_status
316 :param net_id: integer (str or int)
318 :param current_status: (str) PENDING | ACTIVE | DELETED
320 :param delay: time interval between retries
322 :max_wait: (int) timeout in secconds
324 :param wait_cb: if set a progressbar is used to show progress
326 :returns: (str) the new mode if succesfull, (bool) False if timed out
329 def get_status(self, net_id):
330 r = self.get_network_details(net_id)
331 return r['status'], None
334 net_id, current_status, get_status, delay, max_wait, wait_cb)
338 current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
339 """Wait while the public network firewall status is current_status
341 :param server_id: integer (str or int)
343 :param current_status: (str) DISABLED | ENABLED | PROTECTED
345 :param delay: time interval between retries
347 :max_wait: (int) timeout in secconds
349 :param wait_cb: if set a progressbar is used to show progress
351 :returns: (str) the new mode if succesfull, (bool) False if timed out
354 def get_status(self, server_id):
355 return self.get_firewall_profile(server_id), None
358 server_id, current_status, get_status, delay, max_wait, wait_cb)
361 class CycladesNetworkClient(NetworkClient, Waiter):
362 """Cyclades Network API extentions"""
365 'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
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']
372 def create_network(self, type, name=None, shared=None):
373 req = dict(network=dict(type=type, admin_state_up=True))
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']
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)
386 port['security_groups'] = security_groups
389 for fixed_ip in fixed_ips:
390 diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
393 'Invalid format for "fixed_ips", %s missing' % diff)
395 port['fixed_ips'] = fixed_ips
396 r = self.ports_post(json_data=dict(port=port), success=201)
397 return r.json['port']
401 current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
402 """Wait for network while its status is current_status
404 :param net_id: integer (str or int)
406 :param current_status: (str) PENDING | ACTIVE | DELETED
408 :param delay: time interval between retries
410 :max_wait: (int) timeout in secconds
412 :param wait_cb: if set a progressbar is used to show progress
414 :returns: (str) the new mode if succesfull, (bool) False if timed out
417 def get_status(self, net_id):
418 r = self.get_network_details(net_id)
419 return r['status'], None
422 net_id, current_status, get_status, delay, max_wait, wait_cb)