Skip to content
Closed
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
1 change: 1 addition & 0 deletions libcloud/compute/drivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@
"vcloud",
"vpsnet",
"onapp",
"equinixmetal"
]
280 changes: 29 additions & 251 deletions libcloud/compute/drivers/equinixmetal.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
asyncio = None

import json
import datetime

from libcloud.utils.py3 import httplib
from libcloud.common.base import JsonResponse, ConnectionKey
Expand All @@ -33,8 +32,6 @@
NodeImage,
NodeDriver,
NodeLocation,
StorageVolume,
VolumeSnapshot,
)
from libcloud.compute.types import Provider, NodeState, InvalidCredsError

Expand Down Expand Up @@ -238,7 +235,7 @@ def ex_list_nodes_for_project(self, ex_project_id, include="plan", page=1, per_p
return list(map(self._to_node, data))

def list_locations(self):
data = self.connection.request("/metal/v1/facilities").object["facilities"]
data = self.connection.request("/metal/v1/locations/metros").object["metros"]
return list(map(self._to_location, data))

def list_images(self):
Expand Down Expand Up @@ -285,12 +282,15 @@ def create_node(
if not ex_project_id:
raise Exception("ex_project_id needs to be specified")

facility = location.extra["code"]
location_code = location.extra["code"]
if not self._valid_location:
raise ValueError("Failed to create node: valid parameter metro [code] is required in the input")

params = {
"hostname": name,
"plan": size.id,
"operating_system": image.id,
"facility": facility,
"metro": location_code,
"include": "plan",
"billing_cycle": "hourly",
}
Expand Down Expand Up @@ -453,6 +453,8 @@ def _to_node(self, data):
size = None
if "facility" in data:
extra["facility"] = data["facility"]
if "metro" in data and data["metro"] is not None:
extra["metro"] = data["metro"]

for key in extra_keys:
if key in data:
Expand Down Expand Up @@ -491,8 +493,8 @@ def _to_size(self, data):
except KeyError:
cpus = None
regions = [
region.get("href").replace("/metal/v1/facilities/", "")
for region in data.get("available_in", [])
region.get("href").replace("/metal/v1/locations/metros", "")
for region in data.get("available_in_metros", [])
]
extra = {
"description": data["description"],
Expand All @@ -501,8 +503,16 @@ def _to_size(self, data):
"regions": regions,
}
try:
ram = int(data["specs"]["memory"]["total"].replace("GB", "")) * 1024 # noqa
except KeyError:
factor = 1
ram_txt = data["specs"]["memory"]["total"]
if "GB" in ram_txt:
factor = 1024
ram_txt = ram_txt.replace("GB","")
elif "TB" in ram_txt:
factor = 1024 * 1024
ram_txt = ram_txt.replace("TB","")
ram = int(ram_txt) * factor
except Exception as e:
ram = None
disk = None
if data["specs"].get("drives", ""):
Expand Down Expand Up @@ -675,7 +685,7 @@ def ex_request_address_reservation(
"quantity": quantity,
}
if location_id:
params["facility"] = location_id
params["metro"] = location_id
if comments:
params["comments"] = comments
if customdata:
Expand All @@ -701,246 +711,14 @@ def ex_disassociate_address(self, address_uuid, include=None):
result = self.connection.request(path, params=params, method="DELETE").object
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

def _valid_location(self, metro_code):
if metro_code == None or metro_code == "":
return False
metros = self.connection.request("/metal/v1/locations/metros").object["metros"]
for metro in metros:
if metro["code"] == metro_code:
return True
return False

class Project:
def __init__(self, project):
Expand Down
12 changes: 0 additions & 12 deletions libcloud/test/compute/fixtures/equinixmetal/facilities.json

This file was deleted.

10 changes: 10 additions & 0 deletions libcloud/test/compute/fixtures/equinixmetal/metros.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"metros": [
{
"id": "d3d6b29f-042d-43b7-b3ce-0bf53d5754ca",
"name": "Dallas",
"code": "da",
"country": "US"
}
]
}
4 changes: 2 additions & 2 deletions libcloud/test/compute/test_equinixmetal.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ def test_destroy_volume(self):
class EquinixMetalMockHttp(MockHttp):
fixtures = ComputeFileFixtures("equinixmetal")

def _metal_v1_facilities(self, method, url, body, headers):
body = self.fixtures.load("facilities.json")
def _metal_v1_metros(self, method, url, body, headers):
body = self.fixtures.load("metros.json")
return (httplib.OK, body, {}, httplib.responses[httplib.OK])

def _metal_v1_plans(self, method, url, body, headers):
Expand Down