Skip to content

Commit fe343d3

Browse files
image-customize: port to test.thing
This is mostly boring. There is really only one nice cleanup enabled by everything being async: instead of repeating `timeout=1800` everywhere, we can just apply it at the toplevel with a single asyncio.wait_for() invocation. Even though it's not a rewrite, apply a bit of `ruff format` here.
1 parent fed532d commit fe343d3

File tree

1 file changed

+122
-100
lines changed

1 file changed

+122
-100
lines changed

image-customize

Lines changed: 122 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,27 @@
1717
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
1818

1919
import argparse
20+
import asyncio
2021
import os
2122
import subprocess
22-
import sys
2323
from collections.abc import Sequence
24+
from pathlib import Path
2425

25-
from lib.constants import BOTS_DIR, TEST_DIR
26-
from machine import testvm
26+
from lib import testthing
27+
from lib.constants import BOTS_DIR, IMAGES_DIR, MACHINE_DIR, TEST_DIR
28+
from lib.testmap import get_test_image
2729

2830
opt_quick: bool = False
2931
opt_verbose: bool = False
30-
opt_build_options: str = ''
32+
opt_build_options: str = ""
3133
stdout_disposition: int | None = None
3234

3335

3436
def prepare_install_image(base_image: str, install_image: str, resize: str | None, fresh: bool) -> str:
3537
"""Create the necessary layered image for the build/install"""
3638

3739
if "/" not in base_image:
38-
base_image = os.path.join(testvm.IMAGES_DIR, base_image)
40+
base_image = os.path.join(IMAGES_DIR, base_image)
3941
if "/" not in install_image:
4042
install_image = os.path.join(os.path.join(TEST_DIR, "images"), os.path.basename(install_image))
4143

@@ -54,8 +56,16 @@ def prepare_install_image(base_image: str, install_image: str, resize: str | Non
5456
install_image_dir = os.path.dirname(install_image)
5557
os.makedirs(install_image_dir, exist_ok=True)
5658
base_image = os.path.realpath(base_image)
57-
subprocess.check_call(["qemu-img", "create", "-q", "-f", "qcow2",
58-
"-o", f"backing_file={base_image},backing_fmt=qcow2", qcow2_image])
59+
subprocess.check_call([
60+
"qemu-img",
61+
"create",
62+
"-q",
63+
"-f",
64+
"qcow2",
65+
"-o",
66+
f"backing_file={base_image},backing_fmt=qcow2",
67+
qcow2_image,
68+
])
5969
if os.path.lexists(install_image):
6070
os.unlink(install_image)
6171
os.symlink(os.path.basename(qcow2_image), install_image)
@@ -70,76 +80,84 @@ class ActionBase(argparse.Action):
7080
"""Keep an ordered list of actions"""
7181

7282
@staticmethod
73-
def execute(machine_instance: testvm.Machine, argument: str) -> None:
83+
async def execute(machine_instance: testthing.VirtualMachine, argument: str, /) -> None:
7484
raise NotImplementedError
7585

7686
def __call__(
7787
self,
7888
parser: argparse.ArgumentParser,
7989
namespace: argparse.Namespace,
8090
value: str | Sequence[str] | None,
81-
option_string: object = None
91+
option_string: object = None,
8292
) -> None:
8393
getattr(namespace, self.dest).append((self.execute, value))
8494

8595

8696
class InstallAction(ActionBase):
8797
"""Install local rpm or distro package"""
98+
8899
@staticmethod
89-
def execute(machine_instance: testvm.Machine, package: str) -> None:
100+
async def execute(machine_instance: testthing.VirtualMachine, package: str) -> None:
90101
# If we have a '/' in the package name, or if a file with that name
91102
# exists in the current directory, then assume that this is a package
92103
# we're uploading from the host.
93-
if '/' in package or os.path.isfile(package):
104+
if "/" in package or os.path.isfile(package):
94105
dest = "/var/tmp/" + os.path.basename(package)
95-
machine_instance.upload([os.path.abspath(package)], dest)
106+
await machine_instance.scp(os.path.abspath(package), f"vm:{dest}")
96107
package = dest
97108

98109
# requesting install of Python wheel?
99-
if package.endswith('.whl'):
100-
machine_instance.execute(f"python3 -m pip install --no-index --prefix=/usr/local {package}", timeout=120)
110+
if package.endswith(".whl"):
111+
await asyncio.wait_for(
112+
machine_instance.execute(f"python3 -m pip install --no-index --prefix=/usr/local {package}"),
113+
timeout=120,
114+
)
101115
return
102116

103117
# this will fail if neither is available -- exception is clear enough, this is a developer tool
104-
out = machine_instance.execute("which dnf || which yum || which apt-get")
105-
if 'dnf' in out:
118+
out = await machine_instance.execute("which dnf || which yum || which apt-get || which pacman")
119+
if "dnf" in out:
106120
install_command = "dnf install -y"
107-
elif 'yum' in out:
121+
elif "yum" in out:
108122
install_command = "yum --setopt=skip_missing_names_on_install=False -y install"
109-
else:
123+
elif "apt-get" in out:
110124
install_command = "apt-get install -y"
125+
elif "pacman" in out:
126+
install_command = "pacman -S --noconfirm"
127+
else:
128+
raise NotImplementedError(f"unknown build platform: {out}")
111129

112-
machine_instance.execute(f"{install_command} {package}", timeout=1800)
130+
await machine_instance.execute(f"{install_command} {package}")
113131

114132

115133
class BuildAction(ActionBase):
116134
"""Build and install distribution package(s) from dist tarball or source RPM"""
117135

118136
@staticmethod
119-
def execute(machine_instance: testvm.Machine, source: str) -> None:
137+
async def execute(machine_instance: testthing.VirtualMachine, source: str) -> None:
120138
# upload the tarball or srpm
121139
sourcename = os.path.basename(source)
122140

123141
vm_source = os.path.join("/var/tmp", sourcename)
124-
machine_instance.upload([source], vm_source, relative_dir=".")
142+
await machine_instance.scp(source, f"vm:{vm_source}")
125143

126144
# this will fail if neither is available -- exception is clear enough, this is a developer tool
127-
out = machine_instance.execute("(which pbuilder || which mock || which pacman) 2>/dev/null")
128-
if 'pbuilder' in out:
129-
BuildAction.build_deb(machine_instance, vm_source)
130-
elif 'mock' in out:
131-
BuildAction.build_rpm(machine_instance, vm_source)
132-
elif 'pacman' in out:
133-
BuildAction.build_arch(machine_instance, vm_source)
145+
out = await machine_instance.execute("(which pbuilder || which mock || which pacman) 2>/dev/null")
146+
if "pbuilder" in out:
147+
await BuildAction.build_deb(machine_instance, vm_source)
148+
elif "mock" in out:
149+
await BuildAction.build_rpm(machine_instance, vm_source)
150+
elif "pacman" in out:
151+
await BuildAction.build_arch(machine_instance, vm_source)
134152
else:
135153
raise NotImplementedError(f"unknown build platform: {out}")
136154

137155
@staticmethod
138-
def build_deb(machine: testvm.Machine, vm_source: str) -> None:
139-
build_opts = 'nocheck' if opt_quick else ''
156+
async def build_deb(machine: testthing.VirtualMachine, vm_source: str) -> None:
157+
build_opts = "nocheck" if opt_quick else ""
140158

141159
# build source packge
142-
machine.execute(f"""
160+
await machine.execute(f"""
143161
set -eu
144162
rm -rf /var/tmp/build
145163
mkdir -p /var/tmp/build
@@ -156,55 +174,64 @@ class BuildAction(ActionBase):
156174
dpkg-buildpackage -S -us -uc -nc""")
157175

158176
# build binary packages
159-
machine.execute(f"cd /var/tmp/build; DEB_BUILD_OPTIONS='{build_opts}' pbuilder build --buildresult . "
160-
f"{opt_build_options} *.dsc", timeout=1800, stdout=stdout_disposition)
177+
await machine.execute(
178+
f"cd /var/tmp/build; DEB_BUILD_OPTIONS='{build_opts}' pbuilder build --buildresult . "
179+
f"{opt_build_options} *.dsc",
180+
stdout=stdout_disposition,
181+
)
161182

162183
# install packages
163-
machine.execute("dpkg -i /var/tmp/build/*.deb")
184+
await machine.execute("dpkg -i /var/tmp/build/*.deb")
164185

165186
@staticmethod
166-
def build_rpm(machine: testvm.Machine, vm_source: str) -> None:
167-
mock_opts = ''
187+
async def build_rpm(machine: testthing.VirtualMachine, vm_source: str) -> None:
188+
mock_opts = ""
168189
if opt_verbose:
169-
mock_opts += ' --verbose'
190+
mock_opts += " --verbose"
170191
if opt_quick:
171-
mock_opts += ' --nocheck'
192+
mock_opts += " --nocheck"
172193
if opt_build_options:
173-
mock_opts += ' ' + opt_build_options
194+
mock_opts += " " + opt_build_options
174195

175196
# build source package, unless this is running against an srpm already
176197
if vm_source.endswith(".src.rpm"):
177198
srpm = vm_source
178199
else:
179-
machine.execute(f"""
200+
await machine.execute(f"""
180201
set -eu
181202
rm -rf /var/tmp/build
182203
su builder -c 'rpmbuild --define "_topdir /var/tmp/build" -ts "{vm_source}"'
183204
""")
184205
srpm = "/var/tmp/build/SRPMS/*.src.rpm"
185206

186207
# HACK: mock in openSUSE must be called through sudo (but still originally as builder)
187-
if "opensuse" in machine.execute("cat /etc/os-release"):
208+
if "opensuse" in await machine.execute("cat /etc/os-release"):
188209
mock_sudo = "sudo"
189210
else:
190211
mock_sudo = ""
191212

192213
# build binary RPMs from srpm; disable all repositorys as mock insists on
193214
# calling `dnf builddep`, which insists on a cache; our test VMs don't have a cache,
194215
# as the mock is offline and pre-installed
195-
machine.execute(f"su builder -c '{mock_sudo} mock --no-clean --no-cleanup-after --disablerepo=* "
196-
f"--offline --resultdir /var/tmp/build {mock_opts} --rebuild {srpm}'",
197-
timeout=1800, stdout=stdout_disposition)
216+
await machine.execute(
217+
f"su builder -c '{mock_sudo} mock --no-clean --no-cleanup-after --disablerepo=* "
218+
f"--offline --resultdir /var/tmp/build {mock_opts} --rebuild {srpm}'",
219+
stdout=stdout_disposition,
220+
)
198221

199222
# install RPMs
200-
machine.execute('packages=$(find /var/tmp/build -name "*.rpm" -not -name "*.src.rpm"); '
201-
f'rpm -U --force --verbose {"--nodigest --nosignature" if opt_quick else ""} $packages')
223+
await machine.execute(
224+
'packages=$(find /var/tmp/build -name "*.rpm" -not -name "*.src.rpm"); '
225+
f"rpm -U --force --verbose {'--nodigest --nosignature' if opt_quick else ''} $packages"
226+
)
202227

203228
@staticmethod
204-
def build_arch(machine: testvm.Machine, vm_source: str) -> None:
229+
async def build_arch(machine: testthing.VirtualMachine, vm_source: str) -> None:
205230
# unpack source tree's arch packaging directory (PKGBUILD refers to some files)
206231
# and set PKGBUILD variables
207-
machine.write("/var/tmp/mkbuild.sh", f"""#!/bin/sh
232+
await machine.write(
233+
"/var/tmp/mkbuild.sh",
234+
f"""#!/bin/sh
208235
set -eu
209236
rm -rf /var/tmp/build
210237
mkdir -p /var/tmp/build
@@ -215,52 +242,49 @@ class BuildAction(ActionBase):
215242
cp "$archdir"/* .
216243
# tarball must be in same directory as PKGBUILD
217244
cp '{vm_source}' /var/tmp/build/
218-
""", perm="755")
219-
machine.execute(f"su builder {opt_build_options} /var/tmp/mkbuild.sh")
245+
""",
246+
perm="755",
247+
)
248+
await machine.execute(f"su builder {opt_build_options} /var/tmp/mkbuild.sh")
220249

221250
# build binaries
222-
machine.execute("cd /var/tmp/build; makechrootpkg -r /var/lib/archbuild/cockpit -U builder",
223-
timeout=1800, stdout=stdout_disposition)
251+
await machine.execute(
252+
"cd /var/tmp/build; makechrootpkg -r /var/lib/archbuild/cockpit -U builder",
253+
stdout=stdout_disposition,
254+
)
224255

225256
# install packages
226-
machine.execute("pacman -U --noconfirm /var/tmp/build/*.pkg.tar.zst")
257+
await machine.execute("pacman -U --noconfirm /var/tmp/build/*.pkg.tar.zst")
227258

228259

229260
class RunCommandAction(ActionBase):
230261
@staticmethod
231-
def execute(machine_instance: testvm.Machine, command: str) -> None:
232-
try:
233-
machine_instance.execute(command, timeout=1800)
234-
except subprocess.CalledProcessError as e:
235-
sys.stderr.write("%s\n" % e)
236-
sys.exit(e.returncode)
262+
async def execute(machine_instance: testthing.VirtualMachine, command: str) -> None:
263+
await machine_instance.execute(command)
237264

238265

239266
class ScriptAction(ActionBase):
240267
@staticmethod
241-
def execute(machine_instance: testvm.Machine, script: str) -> None:
268+
async def execute(machine_instance: testthing.VirtualMachine, script: str) -> None:
242269
uploadpath = "/var/tmp/" + os.path.basename(script)
243-
machine_instance.upload([os.path.abspath(script)], uploadpath)
244-
machine_instance.execute("chmod a+x %s" % uploadpath)
245-
try:
246-
machine_instance.execute(uploadpath, timeout=1800)
247-
except subprocess.CalledProcessError as e:
248-
sys.stderr.write("%s\n" % e)
249-
sys.exit(e.returncode)
270+
await machine_instance.scp(os.path.abspath(script), f"vm:{uploadpath}")
271+
await machine_instance.execute("chmod a+x %s" % uploadpath)
272+
await machine_instance.execute(uploadpath)
250273

251274

252275
class UploadAction(ActionBase):
253276
@staticmethod
254-
def execute(machine_instance: testvm.Machine, srcdest: str) -> None:
277+
async def execute(machine_instance: testthing.VirtualMachine, srcdest: str) -> None:
255278
src, dest = srcdest.split(":")
256279
abssrc = os.path.abspath(src)
257280
# preserve trailing / for rsync compatibility
258-
if src.endswith('/'):
259-
abssrc += '/'
260-
machine_instance.upload([abssrc], dest)
281+
if src.endswith("/"):
282+
abssrc += "/"
283+
await machine_instance.scp(abssrc, f"vm:{dest}")
261284

262285

263-
def main() -> None:
286+
async def main() -> None:
287+
# fmt: off
264288
parser = argparse.ArgumentParser(
265289
description=('Run command inside or install packages into a Cockpit virtual machine. '
266290
'All actions can be specified multiple times and run in the given order.'))
@@ -284,7 +308,7 @@ def main() -> None:
284308
help="Additional options for mock/pbuilder/arch builder")
285309
parser.add_argument('--resize', help="Resize the image. Size in bytes with using K, M, or G suffix.")
286310
parser.add_argument('-n', '--no-network', action='store_true', help='Do not connect the machine to the Internet')
287-
parser.add_argument('--cpus', type=int, default=None,
311+
parser.add_argument('--cpus', type=int, default=2,
288312
help="Number of CPUs for the virtual machine")
289313
parser.add_argument('--memory-mb', type=int, default=2048,
290314
help="RAM size for the virtual machine")
@@ -294,6 +318,7 @@ def main() -> None:
294318
help='Disable tests during package build with --build')
295319
parser.add_argument('image', help='The image to use (destination name when using --base-image)')
296320
parser.add_argument('--sit', action='store_true', help='Sit and wait if any VM action fails')
321+
# fmt: on
297322
args = parser.parse_args()
298323

299324
if not args.actions and not args.resize:
@@ -302,7 +327,7 @@ def main() -> None:
302327
if not args.base_image:
303328
args.base_image = os.path.basename(args.image)
304329

305-
args.base_image = testvm.get_test_image(args.base_image)
330+
args.base_image = get_test_image(args.base_image)
306331

307332
global opt_quick, opt_verbose, opt_build_options, stdout_disposition
308333
opt_quick = args.quick
@@ -311,30 +336,27 @@ def main() -> None:
311336
if not args.verbose:
312337
stdout_disposition = subprocess.DEVNULL
313338

314-
if '/' not in args.base_image:
339+
if "/" not in args.base_image:
315340
subprocess.check_call([os.path.join(BOTS_DIR, "image-download"), args.base_image])
316-
network = testvm.VirtNetwork(0, image=args.base_image)
317-
machine = testvm.VirtMachine(maintain=True,
318-
verbose=args.verbose,
319-
networking=network.host(restrict=args.no_network),
320-
image=prepare_install_image(args.base_image, args.image, args.resize, args.fresh),
321-
cpus=args.cpus,
322-
memory_mb=args.memory_mb)
323-
machine.start()
324-
machine.wait_boot()
325-
try:
326-
for (handler, arg) in args.actions:
327-
handler(machine, arg)
328-
except Exception as e:
329-
if args.sit:
330-
print(e, file=sys.stderr)
331-
print(machine.diagnose(), file=sys.stderr)
332-
print("Press RET to continue...")
333-
sys.stdin.readline()
334-
raise e
335-
finally:
336-
machine.stop()
337-
338-
339-
if __name__ == '__main__':
340-
main()
341+
342+
cockpit_test_identity = Path(MACHINE_DIR) / "identity"
343+
cockpit_test_identity.chmod(0o600)
344+
345+
with testthing.cli_helper(), testthing.IpcDirectory() as ipc:
346+
async with testthing.VirtualMachine(
347+
prepare_install_image(args.base_image, args.image, args.resize, args.fresh),
348+
cpus=args.cpus,
349+
identity=(cockpit_test_identity, None),
350+
ipc=ipc,
351+
memory=f"{args.memory_mb}M",
352+
networks=[] if args.no_network else [testthing.Network("user")],
353+
sit=args.sit,
354+
snapshot=False,
355+
verbose=args.verbose,
356+
) as vm:
357+
for handler, arg in args.actions:
358+
await asyncio.wait_for(handler(vm, arg), timeout=1800)
359+
360+
361+
if __name__ == "__main__":
362+
asyncio.run(main())

0 commit comments

Comments
 (0)