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

lxd: add support for interim Ubuntu releases #219

Merged
merged 9 commits into from
Feb 24, 2023
4 changes: 3 additions & 1 deletion craft_providers/bases/buildd.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright 2021-2022 Canonical Ltd.
# Copyright 2021-2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
Expand Down Expand Up @@ -144,6 +144,8 @@ class BuilddBaseAlias(enum.Enum):
BIONIC = "18.04"
FOCAL = "20.04"
JAMMY = "22.04"
KINETIC = "22.10"
LUNAR = "23.04"
mr-cal marked this conversation as resolved.
Show resolved Hide resolved


class Snap(pydantic.BaseModel, extra=pydantic.Extra.forbid):
Expand Down
6 changes: 3 additions & 3 deletions craft_providers/lxd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
from .lxc import LXC # noqa: F401
from .lxd import LXD # noqa: F401
from .lxd_instance import LXDInstance # noqa: F401
from .lxd_provider import PROVIDER_BASE_TO_LXD_BASE, LXDProvider # noqa: F401
from .remotes import configure_buildd_image_remote # noqa: F401
from .lxd_provider import LXDProvider # noqa: F401
from .remotes import configure_buildd_image_remote, get_remote_image # noqa: F401

__all__ = [
"LXC",
Expand All @@ -39,7 +39,7 @@
"LXDError",
"LXDInstallationError",
"LXDProvider",
"PROVIDER_BASE_TO_LXD_BASE",
"get_remote_image",
"install",
"is_installed",
"is_initialized",
Expand Down
32 changes: 19 additions & 13 deletions craft_providers/lxd/lxd_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
# Copyright 2022-2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
Expand All @@ -19,29 +19,23 @@
import contextlib
import logging
import pathlib
import warnings
from typing import Generator

from craft_providers import Executor, Provider
from craft_providers.base import Base
from craft_providers.bases import BaseConfigurationError, BuilddBaseAlias
from craft_providers.bases import BaseConfigurationError

from .errors import LXDError
from .installer import ensure_lxd_is_ready, install, is_installed
from .launcher import launch
from .lxc import LXC
from .lxd_instance import LXDInstance
from .remotes import configure_buildd_image_remote
from .remotes import get_remote_image

logger = logging.getLogger(__name__)


PROVIDER_BASE_TO_LXD_BASE = {
BuilddBaseAlias.BIONIC.value: "core18",
BuilddBaseAlias.FOCAL.value: "core20",
BuilddBaseAlias.JAMMY.value: "core22",
}


class LXDProvider(Provider):
"""LXD build environment provider.

Expand Down Expand Up @@ -120,14 +114,26 @@ def launched_environment(

:raises LXDError: if instance cannot be configured and launched
"""
image_remote = configure_buildd_image_remote()
image = get_remote_image(build_base)
image.add_remote()

# we can't guarantee daily and devel images, so explicitly warn the user
if not image.is_stable:
warnings.warn(
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
message=(
f"You are using an daily or devel image {image.image_name!r}"
f" from remote {image.remote_name!r}. Devel or daily images are "
"not guaranteed and are intended for experimental use only."
),
category=UserWarning,
)

try:
instance = launch(
name=instance_name,
base_configuration=base_configuration,
image_name=PROVIDER_BASE_TO_LXD_BASE[build_base],
image_remote=image_remote,
image_name=image.image_name,
image_remote=image.remote_name,
auto_clean=True,
auto_create_project=True,
map_user_uid=True,
Expand Down
183 changes: 150 additions & 33 deletions craft_providers/lxd/remotes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2021-2022 Canonical Ltd.
# Copyright 2021-2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
Expand All @@ -15,50 +16,166 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#

"""Remote helper utilities."""
"""Manages LXD remotes and provides access to remote images."""

import logging
from dataclasses import dataclass
from enum import Enum

from craft_providers.bases import BuilddBaseAlias

from .errors import LXDError
from .lxc import LXC

logger = logging.getLogger(__name__)

BUILDD_REMOTE_NAME = "craft-com.ubuntu.cloud-buildd"
BUILDD_REMOTE_ADDR = "https://cloud-images.ubuntu.com/buildd/releases"
BUILDD_RELEASES_REMOTE_NAME = "craft-com.ubuntu.cloud-buildd"
BUILDD_RELEASES_REMOTE_ADDRESS = "https://cloud-images.ubuntu.com/buildd/releases"

# XXX: lunar and kinetic buildd daily images are not working (LP #2007419)
BUILDD_DAILY_REMOTE_NAME = "craft-com.ubuntu.cloud-buildd-daily"
BUILDD_DAILY_REMOTE_ADDRESS = "https://cloud-images.ubuntu.com/buildd/daily"

def configure_buildd_image_remote(
lxc: LXC = LXC(),
) -> str:
"""Configure buildd remote, adding remote as required.
# temporarily use the cloud release images until daily buildd images are fixed
DAILY_REMOTE_NAME = "ubuntu-daily"
DAILY_REMOTE_ADDRESS = "https://cloud-images.ubuntu.com/daily"

:param lxc: LXC client.

:returns: Name of remote to pass to launcher.
class ProtocolType(Enum):
"""Enumeration of protocols for LXD remotes."""

LXD = "lxd"
SIMPLESTREAMS = "simplestreams"


@dataclass
class RemoteImage:
"""Contains the name, location, and details of a remote LXD image.

:param image_name: Name of the image on the remote (e.g. `core22` or `lunar`).
:param remote_name: Name of the remote server.
:param remote_address: Address of the remote (can be an IP, FDQN, URL, or token)
:param remote_protocol: Remote protocol (options are `lxd` and `simplestreams`)
:param is_stable: True if the image is a stable release. Daily and devel images
are not stable.
"""
if BUILDD_REMOTE_NAME in lxc.remote_list():
logger.debug("Remote %r already exists.", BUILDD_REMOTE_NAME)
else:
try:
lxc.remote_add(
remote=BUILDD_REMOTE_NAME,
addr=BUILDD_REMOTE_ADDR,
protocol="simplestreams",
)
except Exception as exc: # pylint: disable=broad-except
# the remote adding failed, no matter really how: if it was because a race
# condition on remote creation (it's not idempotent) and now the remote is
# there, the purpose of this function is done (otherwise we let the
# original exception fly)
if BUILDD_REMOTE_NAME in lxc.remote_list():
logger.debug(
"Remote %r is present on second check, ignoring exception %r.",
BUILDD_REMOTE_NAME,
exc,

image_name: str
remote_name: str
remote_address: str
remote_protocol: ProtocolType
is_stable: bool

def add_remote(self, lxc: LXC = LXC()) -> None:
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
"""Add the LXD remote for an image.

If the remote already exists, it will not be re-added.

:param lxc: LXC client.
"""
# TODO verify both the remote name and address
if self.remote_name in lxc.remote_list():
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
logger.debug("Remote %r already exists.", self.remote_name)
else:
try:
lxc.remote_add(
remote=self.remote_name,
addr=self.remote_address,
protocol=self.remote_protocol.value,
)

except Exception as exc: # pylint: disable=broad-except
# the remote adding failed, no matter really how: if it was because a
# race condition on remote creation (it's not idempotent) and now the
# remote is there, the purpose of this function is done (otherwise we
# let the original exception fly)
if self.remote_name in lxc.remote_list():
logger.debug(
"Remote %r is present on second check, ignoring exception %r.",
self.remote_name,
exc,
)
else:
raise
else:
raise
else:
logger.debug("Remote %r was successfully added.", BUILDD_REMOTE_NAME)
logger.debug("Remote %r was successfully added.", self.remote_name)


# XXX: support xenial?
# mapping from supported bases to actual lxd remote images
_PROVIDER_BASE_TO_LXD_REMOTE_IMAGE = {
BuilddBaseAlias.BIONIC.value: RemoteImage(
image_name="core18",
remote_name=BUILDD_RELEASES_REMOTE_NAME,
remote_address=BUILDD_RELEASES_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
is_stable=True,
),
BuilddBaseAlias.FOCAL.value: RemoteImage(
image_name="core20",
remote_name=BUILDD_RELEASES_REMOTE_NAME,
remote_address=BUILDD_RELEASES_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
is_stable=True,
),
BuilddBaseAlias.JAMMY.value: RemoteImage(
image_name="core22",
remote_name=BUILDD_RELEASES_REMOTE_NAME,
remote_address=BUILDD_RELEASES_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
is_stable=True,
),
BuilddBaseAlias.KINETIC.value: RemoteImage(
image_name="kinetic",
remote_name=DAILY_REMOTE_NAME,
remote_address=DAILY_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
is_stable=False,
),
BuilddBaseAlias.LUNAR.value: RemoteImage(
image_name="lunar",
remote_name=DAILY_REMOTE_NAME,
remote_address=DAILY_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
is_stable=False,
),
}


def get_remote_image(provider_base: str) -> RemoteImage:
"""Get a RemoteImage for a particular provider base.

:param provider_base: string containing the provider base

:returns: the RemoteImage for the provider base
"""
image = _PROVIDER_BASE_TO_LXD_REMOTE_IMAGE.get(provider_base)
if not image:
raise LXDError(
brief=(
"could not find a lxd remote image for the provider base "
f"{provider_base!r}"
)
)

return image


def configure_buildd_image_remote(lxc: LXC = LXC()) -> str:
"""Configure the default buildd image remote.

This is a deprecated function to maintain the existing API. It will be
removed with the release of craft-providers 2.0.

:param lxc: LXC client.

:returns: Name of remote to pass to launcher.
"""
logger.warning(
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
"configure_buildd_image_remote() is deprecated. Use configure_image_remote()."
)
# configure the buildd remote for core22
image = get_remote_image(BuilddBaseAlias.JAMMY.value)
image.add_remote(lxc)

return BUILDD_REMOTE_NAME
return BUILDD_RELEASES_REMOTE_NAME
3 changes: 2 additions & 1 deletion craft_providers/multipass/multipass_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
# Copyright 2022-2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
Expand Down Expand Up @@ -33,6 +33,7 @@
logger = logging.getLogger(__name__)


# TODO: support KINETIC and LUNAR
PROVIDER_BASE_TO_MULTIPASS_BASE = {
bases.BuilddBaseAlias.BIONIC.value: "snapcraft:18.04",
bases.BuilddBaseAlias.FOCAL.value: "snapcraft:20.04",
Expand Down
20 changes: 16 additions & 4 deletions tests/integration/lxd/test_lxd_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
# Copyright 2022-2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
Expand All @@ -16,6 +16,8 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#

import pytest

from craft_providers.bases import BuilddBase, BuilddBaseAlias
from craft_providers.lxd import LXDProvider, is_installed

Expand All @@ -40,16 +42,26 @@ def test_create_environment(installed_lxd, instance_name):
assert test_instance.exists() is False


def test_launched_environment(installed_lxd, instance_name, tmp_path):
@pytest.mark.parametrize(
"alias",
[
BuilddBaseAlias.BIONIC,
BuilddBaseAlias.FOCAL,
BuilddBaseAlias.JAMMY,
BuilddBaseAlias.KINETIC,
BuilddBaseAlias.LUNAR,
],
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
)
def test_launched_environment(alias, installed_lxd, instance_name, tmp_path):
provider = LXDProvider()

base_configuration = BuilddBase(alias=BuilddBaseAlias.JAMMY)
base_configuration = BuilddBase(alias=alias)

with provider.launched_environment(
project_name="test-project",
project_path=tmp_path,
base_configuration=base_configuration,
build_base=BuilddBaseAlias.JAMMY.value,
build_base=alias.value,
instance_name=instance_name,
) as test_instance:
assert test_instance.exists() is True
Expand Down
Loading