Skip to content

Commit

Permalink
plugins: Add bootc provision plugin
Browse files Browse the repository at this point in the history
This creates a new provision plugin that is built on top of the existing
TestCloud (virtual) plugin. It adds new parameters to pass a
Containerfile or container image. The plugin will then build a container
image (if necessary) then build a bootc disk image from the container
image using bootc image builder. Currently, bootc requires podman to be
run as root when building a disk image. This is typically handled by
running a podman machine as root.

An additional parameter "add-deps" toggles building a derived container
image with the tmt test requirements.

Signed-off-by: Chris Kyrouac <[email protected]>
  • Loading branch information
ckyrouac committed Aug 22, 2024
1 parent 54dfd29 commit 78e86f3
Showing 1 changed file with 169 additions and 0 deletions.
169 changes: 169 additions & 0 deletions tmt/steps/provision/bootc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import dataclasses
from string import Template
from typing import Optional

import tmt
import tmt.log
import tmt.steps
import tmt.steps.provision
import tmt.steps.provision.testcloud
import tmt.utils
from tmt.steps.provision.testcloud import GuestTestcloud
from tmt.utils import field

DEFAULT_IMAGE_BUILDER = "quay.io/centos-bootc/bootc-image-builder:latest"


@dataclasses.dataclass
class BootcData(tmt.steps.provision.testcloud.ProvisionTestcloudData):
containerfile: Optional[str] = field(
default=None,
option=('--containerfile'),
metavar='CONTAINERFILE',
help="""
Select container file to be used to build a container image
that is then used by bootc image builder to create a disk image.
Cannot be used with containerimage.
""")

containerfile_workdir: str = field(
default=".",
option=('--containerfile-workdir'),
metavar='CONTAINERFILE_WORKDIR',
help="""
Select working directory for the podman build invocation.
""")

containerimage: Optional[str] = field(
default=None,
option=('--containerimage'),
metavar='CONTAINERIMAGE',
help="""
Select container image to be used to build a bootc disk.
This takes priority over containerfile.
""")

add_deps: bool = field(
default=True,
is_flag=True,
option=('--add-deps'),
help="""
Add tmt dependencies to the supplied container image or image built
from the supplied Containerfile.
This will cause a derived image to be built from the supplied image.
""")

image_builder: str = field(
default=DEFAULT_IMAGE_BUILDER,
option=('--image-builder'),
metavar='IMAGEBUILDER',
help="""
The full repo:tag url of the bootc image builder image to use for
building the bootc disk image.
""")

def set_image(self, image: str) -> None:
self.image = image


@tmt.steps.provides_method('bootc')
class ProvisionBootc(tmt.steps.provision.ProvisionPlugin[BootcData]):
"""
Provision guest using bootc
Minimal configuration using the latest nothing image:
.. code-block:: yaml
provision:
how: bootc
containerfile: file://Containerfile
"""

_data_class = BootcData
_guest_class = GuestTestcloud
_guest = None

# build a "modified" container image from the base image with tmt dependencies added
def _build_derived_image(self, base_image: str) -> str:
self._logger.debug("Building modified container image with necessary tmt packages/config")
containerfile_template = Template('''
FROM $base_image
RUN \
dnf -y install cloud-init rsync && \
ln -s ../cloud-init.target /usr/lib/systemd/system/default.target.wants && \
rm /usr/local -rf && ln -sr /var/usrlocal /usr/local && mkdir -p /var/usrlocal/bin && \
dnf clean all
''')
containerfile = containerfile_template.substitute({"base_image": base_image})
with open(f'{self.workdir}/Containerfile', 'w') as file:
file.write(containerfile)

image_tag = "localhost/tmtmodified" # TODO: unique ID
tmt.utils.Command(
"podman", "build", f'{self.workdir}',
"-f", f'{self.workdir}/Containerfile',
"-t", image_tag
).run(cwd=self.workdir, stream_output=True, logger=self._logger)

return image_tag

# build the "base" or user supplied container image
def _build_base_image(self, containerfile: str, workdir: str) -> str:
image_tag = "localhost/tmtbase" # TODO: how do I get a unique ID here?
self._logger.debug("Building container image")
tmt.utils.Command(
"podman", "build", workdir,
"-f", containerfile,
"-t", image_tag
).run(cwd=self.workdir, stream_output=True, logger=self._logger)
return image_tag

# build the bootc disk
def _build_bootc_disk(self, containerimage: str, image_builder: str) -> None:
self._logger.debug("Building bootc disk image")
tmt.utils.Command(
"podman", "run", "--rm", "--privileged",
"-v", "/var/lib/containers/storage:/var/lib/containers/storage",
"--security-opt", "label=type:unconfined_t",
"-v", f"{self.workdir}:/output",
image_builder, "build",
"--type", "qcow2",
"--local", containerimage
).run(cwd=self.workdir, stream_output=True, logger=self._logger)

def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
""" Provision the bootc instance """
super().go(logger=logger)

data = BootcData.from_plugin(self)
data.set_image(f"file://{self.workdir}/qcow2/disk.qcow2")
data.show(verbose=self.verbosity_level, logger=self._logger)

if data.containerimage is not None:
containerimage = data.containerimage
if data.add_deps:
containerimage = self._build_derived_image(data.containerimage)
self._build_bootc_disk(containerimage, data.image_builder)
elif data.containerfile is not None:
containerimage = self._build_base_image(data.containerfile, data.containerfile_workdir)
if data.add_deps:
containerimage = self._build_derived_image(containerimage)
self._build_bootc_disk(containerimage, data.image_builder)
else:
self._logger.fail("Either containerfile or containerimage must be specified.")
raise SystemExit(1)

self._guest = GuestTestcloud(
logger=self._logger,
data=data,
name=self.name,
parent=self.step)
self._guest.start()
self._guest.setup()

def guest(self) -> Optional[tmt.steps.provision.Guest]:
return self._guest

0 comments on commit 78e86f3

Please sign in to comment.