Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion libcloud/common/upcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class UpcloudCreateNodeRequestBody:
:param ex_username: User's username, which is created.
Default is 'root'. (optional)
:type ex_username: ``str``

:param ex_storage_devices: Additional UpCloud storage_device dictionaries.
(optional)
:type ex_storage_devices: ``list`` of ``dict``
"""

def __init__(
Expand All @@ -63,15 +67,20 @@ def __init__(
auth=None,
ex_hostname="localhost",
ex_username="root",
ex_storage_devices=None,
):
storage_devices = _StorageDevice(image, size).to_dict()
if ex_storage_devices:
storage_devices["storage_device"].extend(ex_storage_devices)

self.body = {
"server": {
"title": name,
"hostname": ex_hostname,
"plan": size.id,
"zone": location.id,
"login_user": _LoginUser(ex_username, auth).to_dict(),
"storage_devices": _StorageDevice(image, size).to_dict(),
"storage_devices": storage_devices,
}
}

Expand Down Expand Up @@ -172,6 +181,15 @@ def stop_node(self, node_id):
"1.2/server/{}/stop".format(node_id), method="POST", data=json.dumps(body)
)

def start_node(self, node_id):
"""
Starts the node

:param node_id: Id of the Node
:type node_id: ``int``
"""
self.connection.request("1.2/server/{}/start".format(node_id), method="POST")

def get_node_state(self, node_id):
"""
Get the state of the node.
Expand Down
255 changes: 253 additions & 2 deletions libcloud/compute/drivers/upcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@
from libcloud.utils.py3 import b, httplib
from libcloud.common.base import JsonResponse, ConnectionUserAndKey
from libcloud.common.types import InvalidCredsError
from libcloud.compute.base import Node, NodeSize, NodeImage, NodeState, NodeDriver, NodeLocation
from libcloud.compute.types import Provider
from libcloud.compute.base import (
Node,
NodeSize,
NodeImage,
NodeState,
NodeDriver,
NodeLocation,
StorageVolume,
)
from libcloud.compute.types import Provider, StorageVolumeState
from libcloud.common.upcloud import (
PlanPrice,
UpcloudNodeDestroyer,
Expand Down Expand Up @@ -95,6 +103,15 @@ class UpcloudDriver(NodeDriver):
"error": NodeState.ERROR,
}

STORAGE_VOLUME_STATE_MAP = {
"online": StorageVolumeState.AVAILABLE,
"maintenance": StorageVolumeState.UPDATING,
"cloning": StorageVolumeState.CREATING,
"backuping": StorageVolumeState.BACKUP,
"syncing": StorageVolumeState.MIGRATING,
"error": StorageVolumeState.ERROR,
}

def __init__(self, username, password, **kwargs):
super().__init__(key=username, secret=password, **kwargs)

Expand Down Expand Up @@ -148,6 +165,7 @@ def create_node(
auth=None,
ex_hostname="localhost",
ex_username="root",
ex_storage_devices=None,
):
"""
Creates instance to upcloud.
Expand Down Expand Up @@ -179,6 +197,14 @@ def create_node(
Default is 'root'. (optional)
:type ex_username: ``str``

:param ex_storage_devices: Additional UpCloud storage_device
dictionaries to include in the server
creation request. For example, an
``attach`` action can attach an existing
storage and a ``create`` action can create
an extra data disk. (optional)
:type ex_storage_devices: ``list`` of ``dict``

:return: The newly created node.
:rtype: :class:`.Node`
"""
Expand All @@ -190,13 +216,164 @@ def create_node(
auth=auth,
ex_hostname=ex_hostname,
ex_username=ex_username,
ex_storage_devices=ex_storage_devices,
)
response = self.connection.request("1.2/server", method="POST", data=body.to_json())
server = response.object["server"]
# Upcloud server's are in maintenance state when going
# from state to other, it is safe to assume STARTING state
return self._to_node(server, state=NodeState.STARTING)

def list_volumes(self):
"""
List normal storage volumes.

:rtype: ``list`` of :class:`StorageVolume`
"""
response = self.connection.request("1.2/storage/normal")
return self._to_volumes(response.object["storages"]["storage"])

def create_volume(
self,
size,
name,
location=None,
snapshot=None,
ex_tier="maxiops",
ex_backup_rule=None,
):
"""
Create a new storage volume.

:param size: Size of volume in gigabytes. (required)
:type size: ``int``

:param name: Name of the volume to be created. (required)
:type name: ``str``

:param location: Which data center to create a volume in. (required)
:type location: :class:`.NodeLocation`

:param ex_tier: UpCloud storage tier: ``maxiops`` or ``hdd``.
Default is ``maxiops``. (optional)
:type ex_tier: ``str``

:param ex_backup_rule: Backup rule block for automatic backups.
(optional)
:type ex_backup_rule: ``dict``

:rtype: :class:`StorageVolume`
"""
if location is None:
raise ValueError("Must provide `location` value.")

if snapshot is not None:
raise NotImplementedError("Creating a volume from snapshot is not supported.")

storage = {
"size": size,
"title": name,
"zone": location.id,
"tier": ex_tier,
}
if ex_backup_rule is not None:
storage["backup_rule"] = ex_backup_rule

response = self.connection.request(
"1.2/storage",
method="POST",
data=json.dumps({"storage": storage}),
)
return self._to_volume(response.object["storage"])

def attach_volume(
self,
node,
volume,
device=None,
ex_type="disk",
ex_boot_disk=False,
):
"""
Attach a storage volume to a node.

:param node: Node to attach volume to.
:type node: :class:`Node`

:param volume: Volume to attach.
:type volume: :class:`StorageVolume`

:param device: UpCloud device address or bus, for example
``virtio``, ``scsi`` or ``scsi:0:0``. (optional)
:type device: ``str``

:param ex_type: Attached device type, ``disk`` or ``cdrom``.
Default is ``disk``. (optional)
:type ex_type: ``str``

:param ex_boot_disk: Whether the storage should be a boot disk.
Default is False. (optional)
:type ex_boot_disk: ``bool``

:rtype: ``bool``
"""
storage_device = {
"type": ex_type,
"storage": volume.id,
"boot_disk": "1" if ex_boot_disk else "0",
}
if device is not None:
storage_device["address"] = device

self.connection.request(
"1.2/server/{}/storage/attach".format(node.id),
method="POST",
data=json.dumps({"storage_device": storage_device}),
)
return True

def detach_volume(self, volume, ex_node=None, ex_address=None):
"""
Detach a storage volume from its server.

:param volume: Volume to detach.
:type volume: :class:`StorageVolume`

:param ex_node: Node where the volume is attached. Required by the
UpCloud 1.2 detach endpoint.
:type ex_node: :class:`Node`

:param ex_address: Device address to detach, for example
``scsi:0:0``. Required by the UpCloud 1.2 detach
endpoint.
:type ex_address: ``str``

:rtype: ``bool``
"""
if ex_node is None or ex_address is None:
raise ValueError(
"UpCloud API 1.2 requires `ex_node` and `ex_address` " "when detaching a volume."
)

self.connection.request(
"1.2/server/{}/storage/detach".format(ex_node.id),
method="POST",
data=json.dumps({"storage_device": {"address": ex_address}}),
)
return True

def destroy_volume(self, volume):
"""
Destroy a storage volume.

:param volume: Volume to destroy.
:type volume: :class:`StorageVolume`

:rtype: ``bool``
"""
self.connection.request("1.2/storage/{}".format(volume.id), method="DELETE")
return True

def list_nodes(self):
"""
List nodes
Expand Down Expand Up @@ -227,6 +404,47 @@ def reboot_node(self, node):
)
return True

def start_node(self, node):
"""
Start the given node.

:param node: the node to start
:type node: :class:`Node`

:rtype: ``bool``
"""
self.connection.request("1.2/server/{}/start".format(node.id), method="POST")
return True

def stop_node(self, node, ex_stop_type="hard", ex_timeout=None):
"""
Stop the given node.

:param node: the node to stop
:type node: :class:`Node`

:param ex_stop_type: Stop type, ``hard`` or ``soft``. Default is
``hard`` to match the destroy helper behavior.
(optional)
:type ex_stop_type: ``str``

:param ex_timeout: Stop timeout in seconds when using a soft stop.
(optional)
:type ex_timeout: ``int``

:rtype: ``bool``
"""
stop_server = {"stop_type": ex_stop_type}
if ex_timeout is not None:
stop_server["timeout"] = ex_timeout

self.connection.request(
"1.2/server/{}/stop".format(node.id),
method="POST",
data=json.dumps({"stop_server": stop_server}),
)
return True

def destroy_node(self, node):
"""
Destroy the given node
Expand Down Expand Up @@ -312,6 +530,39 @@ def _construct_node_image(self, image):
extra = self._copy_dict(("access", "license", "size", "state", "type"), image)
return NodeImage(id=image["uuid"], name=image["title"], driver=self, extra=extra)

def _to_volumes(self, volumes):
return [self._to_volume(volume) for volume in volumes]

def _to_volume(self, volume):
extra_keys = (
"access",
"backup_rule",
"backups",
"encrypted",
"labels",
"license",
"origin",
"part_of_plan",
"progress",
"servers",
"tier",
"type",
"zone",
)
extra = {}
for key in extra_keys:
if key in volume:
extra[key] = volume[key]

return StorageVolume(
id=volume["uuid"],
name=volume["title"],
size=int(volume["size"]),
driver=self,
state=self.STORAGE_VOLUME_STATE_MAP.get(volume["state"], StorageVolumeState.UNKNOWN),
extra=extra,
)

def _copy_dict(self, keys, d):
extra = {}
for key in keys:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"storage": {
"access": "private",
"backup_rule": "",
"backups": {
"backup": []
},
"license": 0,
"servers": {
"server": [
"00f8c525-7e62-4108-8115-3958df5b43dc"
]
},
"size": 50,
"state": "online",
"tier": "maxiops",
"title": "data",
"type": "normal",
"uuid": "01d4fcd4-e446-433b-8a9c-551a1284952e",
"zone": "fi-hel1"
}
}
Loading
Loading