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

feat(lxd): support launching minimal images on lxd_vm and container (SC-1750) #410

Merged
merged 1 commit into from
Sep 12, 2024
Merged
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1!9.0.2
1!9.1.0
10 changes: 8 additions & 2 deletions examples/lxd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import textwrap

import pycloudlib
from pycloudlib.cloud import ImageType

RELEASE = "bionic"
RELEASE = "noble"


def snapshot_instance():
Expand Down Expand Up @@ -98,13 +99,18 @@ def launch_multiple():
lxd = pycloudlib.LXDContainer("example-multiple")

instances = []
for num in range(3):
for num in range(2):
inst = lxd.launch(name="pycloudlib-%s" % num, image_id=RELEASE)
instances.append(inst)

for instance in instances:
instance.wait()

# Launch daily minimal images
image_id = lxd.daily_image(release=RELEASE, image_type=ImageType.MINIMAL)
inst = lxd.launch(name="pycloudlib-minimal", image_id=image_id)
instances.append(inst)

for instance in instances:
instance.delete()

Expand Down
13 changes: 11 additions & 2 deletions pycloudlib/lxd/_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import logging
from typing import Any, List, Optional, Sequence, Tuple

from pycloudlib.cloud import ImageType
from pycloudlib.util import subp

_REMOTE_DAILY = "ubuntu-daily"
_REMOTE_RELEASE = "ubuntu"
_REMOTE_DAILY_MINIMAL = "ubuntu-minimal-daily"
_REMOTE_RELEASE_MINIMAL = "ubuntu-minimal"
log = logging.getLogger(__name__)


Expand All @@ -18,6 +21,7 @@ def find_last_fingerprint(
release: str,
is_container: bool,
arch: str,
image_type: ImageType = ImageType.GENERIC,
) -> Optional[str]:
"""Find last LXD image fingerprint.

Expand All @@ -30,12 +34,17 @@ def find_last_fingerprint(
Returns:
string, LXD fingerprint of latest image if found
"""
remote = _REMOTE_DAILY if daily else _REMOTE_RELEASE
label = "daily" if daily else "release"
if image_type == ImageType.MINIMAL:
remote = _REMOTE_DAILY_MINIMAL if daily else _REMOTE_RELEASE_MINIMAL
label = f"minimal {label}"
else:
remote = _REMOTE_DAILY if daily else _REMOTE_RELEASE
remote += ":"
base_filters = (
("architecture", arch),
("release", release),
("label", "daily" if daily else "release"),
("label", label),
)
filters = (
*base_filters,
Expand Down
29 changes: 24 additions & 5 deletions pycloudlib/lxd/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import yaml

from pycloudlib.cloud import BaseCloud
from pycloudlib.cloud import BaseCloud, ImageType
from pycloudlib.constants import LOCAL_UBUNTU_ARCH
from pycloudlib.lxd import _images
from pycloudlib.lxd.defaults import base_vm_profiles
Expand Down Expand Up @@ -329,27 +329,43 @@ def launch(

return instance

def released_image(self, release, arch=LOCAL_UBUNTU_ARCH):
def released_image(
self,
release,
arch=LOCAL_UBUNTU_ARCH,
*,
image_type: ImageType = ImageType.GENERIC,
**kwargs,
):
"""Find the LXD fingerprint of the latest released image.

Args:
release: string, Ubuntu release to look for
arch: string, architecture to use
image_type: image type to use: For example GENERIC or MINIMAL.

Returns:
string, LXD fingerprint of latest image

"""
self._log.debug("finding released Ubuntu image for %s", release)
self._log.debug(
"finding released Ubuntu image [%s] for %s", image_type, release
)
return _images.find_last_fingerprint(
daily=False,
release=release,
arch=arch,
is_container=self._is_container,
image_type=image_type,
)

def daily_image(
self, release: str, arch: str = LOCAL_UBUNTU_ARCH, **kwargs
self,
release: str,
arch: str = LOCAL_UBUNTU_ARCH,
*,
image_type: ImageType = ImageType.GENERIC,
**kwargs,
):
"""Find the LXD fingerprint of the latest daily image.

Expand All @@ -361,12 +377,15 @@ def daily_image(
string, LXD fingerprint of latest image

"""
self._log.debug("finding daily Ubuntu image for %s", release)
self._log.debug(
"finding daily Ubuntu image [%s] for %s", image_type, release
)
return _images.find_last_fingerprint(
daily=True,
release=release,
arch=arch,
is_container=self._is_container,
image_type=image_type,
)

def image_serial(self, image_id):
Expand Down
37 changes: 36 additions & 1 deletion tests/integration_tests/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

import pycloudlib
from pycloudlib.cloud import BaseCloud
from pycloudlib.cloud import BaseCloud, ImageType
from pycloudlib.instance import BaseInstance
from pycloudlib.util import LTS_RELEASES, UBUNTU_RELEASE_VERSION_MAP

Expand Down Expand Up @@ -137,3 +137,38 @@ def test_public_api(cloud: BaseCloud):
"Unable to find daily development image for "
f"{cloud._type}:{latest_devel_release}"
)
print(f"Checking latest minimal daily devel image: {latest_devel_release}")

if isinstance(cloud, pycloudlib.LXDContainer):
print(f"Checking latest daily minimal image: {latest_devel_release}")


@pytest.mark.parametrize(
"cloud",
[
pytest.param(pycloudlib.EC2, id="ec2", marks=pytest.mark.main_check),
pytest.param(pycloudlib.GCE, id="gce", marks=pytest.mark.main_check),
pytest.param(
pycloudlib.LXDContainer, id="lxd_container", marks=pytest.mark.ci
),
blackboxsw marked this conversation as resolved.
Show resolved Hide resolved
pytest.param(pycloudlib.LXDVirtualMachine, id="lxd_vm"),
],
indirect=True,
)
def test_public_api_mininal_images(cloud: BaseCloud):
latest_lts = LTS_RELEASES[-1]
print(
f"Checking latest {cloud.__class__.__name__} daily minimal image: "
f"{latest_lts}"
)
released_minimal_image_id = cloud.daily_image(
release=latest_lts, image_type=ImageType.MINIMAL
)
with cloud.launch(image_id=released_minimal_image_id) as instance:
instance.wait()
assert "status: done" == instance.execute("cloud-init status").stdout
assert (
"minimal"
in instance.execute("grep build_name /etc/cloud/build.info").stdout
)
instance.delete()
121 changes: 120 additions & 1 deletion tests/unit_tests/lxd/test_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import pytest

from pycloudlib.lxd.cloud import LXDContainer
from pycloudlib.cloud import ImageType
from pycloudlib.lxd.cloud import LXDContainer, LXDVirtualMachine

M_PATH = "pycloudlib.lxd.cloud."

Expand Down Expand Up @@ -148,3 +149,121 @@ def test_create_profile_that_does_not_exist(self, m_subp):
["lxc", "profile", "edit", profile_name], data=profile_config
),
]


class TestReleaseImage:
@pytest.mark.parametrize(
"cloud_cls,release,arch,image_type,expected_kwargs",
(
(
(
LXDContainer,
"bionic",
None,
None,
{
"daily": False,
"release": "bionic",
"arch": "amd64",
"image_type": ImageType.GENERIC,
"is_container": True,
},
),
(
LXDVirtualMachine,
"jammy",
"powerpc",
ImageType.MINIMAL,
{
"daily": False,
"release": "jammy",
"arch": "powerpc",
"image_type": ImageType.MINIMAL,
"is_container": False,
},
),
)
),
)
@mock.patch(M_PATH + "_images.find_last_fingerprint")
def test_release_image(
self,
find_last_fingerprint,
cloud_cls,
release,
arch,
image_type,
expected_kwargs,
caplog,
):
"""release_image only searches released image fingerprints."""
find_last_fingerprint.return_value = "1234"
kwargs = {
"release": release,
}
if arch:
kwargs["arch"] = arch
if image_type:
kwargs["image_type"] = image_type
cloud = cloud_cls(tag="test", config_file=io.StringIO(CONFIG))
assert "1234" == cloud.released_image(**kwargs)
find_last_fingerprint.assert_called_once_with(**expected_kwargs)


class TestDailyImage:
@pytest.mark.parametrize(
"cloud_cls,release,arch,image_type,expected_kwargs",
(
(
(
LXDContainer,
"bionic",
None,
None,
{
"daily": True,
"release": "bionic",
"arch": "amd64",
"image_type": ImageType.GENERIC,
"is_container": True,
},
),
(
LXDVirtualMachine,
"jammy",
"powerpc",
ImageType.MINIMAL,
{
"daily": True,
"release": "jammy",
"arch": "powerpc",
"image_type": ImageType.MINIMAL,
"is_container": False,
},
),
)
),
)
@mock.patch(M_PATH + "_images.find_last_fingerprint")
def test_release_image(
self,
find_last_fingerprint,
cloud_cls,
release,
arch,
image_type,
expected_kwargs,
caplog,
):
"""release_image only searches released image fingerprints."""
find_last_fingerprint.return_value = "1234"
kwargs = {
"release": release,
}
if arch:
kwargs["arch"] = arch
if image_type:
kwargs["image_type"] = image_type
cloud = cloud_cls(tag="test", config_file=io.StringIO(CONFIG))
assert "1234" == cloud.daily_image(**kwargs)
find_last_fingerprint.assert_called_once_with(**expected_kwargs)
Loading
Loading