555 lines
19 KiB
Python
555 lines
19 KiB
Python
# 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 (NodeDriver, NodeSize, Node, NodeLocation,
|
|
NodeImage, StorageVolume, VolumeSnapshot)
|
|
from libcloud.compute.types import (Provider, StorageVolumeState,
|
|
VolumeSnapshotState)
|
|
from libcloud.compute.drivers.openstack import OpenStackNodeDriver
|
|
from libcloud.compute.drivers.openstack import OpenStackKeyPair
|
|
|
|
|
|
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 = '%s/cloud/project/%s/' % (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}
|