blob: 072c4de2dd32e754c060112b84b1a80a0da7b8c6 [file] [log] [blame]
# 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.
"""
Ovh driver
"""
from libcloud.utils.py3 import httplib
from libcloud.common.ovh import API_ROOT, OvhConnection
from libcloud.compute.base import (
Node,
NodeSize,
NodeImage,
NodeDriver,
NodeLocation,
StorageVolume,
VolumeSnapshot,
)
from libcloud.compute.types import Provider, StorageVolumeState, VolumeSnapshotState
from libcloud.compute.drivers.openstack import OpenStackKeyPair, OpenStackNodeDriver
class OvhNodeDriver(NodeDriver):
"""
Libcloud driver for the Ovh API
For more information on the Ovh API, read the official reference:
https://api.ovh.com/console/
"""
type = Provider.OVH
name = "Ovh"
website = "https://www.ovh.com/"
connectionCls = OvhConnection
features = {"create_node": ["ssh_key"]}
api_name = "ovh"
NODE_STATE_MAP = OpenStackNodeDriver.NODE_STATE_MAP
VOLUME_STATE_MAP = OpenStackNodeDriver.VOLUME_STATE_MAP
SNAPSHOT_STATE_MAP = OpenStackNodeDriver.SNAPSHOT_STATE_MAP
def __init__(self, key, secret, ex_project_id, ex_consumer_key=None, region=None):
"""
Instantiate the driver with the given API credentials.
:param key: Your application key (required)
:type key: ``str``
:param secret: Your application secret (required)
:type secret: ``str``
:param ex_project_id: Your project ID
:type ex_project_id: ``str``
:param ex_consumer_key: Your consumer key (required)
:type ex_consumer_key: ``str``
:param region: The datacenter to connect to (optional)
:type region: ``str``
:rtype: ``None``
"""
self.region = region
self.project_id = ex_project_id
self.consumer_key = ex_consumer_key
NodeDriver.__init__(self, key, secret, ex_consumer_key=ex_consumer_key, region=region)
def _get_project_action(self, suffix):
base_url = "{}/cloud/project/{}/".format(API_ROOT, self.project_id)
return base_url + suffix
@classmethod
def list_regions(cls):
return ["eu", "ca"]
def list_nodes(self, location=None):
"""
List all nodes.
:keyword location: Location (region) used as filter
:type location: :class:`NodeLocation`
:return: List of node objects
:rtype: ``list`` of :class:`Node`
"""
action = self._get_project_action("instance")
data = {}
if location:
data["region"] = location.id
response = self.connection.request(action, data=data)
return self._to_nodes(response.object)
def ex_get_node(self, node_id):
"""
Get a individual node.
:keyword node_id: Node's ID
:type node_id: ``str``
:return: Created node
:rtype : :class:`Node`
"""
action = self._get_project_action("instance/%s" % node_id)
response = self.connection.request(action, method="GET")
return self._to_node(response.object)
def create_node(self, name, image, size, location, ex_keyname=None):
"""
Create a new node
:keyword name: Name of created node
:type name: ``str``
:keyword image: Image used for node
:type image: :class:`NodeImage`
:keyword size: Size (flavor) used for node
:type size: :class:`NodeSize`
:keyword location: Location (region) where to create node
:type location: :class:`NodeLocation`
:keyword ex_keyname: Name of SSH key used
:type ex_keyname: ``str``
:return: Created node
:rtype : :class:`Node`
"""
action = self._get_project_action("instance")
data = {
"name": name,
"imageId": image.id,
"flavorId": size.id,
"region": location.id,
}
if ex_keyname:
key_id = self.get_key_pair(ex_keyname, location).extra["id"]
data["sshKeyId"] = key_id
response = self.connection.request(action, data=data, method="POST")
return self._to_node(response.object)
def destroy_node(self, node):
action = self._get_project_action("instance/%s" % node.id)
self.connection.request(action, method="DELETE")
return True
def list_sizes(self, location=None):
action = self._get_project_action("flavor")
params = {}
if location:
params["region"] = location.id
response = self.connection.request(action, params=params)
return self._to_sizes(response.object)
def ex_get_size(self, size_id):
"""
Get an individual size (flavor).
:keyword size_id: Size's ID
:type size_id: ``str``
:return: Size
:rtype: :class:`NodeSize`
"""
action = self._get_project_action("flavor/%s" % size_id)
response = self.connection.request(action)
return self._to_size(response.object)
def list_images(self, location=None, ex_size=None):
"""
List available images
:keyword location: Location (region) used as filter
:type location: :class:`NodeLocation`
:keyword ex_size: Exclude images which are uncompatible with given size
:type ex_size: :class:`NodeImage`
:return: List of images
:rtype : ``list`` of :class:`NodeImage`
"""
action = self._get_project_action("image")
params = {}
if location:
params["region"] = location.id
if ex_size:
params["flavorId"] = ex_size.id
response = self.connection.request(action, params=params)
return self._to_images(response.object)
def get_image(self, image_id):
action = self._get_project_action("image/%s" % image_id)
response = self.connection.request(action)
return self._to_image(response.object)
def list_locations(self):
action = self._get_project_action("region")
data = self.connection.request(action)
return self._to_locations(data.object)
def list_key_pairs(self, ex_location=None):
"""
List available SSH public keys.
:keyword ex_location: Location (region) used as filter
:type ex_location: :class:`NodeLocation`
:return: Public keys
:rtype: ``list``of :class:`KeyPair`
"""
action = self._get_project_action("sshkey")
params = {}
if ex_location:
params["region"] = ex_location.id
response = self.connection.request(action, params=params)
return self._to_key_pairs(response.object)
def get_key_pair(self, name, ex_location=None):
"""
Get an individual SSH public key by its name and location.
:param name: Name of the key pair to retrieve.
:type name: ``str``
:keyword ex_location: Key's region
:type ex_location: :class:`NodeLocation`
:return: Public key
:rtype: :class:`KeyPair`
"""
# Keys are indexed with ID
keys = [key for key in self.list_key_pairs(ex_location) if key.name == name]
if not keys:
raise Exception("No key named '%s'" % name)
return keys[0]
def import_key_pair_from_string(self, name, key_material, ex_location):
"""
Import a new public key from string.
:param name: Key pair name.
:type name: ``str``
:param key_material: Public key material.
:type key_material: ``str``
:param ex_location: Location where to store the key
:type ex_location: :class:`NodeLocation`
:return: Imported key pair object.
:rtype: :class:`KeyPair`
"""
action = self._get_project_action("sshkey")
data = {"name": name, "publicKey": key_material, "region": ex_location.id}
response = self.connection.request(action, data=data, method="POST")
return self._to_key_pair(response.object)
def delete_key_pair(self, key_pair):
action = self._get_project_action("sshkey/%s" % key_pair.extra["id"])
params = {"keyId": key_pair.extra["id"]}
self.connection.request(action, params=params, method="DELETE")
return True
def create_volume(
self,
size,
name,
location,
snapshot=None,
ex_volume_type="classic",
ex_description=None,
):
"""
Create a volume.
:param size: Size of volume to create (in GB).
:type size: ``int``
:param name: Name of volume to create
:type name: ``str``
:param location: Location to create the volume in
:type location: :class:`NodeLocation` or ``None``
:param snapshot: Snapshot from which to create the new
volume. (optional)
:type snapshot: :class:`.VolumeSnapshot`
:keyword ex_volume_type: ``'classic'`` or ``'high-speed'``
:type ex_volume_type: ``str``
:keyword ex_description: Optionnal description of volume
:type ex_description: str
:return: Storage Volume object
:rtype: :class:`StorageVolume`
"""
action = self._get_project_action("volume")
data = {
"name": name,
"region": location.id,
"size": size,
"type": ex_volume_type,
}
if ex_description:
data["description"] = ex_description
response = self.connection.request(action, data=data, method="POST")
return self._to_volume(response.object)
def destroy_volume(self, volume):
action = self._get_project_action("volume/%s" % volume.id)
self.connection.request(action, method="DELETE")
return True
def list_volumes(self, ex_location=None):
"""
Return a list of volumes.
:keyword ex_location: Location used to filter
:type ex_location: :class:`NodeLocation` or ``None``
:return: A list of volume objects.
:rtype: ``list`` of :class:`StorageVolume`
"""
action = self._get_project_action("volume")
data = {}
if ex_location:
data["region"] = ex_location.id
response = self.connection.request(action, data=data)
return self._to_volumes(response.object)
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`
"""
action = self._get_project_action("volume/%s" % volume_id)
response = self.connection.request(action)
return self._to_volume(response.object)
def attach_volume(self, node, volume, device=None):
"""
Attach a volume to a node.
:param node: Node where to attach volume
:type node: :class:`Node`
:param volume: The ID of the volume
:type volume: :class:`StorageVolume`
:param device: Unsed parameter
:return: True or False representing operation successful
:rtype: ``bool``
"""
action = self._get_project_action("volume/%s/attach" % volume.id)
data = {"instanceId": node.id, "volumeId": volume.id}
self.connection.request(action, data=data, method="POST")
return True
def detach_volume(self, volume, ex_node=None):
"""
Detach a volume to a node.
:param volume: The ID of the volume
:type volume: :class:`StorageVolume`
:param ex_node: Node to detach from (optionnal if volume is attached
to only one node)
:type ex_node: :class:`Node`
:return: True or False representing operation successful
:rtype: ``bool``
:raises: Exception: If ``ex_node`` is not provided and more than one
node is attached to the volume
"""
action = self._get_project_action("volume/%s/detach" % volume.id)
if ex_node is None:
if len(volume.extra["attachedTo"]) != 1:
err_msg = (
"Volume '%s' has more or less than one attached" "nodes, you must specify one."
)
raise Exception(err_msg)
ex_node = self.ex_get_node(volume.extra["attachedTo"][0])
data = {"instanceId": ex_node.id}
self.connection.request(action, data=data, method="POST")
return True
def ex_list_snapshots(self, location=None):
"""
List all snapshots.
:keyword location: Location used to filter
:type location: :class:`NodeLocation` or ``None``
:rtype: ``list`` of :class:`VolumeSnapshot`
"""
action = self._get_project_action("volume/snapshot")
params = {}
if location:
params["region"] = location.id
response = self.connection.request(action, params=params)
return self._to_snapshots(response.object)
def ex_get_volume_snapshot(self, snapshot_id):
"""
Returns a single volume snapshot.
:param snapshot_id: Node to run the task on.
:type snapshot_id: ``str``
:rtype :class:`.VolumeSnapshot`:
:return: Volume snapshot.
"""
action = self._get_project_action("volume/snapshot/%s" % snapshot_id)
response = self.connection.request(action)
return self._to_snapshot(response.object)
def list_volume_snapshots(self, volume):
action = self._get_project_action("volume/snapshot")
params = {"region": volume.extra["region"]}
response = self.connection.request(action, params=params)
snapshots = self._to_snapshots(response.object)
return [snap for snap in snapshots if snap.extra["volume_id"] == volume.id]
def create_volume_snapshot(self, volume, name=None, ex_description=None):
"""
Create snapshot from volume
:param volume: Instance of `StorageVolume`
:type volume: `StorageVolume`
:param name: Name of snapshot (optional)
:type name: `str` | `NoneType`
:param ex_description: Description of the snapshot (optional)
:type ex_description: `str` | `NoneType`
:rtype: :class:`VolumeSnapshot`
"""
action = self._get_project_action("volume/%s/snapshot/" % volume.id)
data = {}
if name:
data["name"] = name
if ex_description:
data["description"] = ex_description
response = self.connection.request(action, data=data, method="POST")
return self._to_snapshot(response.object)
def destroy_volume_snapshot(self, snapshot):
action = self._get_project_action("volume/snapshot/%s" % snapshot.id)
response = self.connection.request(action, method="DELETE")
return response.status == httplib.OK
def ex_get_pricing(self, size_id, subsidiary="US"):
action = "%s/cloud/subsidiaryPrice" % (API_ROOT)
params = {"flavorId": size_id, "ovhSubsidiary": subsidiary}
pricing = self.connection.request(action, params=params).object["instances"][0]
return {
"hourly": pricing["price"]["value"],
"monthly": pricing["monthlyPrice"]["value"],
}
def _to_volume(self, obj):
extra = obj.copy()
extra.pop("id")
extra.pop("name")
extra.pop("size")
state = self.VOLUME_STATE_MAP.get(obj.pop("status", None), StorageVolumeState.UNKNOWN)
return StorageVolume(
id=obj["id"],
name=obj["name"],
size=obj["size"],
state=state,
extra=extra,
driver=self,
)
def _to_volumes(self, objs):
return [self._to_volume(obj) for obj in objs]
def _to_location(self, obj):
location = self.connectionCls.LOCATIONS[obj]
return NodeLocation(driver=self, **location)
def _to_locations(self, objs):
return [self._to_location(obj) for obj in objs]
def _to_node(self, obj):
extra = obj.copy()
if "ipAddresses" in extra:
public_ips = [ip["ip"] for ip in extra["ipAddresses"]]
del extra["id"]
del extra["name"]
return Node(
id=obj["id"],
name=obj["name"],
state=self.NODE_STATE_MAP[obj["status"]],
public_ips=public_ips,
private_ips=[],
driver=self,
extra=extra,
)
def _to_nodes(self, objs):
return [self._to_node(obj) for obj in objs]
def _to_size(self, obj):
extra = {"vcpus": obj["vcpus"], "type": obj["type"], "region": obj["region"]}
return NodeSize(
id=obj["id"],
name=obj["name"],
ram=obj["ram"],
disk=obj["disk"],
bandwidth=obj["outboundBandwidth"],
price=None,
driver=self,
extra=extra,
)
def _to_sizes(self, objs):
return [self._to_size(obj) for obj in objs]
def _to_image(self, obj):
extra = {"region": obj["region"], "visibility": obj["visibility"]}
return NodeImage(id=obj["id"], name=obj["name"], driver=self, extra=extra)
def _to_images(self, objs):
return [self._to_image(obj) for obj in objs]
def _to_key_pair(self, obj):
extra = {"regions": obj["regions"], "id": obj["id"]}
return OpenStackKeyPair(
name=obj["name"],
public_key=obj["publicKey"],
driver=self,
fingerprint=None,
extra=extra,
)
def _to_key_pairs(self, objs):
return [self._to_key_pair(obj) for obj in objs]
def _to_snapshot(self, obj):
extra = {
"volume_id": obj["volumeId"],
"region": obj["region"],
"description": obj["description"],
"status": obj["status"],
}
state = self.SNAPSHOT_STATE_MAP.get(obj["status"], VolumeSnapshotState.UNKNOWN)
snapshot = VolumeSnapshot(
id=obj["id"],
driver=self,
size=obj["size"],
extra=extra,
created=obj["creationDate"],
state=state,
name=obj["name"],
)
return snapshot
def _to_snapshots(self, objs):
return [self._to_snapshot(obj) for obj in objs]
def _ex_connection_class_kwargs(self):
return {"ex_consumer_key": self.consumer_key, "region": self.region}