# 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
"""
from datetime import datetime
from libcloud.common.gandi import (
Disk,
IPAddress,
GandiException,
BaseGandiDriver,
NetworkInterface,
)
from libcloud.compute.base import (
Node,
KeyPair,
NodeSize,
NodeImage,
NodeDriver,
NodeLocation,
StorageVolume,
)
from libcloud.compute.types import Provider, NodeState
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": 10240,
},
"medium": {
"id": "medium",
"name": "Medium instance",
"cpu": 1,
"memory": 1024,
"disk": 20,
"bandwidth": 10240,
},
"large": {
"id": "large",
"name": "Large instance",
"cpu": 2,
"memory": 2048,
"disk": 50,
"bandwidth": 10240,
},
"x-large": {
"id": "x-large",
"name": "Extra Large instance",
"cpu": 4,
"memory": 4096,
"disk": 100,
"bandwidth": 10240,
},
}
[docs]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: :class:`NodeDriver.__init__`
"""
super().__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 as e:
raise GandiException(1003, e)
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]
[docs] def list_nodes(self):
"""
Return a list of nodes in the current zone or all zones.
:return: List of Node objects
:rtype: ``list`` of :class:`Node`
"""
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
[docs] def ex_get_node(self, node_id):
"""
Return a Node object based on a node id.
:param name: The ID of the node
:type name: ``int``
:return: A Node object for the node
:rtype: :class:`Node`
"""
vm = self.connection.request("hosting.vm.info", int(node_id)).object
ips = self.connection.request("hosting.ip.list").object
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)
node = self._to_node(vm)
return node
[docs] def reboot_node(self, node):
"""
Reboot a node.
:param node: Node to be rebooted
:type node: :class:`Node`
:return: True if successful, False if not
:rtype: ``bool``
"""
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
[docs] def destroy_node(self, node):
"""
Destroy a node.
:param node: Node object to destroy
:type node: :class:`Node`
:return: True if successful
:rtype: ``bool``
"""
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
[docs] def deploy_node(self, **kwargs):
"""
deploy_node is not implemented for gandi driver
:rtype: ``bool``
"""
raise NotImplementedError("deploy_node not implemented for gandi driver")
[docs] def create_node(
self,
name,
size,
image,
location=None,
login=None,
password=None,
inet_family=4,
keypairs=None,
):
"""
Create a new Gandi node
:keyword name: String with a name for this new node (required)
:type name: ``str``
:keyword image: OS Image to boot on node. (required)
:type image: :class:`NodeImage`
:keyword location: Which data center to create a node in. If empty,
undefined behavior will be selected. (optional)
:type location: :class:`NodeLocation`
:keyword size: The size of resources allocated to this node.
(required)
:type size: :class:`NodeSize`
:keyword login: user name to create for login on machine (required)
:type login: ``str``
:keyword password: password for user that'll be created (required)
:type password: ``str``
:keyword inet_family: version of ip to use, default 4 (optional)
:type inet_family: ``int``
:keyword keypairs: IDs of keypairs or Keypairs object
:type keypairs: list of ``int`` or :class:`.KeyPair`
:rtype: :class:`Node`
"""
keypairs = keypairs or []
if not login and not keypairs:
raise GandiException(
1020,
"Login and password or ssh keypair " "must be defined for node creation",
)
if location and isinstance(location, NodeLocation):
dc_id = int(location.id)
else:
raise GandiException(1021, "location must be a subclass of NodeLocation")
if not size and not isinstance(size, NodeSize):
raise GandiException(1022, "size must be a subclass of NodeSize")
keypair_ids = [k if isinstance(k, int) else k.extra["id"] for k in keypairs]
# 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(image.id)
disk_spec = {"datacenter_id": dc_id, "name": "disk_%s" % name}
vm_spec = {
"datacenter_id": dc_id,
"hostname": name,
"memory": int(size.ram),
"cores": cores,
"bandwidth": int(size.bandwidth),
"ip_version": inet_family,
}
if login and password:
vm_spec.update({"login": login, "password": password}) # TODO : use NodeAuthPassword
if keypair_ids:
vm_spec["keys"] = keypair_ids
# 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)
[docs] def list_images(self, location=None):
"""
Return a list of image objects.
:keyword location: Which data center to filter a images in.
:type location: :class:`NodeLocation`
:return: List of GCENodeImage objects
:rtype: ``list`` of :class:`GCENodeImage`
"""
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 as e:
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,
)
[docs] def list_instance_type(self, location=None):
return [self._instance_type_to_size(instance) for name, instance in INSTANCE_TYPES.items()]
[docs] def list_sizes(self, location=None):
"""
Return a list of sizes (machineTypes) in a zone.
:keyword location: Which data center to filter a sizes in.
:type location: :class:`NodeLocation` or ``None``
:return: List of NodeSize objects
:rtype: ``list`` of :class:`NodeSize`
"""
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["dc_code"], country=loc["country"], driver=self)
[docs] def list_locations(self):
"""
Return a list of locations (datacenters).
:return: List of NodeLocation objects
:rtype: ``list`` of :class:`NodeLocation`
"""
res = self.connection.request("hosting.datacenter.list")
return [self._to_loc(loc) for loc in res.object]
[docs] def list_volumes(self):
"""
Return a list of volumes.
:return: A list of volume objects.
:rtype: ``list`` of :class:`StorageVolume`
"""
res = self.connection.request("hosting.disk.list", {})
return self._to_volumes(res.object)
[docs] def ex_get_volume(self, volume_id):
"""
Return a Volume object based on a volume ID.
:param volume_id: The ID of the volume
:type volume_id: ``int``
:return: A StorageVolume object for the volume
:rtype: :class:`StorageVolume`
"""
res = self.connection.request("hosting.disk.info", volume_id)
return self._to_volume(res.object)
[docs] def create_volume(self, size, name, location=None, snapshot=None):
"""
Create a volume (disk).
:param size: Size of volume to create (in GB).
:type size: ``int``
:param name: Name of volume to create
:type name: ``str``
:keyword location: Location (zone) to create the volume in
:type location: :class:`NodeLocation` or ``None``
:keyword snapshot: Snapshot to create image from
:type snapshot: :class:`Snapshot`
:return: Storage Volume object
:rtype: :class:`StorageVolume`
"""
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
[docs] def attach_volume(self, node, volume, device=None):
"""
Attach a volume to a node.
:param node: The node to attach the volume to
:type node: :class:`Node`
:param volume: The volume to attach.
:type volume: :class:`StorageVolume`
:keyword device: Not used in this cloud.
:type device: ``None``
:return: True if successful
:rtype: ``bool``
"""
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
[docs] def detach_volume(self, node, volume):
"""
Detaches a volume from a node.
:param node: Node which should be used
:type node: :class:`Node`
:param volume: Volume to be detached
:type volume: :class:`StorageVolume`
:rtype: ``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
[docs] def destroy_volume(self, volume):
"""
Destroy a volume.
:param volume: Volume object to destroy
:type volume: :class:`StorageVolume`
:return: True if successful
:rtype: ``bool``
"""
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]
[docs] def ex_list_interfaces(self):
"""
Specific method to list network interfaces
:rtype: ``list`` of :class:`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]
[docs] def ex_list_disks(self):
"""
Specific method to list all disk
:rtype: ``list`` of :class:`GandiDisk`
"""
res = self.connection.request("hosting.disk.list", {})
return self._to_disks(res.object)
[docs] 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: :class:`Node`
:param disk: Disk which should be used
:type disk: :class:`GandiDisk`
:rtype: ``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
[docs] 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: :class:`Node`
:param disk: Disk which should be used
:type disk: :class:`GandiDisk`
:rtype: ``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
[docs] 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: :class:`Node`
:param iface: Network interface which should be used
:type iface: :class:`GandiNetworkInterface`
:rtype: ``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
[docs] 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: :class:`Node`
:param iface: Network interface which should be used
:type iface: :class:`GandiNetworkInterface`
:rtype: ``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
[docs] 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: :class:`GandiDisk`
:param name: Name which should be used
:type name: ``str``
:rtype: ``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
[docs] 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: :class:`GandiDisk`
:param new_size: New size
:type new_size: ``int``
:param new_name: New name
:type new_name: ``str``
:rtype: ``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
def _to_key_pair(self, data):
key_pair = KeyPair(
name=data["name"],
fingerprint=data["fingerprint"],
public_key=data.get("value", None),
private_key=data.get("privatekey", None),
driver=self,
extra={"id": data["id"]},
)
return key_pair
def _to_key_pairs(self, data):
return [self._to_key_pair(k) for k in data]
[docs] def list_key_pairs(self):
"""
List registered key pairs.
:return: A list of key par objects.
:rtype: ``list`` of :class:`libcloud.compute.base.KeyPair`
"""
kps = self.connection.request("hosting.ssh.list").object
return self._to_key_pairs(kps)
[docs] def get_key_pair(self, name):
"""
Retrieve a single key pair.
:param name: Name of the key pair to retrieve.
:type name: ``str``
:rtype: :class:`.KeyPair`
"""
filter_params = {"name": name}
kps = self.connection.request("hosting.ssh.list", filter_params).object
return self._to_key_pair(kps[0])
[docs] def import_key_pair_from_string(self, name, key_material):
"""
Create a new key pair object.
:param name: Key pair name.
:type name: ``str``
:param key_material: Public key material.
:type key_material: ``str``
:return: Imported key pair object.
:rtype: :class:`.KeyPair`
"""
params = {"name": name, "value": key_material}
kp = self.connection.request("hosting.ssh.create", params).object
return self._to_key_pair(kp)
[docs] def delete_key_pair(self, key_pair):
"""
Delete an existing key pair.
:param key_pair: Key pair object or ID.
:type key_pair: :class.KeyPair` or ``int``
:return: True of False based on success of Keypair deletion
:rtype: ``bool``
"""
key_id = key_pair if isinstance(key_pair, int) else key_pair.extra["id"]
success = self.connection.request("hosting.ssh.delete", key_id).object
return success