Skip to content

Commit 2c5c8a4

Browse files
committed
Testsuite: T8602: add QEMU smoketest for add-system-image over HTTP (default + VRF)
Extend check-qemu-install with --test-image-update: build a nested installer ISO as a second CD-ROM, serve the inner image over HTTP, and exercise add/delete system image in the default routing context and in a named VRF. Add make test-image-update and ignore/clean nested_installer_payload.iso artefacts.
1 parent c6dc300 commit 2c5c8a4

3 files changed

Lines changed: 214 additions & 9 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ rootfs/*
1212
/testinstall*.efivars
1313
/ci_data
1414
/ci_seed.iso
15+
/nested_installer_payload.iso
1516
/*.qcow2
1617
/*.tar
1718
.DS_Store

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ test-ci-qcow2:
7272
rm -f cloud-init-image-$(ARCH).qcow2 ; cp $$(ls -t build/*.qcow2 | head -n 1) cloud-init-image-$(ARCH).qcow2
7373
scripts/check-qemu-install --debug --cloud-init --disk cloud-init-image-$(ARCH).qcow2 $(filter-out $@,$(MAKECMDGOALS))
7474

75+
.PHONY: test-image-update
76+
.ONESHELL:
77+
test-image-update:
78+
scripts/check-qemu-install --debug --test-image-update --iso $(ISO_PATH) $(filter-out $@,$(MAKECMDGOALS))
79+
7580
.PHONY: qemu-live
7681
.ONESHELL:
7782
qemu-live:
@@ -105,7 +110,7 @@ clean:
105110

106111
.PHONY: purge
107112
purge:
108-
rm -rf build packer_build packer_cache testinstall-*.raw ci_data ci_seed.iso
113+
rm -rf build packer_build packer_cache testinstall-*.raw ci_data ci_seed.iso nested_iso_data nested_installer_payload.iso
109114

110115
.PHONY: ansible-install ansible-check ansible-clean
111116
.ONESHELL:

scripts/check-qemu-install

Lines changed: 207 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ import pexpect
5454

5555
EXCEPTION = 0
5656
DISK_IMAGE_EXTENSION = '.raw'
57+
58+
# Nested installer-ISO QEMU test: mkisofs payload (see --nested-installer-iso-test)
59+
NESTED_ISO_DATA_DIR = 'nested_iso_data'
60+
NESTED_INSTALLER_PAYLOAD_ISO = 'nested_installer_payload.iso'
61+
NESTED_INNER_ISO_NAME = 'vyos-installer.iso'
62+
63+
# Local-only HTTP nested-ISO upgrade test: default VRF (main RIB) then named VRF purple
64+
NESTED_HTTP_SERV_PORT = 18088
65+
NESTED_HTTP_IMAGE_NAME = 'test-image-update'
66+
NESTED_HTTP_VRF_NAME = 'purple'
67+
NESTED_HTTP_VRF_DUMMY = 'dum8000'
68+
NESTED_HTTP_VRF_TABLE = '42042'
69+
NESTED_HTTP_VRF_ADDR = '198.51.100.99' # RFC 5737 TEST-NET-3: bind address for http.server inside VRF
70+
NESTED_HTTP_MOUNT_POINT = '/mnt/nested_installer_iso'
71+
72+
# Cloud-Init data
5773
CI_CONFIG_ARGS = {
5874
'hostname': 'vyos-CLOUD-INIT',
5975
'eth0_ipv4': '192.0.2.1/25',
@@ -125,6 +141,8 @@ parser.add_argument('--sbtest', help='Execute Secure Boot tests',
125141
action='store_true', default=False)
126142
parser.add_argument('--cloud-init', help='Execute cloud-init tests',
127143
action='store_true', default=False)
144+
parser.add_argument('--test-image-update', help='Test ISO image update in default VRF and custom VRF',
145+
action='store_true', default=False)
128146
parser.add_argument('--qemu-cmd', help='Only generate QEMU launch command',
129147
action='store_true', default=False)
130148
parser.add_argument('--cpu', help='Set QEMU CPU', type=int, default=2)
@@ -188,7 +206,8 @@ class EarlyExit(Exception):
188206
def _kvm_exists():
189207
return os.path.exists("/dev/kvm")
190208

191-
def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False, vnc_enabled=False, secure_boot=False):
209+
def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False,
210+
vnc_enabled=False, secure_boot=False, nested_cdrom_iso=None):
192211
uefi = ""
193212
uuid = "f48b60b2-e6ad-49ef-9d09-4245d0585e52"
194213
accel = ',accel=kvm' if _kvm_exists() else ''
@@ -227,12 +246,27 @@ def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False
227246
vga = '-vga virtio'
228247

229248
cdrom = ""
249+
nested_cdrom = ""
230250
if iso_img:
251+
bootindex = '10'
231252
cdrom = f' -drive file={iso_img},format=raw,if=none,media=cdrom,id=drive-cd1,readonly=on'
232253
if architecture == 'arm64':
233-
cdrom = f' {cdrom} -device scsi-cd,bus=scsi0.0,drive=drive-cd1,id=cd1,bootindex=10'
254+
cdrom = f' {cdrom} -device scsi-cd,bus=scsi0.0,drive=drive-cd1,id=cd1,bootindex={bootindex}'
255+
else:
256+
cdrom = f' {cdrom} -device ahci,id=achi0 -device ide-cd,bus=achi0.0,drive=drive-cd1,id=cd1,bootindex={bootindex}'
257+
258+
if nested_cdrom_iso:
259+
drive_settings = 'drive=drive-cd2,id=cd2,bootindex=11'
260+
nested_cdrom = f' -drive file={nested_cdrom_iso},format=raw,if=none,media=cdrom,id=drive-cd2,readonly=on'
261+
if architecture == 'arm64':
262+
nested_cdrom = f' {nested_cdrom} -device scsi-cd,bus=scsi0.0,{drive_settings}'
234263
else:
235-
cdrom = f' {cdrom} -device ahci,id=achi0 -device ide-cd,bus=achi0.0,drive=drive-cd1,id=cd1,bootindex=10'
264+
if not iso_img:
265+
# Second IDE CD on its own AHCI controller (no installer CD in this command)
266+
nested_cdrom = f'{nested_cdrom} -device ahci,id=nestedahci' \
267+
f' -device ide-cd,bus=nestedahci.0,{drive_settings}'
268+
else:
269+
nested_cdrom = f'{nested_cdrom} -device ide-cd,bus=achi0.1,{drive_settings}'
236270

237271
# RFC7042 section 2.1.2 MAC addresses used for documentation
238272
macbase = '00:00:5E:00:53'
@@ -261,7 +295,7 @@ def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False
261295
-netdev user,id=n6 -device virtio-net-pci,netdev=n6,mac={macbase}:06,romfile="" \
262296
-netdev user,id=n7 -device virtio-net-pci,netdev=n7,mac={macbase}:07,romfile="" \
263297
-device virtio-scsi-pci,id=scsi0 \
264-
{cdrom} \
298+
{cdrom}{nested_cdrom} \
265299
-drive format={disk_format},file={disk_img},if=none,media=disk,id=drive-hd1,readonly=off \
266300
-device scsi-hd,bus=scsi0.0,drive=drive-hd1,id=hd1,bootindex=1'
267301

@@ -349,6 +383,8 @@ log.addHandler(handler)
349383
_primary_modes = []
350384
if args.cloud_init:
351385
_primary_modes.append('--cloud-init')
386+
if args.test_image_update:
387+
_primary_modes.append('--test-image-update')
352388
if args.tpmtest:
353389
_primary_modes.append('--tpmtest')
354390
if args.raid:
@@ -361,7 +397,7 @@ if args.sbtest:
361397
_primary_modes.append('--sbtest')
362398
if len(_primary_modes) > 1:
363399
log.error('Incompatible combination of testcase flags (%s): only one of '
364-
'--cloud-init, --tpmtest, --raid, --smoketest, '
400+
'--cloud-init, --test-image-update, --tpmtest, --raid, --smoketest, '
365401
'--configtest, --sbtest may be set.', ', '.join(_primary_modes))
366402
sys.exit(1)
367403

@@ -400,6 +436,10 @@ if args.iso and not os.path.isfile(args.iso):
400436
log.error('Unable to find VyOS ISO image needed by testcases!')
401437
sys.exit(1)
402438

439+
if args.test_image_update and not args.iso:
440+
log.error('--test-image-update requires --iso file!')
441+
sys.exit(1)
442+
403443
OVMF_CODE = '/usr/share/OVMF/OVMF_CODE_4M.secboot.fd'
404444
OVMF_VARS_TMP = args.disk.replace(DISK_IMAGE_EXTENSION, '.efivars')
405445
if args.sbtest:
@@ -410,7 +450,7 @@ diskname_raid = None
410450
def gen_disk(name):
411451
if not os.path.isfile(name):
412452
log.info(f'Creating Disk image {name}')
413-
c = subprocess.check_output(['qemu-img', 'create', name, '2G'])
453+
c = subprocess.check_output(['qemu-img', 'create', name, '5G'])
414454
log.debug(c.decode())
415455
else:
416456
log.info(f'Re-using already existing disk image "{name}".')
@@ -425,6 +465,22 @@ if args.raid:
425465
# must be called after the raid disk as args.disk name is altered in the RAID path
426466
gen_disk(args.disk)
427467

468+
if args.test_image_update:
469+
if os.path.isdir(NESTED_ISO_DATA_DIR):
470+
shutil.rmtree(NESTED_ISO_DATA_DIR)
471+
os.makedirs(NESTED_ISO_DATA_DIR)
472+
shutil.copy2(args.iso, os.path.join(NESTED_ISO_DATA_DIR, NESTED_INNER_ISO_NAME))
473+
if os.path.isfile(NESTED_INSTALLER_PAYLOAD_ISO):
474+
os.unlink(NESTED_INSTALLER_PAYLOAD_ISO)
475+
log.info('Assembling nested installer payload ISO (second CD-ROM, drive-cd2)')
476+
subprocess.check_call([
477+
'mkisofs', '-joliet', '-rock', '-volid', 'VYOSNESTED',
478+
'-output', NESTED_INSTALLER_PAYLOAD_ISO, NESTED_ISO_DATA_DIR,
479+
])
480+
nested_payload_iso_path = os.path.abspath(NESTED_INSTALLER_PAYLOAD_ISO)
481+
log.info('Removing nested ISO staging directory %s', NESTED_ISO_DATA_DIR)
482+
shutil.rmtree(NESTED_ISO_DATA_DIR)
483+
428484
# Create software emulated TPM - clear existing TPM data first - might be a
429485
# leftover from a previous run
430486
clearTPM()
@@ -622,8 +678,116 @@ def basic_cli_tests(c):
622678
c.expect(f'set console_type="{console_type}"')
623679
c.expect(op_mode_prompt)
624680

681+
def _image_update_cli_sequence(c, log, new_image_name, server_bind_host='127.0.0.1', use_vrf=False):
682+
"""One add-system-image/delete cycle for nested ISO over HTTP (optional Linux VRF + VyOS vrf arg)."""
683+
url = f'http://{server_bind_host}:{NESTED_HTTP_SERV_PORT}/{NESTED_INNER_ISO_NAME}'
684+
685+
vrf_exec = ''
686+
if use_vrf:
687+
log.info(f'Configure VRF ({NESTED_HTTP_VRF_NAME}) for HTTP server - used for image update ')
688+
vrf_exec = f'ip vrf exec {NESTED_HTTP_VRF_NAME} '
689+
c.sendline('configure')
690+
c.expect(cfg_mode_prompt)
691+
c.sendline(f'set vrf name {NESTED_HTTP_VRF_NAME} table {NESTED_HTTP_VRF_TABLE}')
692+
c.expect(cfg_mode_prompt)
693+
c.sendline(f'set interfaces dummy {NESTED_HTTP_VRF_DUMMY} address {server_bind_host}/32')
694+
c.expect(cfg_mode_prompt)
695+
c.sendline(f'set interfaces dummy {NESTED_HTTP_VRF_DUMMY} vrf {NESTED_HTTP_VRF_NAME}')
696+
c.expect(cfg_mode_prompt)
697+
c.sendline('commit')
698+
c.expect(cfg_mode_prompt)
699+
c.sendline('exit')
700+
c.expect(op_mode_prompt)
701+
702+
c.sendline(
703+
f'sudo bash -c \'{vrf_exec}python3 -m http.server {NESTED_HTTP_SERV_PORT} --bind {server_bind_host} '
704+
f'--directory {NESTED_HTTP_MOUNT_POINT} </dev/null >/tmp/nested_http.log 2>&1 &\''
705+
)
706+
c.expect(op_mode_prompt)
707+
708+
time.sleep(5) # Wait for HTTP server to start
709+
710+
update_cmd_cli = f'TERM=dumb add system image {url}'
711+
if use_vrf:
712+
update_cmd_cli = f'{update_cmd_cli} vrf {NESTED_HTTP_VRF_NAME}'
713+
c.sendline(update_cmd_cli)
714+
715+
timeout = 600
716+
deadline = time.time() + timeout
717+
while True:
718+
if time.time() > deadline:
719+
raise Exception(f'add system image timed out after {timeout}s')
720+
i = c.expect([
721+
'What would you like to name this image',
722+
'Would you like to set the new image as the default one for boot',
723+
'An active configuration was found. Would you like to copy it to the new image',
724+
'Would you like to copy SSH host keys',
725+
'Would you like to save the SSH known hosts (fingerprints)',
726+
'Signature is not available. Do you want to continue with installation',
727+
'There are unsaved changes to the configuration',
728+
'Would you like to continue',
729+
'Unable to',
730+
'Error:',
731+
op_mode_prompt,
732+
], timeout=300)
733+
if i == 0:
734+
c.sendline(new_image_name)
735+
elif i == 1:
736+
c.sendline('n')
737+
elif i == 2:
738+
c.sendline('y')
739+
elif i == 3:
740+
c.sendline('y')
741+
elif i == 4:
742+
c.sendline('y')
743+
elif i == 5:
744+
c.sendline('y')
745+
elif i == 6:
746+
c.sendline('y')
747+
elif i == 7:
748+
c.sendline('y')
749+
elif i == 8 or i == 9:
750+
raise Exception('add system image reported an error')
751+
elif i == 10:
752+
log.info('add system image completed')
753+
break
754+
755+
c.sendline('show system image')
756+
c.expect(new_image_name)
757+
c.expect(op_mode_prompt)
758+
759+
c.sendline(f'TERM=dumb delete system image {new_image_name}')
760+
deadline = time.time() + timeout
761+
while time.time() < deadline:
762+
j = c.expect([
763+
'Are you sure you want to delete',
764+
'Do you really want to delete the image',
765+
'Cannot ',
766+
'Error:',
767+
'Unable to ',
768+
op_mode_prompt,
769+
], timeout=300)
770+
if j == 0 or j == 1:
771+
c.sendline('y')
772+
elif j == 2 or j == 3 or j == 4:
773+
raise Exception('delete system image failed')
774+
elif j == 5:
775+
break
776+
else:
777+
raise Exception('delete system image timed out')
778+
779+
c.sendline('show system image')
780+
c.expect(op_mode_prompt)
781+
if new_image_name in c.before.decode(errors='replace'):
782+
raise Exception(f'Image {new_image_name!r} still listed after delete')
783+
784+
c.sendline(f'sudo fuser -k {NESTED_HTTP_SERV_PORT}/tcp 2>/dev/null || true')
785+
c.expect(op_mode_prompt)
786+
625787
if args.qemu_cmd:
626-
tmp = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid, iso_img=args.iso, vnc_enabled=args.vnc, secure_boot=args.sbtest)
788+
tmp = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid,
789+
iso_img=args.iso, vnc_enabled=args.vnc, secure_boot=args.sbtest,
790+
nested_cdrom_iso=nested_payload_iso_path)
627791
os.system(tmp)
628792
exit(0)
629793

@@ -680,7 +844,9 @@ try:
680844
# Installing image to disk
681845
#################################################
682846
log.info('Installing system')
683-
cmd = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid, tpm=args.tpmtest, iso_img=args.iso, vnc_enabled=args.vnc, secure_boot=args.sbtest)
847+
cmd = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid,
848+
tpm=args.tpmtest, iso_img=args.iso, vnc_enabled=args.vnc,
849+
secure_boot=args.sbtest, nested_cdrom_iso=nested_payload_iso_path)
684850
log.debug(f'Executing command: {cmd}')
685851
c = pexpect.spawn(cmd, logfile=stl, timeout=60)
686852

@@ -1272,6 +1438,29 @@ try:
12721438
tmp = 'Configtest failed :/ - check debug output'
12731439
log.error(tmp)
12741440
raise Exception(tmp)
1441+
elif args.test_image_update:
1442+
log.info(f'Nested installer ISO: mount second CD-ROM ({NESTED_INNER_ISO_NAME})')
1443+
c.sendline(f'sudo mkdir -p {NESTED_HTTP_MOUNT_POINT}')
1444+
c.expect(op_mode_prompt)
1445+
1446+
# After drive-cd1 eject the first SCSI CD node is often an empty tray; real
1447+
# media may appear on sr1+. Try every optical device until one mounts.
1448+
c.sendline(
1449+
f'for d in /dev/sr*; do sudo mount -o ro "$d" {NESTED_HTTP_MOUNT_POINT} 2>/dev/null '
1450+
'&& echo NESTED_ISO_MOUNT_OK && break; done; '
1451+
f'mountpoint -q {NESTED_HTTP_MOUNT_POINT} || echo NESTED_ISO_MOUNT_FAIL'
1452+
)
1453+
if c.expect(['NESTED_ISO_MOUNT_OK', 'NESTED_ISO_MOUNT_FAIL']) != 0:
1454+
raise Exception('Failed to mount nested installer payload ISO (no usable /dev/sr* media)')
1455+
c.expect(op_mode_prompt)
1456+
1457+
_image_update_cli_sequence(c, log, NESTED_HTTP_IMAGE_NAME)
1458+
_image_update_cli_sequence(c, log, f'{NESTED_HTTP_IMAGE_NAME}-{NESTED_HTTP_VRF_NAME}', NESTED_HTTP_VRF_ADDR, use_vrf=True)
1459+
1460+
log.info('Unmount nested installer ISO')
1461+
c.sendline(f'sudo umount {NESTED_HTTP_MOUNT_POINT}')
1462+
c.expect(op_mode_prompt)
1463+
log.info('Nested installer ISO testcase complete')
12751464
elif args.sbtest:
12761465
c.sendline('show secure-boot')
12771466
c.expect('SecureBoot enabled')
@@ -1310,6 +1499,16 @@ if tpm_process:
13101499
tpm_process.terminate()
13111500
tpm_process.join()
13121501

1502+
if args.test_image_update:
1503+
try:
1504+
if os.path.isdir(NESTED_ISO_DATA_DIR):
1505+
shutil.rmtree(NESTED_ISO_DATA_DIR)
1506+
if nested_payload_iso_path and os.path.isfile(nested_payload_iso_path) \
1507+
and not args.keep:
1508+
os.remove(nested_payload_iso_path)
1509+
except OSError:
1510+
log.warning('Could not remove nested installer ISO artefacts', exc_info=True)
1511+
13131512
if not args.keep:
13141513
log.info(f'Removing disk file: {args.disk}')
13151514
try:

0 commit comments

Comments
 (0)