Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project: Image Services Gen2 #445

Merged
merged 7 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
51 changes: 37 additions & 14 deletions linode_api4/groups/image.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import BinaryIO, Tuple
from typing import BinaryIO, List, Optional, Tuple, Union

import requests

from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import Base, Image
from linode_api4.objects import Base, Disk, Image
from linode_api4.util import drop_null_keys


Expand All @@ -29,39 +29,45 @@ def __call__(self, *filters):
"""
return self.client._get_and_filter(Image, *filters)

def create(self, disk, label=None, description=None, cloud_init=False):
def create(
self,
disk: Union[Disk, int],
label: str = None,
description: str = None,
cloud_init: bool = False,
tags: Optional[List[str]] = None,
):
"""
Creates a new Image from a disk you own.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-image

:param disk: The Disk to imagize.
:type disk: Disk or int
:type disk: Union[Disk, int]
:param label: The label for the resulting Image (defaults to the disk's
label.
:type label: str
:param description: The description for the new Image.
:type description: str
:param cloud_init: Whether this Image supports cloud-init.
:type cloud_init: bool
:param tags: A list of customized tags of this new Image.
:type tags: Optional[List[str]]

:returns: The new Image.
:rtype: Image
"""
params = {
"disk_id": disk.id if issubclass(type(disk), Base) else disk,
"label": label,
"description": description,
"tags": tags,
}

if label is not None:
params["label"] = label

if description is not None:
params["description"] = description

if cloud_init:
params["cloud_init"] = cloud_init

result = self.client.post("/images", data=params)
result = self.client.post("/images", data=drop_null_keys(params))

if not "id" in result:
raise UnexpectedResponseError(
Expand All @@ -78,6 +84,7 @@ def create_upload(
region: str,
description: str = None,
cloud_init: bool = False,
tags: Optional[List[str]] = None,
) -> Tuple[Image, str]:
"""
Creates a new Image and returns the corresponding upload URL.
Expand All @@ -92,11 +99,18 @@ def create_upload(
:type description: str
:param cloud_init: Whether this Image supports cloud-init.
:type cloud_init: bool
:param tags: A list of customized tags of this Image.
:type tags: Optional[List[str]]

:returns: A tuple containing the new image and the image upload URL.
:rtype: (Image, str)
"""
params = {"label": label, "region": region, "description": description}
params = {
"label": label,
"region": region,
"description": description,
"tags": tags,
}

if cloud_init:
params["cloud_init"] = cloud_init
Expand All @@ -114,7 +128,12 @@ def create_upload(
return Image(self.client, result_image["id"], result_image), result_url

def upload(
self, label: str, region: str, file: BinaryIO, description: str = None
self,
label: str,
region: str,
file: BinaryIO,
description: str = None,
tags: Optional[List[str]] = None,
) -> Image:
"""
Creates and uploads a new image.
Expand All @@ -128,12 +147,16 @@ def upload(
:param file: The BinaryIO object to upload to the image. This is generally obtained from open("myfile", "rb").
:param description: The description for the new Image.
:type description: str
:param tags: A list of customized tags of this Image.
:type tags: Optional[List[str]]

:returns: The resulting image.
:rtype: Image
"""

image, url = self.create_upload(label, region, description=description)
image, url = self.create_upload(
label, region, description=description, tags=tags
)

requests.put(
url,
Expand Down
29 changes: 22 additions & 7 deletions linode_api4/linode_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import logging
from importlib.metadata import version
from typing import BinaryIO, Tuple
from typing import BinaryIO, List, Tuple
from urllib import parse

import requests
Expand Down Expand Up @@ -378,32 +378,47 @@ def __setattr__(self, key, value):

super().__setattr__(key, value)

def image_create(self, disk, label=None, description=None):
def image_create(self, disk, label=None, description=None, tags=None):
"""
.. note:: This method is an alias to maintain backwards compatibility.
Please use :meth:`LinodeClient.images.create(...) <.ImageGroup.create>` for all new projects.
"""
return self.images.create(disk, label=label, description=description)
return self.images.create(
disk, label=label, description=description, tags=tags
)

def image_create_upload(
self, label: str, region: str, description: str = None
self,
label: str,
region: str,
description: str = None,
tags: List[str] = None,
) -> Tuple[Image, str]:
"""
.. note:: This method is an alias to maintain backwards compatibility.
Please use :meth:`LinodeClient.images.create_upload(...) <.ImageGroup.create_upload>`
for all new projects.
"""

return self.images.create_upload(label, region, description=description)
return self.images.create_upload(
label, region, description=description, tags=tags
)

def image_upload(
self, label: str, region: str, file: BinaryIO, description: str = None
self,
label: str,
region: str,
file: BinaryIO,
description: str = None,
tags: List[str] = None,
) -> Image:
"""
.. note:: This method is an alias to maintain backwards compatibility.
Please use :meth:`LinodeClient.images.upload(...) <.ImageGroup.upload>` for all new projects.
"""
return self.images.upload(label, region, file, description=description)
return self.images.upload(
label, region, file, description=description, tags=tags
)

def nodebalancer_create(self, region, **kwargs):
"""
Expand Down
60 changes: 59 additions & 1 deletion linode_api4/objects/image.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
from linode_api4.objects import Base, Property
from dataclasses import dataclass
from typing import List, Union

from linode_api4.objects import Base, Property, Region
from linode_api4.objects.serializable import JSONObject, StrEnum


class ReplicationStatus(StrEnum):
"""
The Enum class represents image replication status.
"""

pending_replication = "pending replication"
pending_deletion = "pending deletion"
available = "available"
creating = "creating"
pending = "pending"
replicating = "replicating"


@dataclass
class ImageRegion(JSONObject):
"""
The region and status of an image replica.
"""

region: str = ""
status: ReplicationStatus = None


class Image(Base):
Expand Down Expand Up @@ -28,4 +55,35 @@ class Image(Base):
"capabilities": Property(
unordered=True,
),
"tags": Property(mutable=True, unordered=True),
"total_size": Property(),
"regions": Property(json_object=ImageRegion, unordered=True),
}

def replicate(self, regions: Union[List[str], List[Region]]):
"""
Replicate the image to other regions.

Note: Image replication may not currently be available to all users.

API Documentation: TODO

:param regions: A list of regions that the customer wants to replicate this image in.
At least one valid region is required and only core regions allowed.
Existing images in the regions not passed will be removed.
:type regions: List[str]
"""
params = {
"regions": [
region.id if isinstance(region, Region) else region
for region in regions
]
}

result = self._client.post(
"{}/regions".format(self.api_endpoint), model=self, data=params
)

# The replicate endpoint returns the updated Image, so we can use this
# as an opportunity to refresh the object
self._populate(result)
24 changes: 22 additions & 2 deletions test/fixtures/images.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
"eol": "2026-07-01T04:00:00",
"expiry": "2026-08-01T04:00:00",
"updated": "2020-07-01T04:00:00",
"capabilities": []
"capabilities": [],
"tags": ["tests"],
"total_size": 1100,
"regions": [
{
"region": "us-east",
"status": "available"
}
]
},
{
"created": "2017-01-01T00:01:01",
Expand All @@ -35,7 +43,19 @@
"eol": "2026-07-01T04:00:00",
"expiry": "2026-08-01T04:00:00",
"updated": "2020-07-01T04:00:00",
"capabilities": []
"capabilities": [],
"tags": ["tests"],
"total_size": 3000,
"regions": [
{
"region": "us-east",
"status": "available"
},
{
"region": "us-mia",
"status": "pending"
}
]
},
{
"created": "2017-01-01T00:01:01",
Expand Down
29 changes: 29 additions & 0 deletions test/fixtures/images_private_123_regions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"created": "2017-08-20T14:01:01",
"description": null,
"deprecated": false,
"status": "available",
"created_by": "testguy",
"id": "private/123",
"label": "Gold Master",
"size": 650,
"is_public": false,
"type": "manual",
"vendor": null,
"eol": "2026-07-01T04:00:00",
"expiry": "2026-08-01T04:00:00",
"updated": "2020-07-01T04:00:00",
"capabilities": ["cloud-init"],
"tags": ["tests"],
"total_size": 1300,
"regions": [
{
"region": "us-east",
"status": "available"
},
{
"region": "us-west",
"status": "pending replication"
}
]
}
3 changes: 2 additions & 1 deletion test/fixtures/images_upload.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"type": "manual",
"updated": "2021-08-14T22:44:02",
"vendor": "Debian",
"capabilities": ["cloud-init"]
"capabilities": ["cloud-init"],
"tags": ["test_tag", "test2"]
},
"upload_to": "https://linode.com/"
}
Loading