@@ -54,6 +54,22 @@ import pexpect
5454
5555EXCEPTION = 0
5656DISK_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
5773CI_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 )
126142parser .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 )
128146parser .add_argument ('--qemu-cmd' , help = 'Only generate QEMU launch command' ,
129147 action = 'store_true' , default = False )
130148parser .add_argument ('--cpu' , help = 'Set QEMU CPU' , type = int , default = 2 )
@@ -188,7 +206,8 @@ class EarlyExit(Exception):
188206def _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 = []
350384if args .cloud_init :
351385 _primary_modes .append ('--cloud-init' )
386+ if args .test_image_update :
387+ _primary_modes .append ('--test-image-update' )
352388if args .tpmtest :
353389 _primary_modes .append ('--tpmtest' )
354390if args .raid :
@@ -361,7 +397,7 @@ if args.sbtest:
361397 _primary_modes .append ('--sbtest' )
362398if 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+
403443OVMF_CODE = '/usr/share/OVMF/OVMF_CODE_4M.secboot.fd'
404444OVMF_VARS_TMP = args .disk .replace (DISK_IMAGE_EXTENSION , '.efivars' )
405445if args .sbtest :
@@ -410,7 +450,7 @@ diskname_raid = None
410450def 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
426466gen_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
430486clearTPM ()
@@ -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+
625787if 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
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+
13131512if not args .keep :
13141513 log .info (f'Removing disk file: { args .disk } ' )
13151514 try :
0 commit comments