Skip to content
Draft
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
3 changes: 3 additions & 0 deletions imagecraft/models/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ class Role(str, enum.Enum):
SYSTEM_BOOT = "system-boot"
"""The partition stores the image's boot assets."""

SYSTEM_SEED = "system-seed"
"""The partition stores the image's initial seed data used during first boot."""


class StructureItem(CraftBaseModel):
"""Structure item of the image."""
Expand Down
8 changes: 7 additions & 1 deletion imagecraft/plugins/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@
from craft_parts.plugins.plugins import PluginType

from .mmdebstrap_plugin import MmdebstrapPlugin
from .snap_preseed_plugin import SnapPreseedPlugin
from .uc_prepare_plugin import UcPreparePlugin


def get_app_plugins() -> dict[str, PluginType]:
"""Get Imagecraft-specific craft-parts plugins.

:returns: A dict mapping plugin names to plugins
"""
return {"mmdebstrap": MmdebstrapPlugin}
return {
"mmdebstrap": MmdebstrapPlugin,
"snap-preseed": SnapPreseedPlugin,
"uc-prepare": UcPreparePlugin,
}


def setup_plugins() -> None:
Expand Down
93 changes: 93 additions & 0 deletions imagecraft/plugins/snap_preseed_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# This file is part of imagecraft.
#
# Copyright 2026 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

"""The snap-preseed plugin."""

import shlex
from typing import Literal, cast

from craft_parts.plugins import Plugin, PluginProperties
from typing_extensions import override


class SnapPreseedPluginProperties(PluginProperties, frozen=True):
"""Properties for the 'snap-preseed' plugin."""

plugin: Literal["snap-preseed"] = "snap-preseed"

snap_preseed_snaps: list[str]
snap_preseed_channel: str | None = None
snap_preseed_model_assert: str = ""
snap_preseed_validation: Literal["ignore", "enforce"] = "ignore"
snap_preseed_assertions: list[str] = []
snap_preseed_revisions: str | None = None


class SnapPreseedPlugin(Plugin):
"""Prepare snaps for Ubuntu Classic images using 'snap prepare-image'."""

properties_class = SnapPreseedPluginProperties

@override
def get_build_snaps(self) -> set[str]:
"""Return a set of required snaps to install in the build environment."""
return set()

@override
def get_build_packages(self) -> set[str]:
"""Return a set of required packages to install in the build environment."""
return set()

@override
def get_build_environment(self) -> dict[str, str]:
"""Return a dictionary with the environment to use in the build step."""
return {}

@override
def get_build_commands(self) -> list[str]:
"""Return a list of commands to run during the build step."""
options = cast(SnapPreseedPluginProperties, self._options)
cmd = [
"snap",
"prepare-image",
"--classic",
f"--arch={self._part_info.target_arch}",
f"--validation={options.snap_preseed_validation}",
]
if options.snap_preseed_channel:
cmd.append(f"--channel={options.snap_preseed_channel}")

if options.snap_preseed_revisions:
cmd.append(f"--revisions={options.snap_preseed_revisions}")

cmd.extend(
f"--assert={assertion}" for assertion in options.snap_preseed_assertions
)

cmd.extend(
f"--snap={self._resolve_snap(snap)}" for snap in options.snap_preseed_snaps
)

cmd.append(options.snap_preseed_model_assert)
cmd.append(str(self._part_info.part_install_dir))
return [shlex.join(cmd)]

def _resolve_snap(self, snap: str) -> str:
snap = snap.strip()
if "/" in snap and not snap.endswith(".snap"):
name, channel = snap.split("/", 1)
snap = f"{name}={channel}"
return snap
135 changes: 135 additions & 0 deletions imagecraft/plugins/uc_prepare_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# This file is part of imagecraft.
#
# Copyright 2026 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

"""The uc-prepare plugin."""

from typing import Literal, cast

from craft_parts.plugins import Plugin, PluginProperties
from pydantic import model_validator
from typing_extensions import Self, override


class UcPreparePluginProperties(PluginProperties, frozen=True):
"""Properties for the uc-prepare plugin."""

plugin: Literal["uc-prepare"] = "uc-prepare"

uc_prepare_model_assert: str
uc_prepare_snaps: list[str] = []
uc_prepare_channel: str | None = None
uc_prepare_validation: Literal["ignore", "enforce"] = "ignore"
uc_prepare_assertions: list[str] = []
uc_prepare_revisions: str | None = None
uc_prepare_preseed: bool = False
uc_prepare_preseed_sign_key: str | None = None
uc_prepare_apparmor_features_dir: str | None = None
uc_prepare_sysfs_overlay: str | None = None

@model_validator(mode="after")
def sign_key_requires_preseed(self) -> Self:
"""preseed-sign-key requires preseed to be enabled."""
if self.uc_prepare_preseed_sign_key and not self.uc_prepare_preseed:
raise ValueError(
"uc-prepare-preseed-sign-key cannot be used without uc-prepare-preseed"
)
return self

@model_validator(mode="after")
def sysfs_overlay_requires_preseed(self) -> Self:
"""sysfs-overlay requires preseed to be enabled."""
if self.uc_prepare_sysfs_overlay and not self.uc_prepare_preseed:
raise ValueError(
"uc-prepare-sysfs-overlay cannot be used without uc-prepare-preseed"
)
return self


class UcPreparePlugin(Plugin):
"""Prepare snaps for Ubuntu Core using 'snap prepare-image'."""

properties_class = UcPreparePluginProperties

@override
def get_build_snaps(self) -> set[str]:
"""Return a set of required snaps to install in the build environment."""
return set()

@override
def get_build_packages(self) -> set[str]:
"""Return a set of required packages to install in the build environment."""
options = cast(UcPreparePluginProperties, self._options)
if (
options.uc_prepare_preseed
and self._part_info.host_arch != self._part_info.target_arch
):
return {"qemu-user-static"}
return set()

@override
def get_build_environment(self) -> dict[str, str]:
"""Return a dictionary with the environment to use in the build step."""
return {}

@override
def get_build_commands(self) -> list[str]:
"""Return a list of commands to run during the build step."""
options = cast(UcPreparePluginProperties, self._options)

cmd = ["snap", "prepare-image"]

if options.uc_prepare_preseed:
cmd.append("--preseed")

if options.uc_prepare_preseed_sign_key:
cmd.append(f"--preseed-sign-key={options.uc_prepare_preseed_sign_key}")

if options.uc_prepare_apparmor_features_dir:
cmd.append(
f"--apparmor-features-dir={options.uc_prepare_apparmor_features_dir}"
)

if options.uc_prepare_sysfs_overlay:
cmd.append(f"--sysfs-overlay={options.uc_prepare_sysfs_overlay}")

cmd.append(f"--validation={options.uc_prepare_validation}")

if options.uc_prepare_channel:
cmd.append(f"--channel={options.uc_prepare_channel}")

if options.uc_prepare_revisions:
cmd.append(f"--revisions={options.uc_prepare_revisions}")

cmd.extend(
f"--assert={assertion}" for assertion in options.uc_prepare_assertions
)

cmd.extend(
f"--snap={self._resolve_snap(snap)}" for snap in options.uc_prepare_snaps
)

cmd.append(
f"{options.uc_prepare_model_assert} {self._part_info.part_install_dir}"
)

return [" ".join(cmd)]

def _resolve_snap(self, snap: str) -> str:
snap = snap.strip()
if "/" in snap and not snap.endswith(".snap"):
name, channel = snap.split("/", 1)
snap = f"{name}={channel}"
return snap
33 changes: 33 additions & 0 deletions tests/spread/plugins/snap-preseed/imagecraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: snap-preseed-test
version: "0.1"
summary: Test snap-preseed plugin
description: Test snap-preseed plugin
base: bare
build-base: ubuntu@24.04

platforms:
amd64:

volumes:
disk:
schema: gpt
structure:
- name: rootfs
role: system-data
type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
filesystem: ext4
filesystem-label: writable
size: 2G

filesystems:
default:
- mount: /
device: (volume/disk/rootfs)

parts:
snaps:
plugin: snap-preseed
snap-preseed-snaps:
- hello-world/latest/stable
organize:
"var/*": (overlay)/var/
15 changes: 15 additions & 0 deletions tests/spread/plugins/snap-preseed/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
summary: Test snap-preseed plugin

systems: [ubuntu-24.04-64]

execute: |
imagecraft pack --verbose --destructive-mode

test -f disk.img

test -d prime/var/lib/snapd/seed/
test -f prime/var/lib/snapd/seed/snaps/hello-world_*.snap

restore: |
imagecraft clean --destructive-mode
rm -rf disk.img || true
36 changes: 36 additions & 0 deletions tests/spread/plugins/uc-prepare/imagecraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: uc-prepare-test
version: "1.0"
summary: Test uc-prepare plugin
description: Test uc-prepare plugin
base: bare
build-base: ubuntu@24.04

platforms:
amd64:

filesystems:
default:
- mount: /
device: (volume/disk/ubuntu-seed)

parts:
uc-seed:
plugin: uc-prepare
source: .
uc-prepare-model-assert: model.assert
organize:
"system-seed/*": (volume/disk/ubuntu-seed)/
prime:
- -kernel
- -gadget
- -resolved-content

volumes:
disk:
schema: gpt
structure:
- name: ubuntu-seed
role: system-seed
type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
filesystem: vfat
size: 1500M
48 changes: 48 additions & 0 deletions tests/spread/plugins/uc-prepare/model.assert
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
type: model
authority-id: canonical
series: 16
brand-id: canonical
model: ubuntu-core-24-amd64
architecture: amd64
base: core24
grade: signed
snaps:
-
default-channel: 24/stable
id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH
name: pc
type: gadget
-
default-channel: 24/stable
id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza
name: pc-kernel
type: kernel
-
default-channel: latest/stable
id: dwTAh7MZZ01zyriOZErqd1JynQLiOGvM
name: core24
type: base
-
default-channel: latest/stable
id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
name: snapd
type: snapd
-
default-channel: 24/stable
id: ASctKBEHzVt3f1pbZLoekCvcigRjtuqw
name: console-conf
presence: optional
type: app
timestamp: 2024-04-19T08:42:32+00:00
sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn

AcLBXAQAAQoABgUCZiLKhgAKCRDgT5vottzAEmmwD/9xNRSiN7yoPMsERG2aSOLr+2ZTJddBhdj3
bEuS6ifCb1K/t7CpNhmJt1iMasdcUl7q1DJCUr/uunLgoVKODxE0Vx4k3UCtoEH2v2Mj6oqjPdAh
d90gsEBP0nVhf+LjoS34zF3O7ScrJU69mjUPhcDx0mgOslSY5aHbFTIKsT9InJ3tKgg41+RFxvwZ
V1Ksf4ITkuy9ap7CjqhczFyNMSMn0cmF9OKK39jvmtfpXRVmjTDCoF0mLXmJ+RDyKFbjMuQU1O3S
opv6gJPE//hWj9Hn5S3DIVgjcpWWOqGk+wREFGgMs/g9aT5DoLMp/FvcMVPH8sZNLiZF3wrVJCjZ
Tm+fD3SGBlht5GJGKWL5eAlGRVOUNh0cmX1WTYuZKw+OOnNuQyPykfkEFE2OKwSQwXP+ZICuGIcX
KlO56GB8OLs8hdQQrOZH8ysO5qM3Ee0Iq/keJiQZIYIv63ODLn4cPkTa2Es33alVf59xiFvUFAfd
2hWOAFDbxSJSETNmCReSMNXR01U8YTKpQMB9CA5tlX7RlrHCeLrPld8ytSCLUf29ofsJE3yavgTK
2wt5eeX6M5okmq2TR78iH6xbX3qeK8J6Ld1ElhGIikivnXMPyA5BApdQa6aoHtHVx9jHRlgWNOZs
X5uwovE8pMriAGI4l+AWYFG6SeVyY90qqlAmzaybsw==
Loading