| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """ |
| Gandi driver for compute |
| """ |
| import sys |
| from datetime import datetime |
| |
| from libcloud.common.gandi import BaseGandiDriver, GandiException,\ |
| NetworkInterface, IPAddress, Disk |
| from libcloud.compute.base import StorageVolume |
| from libcloud.compute.types import NodeState, Provider |
| from libcloud.compute.base import Node, NodeDriver |
| from libcloud.compute.base import NodeSize, NodeImage, NodeLocation |
| |
| |
| NODE_STATE_MAP = { |
| 'running': NodeState.RUNNING, |
| 'halted': NodeState.TERMINATED, |
| 'paused': NodeState.TERMINATED, |
| 'locked': NodeState.TERMINATED, |
| 'being_created': NodeState.PENDING, |
| 'invalid': NodeState.UNKNOWN, |
| 'legally_locked': NodeState.PENDING, |
| 'deleted': NodeState.TERMINATED |
| } |
| |
| NODE_PRICE_HOURLY_USD = 0.02 |
| |
| INSTANCE_TYPES = { |
| 'small': { |
| 'id': 'small', |
| 'name': 'Small instance', |
| 'cpu': 1, |
| 'memory': 256, |
| 'disk': 3, |
| 'bandwidth': 100, |
| }, |
| 'medium': { |
| 'id': 'medium', |
| 'name': 'Medium instance', |
| 'cpu': 1, |
| 'memory': 1024, |
| 'disk': 20, |
| 'bandwidth': 100, |
| }, |
| 'large': { |
| 'id': 'large', |
| 'name': 'Large instance', |
| 'cpu': 2, |
| 'memory': 2048, |
| 'disk': 50, |
| 'bandwidth': 100, |
| }, |
| 'x-large': { |
| 'id': 'x-large', |
| 'name': 'Extra Large instance', |
| 'cpu': 4, |
| 'memory': 4096, |
| 'disk': 100, |
| 'bandwidth': 100, |
| }, |
| } |
| |
| |
| class GandiNodeDriver(BaseGandiDriver, NodeDriver): |
| """ |
| Gandi node driver |
| |
| """ |
| api_name = 'gandi' |
| friendly_name = 'Gandi.net' |
| website = 'http://www.gandi.net/' |
| country = 'FR' |
| type = Provider.GANDI |
| # TODO : which features to enable ? |
| features = {} |
| |
| def __init__(self, *args, **kwargs): |
| """ |
| @inherits: L{NodeDriver.__init__} |
| """ |
| super(BaseGandiDriver, self).__init__(*args, **kwargs) |
| |
| def _resource_info(self, type, id): |
| try: |
| obj = self.connection.request('hosting.%s.info' % type, int(id)) |
| return obj.object |
| except Exception: |
| e = sys.exc_info()[1] |
| raise GandiException(1003, e) |
| return None |
| |
| def _node_info(self, id): |
| return self._resource_info('vm', id) |
| |
| def _volume_info(self, id): |
| return self._resource_info('disk', id) |
| |
| # Generic methods for driver |
| def _to_node(self, vm): |
| return Node( |
| id=vm['id'], |
| name=vm['hostname'], |
| state=NODE_STATE_MAP.get( |
| vm['state'], |
| NodeState.UNKNOWN |
| ), |
| public_ips=vm.get('ips', []), |
| private_ips=[], |
| driver=self, |
| extra={ |
| 'ai_active': vm.get('ai_active'), |
| 'datacenter_id': vm.get('datacenter_id'), |
| 'description': vm.get('description') |
| } |
| ) |
| |
| def _to_nodes(self, vms): |
| return [self._to_node(v) for v in vms] |
| |
| def _to_volume(self, disk): |
| extra = {'can_snapshot': disk['can_snapshot']} |
| return StorageVolume( |
| id=disk['id'], |
| name=disk['name'], |
| size=int(disk['size']), |
| driver=self, |
| extra=extra) |
| |
| def _to_volumes(self, disks): |
| return [self._to_volume(d) for d in disks] |
| |
| def list_nodes(self): |
| vms = self.connection.request('hosting.vm.list').object |
| ips = self.connection.request('hosting.ip.list').object |
| for vm in vms: |
| vm['ips'] = [] |
| for ip in ips: |
| if vm['ifaces_id'][0] == ip['iface_id']: |
| ip = ip.get('ip', None) |
| if ip: |
| vm['ips'].append(ip) |
| |
| nodes = self._to_nodes(vms) |
| return nodes |
| |
| def reboot_node(self, node): |
| op = self.connection.request('hosting.vm.reboot', int(node.id)) |
| self._wait_operation(op.object['id']) |
| vm = self._node_info(int(node.id)) |
| if vm['state'] == 'running': |
| return True |
| return False |
| |
| def destroy_node(self, node): |
| vm = self._node_info(node.id) |
| if vm['state'] == 'running': |
| # Send vm_stop and wait for accomplish |
| op_stop = self.connection.request('hosting.vm.stop', int(node.id)) |
| if not self._wait_operation(op_stop.object['id']): |
| raise GandiException(1010, 'vm.stop failed') |
| # Delete |
| op = self.connection.request('hosting.vm.delete', int(node.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def deploy_node(self, **kwargs): |
| """ |
| deploy_node is not implemented for gandi driver |
| |
| @rtype: C{bool} |
| """ |
| raise NotImplementedError( |
| 'deploy_node not implemented for gandi driver') |
| |
| def create_node(self, **kwargs): |
| """ |
| Create a new Gandi node |
| |
| @keyword name: String with a name for this new node (required) |
| @type name: C{str} |
| |
| @keyword image: OS Image to boot on node. (required) |
| @type image: L{NodeImage} |
| |
| @keyword location: Which data center to create a node in. If empty, |
| undefined behavior will be selected. (optional) |
| @type location: L{NodeLocation} |
| |
| @keyword size: The size of resources allocated to this node. |
| (required) |
| @type size: L{NodeSize} |
| |
| @keyword login: user name to create for login on machine (required) |
| @type login: C{str} |
| |
| @keyword password: password for user that'll be created (required) |
| @type password: C{str} |
| |
| @keyword inet_family: version of ip to use, default 4 (optional) |
| @type inet_family: C{int} |
| |
| @rtype: L{Node} |
| """ |
| |
| if kwargs.get('login') is None or kwargs.get('password') is None: |
| raise GandiException( |
| 1020, 'login and password must be defined for node creation') |
| |
| location = kwargs.get('location') |
| if location and isinstance(location, NodeLocation): |
| dc_id = int(location.id) |
| else: |
| raise GandiException( |
| 1021, 'location must be a subclass of NodeLocation') |
| |
| size = kwargs.get('size') |
| if not size and not isinstance(size, NodeSize): |
| raise GandiException( |
| 1022, 'size must be a subclass of NodeSize') |
| |
| # If size name is in INSTANCE_TYPE we use new rating model |
| instance = INSTANCE_TYPES.get(size.id) |
| cores = instance['cpu'] if instance else int(size.id) |
| |
| src_disk_id = int(kwargs['image'].id) |
| |
| disk_spec = { |
| 'datacenter_id': dc_id, |
| 'name': 'disk_%s' % kwargs['name'] |
| } |
| |
| vm_spec = { |
| 'datacenter_id': dc_id, |
| 'hostname': kwargs['name'], |
| 'login': kwargs['login'], |
| 'password': kwargs['password'], # TODO : use NodeAuthPassword |
| 'memory': int(size.ram), |
| 'cores': cores, |
| 'bandwidth': int(size.bandwidth), |
| 'ip_version': kwargs.get('inet_family', 4), |
| } |
| |
| # Call create_from helper api. Return 3 operations : disk_create, |
| # iface_create,vm_create |
| (op_disk, op_iface, op_vm) = self.connection.request( |
| 'hosting.vm.create_from', |
| vm_spec, disk_spec, src_disk_id |
| ).object |
| |
| # We wait for vm_create to finish |
| if self._wait_operation(op_vm['id']): |
| # after successful operation, get ip information |
| # thru first interface |
| node = self._node_info(op_vm['vm_id']) |
| ifaces = node.get('ifaces') |
| if len(ifaces) > 0: |
| ips = ifaces[0].get('ips') |
| if len(ips) > 0: |
| node['ip'] = ips[0]['ip'] |
| return self._to_node(node) |
| |
| return None |
| |
| def _to_image(self, img): |
| return NodeImage( |
| id=img['disk_id'], |
| name=img['label'], |
| driver=self.connection.driver |
| ) |
| |
| def list_images(self, location=None): |
| try: |
| if location: |
| filtering = {'datacenter_id': int(location.id)} |
| else: |
| filtering = {} |
| images = self.connection.request('hosting.image.list', filtering) |
| return [self._to_image(i) for i in images.object] |
| except Exception: |
| e = sys.exc_info()[1] |
| raise GandiException(1011, e) |
| |
| def _to_size(self, id, size): |
| return NodeSize( |
| id=id, |
| name='%s cores' % id, |
| ram=size['memory'], |
| disk=size['disk'], |
| bandwidth=size['bandwidth'], |
| price=(self._get_size_price(size_id='1') * id), |
| driver=self.connection.driver, |
| ) |
| |
| def _instance_type_to_size(self, instance): |
| return NodeSize( |
| id=instance['id'], |
| name=instance['name'], |
| ram=instance['memory'], |
| disk=instance['disk'], |
| bandwidth=instance['bandwidth'], |
| price=self._get_size_price(size_id=instance['id']), |
| driver=self.connection.driver, |
| ) |
| |
| def list_instance_type(self, location=None): |
| return [self._instance_type_to_size(instance) |
| for name, instance in INSTANCE_TYPES.items()] |
| |
| def list_sizes(self, location=None): |
| account = self.connection.request('hosting.account.info').object |
| if account.get('rating_enabled'): |
| # This account use new rating model |
| return self.list_instance_type(location) |
| # Look for available shares, and return a list of share_definition |
| available_res = account['resources']['available'] |
| |
| if available_res['shares'] == 0: |
| return None |
| else: |
| share_def = account['share_definition'] |
| available_cores = available_res['cores'] |
| # 0.75 core given when creating a server |
| max_core = int(available_cores + 0.75) |
| shares = [] |
| if available_res['servers'] < 1: |
| # No server quota, no way |
| return shares |
| for i in range(1, max_core + 1): |
| share = {id: i} |
| share_is_available = True |
| for k in ['memory', 'disk', 'bandwidth']: |
| if share_def[k] * i > available_res[k]: |
| # We run out for at least one resource inside |
| share_is_available = False |
| else: |
| share[k] = share_def[k] * i |
| if share_is_available: |
| nb_core = i |
| shares.append(self._to_size(nb_core, share)) |
| return shares |
| |
| def _to_loc(self, loc): |
| return NodeLocation( |
| id=loc['id'], |
| name=loc['name'], |
| country=loc['country'], |
| driver=self |
| ) |
| |
| def list_locations(self): |
| res = self.connection.request('hosting.datacenter.list') |
| return [self._to_loc(l) for l in res.object] |
| |
| def list_volumes(self): |
| """ |
| |
| @rtype: C{list} of L{StorageVolume} |
| """ |
| res = self.connection.request('hosting.disk.list', {}) |
| return self._to_volumes(res.object) |
| |
| def create_volume(self, size, name, location=None, snapshot=None): |
| disk_param = { |
| 'name': name, |
| 'size': int(size), |
| 'datacenter_id': int(location.id) |
| } |
| if snapshot: |
| op = self.connection.request('hosting.disk.create_from', |
| disk_param, int(snapshot.id)) |
| else: |
| op = self.connection.request('hosting.disk.create', disk_param) |
| if self._wait_operation(op.object['id']): |
| disk = self._volume_info(op.object['disk_id']) |
| return self._to_volume(disk) |
| return None |
| |
| def attach_volume(self, node, volume, device=None): |
| op = self.connection.request('hosting.vm.disk_attach', |
| int(node.id), int(volume.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def detach_volume(self, node, volume): |
| """ |
| Detaches a volume from a node. |
| |
| @param node: Node which should be used |
| @type node: L{Node} |
| |
| @param volume: Volume to be detached |
| @type volume: L{StorageVolume} |
| |
| @rtype: C{bool} |
| """ |
| op = self.connection.request('hosting.vm.disk_detach', |
| int(node.id), int(volume.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def destroy_volume(self, volume): |
| op = self.connection.request('hosting.disk.delete', int(volume.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def _to_iface(self, iface): |
| ips = [] |
| for ip in iface.get('ips', []): |
| new_ip = IPAddress( |
| ip['id'], |
| NODE_STATE_MAP.get( |
| ip['state'], |
| NodeState.UNKNOWN |
| ), |
| ip['ip'], |
| self.connection.driver, |
| version=ip.get('version'), |
| extra={'reverse': ip['reverse']} |
| ) |
| ips.append(new_ip) |
| return NetworkInterface( |
| iface['id'], |
| NODE_STATE_MAP.get( |
| iface['state'], |
| NodeState.UNKNOWN |
| ), |
| mac_address=None, |
| driver=self.connection.driver, |
| ips=ips, |
| node_id=iface.get('vm_id'), |
| extra={'bandwidth': iface['bandwidth']}, |
| ) |
| |
| def _to_ifaces(self, ifaces): |
| return [self._to_iface(i) for i in ifaces] |
| |
| def ex_list_interfaces(self): |
| """ |
| Specific method to list network interfaces |
| |
| @rtype: C{list} of L{GandiNetworkInterface} |
| """ |
| ifaces = self.connection.request('hosting.iface.list').object |
| ips = self.connection.request('hosting.ip.list').object |
| for iface in ifaces: |
| iface['ips'] = list( |
| filter(lambda i: i['iface_id'] == iface['id'], ips)) |
| return self._to_ifaces(ifaces) |
| |
| def _to_disk(self, element): |
| disk = Disk( |
| id=element['id'], |
| state=NODE_STATE_MAP.get( |
| element['state'], |
| NodeState.UNKNOWN |
| ), |
| name=element['name'], |
| driver=self.connection.driver, |
| size=element['size'], |
| extra={'can_snapshot': element['can_snapshot']} |
| ) |
| return disk |
| |
| def _to_disks(self, elements): |
| return [self._to_disk(el) for el in elements] |
| |
| def ex_list_disks(self): |
| """ |
| Specific method to list all disk |
| |
| @rtype: C{list} of L{GandiDisk} |
| """ |
| res = self.connection.request('hosting.disk.list', {}) |
| return self._to_disks(res.object) |
| |
| def ex_node_attach_disk(self, node, disk): |
| """ |
| Specific method to attach a disk to a node |
| |
| @param node: Node which should be used |
| @type node: L{Node} |
| |
| @param disk: Disk which should be used |
| @type disk: L{GandiDisk} |
| |
| @rtype: C{bool} |
| """ |
| op = self.connection.request('hosting.vm.disk_attach', |
| int(node.id), int(disk.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def ex_node_detach_disk(self, node, disk): |
| """ |
| Specific method to detach a disk from a node |
| |
| @param node: Node which should be used |
| @type node: L{Node} |
| |
| @param disk: Disk which should be used |
| @type disk: L{GandiDisk} |
| |
| @rtype: C{bool} |
| """ |
| op = self.connection.request('hosting.vm.disk_detach', |
| int(node.id), int(disk.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def ex_node_attach_interface(self, node, iface): |
| """ |
| Specific method to attach an interface to a node |
| |
| @param node: Node which should be used |
| @type node: L{Node} |
| |
| |
| @param iface: Network interface which should be used |
| @type iface: L{GandiNetworkInterface} |
| |
| @rtype: C{bool} |
| """ |
| op = self.connection.request('hosting.vm.iface_attach', |
| int(node.id), int(iface.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def ex_node_detach_interface(self, node, iface): |
| """ |
| Specific method to detach an interface from a node |
| |
| @param node: Node which should be used |
| @type node: L{Node} |
| |
| |
| @param iface: Network interface which should be used |
| @type iface: L{GandiNetworkInterface} |
| |
| @rtype: C{bool} |
| """ |
| op = self.connection.request('hosting.vm.iface_detach', |
| int(node.id), int(iface.id)) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def ex_snapshot_disk(self, disk, name=None): |
| """ |
| Specific method to make a snapshot of a disk |
| |
| @param disk: Disk which should be used |
| @type disk: L{GandiDisk} |
| |
| @param name: Name which should be used |
| @type name: C{str} |
| |
| @rtype: C{bool} |
| """ |
| if not disk.extra.get('can_snapshot'): |
| raise GandiException(1021, 'Disk %s can\'t snapshot' % disk.id) |
| if not name: |
| suffix = datetime.today().strftime('%Y%m%d') |
| name = 'snap_%s' % (suffix) |
| op = self.connection.request( |
| 'hosting.disk.create_from', |
| {'name': name, 'type': 'snapshot', }, |
| int(disk.id), |
| ) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |
| |
| def ex_update_disk(self, disk, new_size=None, new_name=None): |
| """Specific method to update size or name of a disk |
| WARNING: if a server is attached it'll be rebooted |
| |
| @param disk: Disk which should be used |
| @type disk: L{GandiDisk} |
| |
| @param new_size: New size |
| @type new_size: C{int} |
| |
| @param new_name: New name |
| @type new_name: C{str} |
| |
| @rtype: C{bool} |
| """ |
| params = {} |
| if new_size: |
| params.update({'size': new_size}) |
| if new_name: |
| params.update({'name': new_name}) |
| op = self.connection.request('hosting.disk.update', |
| int(disk.id), |
| params) |
| if self._wait_operation(op.object['id']): |
| return True |
| return False |