-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
1 changed file
with
169 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |