diff --git a/libcloud/compute/drivers/equinixmetal.py b/libcloud/compute/drivers/equinixmetal.py index 6d48fcada2..23497d653d 100644 --- a/libcloud/compute/drivers/equinixmetal.py +++ b/libcloud/compute/drivers/equinixmetal.py @@ -33,8 +33,6 @@ NodeImage, NodeDriver, NodeLocation, - StorageVolume, - VolumeSnapshot, ) from libcloud.compute.types import Provider, NodeState, InvalidCredsError @@ -784,277 +782,6 @@ def ex_disassociate_address(self, address_uuid, include=None): return result - def list_volumes(self, ex_project_id=None): - if ex_project_id: - return self.ex_list_volumes_for_project(ex_project_id=ex_project_id) - - # if project has been specified during driver initialization, then - # return nodes for this project only - - if self.project_id: - return self.ex_list_volumes_for_project(ex_project_id=self.project_id) - - # In case of Python2 perform requests serially - - if not use_asyncio(): - nodes = [] - - for project in self.projects: - nodes.extend(self.ex_list_volumes_for_project(ex_project_id=project.id)) - - return nodes - # In case of Python3 use asyncio to perform requests in parallel - - return self.list_resources_async("volumes") - - def ex_list_volumes_for_project(self, ex_project_id, include="plan", page=1, per_page=1000): - params = {"include": include, "page": page, "per_page": per_page} - data = self.connection.request( - "/metal/v1/projects/%s/storage" % (ex_project_id), params=params - ).object["volumes"] - - return list(map(self._to_volume, data)) - - def _to_volume(self, data): - return StorageVolume( - id=data["id"], name=data["name"], size=data["size"], driver=self, extra=data - ) - - def create_volume( - self, - size, - location, - plan="storage_1", - description="", - ex_project_id=None, - locked=False, - billing_cycle=None, - customdata="", - snapshot_policies=None, - **kwargs, - ): - """ - Create a new volume. - - :param size: Size of volume in gigabytes (required) - :type size: ``int`` - - :param location: Which data center to create a volume in. If - empty, undefined behavior will be selected. - (optional) - :type location: :class:`.NodeLocation` - :return: The newly created volume. - :rtype: :class:`StorageVolume` - """ - path = "/metal/v1/projects/%s/storage" % (ex_project_id or self.projects[0].id) - try: - facility = location.extra["code"] - except AttributeError: - facility = location - params = {"facility": facility, "plan": plan, "size": size, "locked": locked} - params.update(kwargs) - - if description: - params["description"] = description - - if customdata: - params["customdata"] = customdata - - if billing_cycle: - params["billing_cycle"] = billing_cycle - - if snapshot_policies: - params["snapshot_policies"] = snapshot_policies - data = self.connection.request(path, params=params, method="POST").object - - return self._to_volume(data) - - def destroy_volume(self, volume): - """ - Destroys a storage volume. - - :param volume: Volume to be destroyed - :type volume: :class:`StorageVolume` - - :rtype: ``bool`` - """ - path = "/metal/v1/storage/%s" % volume.id - res = self.connection.request(path, method="DELETE") - - return res.status == httplib.NO_CONTENT - - def attach_volume(self, node, volume): - """ - Attaches volume to node. - - :param node: Node to attach volume to. - :type node: :class:`.Node` - - :param volume: Volume to attach. - :type volume: :class:`.StorageVolume` - - :rytpe: ``bool`` - """ - path = "/metal/v1/storage/%s/attachments" % volume.id - params = {"device_id": node.id} - res = self.connection.request(path, params=params, method="POST") - - return res.status == httplib.OK - - def detach_volume(self, volume, ex_node=None, ex_attachment_id=""): - """ - Detaches a volume from a node. - - :param volume: Volume to be detached - :type volume: :class:`.StorageVolume` - - :param ex_attachment_id: Attachment id to be detached, if empty detach - all attachments - :type name: ``str`` - - :rtype: ``bool`` - """ - path = "/metal/v1/storage/%s/attachments" % volume.id - attachments = volume.extra["attachments"] - assert len(attachments) > 0, "Volume is not attached to any node" - success = True - result = None - - for attachment in attachments: - if not ex_attachment_id or ex_attachment_id in attachment["href"]: - attachment_id = attachment["href"].split("/")[-1] - - if ex_node: - node_id = self.ex_describe_attachment(attachment_id)["device"]["href"].split( - "/" - )[-1] - - if node_id != ex_node.id: - continue - path = "/metal/v1/storage/attachments/%s" % (ex_attachment_id or attachment_id) - result = self.connection.request(path, method="DELETE") - success = success and result.status == httplib.NO_CONTENT - - return result and success - - def create_volume_snapshot(self, volume, name=""): - """ - Create a new volume snapshot. - - :param volume: Volume to create a snapshot for - :type volume: class:`StorageVolume` - - :return: The newly created volume snapshot. - :rtype: :class:`VolumeSnapshot` - """ - path = "/metal/v1/storage/%s/snapshots" % volume.id - res = self.connection.request(path, method="POST") - assert res.status == httplib.ACCEPTED - - return volume.list_snapshots()[-1] - - def destroy_volume_snapshot(self, snapshot): - """ - Delete a volume snapshot - - :param snapshot: volume snapshot to delete - :type snapshot: class:`VolumeSnapshot` - - :rtype: ``bool`` - """ - volume_id = snapshot.extra["volume"]["href"].split("/")[-1] - path = "/metal/v1/storage/{}/snapshots/{}".format(volume_id, snapshot.id) - res = self.connection.request(path, method="DELETE") - - return res.status == httplib.NO_CONTENT - - def list_volume_snapshots(self, volume, include=""): - """ - List snapshots for a volume. - - :param volume: Volume to list snapshots for - :type volume: class:`StorageVolume` - - :return: List of volume snapshots. - :rtype: ``list`` of :class: `VolumeSnapshot` - """ - path = "/metal/v1/storage/%s/snapshots" % volume.id - params = {} - - if include: - params["include"] = include - data = self.connection.request(path, params=params).object["snapshots"] - - return list(map(self._to_volume_snapshot, data)) - - def _to_volume_snapshot(self, data): - created = datetime.datetime.strptime(data["created_at"], "%Y-%m-%dT%H:%M:%S") - - return VolumeSnapshot( - id=data["id"], - name=data["id"], - created=created, - state=data["status"], - driver=self, - extra=data, - ) - - def ex_modify_volume( - self, - volume, - description=None, - size=None, - locked=None, - billing_cycle=None, - customdata=None, - ): - path = "/metal/v1/storage/%s" % volume.id - params = {} - - if description: - params["description"] = description - - if size: - params["size"] = size - - if locked is not None: - params["locked"] = locked - - if billing_cycle: - params["billing_cycle"] = billing_cycle - res = self.connection.request(path, params=params, method="PUT") - - return self._to_volume(res.object) - - def ex_restore_volume(self, snapshot): - volume_id = snapshot.extra["volume"]["href"].split("/")[-1] - ts = snapshot.extra["timestamp"] - path = "/metal/v1/storage/{}/restore?restore_point={}".format(volume_id, ts) - res = self.connection.request(path, method="POST") - - return res.status == httplib.NO_CONTENT - - def ex_clone_volume(self, volume, snapshot=None): - path = "/metal/v1/storage/%s/clone" % volume.id - - if snapshot: - path += "?snapshot_timestamp=%s" % snapshot.extra["timestamp"] - res = self.connection.request(path, method="POST") - - return res.status == httplib.NO_CONTENT - - def ex_describe_volume(self, volume_id): - path = "/metal/v1/storage/%s" % volume_id - data = self.connection.request(path).object - - return self._to_volume(data) - - def ex_describe_attachment(self, attachment_id): - path = "/metal/v1/storage/attachments/%s" % attachment_id - data = self.connection.request(path).object - - return data - class Project: def __init__(self, project): diff --git a/libcloud/test/compute/test_equinixmetal.py b/libcloud/test/compute/test_equinixmetal.py index 9c7f3b9288..8105e1ee4c 100644 --- a/libcloud/test/compute/test_equinixmetal.py +++ b/libcloud/test/compute/test_equinixmetal.py @@ -251,66 +251,6 @@ def test_ex_disassociate_address_with_node(self): break - def test_list_volumes(self): - volumes = self.driver.list_volumes() - assert len(volumes) == 2 - assert len(volumes[0].extra["attachments"]) == 0 - - def test_create_volume(self): - location = self.driver.list_locations()[0] - volume = self.driver.create_volume( - 10, - location, - description="test volume", - plan="storage_1", - ex_project_id="3d27fd13-0466-4878-be22-9a4b5595a3df", - ) - assert len(volume.extra["attachments"]) == 0 - assert not volume.extra["locked"] - - def test_attach_volume(self): - attached = False - volumes = self.driver.ex_list_volumes_for_project( - ex_project_id="3d27fd13-0466-4878-be22-9a4b5595a3df" - ) - node = self.driver.ex_list_nodes_for_project( - ex_project_id="3d27fd13-0466-4878-be22-9a4b5595a3df" - )[0] - - for vol in volumes: - if len(vol.extra["attachments"]) == 0: - attached = self.driver.attach_volume(node, vol) - - break - assert attached - - def test_detach_volume(self): - detached = False - volumes = self.driver.ex_list_volumes_for_project( - ex_project_id="3d27fd13-0466-4878-be22-9a4b5595a3df" - ) - - for vol in volumes: - if len(vol.extra["attachments"]) > 0: - detached = self.driver.detach_volume(vol) - - break - assert detached - - def test_destroy_volume(self): - destroyed = False - volumes = self.driver.ex_list_volumes_for_project( - ex_project_id="3d27fd13-0466-4878-be22-9a4b5595a3df" - ) - - for vol in volumes: - if len(vol.extra["attachments"]) == 0: - destroyed = self.driver.destroy_volume(vol) - - break - assert destroyed - - class EquinixMetalMockHttp(MockHttp): fixtures = ComputeFileFixtures("equinixmetal") @@ -522,54 +462,5 @@ def _metal_v1_ips_aea4ee0c_675f_4b77_8337_8e13b868dd9c(self, method, url, body, if method == "DELETE": return (httplib.OK, "", {}, httplib.responses[httplib.OK]) - def _metal_v1_projects_3d27fd13_0466_4878_be22_9a4b5595a3df_storage( - self, method, url, body, headers - ): - if method == "GET": - body = self.fixtures.load("volumes.json") - elif method == "POST": - body = self.fixtures.load("create_volume.json") - - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _metal_v1_projects_4a4bce6b_d2ef_41f8_95cf_0e2f32996440_storage( - self, method, url, body, headers - ): - if method == "GET": - body = json.dumps({"volumes": []}) - - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _metal_v1_projects_4b653fce_6405_4300_9f7d_c587b7888fe5_storage( - self, method, url, body, headers - ): - if method == "GET": - body = json.dumps({"volumes": []}) - - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _metal_v1_storage_74f11291_fde8_4abf_8150_e51cda7308c3(self, method, url, body, headers): - if method == "DELETE": - return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT]) - - def _metal_v1_storage_a08aaf76_e0ce_43aa_b9cd_cce0d4ae4f4c_attachments( - self, method, url, body, headers - ): - if method == "POST": - body = self.fixtures.load("attach_volume.json") - - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _metal_v1_storage_a08aaf76_e0ce_43aa_b9cd_cce0d4ae4f4c(self, method, url, body, headers): - if method == "DELETE": - return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT]) - - def _metal_v1_storage_attachments_2c16a96f_bb4f_471b_8e2e_b5820b9e1603( - self, method, url, body, headers - ): - if method == "DELETE": - return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.NO_CONTENT]) - - if __name__ == "__main__": sys.exit(unittest.main())