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
11 changes: 6 additions & 5 deletions craft_providers/lxd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright 2021 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 @@ -17,7 +17,7 @@

"""LXD environment provider."""

from .errors import LXDError, LXDInstallationError # noqa: F401
from .errors import LXDError, LXDInstallationError, LXDUnstableImageError # noqa: F401
from .installer import ( # noqa: F401
ensure_lxd_is_ready,
install,
Expand All @@ -29,17 +29,18 @@
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",
"LXD",
"LXDInstance",
"LXDError",
"LXDInstallationError",
"LXDUnstableImageError",
"LXDProvider",
"PROVIDER_BASE_TO_LXD_BASE",
"get_remote_image",
"install",
"is_installed",
"is_initialized",
Expand Down
16 changes: 16 additions & 0 deletions craft_providers/lxd/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ def __init__(
brief = f"Failed to install LXD: {reason}."
resolution = LXD_INSTALL_HELP
super().__init__(brief=brief, details=details, resolution=resolution)


class LXDUnstableImageError(LXDError):
"""LXD Unstable Image Error.

:param brief: Brief description of error.
"""

def __init__(self, brief: str) -> None:
super().__init__(
brief=brief,
details=(
"Devel or daily images are not guaranteed and are intended for "
"experimental use only."
),
)
33 changes: 19 additions & 14 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 @@ -23,25 +23,18 @@

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 .errors import LXDError, LXDUnstableImageError
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 @@ -105,6 +98,7 @@ def launched_environment(
base_configuration: Base,
build_base: str,
instance_name: str,
allow_unstable: bool = False,
) -> Generator[Executor, None, None]:
"""Configure and launch environment for specified base.

Expand All @@ -117,17 +111,28 @@ def launched_environment(
:param base_configuration: Base configuration to apply to instance.
:param build_base: Base to build from.
:param instance_name: Name of the instance to launch.
:param allow_unstable: If true, allow unstable images to be launched

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

# only allow launching unstable images when opted-in with `allow_unstable`
if not image.is_stable and not allow_unstable:
raise LXDUnstableImageError(
brief=(
f"Cannot launch an unstable image {image.image_name!r} from remote "
f"{image.remote_name!r}"
),
)

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
194 changes: 161 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,177 @@
# 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
import warnings
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`)
"""
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

@property
def is_stable(self) -> bool:
"""Check if the image is stable.

Images are considered stable if they are from a release remote. Images from
daily or devel remotes are not considered.

:returns: True if the image is stable.
"""
return (
self.remote_name == BUILDD_RELEASES_REMOTE_NAME
and self.remote_address == BUILDD_RELEASES_REMOTE_ADDRESS
)

def add_remote(self, lxc: LXC) -> None:
"""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,
),
BuilddBaseAlias.FOCAL.value: RemoteImage(
image_name="core20",
remote_name=BUILDD_RELEASES_REMOTE_NAME,
remote_address=BUILDD_RELEASES_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
),
BuilddBaseAlias.JAMMY.value: RemoteImage(
image_name="core22",
remote_name=BUILDD_RELEASES_REMOTE_NAME,
remote_address=BUILDD_RELEASES_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
),
BuilddBaseAlias.KINETIC.value: RemoteImage(
image_name="kinetic",
remote_name=DAILY_REMOTE_NAME,
remote_address=DAILY_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
),
BuilddBaseAlias.LUNAR.value: RemoteImage(
image_name="lunar",
remote_name=DAILY_REMOTE_NAME,
remote_address=DAILY_REMOTE_ADDRESS,
remote_protocol=ProtocolType.SIMPLESTREAMS,
),
}


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.
"""
warnings.warn(
message=(
"configure_buildd_image_remote() is deprecated. "
"Use configure_image_remote()."
),
category=DeprecationWarning,
)
# 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
Loading