You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by mi...@apache.org on 2026/07/03 10:06:45 UTC

(libcloud) branch improve_upload created (now cf7498538)

This is an automated email from the ASF dual-hosted git repository.

micafer pushed a change to branch improve_upload
in repository https://gitbox.apache.org/repos/asf/libcloud.git


      at cf7498538 Add volume support and start stop operations

This branch includes the following new commits:

     new cf7498538 Add volume support and start stop operations

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(libcloud) 01/01: Add volume support and start stop operations

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

micafer pushed a commit to branch improve_upload
in repository https://gitbox.apache.org/repos/asf/libcloud.git

commit cf74985387d859265ac8256a402679f7ac793846
Author: Miguel Caballer <mi...@upv.es>
AuthorDate: Fri Jul 3 12:06:31 2026 +0200

    Add volume support and start stop operations
---
 libcloud/common/upcloud.py                         |  24 +-
 libcloud/compute/drivers/upcloud.py                | 283 ++++++++++++++++++++-
 ...orage_01d4fcd4-e446-433b-8a9c-551a1284952e.json |  29 +++
 .../fixtures/upcloud/api_1_2_storage_create.json   |  27 ++
 .../fixtures/upcloud/api_1_2_storage_normal.json   |  40 +++
 libcloud/test/compute/test_upcloud.py              | 173 ++++++++++++-
 6 files changed, 572 insertions(+), 4 deletions(-)

diff --git a/libcloud/common/upcloud.py b/libcloud/common/upcloud.py
index 5affb939a..632695335 100644
--- a/libcloud/common/upcloud.py
+++ b/libcloud/common/upcloud.py
@@ -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__(
@@ -63,7 +67,12 @@ class UpcloudCreateNodeRequestBody:
         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,
@@ -71,7 +80,7 @@ class UpcloudCreateNodeRequestBody:
                 "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,
             }
         }
 
@@ -172,6 +181,19 @@ class UpcloudNodeOperations:
             "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",
+            data=json.dumps({"server": {}}),
+        )
+
     def get_node_state(self, node_id):
         """
         Get the state of the node.
diff --git a/libcloud/compute/drivers/upcloud.py b/libcloud/compute/drivers/upcloud.py
index 700c46501..1f781d19a 100644
--- a/libcloud/compute/drivers/upcloud.py
+++ b/libcloud/compute/drivers/upcloud.py
@@ -22,8 +22,16 @@ import base64
 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,
@@ -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)
 
@@ -148,6 +165,7 @@ class UpcloudDriver(NodeDriver):
         auth=None,
         ex_hostname="localhost",
         ex_username="root",
+        ex_storage_devices=None,
     ):
         """
         Creates instance to upcloud.
@@ -179,6 +197,14 @@ class UpcloudDriver(NodeDriver):
                             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`
         """
@@ -190,6 +216,7 @@ class UpcloudDriver(NodeDriver):
             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"]
@@ -197,6 +224,153 @@ class UpcloudDriver(NodeDriver):
         # 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_encrypted=False,
+        ex_labels=None,
+        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``, ``standard``, or
+                        ``hdd``. Default is ``maxiops``. (optional)
+        :type ex_tier: ``str``
+
+        :param ex_encrypted: Create the volume encrypted at rest. Default is
+                             False. (optional)
+        :type ex_encrypted: ``bool``
+
+        :param ex_labels: Labels for the volume. (optional)
+        :type ex_labels: ``list`` of ``dict``
+
+        :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,
+            "encrypted": "yes" if ex_encrypted else "no",
+        }
+        if ex_labels is not None:
+            storage["labels"] = ex_labels
+        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: :class:`StorageVolume`
+        """
+        storage_device = {
+            "type": ex_type,
+            "server": node.id,
+            "boot_disk": "1" if ex_boot_disk else "0",
+        }
+        if device is not None:
+            storage_device["address"] = device
+
+        response = self.connection.request(
+            "1.2/storage/{}/attach".format(volume.id),
+            method="POST",
+            data=json.dumps({"storage_device": storage_device}),
+        )
+        return self._to_volume(response.object["storage"])
+
+    def detach_volume(self, volume):
+        """
+        Detach a storage volume from its server.
+
+        :param volume: Volume to detach.
+        :type volume: :class:`StorageVolume`
+
+        :rtype: ``bool``
+        """
+        self.connection.request(
+            "1.2/storage/{}/detach".format(volume.id),
+            method="POST",
+        )
+        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
@@ -227,6 +401,76 @@ class UpcloudDriver(NodeDriver):
         )
         return True
 
+    def start_node(
+        self,
+        node,
+        ex_host=None,
+        ex_avoid_host=None,
+        ex_start_type=None,
+    ):
+        """
+        Start the given node.
+
+        :param node: the node to start
+        :type node: :class:`Node`
+
+        :param ex_host: Host id to start the node on. Only available for
+                        private cloud hosts. (optional)
+        :type ex_host: ``int``
+
+        :param ex_avoid_host: Host id to avoid when starting the node.
+                              (optional)
+        :type ex_avoid_host: ``int``
+
+        :param ex_start_type: Start type, ``sync`` or ``async``. (optional)
+        :type ex_start_type: ``str``
+
+        :rtype: ``bool``
+        """
+        server = {}
+        if ex_host is not None:
+            server["host"] = ex_host
+        if ex_avoid_host is not None:
+            server["avoid_host"] = ex_avoid_host
+        if ex_start_type is not None:
+            server["start_type"] = ex_start_type
+
+        self.connection.request(
+            "1.2/server/{}/start".format(node.id),
+            method="POST",
+            data=json.dumps({"server": server}),
+        )
+        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
@@ -312,6 +556,41 @@ class UpcloudDriver(NodeDriver):
         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:
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_01d4fcd4-e446-433b-8a9c-551a1284952e.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_01d4fcd4-e446-433b-8a9c-551a1284952e.json
new file mode 100644
index 000000000..2269ac415
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_01d4fcd4-e446-433b-8a9c-551a1284952e.json
@@ -0,0 +1,29 @@
+{
+  "storage": {
+    "access": "private",
+    "encrypted": "yes",
+    "backup_rule": "",
+    "backups": {
+      "backup": []
+    },
+    "labels": [
+      {
+        "key": "env",
+        "value": "test"
+      }
+    ],
+    "license": 0,
+    "servers": {
+      "server": [
+        "00f8c525-7e62-4108-8115-3958df5b43dc"
+      ]
+    },
+    "size": 50,
+    "state": "online",
+    "tier": "standard",
+    "title": "data",
+    "type": "normal",
+    "uuid": "01d4fcd4-e446-433b-8a9c-551a1284952e",
+    "zone": "fi-hel1"
+  }
+}
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_create.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_create.json
new file mode 100644
index 000000000..aa16983f1
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_create.json
@@ -0,0 +1,27 @@
+{
+  "storage": {
+    "access": "private",
+    "encrypted": "yes",
+    "backup_rule": "",
+    "backups": {
+      "backup": []
+    },
+    "labels": [
+      {
+        "key": "env",
+        "value": "test"
+      }
+    ],
+    "license": 0,
+    "servers": {
+      "server": []
+    },
+    "size": 50,
+    "state": "online",
+    "tier": "standard",
+    "title": "data",
+    "type": "normal",
+    "uuid": "01d4fcd4-e446-433b-8a9c-551a1284952e",
+    "zone": "fi-hel1"
+  }
+}
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_normal.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_normal.json
new file mode 100644
index 000000000..2c274c5d2
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_normal.json
@@ -0,0 +1,40 @@
+{
+  "storages": {
+    "storage": [
+      {
+        "access": "private",
+        "encrypted": "no",
+        "labels": [],
+        "license": 0,
+        "size": 10,
+        "state": "online",
+        "tier": "hdd",
+        "title": "Operating system disk",
+        "type": "normal",
+        "uuid": "01eff7ad-168e-413e-83b0-054f6a28fa23",
+        "zone": "uk-lon1"
+      },
+      {
+        "access": "private",
+        "encrypted": "yes",
+        "labels": [
+          {
+            "key": "env",
+            "value": "test"
+          }
+        ],
+        "license": 0,
+        "servers": {
+          "server": []
+        },
+        "size": 50,
+        "state": "online",
+        "tier": "standard",
+        "title": "data",
+        "type": "normal",
+        "uuid": "01d4fcd4-e446-433b-8a9c-551a1284952e",
+        "zone": "fi-hel1"
+      }
+    ]
+  }
+}
diff --git a/libcloud/test/compute/test_upcloud.py b/libcloud/test/compute/test_upcloud.py
index d6f86890b..06d5a7c6b 100644
--- a/libcloud/test/compute/test_upcloud.py
+++ b/libcloud/test/compute/test_upcloud.py
@@ -23,7 +23,14 @@ from libcloud.test import MockHttp, LibcloudTestCase, unittest
 from libcloud.compute import providers
 from libcloud.utils.py3 import httplib, ensure_string
 from libcloud.common.types import InvalidCredsError
-from libcloud.compute.base import Node, NodeSize, NodeImage, NodeLocation, NodeAuthSSHKey
+from libcloud.compute.base import (
+    Node,
+    NodeSize,
+    NodeImage,
+    NodeLocation,
+    NodeAuthSSHKey,
+    StorageVolume,
+)
 from libcloud.test.secrets import UPCLOUD_PARAMS
 from libcloud.compute.types import Provider, NodeState
 from libcloud.test.file_fixtures import ComputeFileFixtures
@@ -192,6 +199,102 @@ class UpcloudDriverTests(LibcloudTestCase):
         self.assertTrue(len(node.private_ips) > 0)
         self.assertEqual(node.driver, self.driver)
 
+    def test_create_node_with_extra_storage_devices(self):
+        image = NodeImage(
+            id="01000000-0000-4000-8000-000030060200",
+            name="Ubuntu Server 16.04 LTS (Xenial Xerus)",
+            extra={"type": "template"},
+            driver=self.driver,
+        )
+        location = NodeLocation(id="fi-hel1", name="Helsinki #1", country="FI", driver=self.driver)
+        size = NodeSize(
+            id="1xCPU-1GB",
+            name="1xCPU-1GB",
+            ram=1024,
+            disk=30,
+            bandwidth=2048,
+            extra={"storage_tier": "maxiops"},
+            price=None,
+            driver=self.driver,
+        )
+        extra_storage = {
+            "action": "create",
+            "title": "data",
+            "size": 25,
+            "tier": "standard",
+        }
+
+        self.driver.create_node(
+            name="test_server",
+            size=size,
+            image=image,
+            location=location,
+            ex_storage_devices=[extra_storage],
+        )
+
+        storage_devices = UpcloudMockHttp.last_request_body["server"]["storage_devices"][
+            "storage_device"
+        ]
+        self.assertEqual(len(storage_devices), 2)
+        self.assertEqual(storage_devices[1], extra_storage)
+
+    def test_list_volumes(self):
+        volumes = self.driver.list_volumes()
+        self.assertEqual(len(volumes), 2)
+
+        volume = volumes[0]
+        self.assertIsInstance(volume, StorageVolume)
+        self.assertEqual(volume.id, "01eff7ad-168e-413e-83b0-054f6a28fa23")
+        self.assertEqual(volume.name, "Operating system disk")
+        self.assertEqual(volume.size, 10)
+        self.assertEqual(volume.extra["tier"], "hdd")
+        self.assertEqual(volume.extra["zone"], "uk-lon1")
+
+    def test_create_volume(self):
+        location = NodeLocation(id="fi-hel1", name="Helsinki #1", country="FI", driver=self.driver)
+        volume = self.driver.create_volume(
+            size=50,
+            name="data",
+            location=location,
+            ex_tier="standard",
+            ex_encrypted=True,
+            ex_labels=[{"key": "env", "value": "test"}],
+        )
+
+        self.assertEqual(volume.id, "01d4fcd4-e446-433b-8a9c-551a1284952e")
+        self.assertEqual(volume.name, "data")
+        self.assertEqual(volume.size, 50)
+        self.assertEqual(volume.extra["encrypted"], "yes")
+        request_storage = UpcloudMockHttp.last_request_body["storage"]
+        self.assertEqual(request_storage["tier"], "standard")
+        self.assertEqual(request_storage["encrypted"], "yes")
+        self.assertEqual(request_storage["labels"], [{"key": "env", "value": "test"}])
+
+    def test_create_volume_requires_location(self):
+        with self.assertRaises(ValueError):
+            self.driver.create_volume(size=50, name="data")
+
+    def test_attach_volume(self):
+        node = self.driver.list_nodes()[0]
+        volume = self.driver.list_volumes()[1]
+
+        attached_volume = self.driver.attach_volume(node, volume, device="scsi")
+
+        self.assertIsInstance(attached_volume, StorageVolume)
+        self.assertEqual(attached_volume.id, volume.id)
+        request_device = UpcloudMockHttp.last_request_body["storage_device"]
+        self.assertEqual(request_device["server"], node.id)
+        self.assertEqual(request_device["address"], "scsi")
+        self.assertEqual(request_device["boot_disk"], "0")
+
+    def test_detach_volume(self):
+        volume = self.driver.list_volumes()[1]
+        self.assertTrue(self.driver.detach_volume(volume))
+
+    def test_destroy_volume(self):
+        volume = self.driver.list_volumes()[1]
+        self.assertTrue(self.driver.destroy_volume(volume))
+
     def test_list_nodes(self):
         nodes = self.driver.list_nodes()
 
@@ -208,6 +311,37 @@ class UpcloudDriverTests(LibcloudTestCase):
         success = self.driver.reboot_node(nodes[0])
         self.assertTrue(success)
 
+    def test_start_node(self):
+        nodes = self.driver.list_nodes()
+        success = self.driver.start_node(
+            nodes[0],
+            ex_host=8055964291,
+            ex_avoid_host=7653311107,
+            ex_start_type="async",
+        )
+
+        self.assertTrue(success)
+        self.assertEqual(
+            UpcloudMockHttp.last_request_body,
+            {
+                "server": {
+                    "host": 8055964291,
+                    "avoid_host": 7653311107,
+                    "start_type": "async",
+                }
+            },
+        )
+
+    def test_stop_node(self):
+        nodes = self.driver.list_nodes()
+        success = self.driver.stop_node(nodes[0], ex_stop_type="soft", ex_timeout=60)
+
+        self.assertTrue(success)
+        self.assertEqual(
+            UpcloudMockHttp.last_request_body,
+            {"stop_server": {"stop_type": "soft", "timeout": 60}},
+        )
+
     def test_destroy_node(self):
         if UpcloudDriver.connectionCls.conn_class == UpcloudMockHttp:
             nodes = [
@@ -258,6 +392,7 @@ class UpcloudDriverTests(LibcloudTestCase):
 
 class UpcloudMockHttp(MockHttp):
     fixtures = ComputeFileFixtures("upcloud")
+    last_request_body = None
 
     def _1_2_zone(self, method, url, body, headers):
         auth = headers["Authorization"].split(" ")[1]
@@ -289,6 +424,7 @@ class UpcloudMockHttp(MockHttp):
     def _1_2_server(self, method, url, body, headers):
         if method == "POST":
             dbody = json.loads(body)
+            self.__class__.last_request_body = dbody
             storages = dbody["server"]["storage_devices"]["storage_device"]
             if any(["type" in storage and storage["type"] == "cdrom" for storage in storages]):
                 body = self.fixtures.load("api_1_2_server_from_cdrom.json")
@@ -298,6 +434,31 @@ class UpcloudMockHttp(MockHttp):
             body = self.fixtures.load("api_1_2_server.json")
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
 
+    def _1_2_storage_normal(self, method, url, body, headers):
+        body = self.fixtures.load("api_1_2_storage_normal.json")
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_storage(self, method, url, body, headers):
+        self.__class__.last_request_body = json.loads(body)
+        body = self.fixtures.load("api_1_2_storage_create.json")
+        return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED])
+
+    def _1_2_storage_01d4fcd4_e446_433b_8a9c_551a1284952e_attach(
+        self, method, url, body, headers
+    ):
+        self.__class__.last_request_body = json.loads(body)
+        body = self.fixtures.load("api_1_2_storage_01d4fcd4-e446-433b-8a9c-551a1284952e.json")
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_storage_01d4fcd4_e446_433b_8a9c_551a1284952e_detach(
+        self, method, url, body, headers
+    ):
+        body = self.fixtures.load("api_1_2_storage_01d4fcd4-e446-433b-8a9c-551a1284952e.json")
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_storage_01d4fcd4_e446_433b_8a9c_551a1284952e(self, method, url, body, headers):
+        return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT])
+
     def _1_2_server_00f8c525_7e62_4108_8115_3958df5b43dc(self, method, url, body, headers):
         body = self.fixtures.load("api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json")
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
@@ -308,6 +469,16 @@ class UpcloudMockHttp(MockHttp):
         )
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
 
+    def _1_2_server_00f8c525_7e62_4108_8115_3958df5b43dc_start(self, method, url, body, headers):
+        self.__class__.last_request_body = json.loads(body)
+        body = self.fixtures.load("api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json")
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_server_00f8c525_7e62_4108_8115_3958df5b43dc_stop(self, method, url, body, headers):
+        self.__class__.last_request_body = json.loads(body)
+        body = self.fixtures.load("api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json")
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
     def _1_2_server_00893c98_5d5a_4363_b177_88df518a2b60(self, method, url, body, headers):
         body = self.fixtures.load("api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.json")
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])