Skip to content

Commit 7856ce0

Browse files
authored
Merge pull request #789 from ckyrouac/more-lbi-tests
tests: Add bootc provision tmt plugin
2 parents 830bafe + 9d7d704 commit 7856ce0

File tree

4 files changed

+1054
-0
lines changed

4 files changed

+1054
-0
lines changed

tests/plugins/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

tests/plugins/bootc-install.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import dataclasses
2+
import os
3+
import uuid
4+
from typing import Optional, cast
5+
6+
import tmt
7+
import tmt.base
8+
import tmt.log
9+
import tmt.steps
10+
import tmt.steps.provision
11+
import tmt.steps.provision.testcloud
12+
import tmt.utils
13+
from tmt.steps.provision.testcloud import GuestTestcloud
14+
from tmt.utils import field
15+
from tmt.utils.templates import render_template
16+
17+
DEFAULT_IMAGE_BUILDER = "quay.io/centos-bootc/bootc-image-builder:latest"
18+
CONTAINER_STORAGE_DIR = "/var/lib/containers/storage"
19+
20+
21+
class GuestBootc(GuestTestcloud):
22+
containerimage: str
23+
24+
def __init__(self,
25+
*,
26+
data: tmt.steps.provision.GuestData,
27+
name: Optional[str] = None,
28+
parent: Optional[tmt.utils.Common] = None,
29+
logger: tmt.log.Logger,
30+
containerimage: Optional[str]) -> None:
31+
super().__init__(data=data, logger=logger, parent=parent, name=name)
32+
33+
if containerimage:
34+
self.containerimage = containerimage
35+
36+
def remove(self) -> None:
37+
tmt.utils.Command(
38+
"podman", "rmi", self.containerimage
39+
).run(cwd=self.workdir, stream_output=True, logger=self._logger)
40+
41+
super().remove()
42+
43+
44+
@dataclasses.dataclass
45+
class BootcData(tmt.steps.provision.testcloud.ProvisionTestcloudData):
46+
containerfile: Optional[str] = field(
47+
default=None,
48+
option='--containerfile',
49+
metavar='CONTAINERFILE',
50+
help="""
51+
Select container file to be used to build a container image
52+
that is then used by bootc image builder to create a disk image.
53+
54+
Cannot be used with containerimage.
55+
""")
56+
57+
containerfile_workdir: str = field(
58+
default=".",
59+
option=('--containerfile-workdir'),
60+
metavar='CONTAINERFILE_WORKDIR',
61+
help="""
62+
Select working directory for the podman build invocation.
63+
""")
64+
65+
containerimage: Optional[str] = field(
66+
default=None,
67+
option=('--containerimage'),
68+
metavar='CONTAINERIMAGE',
69+
help="""
70+
Select container image to be used to build a bootc disk.
71+
This takes priority over containerfile.
72+
""")
73+
74+
add_deps: bool = field(
75+
default=True,
76+
is_flag=True,
77+
option=('--add-deps'),
78+
help="""
79+
Add tmt dependencies to the supplied container image or image built
80+
from the supplied Containerfile.
81+
This will cause a derived image to be built from the supplied image.
82+
""")
83+
84+
image_builder: str = field(
85+
default=DEFAULT_IMAGE_BUILDER,
86+
option=('--image-builder'),
87+
metavar='IMAGEBUILDER',
88+
help="""
89+
The full repo:tag url of the bootc image builder image to use for
90+
building the bootc disk image.
91+
""")
92+
93+
94+
@tmt.steps.provides_method('bootc')
95+
class ProvisionBootc(tmt.steps.provision.ProvisionPlugin[BootcData]):
96+
"""
97+
Provision a local virtual machine using a bootc container image
98+
99+
Minimal config which uses the Fedora bootc image:
100+
101+
.. code-block:: yaml
102+
103+
provision:
104+
how: bootc
105+
containerimage: quay.io/fedora/fedora-bootc:40
106+
107+
Here's a config example using a containerfile:
108+
109+
.. code-block:: yaml
110+
111+
provision:
112+
how: bootc
113+
containerfile: "./my-custom-image.containerfile"
114+
containerfile-workdir: .
115+
image_builder: quay.io/centos-bootc/bootc-image-builder:stream9
116+
disk: 100
117+
118+
Another config example using an image that includes tmt dependencies:
119+
120+
.. code-block:: yaml
121+
122+
provision:
123+
how: bootc
124+
add_deps: false
125+
containerimage: localhost/my-image-with-deps
126+
127+
This plugin is an extension of the virtual.testcloud plugin.
128+
Essentially, it takes a container image as input, builds a
129+
bootc disk image from the container image, then uses the virtual.testcloud
130+
plugin to create a virtual machine using the bootc disk image.
131+
132+
The bootc disk creation requires running podman as root, this is typically
133+
done by running the command in a rootful podman-machine. The podman-machine
134+
also needs access to ``/var/tmp/tmt``. An example command to initialize the
135+
machine:
136+
137+
.. code-block:: shell
138+
139+
podman machine init --rootful --disk-size 200 --memory 8192 \
140+
--cpus 8 -v /var/tmp/tmt:/var/tmp/tmt -v $HOME:$HOME
141+
"""
142+
143+
_data_class = BootcData
144+
_guest_class = GuestTestcloud
145+
_guest = None
146+
_id = str(uuid.uuid4())[:8]
147+
148+
def _get_id(self) -> str:
149+
# FIXME: cast() - https://github.com/teemtee/tmt/issues/1372
150+
parent = cast(tmt.steps.provision.Provision, self.parent)
151+
assert parent.plan is not None
152+
assert parent.plan.my_run is not None
153+
assert parent.plan.my_run.unique_id is not None
154+
return parent.plan.my_run.unique_id
155+
156+
def _expand_path(self, relative_path: str) -> str:
157+
""" Expand the path to the full path relative to the current working dir """
158+
if relative_path.startswith("/"):
159+
return relative_path
160+
return f"{os.getcwd()}/{relative_path}"
161+
162+
def _build_derived_image(self, base_image: str) -> str:
163+
""" Build a "derived" container image from the base image with tmt dependencies added """
164+
if not self.workdir:
165+
raise tmt.utils.ProvisionError(
166+
"self.workdir must be defined")
167+
168+
self._logger.debug("Building modified container image with necessary tmt packages/config")
169+
containerfile_template = '''
170+
FROM {{ base_image }}
171+
172+
RUN \
173+
dnf -y install cloud-init rsync && \
174+
ln -s ../cloud-init.target /usr/lib/systemd/system/default.target.wants && \
175+
rm /usr/local -rf && ln -sr /var/usrlocal /usr/local && mkdir -p /var/usrlocal/bin && \
176+
dnf clean all
177+
'''
178+
containerfile_parsed = render_template(
179+
containerfile_template,
180+
base_image=base_image)
181+
(self.workdir / 'Containerfile').write_text(containerfile_parsed)
182+
183+
image_tag = f'localhost/tmtmodified-{self._get_id()}'
184+
tmt.utils.Command(
185+
"podman", "build", f'{self.workdir}',
186+
"-f", f'{self.workdir}/Containerfile',
187+
"-t", image_tag
188+
).run(cwd=self.workdir, stream_output=True, logger=self._logger)
189+
190+
return image_tag
191+
192+
def _build_base_image(self, containerfile: str, workdir: str) -> str:
193+
""" Build the "base" or user supplied container image """
194+
image_tag = f'localhost/tmtbase-{self._get_id()}'
195+
self._logger.debug("Building container image")
196+
tmt.utils.Command(
197+
"podman", "build", self._expand_path(workdir),
198+
"-f", self._expand_path(containerfile),
199+
"-t", image_tag
200+
).run(cwd=self.workdir, stream_output=True, logger=self._logger)
201+
return image_tag
202+
203+
def _build_bootc_disk(self, containerimage: str, image_builder: str) -> None:
204+
""" Build the bootc disk from a container image using bootc image builder """
205+
self._logger.debug("Building bootc disk image")
206+
tmt.utils.Command(
207+
"podman", "run", "--rm", "--privileged",
208+
"-v", f'{CONTAINER_STORAGE_DIR}:{CONTAINER_STORAGE_DIR}',
209+
"--security-opt", "label=type:unconfined_t",
210+
"-v", f"{self.workdir}:/output",
211+
image_builder, "build",
212+
"--type", "qcow2",
213+
"--local", containerimage
214+
).run(cwd=self.workdir, stream_output=True, logger=self._logger)
215+
216+
def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
217+
""" Provision the bootc instance """
218+
super().go(logger=logger)
219+
220+
data = BootcData.from_plugin(self)
221+
data.image = f"file://{self.workdir}/qcow2/disk.qcow2"
222+
data.show(verbose=self.verbosity_level, logger=self._logger)
223+
224+
if data.containerimage is not None:
225+
containerimage = data.containerimage
226+
if data.add_deps:
227+
containerimage = self._build_derived_image(data.containerimage)
228+
self._build_bootc_disk(containerimage, data.image_builder)
229+
elif data.containerfile is not None:
230+
containerimage = self._build_base_image(data.containerfile, data.containerfile_workdir)
231+
if data.add_deps:
232+
containerimage = self._build_derived_image(containerimage)
233+
self._build_bootc_disk(containerimage, data.image_builder)
234+
else:
235+
raise tmt.utils.ProvisionError(
236+
"Either containerfile or containerimage must be specified.")
237+
238+
self._guest = GuestBootc(
239+
logger=self._logger,
240+
data=data,
241+
name=self.name,
242+
parent=self.step,
243+
containerimage=containerimage)
244+
self._guest.start()
245+
self._guest.setup()
246+
247+
def guest(self) -> Optional[tmt.steps.provision.Guest]:
248+
return self._guest

0 commit comments

Comments
 (0)