11import dataclasses
22import os
3- import uuid
43from typing import Optional , cast
54
65import tmt
1514from tmt .utils .templates import render_template
1615
1716DEFAULT_IMAGE_BUILDER = "quay.io/centos-bootc/bootc-image-builder:latest"
18- CONTAINER_STORAGE_DIR = "/var/lib/containers/storage"
17+ CONTAINER_STORAGE_DIR = tmt . utils . Path ( "/var/lib/containers/storage" )
1918
19+ PODMAN_MACHINE_NAME = 'podman-machine-tmt'
20+ PODMAN_ENV = tmt .utils .Environment .from_dict ({"CONTAINER_CONNECTION" : f'{ PODMAN_MACHINE_NAME } -root' })
21+ PODMAN_MACHINE_CPU = os .getenv ('TMT_BOOTC_PODMAN_MACHINE_CPU' , '2' )
22+ PODMAN_MACHINE_MEM = os .getenv ('TMT_BOOTC_PODMAN_MACHINE_MEM' , '2048' )
23+ PODMAN_MACHINE_DISK_SIZE = os .getenv ('TMT_BOOTC_PODMAN_MACHINE_DISK_SIZE' , '50' )
2024
2125class GuestBootc (GuestTestcloud ):
2226 containerimage : str
27+ _rootless : bool
2328
2429 def __init__ (self ,
2530 * ,
2631 data : tmt .steps .provision .GuestData ,
2732 name : Optional [str ] = None ,
2833 parent : Optional [tmt .utils .Common ] = None ,
2934 logger : tmt .log .Logger ,
30- containerimage : Optional [str ]) -> None :
35+ containerimage : str ,
36+ rootless : bool ) -> None :
3137 super ().__init__ (data = data , logger = logger , parent = parent , name = name )
32-
33- if containerimage :
34- self .containerimage = containerimage
38+ self .containerimage = containerimage
39+ self ._rootless = rootless
3540
3641 def remove (self ) -> None :
3742 tmt .utils .Command (
3843 "podman" , "rmi" , self .containerimage
39- ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
44+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger , env = PODMAN_ENV if self ._rootless else None )
45+
46+ try :
47+ tmt .utils .Command (
48+ "podman" , "machine" , "rm" , "-f" , PODMAN_MACHINE_NAME
49+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
50+ except Exception :
51+ self ._logger .debug ("Unable to remove podman machine it might not exist" )
4052
4153 super ().remove ()
4254
@@ -129,21 +141,20 @@ class ProvisionBootc(tmt.steps.provision.ProvisionPlugin[BootcData]):
129141 bootc disk image from the container image, then uses the virtual.testcloud
130142 plugin to create a virtual machine using the bootc disk image.
131143
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:
144+ The bootc disk creation requires running podman as root. The plugin will
145+ automatically check if the current podman connection is rootless. If it is,
146+ a podman machine will be spun up and used to build the bootc disk. The
147+ podman machine can be configured with the following environment variables :
136148
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
149+ TMT_BOOTC_PODMAN_MACHINE_CPU='2'
150+ TMT_BOOTC_PODMAN_MACHINE_MEM='2048'
151+ TMT_BOOTC_PODMAN_MACHINE_DISK_SIZE='50'
141152 """
142153
143154 _data_class = BootcData
144155 _guest_class = GuestTestcloud
145156 _guest = None
146- _id = str ( uuid . uuid4 ())[: 8 ]
157+ _rootless = True
147158
148159 def _get_id (self ) -> str :
149160 # FIXME: cast() - https://github.com/teemtee/tmt/issues/1372
@@ -161,31 +172,47 @@ def _expand_path(self, relative_path: str) -> str:
161172
162173 def _build_derived_image (self , base_image : str ) -> str :
163174 """ 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" )
175+ assert self .workdir is not None # narrow type
176+
177+ simple_http_start_guest = \
178+ """
179+ python3 -m http.server {0} || python -m http.server {0} ||
180+ /usr/libexec/platform-python -m http.server {0} || python2 -m SimpleHTTPServer {0} || python -m SimpleHTTPServer {0}
181+ """ .format (10022 ).replace ('\n ' , ' ' )
167182
168183 self ._logger .debug ("Building modified container image with necessary tmt packages/config" )
169184 containerfile_template = '''
170185 FROM {{ base_image }}
171186
172- RUN \
173- dnf -y install cloud-init rsync && \
187+ RUN dnf -y install cloud-init rsync && \
174188 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
189+ touch /etc/environment && \
190+ echo "export PATH=$PATH:/var/lib/tmt/scripts" >> /etc/environment && \
191+ dnf clean all && \
192+ echo "{{ testcloud_guest }}" >> /opt/testcloud-guest.sh && \
193+ chmod +x /opt/testcloud-guest.sh && \
194+ echo "[Unit]" >> /etc/systemd/system/testcloud.service && \
195+ echo "Description=Testcloud guest integration" >> /etc/systemd/system/testcloud.service && \
196+ echo "After=cloud-init.service" >> /etc/systemd/system/testcloud.service && \
197+ echo "[Service]" >> /etc/systemd/system/testcloud.service && \
198+ echo "ExecStart=/bin/bash /opt/testcloud-guest.sh" >> /etc/systemd/system/testcloud.service && \
199+ echo "[Install]" >> /etc/systemd/system/testcloud.service && \
200+ echo "WantedBy=multi-user.target" >> /etc/systemd/system/testcloud.service && \
201+ systemctl enable testcloud.service
177202 '''
203+
178204 containerfile_parsed = render_template (
179205 containerfile_template ,
180- base_image = base_image )
206+ base_image = base_image ,
207+ testcloud_guest = simple_http_start_guest )
181208 (self .workdir / 'Containerfile' ).write_text (containerfile_parsed )
182209
183210 image_tag = f'localhost/tmtmodified-{ self ._get_id ()} '
184211 tmt .utils .Command (
185212 "podman" , "build" , f'{ self .workdir } ' ,
186213 "-f" , f'{ self .workdir } /Containerfile' ,
187214 "-t" , image_tag
188- ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
215+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger , env = PODMAN_ENV if self . _rootless else None )
189216
190217 return image_tag
191218
@@ -197,12 +224,13 @@ def _build_base_image(self, containerfile: str, workdir: str) -> str:
197224 "podman" , "build" , self ._expand_path (workdir ),
198225 "-f" , self ._expand_path (containerfile ),
199226 "-t" , image_tag
200- ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
227+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger , env = PODMAN_ENV if self . _rootless else None )
201228 return image_tag
202229
203230 def _build_bootc_disk (self , containerimage : str , image_builder : str ) -> None :
204231 """ Build the bootc disk from a container image using bootc image builder """
205232 self ._logger .debug ("Building bootc disk image" )
233+
206234 tmt .utils .Command (
207235 "podman" , "run" , "--rm" , "--privileged" ,
208236 "-v" , f'{ CONTAINER_STORAGE_DIR } :{ CONTAINER_STORAGE_DIR } ' ,
@@ -211,16 +239,51 @@ def _build_bootc_disk(self, containerimage: str, image_builder: str) -> None:
211239 image_builder , "build" ,
212240 "--type" , "qcow2" ,
213241 "--local" , containerimage
242+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger , env = PODMAN_ENV if self ._rootless else None )
243+
244+ def _init_podman_machine (self ) -> None :
245+ try :
246+ tmt .utils .Command (
247+ "podman" , "machine" , "rm" , "-f" , PODMAN_MACHINE_NAME
248+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
249+ except Exception :
250+ self ._logger .debug ("Unable to remove existing podman machine (it might not exist)" )
251+
252+ self ._logger .debug ("Initializing podman machine" )
253+ tmt .utils .Command (
254+ "podman" , "machine" , "init" , "--rootful" ,
255+ "--disk-size" , PODMAN_MACHINE_DISK_SIZE ,
256+ "--memory" , PODMAN_MACHINE_MEM ,
257+ "--cpus" , PODMAN_MACHINE_CPU ,
258+ "-v" , "/var/tmp/tmt:/var/tmp/tmt" ,
259+ "-v" , "$HOME:$HOME" ,
260+ PODMAN_MACHINE_NAME
214261 ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
215262
263+ self ._logger .debug ("Starting podman machine" )
264+ tmt .utils .Command (
265+ "podman" , "machine" , "start" , PODMAN_MACHINE_NAME
266+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
267+
268+ def _check_if_podman_is_rootless (self ) -> None :
269+ output = tmt .utils .Command (
270+ "podman" , "info" , "--format" , "{{.Host.Security.Rootless}}"
271+ ).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
272+ self ._rootless = output .stdout == "true\n "
273+
216274 def go (self , * , logger : Optional [tmt .log .Logger ] = None ) -> None :
217275 """ Provision the bootc instance """
218276 super ().go (logger = logger )
219277
278+ self ._check_if_podman_is_rootless ()
279+
220280 data = BootcData .from_plugin (self )
221281 data .image = f"file://{ self .workdir } /qcow2/disk.qcow2"
222282 data .show (verbose = self .verbosity_level , logger = self ._logger )
223283
284+ if self ._rootless :
285+ self ._init_podman_machine ()
286+
224287 if data .containerimage is not None :
225288 containerimage = data .containerimage
226289 if data .add_deps :
@@ -240,7 +303,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
240303 data = data ,
241304 name = self .name ,
242305 parent = self .step ,
243- containerimage = containerimage )
306+ containerimage = containerimage ,
307+ rootless = self ._rootless )
244308 self ._guest .start ()
245309 self ._guest .setup ()
246310
0 commit comments