1
1
import dataclasses
2
2
import os
3
- import uuid
4
3
from typing import Optional , cast
5
4
6
5
import tmt
15
14
from tmt .utils .templates import render_template
16
15
17
16
DEFAULT_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" )
19
18
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' )
20
24
21
25
class GuestBootc (GuestTestcloud ):
22
26
containerimage : str
27
+ _rootless : bool
23
28
24
29
def __init__ (self ,
25
30
* ,
26
31
data : tmt .steps .provision .GuestData ,
27
32
name : Optional [str ] = None ,
28
33
parent : Optional [tmt .utils .Common ] = None ,
29
34
logger : tmt .log .Logger ,
30
- containerimage : Optional [str ]) -> None :
35
+ containerimage : str ,
36
+ rootless : bool ) -> None :
31
37
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
35
40
36
41
def remove (self ) -> None :
37
42
tmt .utils .Command (
38
43
"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" )
40
52
41
53
super ().remove ()
42
54
@@ -129,21 +141,20 @@ class ProvisionBootc(tmt.steps.provision.ProvisionPlugin[BootcData]):
129
141
bootc disk image from the container image, then uses the virtual.testcloud
130
142
plugin to create a virtual machine using the bootc disk image.
131
143
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 :
136
148
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'
141
152
"""
142
153
143
154
_data_class = BootcData
144
155
_guest_class = GuestTestcloud
145
156
_guest = None
146
- _id = str ( uuid . uuid4 ())[: 8 ]
157
+ _rootless = True
147
158
148
159
def _get_id (self ) -> str :
149
160
# FIXME: cast() - https://github.com/teemtee/tmt/issues/1372
@@ -161,31 +172,47 @@ def _expand_path(self, relative_path: str) -> str:
161
172
162
173
def _build_derived_image (self , base_image : str ) -> str :
163
174
""" 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 ' , ' ' )
167
182
168
183
self ._logger .debug ("Building modified container image with necessary tmt packages/config" )
169
184
containerfile_template = '''
170
185
FROM {{ base_image }}
171
186
172
- RUN \
173
- dnf -y install cloud-init rsync && \
187
+ RUN dnf -y install cloud-init rsync && \
174
188
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
177
202
'''
203
+
178
204
containerfile_parsed = render_template (
179
205
containerfile_template ,
180
- base_image = base_image )
206
+ base_image = base_image ,
207
+ testcloud_guest = simple_http_start_guest )
181
208
(self .workdir / 'Containerfile' ).write_text (containerfile_parsed )
182
209
183
210
image_tag = f'localhost/tmtmodified-{ self ._get_id ()} '
184
211
tmt .utils .Command (
185
212
"podman" , "build" , f'{ self .workdir } ' ,
186
213
"-f" , f'{ self .workdir } /Containerfile' ,
187
214
"-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 )
189
216
190
217
return image_tag
191
218
@@ -197,12 +224,13 @@ def _build_base_image(self, containerfile: str, workdir: str) -> str:
197
224
"podman" , "build" , self ._expand_path (workdir ),
198
225
"-f" , self ._expand_path (containerfile ),
199
226
"-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 )
201
228
return image_tag
202
229
203
230
def _build_bootc_disk (self , containerimage : str , image_builder : str ) -> None :
204
231
""" Build the bootc disk from a container image using bootc image builder """
205
232
self ._logger .debug ("Building bootc disk image" )
233
+
206
234
tmt .utils .Command (
207
235
"podman" , "run" , "--rm" , "--privileged" ,
208
236
"-v" , f'{ CONTAINER_STORAGE_DIR } :{ CONTAINER_STORAGE_DIR } ' ,
@@ -211,16 +239,51 @@ def _build_bootc_disk(self, containerimage: str, image_builder: str) -> None:
211
239
image_builder , "build" ,
212
240
"--type" , "qcow2" ,
213
241
"--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
214
261
).run (cwd = self .workdir , stream_output = True , logger = self ._logger )
215
262
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
+
216
274
def go (self , * , logger : Optional [tmt .log .Logger ] = None ) -> None :
217
275
""" Provision the bootc instance """
218
276
super ().go (logger = logger )
219
277
278
+ self ._check_if_podman_is_rootless ()
279
+
220
280
data = BootcData .from_plugin (self )
221
281
data .image = f"file://{ self .workdir } /qcow2/disk.qcow2"
222
282
data .show (verbose = self .verbosity_level , logger = self ._logger )
223
283
284
+ if self ._rootless :
285
+ self ._init_podman_machine ()
286
+
224
287
if data .containerimage is not None :
225
288
containerimage = data .containerimage
226
289
if data .add_deps :
@@ -240,7 +303,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:
240
303
data = data ,
241
304
name = self .name ,
242
305
parent = self .step ,
243
- containerimage = containerimage )
306
+ containerimage = containerimage ,
307
+ rootless = self ._rootless )
244
308
self ._guest .start ()
245
309
self ._guest .setup ()
246
310
0 commit comments