@@ -382,6 +382,58 @@ fn cleanup_domain(domain_name: &str) {
382382 }
383383}
384384
385+ /// Wait for SSH to become available on a domain with a timeout
386+ fn wait_for_ssh_available (
387+ bck : & str ,
388+ domain_name : & str ,
389+ timeout_secs : u64 ,
390+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
391+ let start_time = std:: time:: Instant :: now ( ) ;
392+ let timeout_duration = std:: time:: Duration :: from_secs ( timeout_secs) ;
393+
394+ println ! (
395+ "Waiting for SSH to become available on domain: {}" ,
396+ domain_name
397+ ) ;
398+
399+ loop {
400+ // Try a simple SSH command to test connectivity
401+ let ssh_test = Command :: new ( "timeout" )
402+ . args ( [
403+ "10s" , // Short timeout for individual SSH attempts
404+ bck,
405+ "libvirt" ,
406+ "ssh" ,
407+ domain_name,
408+ "--" ,
409+ "echo" ,
410+ "ssh-ready" ,
411+ ] )
412+ . output ( ) ;
413+
414+ match ssh_test {
415+ Ok ( output) if output. status . success ( ) => {
416+ println ! ( "✓ SSH is now available" ) ;
417+ return Ok ( ( ) ) ;
418+ }
419+ Ok ( _) => {
420+ // SSH command failed, but that's expected while VM is booting
421+ }
422+ Err ( e) => {
423+ println ! ( "SSH test error (expected while booting): {}" , e) ;
424+ }
425+ }
426+
427+ // Check if we've exceeded the timeout
428+ if start_time. elapsed ( ) >= timeout_duration {
429+ return Err ( format ! ( "Timeout waiting for SSH after {} seconds" , timeout_secs) . into ( ) ) ;
430+ }
431+
432+ // Wait 5 seconds before next attempt
433+ std:: thread:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) ;
434+ }
435+ }
436+
385437/// Test VM startup and shutdown with libvirt run
386438pub fn test_libvirt_vm_lifecycle ( ) {
387439 // Skip if running in CI/container environment without libvirt
@@ -511,6 +563,239 @@ pub fn test_libvirt_vm_lifecycle() {
511563 println ! ( "VM lifecycle test completed" ) ;
512564}
513565
566+ /// Test container storage binding functionality end-to-end
567+ pub fn test_libvirt_bind_storage_ro ( ) {
568+ let bck = get_bck_command ( ) . unwrap ( ) ;
569+ let test_image = get_test_image ( ) ;
570+
571+ // First check if libvirt supports readonly virtiofs
572+ println ! ( "Checking libvirt capabilities..." ) ;
573+ let status_output = Command :: new ( & bck)
574+ . args ( & [ "libvirt" , "status" , "--format" , "json" ] )
575+ . output ( )
576+ . expect ( "Failed to get libvirt status" ) ;
577+
578+ if !status_output. status . success ( ) {
579+ let stderr = String :: from_utf8_lossy ( & status_output. stderr ) ;
580+ panic ! ( "Failed to get libvirt status: {}" , stderr) ;
581+ }
582+
583+ let status: serde_json:: Value =
584+ serde_json:: from_slice ( & status_output. stdout ) . expect ( "Failed to parse libvirt status JSON" ) ;
585+
586+ let supports_readonly = status[ "supports_readonly_virtiofs" ]
587+ . as_bool ( )
588+ . expect ( "Missing supports_readonly_virtiofs field in status output" ) ;
589+
590+ if !supports_readonly {
591+ println ! ( "Skipping test: libvirt does not support readonly virtiofs" ) ;
592+ println ! ( "libvirt version: {:?}" , status[ "version" ] ) ;
593+ println ! ( "Requires libvirt 6.2+ for readonly virtiofs support" ) ;
594+ return ;
595+ }
596+
597+ // Generate unique domain name for this test
598+ let domain_name = format ! (
599+ "test-bind-storage-{}" ,
600+ std:: time:: SystemTime :: now( )
601+ . duration_since( std:: time:: UNIX_EPOCH )
602+ . unwrap( )
603+ . as_secs( )
604+ ) ;
605+
606+ println ! ( "Testing --bind-storage-ro with domain: {}" , domain_name) ;
607+
608+ // Cleanup any existing domain with this name
609+ let _ = Command :: new ( "virsh" )
610+ . args ( & [ "destroy" , & domain_name] )
611+ . output ( ) ;
612+ let _ = Command :: new ( "virsh" )
613+ . args ( & [ "undefine" , & domain_name] )
614+ . output ( ) ;
615+
616+ // Create domain with --bind-storage-ro flag
617+ println ! ( "Creating libvirt domain with --bind-storage-ro..." ) ;
618+ let create_output = Command :: new ( "timeout" )
619+ . args ( [
620+ "300s" , // 5 minute timeout for domain creation
621+ & bck,
622+ "libvirt" ,
623+ "run" ,
624+ "--name" ,
625+ & domain_name,
626+ "--bind-storage-ro" ,
627+ "--filesystem" ,
628+ "ext4" ,
629+ & test_image,
630+ ] )
631+ . output ( )
632+ . expect ( "Failed to run libvirt run with --bind-storage-ro" ) ;
633+
634+ let create_stdout = String :: from_utf8_lossy ( & create_output. stdout ) ;
635+ let create_stderr = String :: from_utf8_lossy ( & create_output. stderr ) ;
636+
637+ println ! ( "Create stdout: {}" , create_stdout) ;
638+ println ! ( "Create stderr: {}" , create_stderr) ;
639+
640+ if !create_output. status . success ( ) {
641+ cleanup_domain ( & domain_name) ;
642+ panic ! (
643+ "Failed to create domain with --bind-storage-ro: {}" ,
644+ create_stderr
645+ ) ;
646+ }
647+
648+ println ! ( "Successfully created domain: {}" , domain_name) ;
649+
650+ // Check that the domain was created with virtiofs filesystem
651+ println ! ( "Checking domain XML for virtiofs filesystem..." ) ;
652+ let dumpxml_output = Command :: new ( "virsh" )
653+ . args ( & [ "dumpxml" , & domain_name] )
654+ . output ( )
655+ . expect ( "Failed to dump domain XML" ) ;
656+
657+ if !dumpxml_output. status . success ( ) {
658+ cleanup_domain ( & domain_name) ;
659+ let stderr = String :: from_utf8_lossy ( & dumpxml_output. stderr ) ;
660+ panic ! ( "Failed to dump domain XML: {}" , stderr) ;
661+ }
662+
663+ let domain_xml = String :: from_utf8_lossy ( & dumpxml_output. stdout ) ;
664+ println ! (
665+ "Domain XML snippet: {}" ,
666+ & domain_xml[ ..std:: cmp:: min( 500 , domain_xml. len( ) ) ]
667+ ) ;
668+
669+ // Verify that the domain XML contains virtiofs configuration
670+ assert ! (
671+ domain_xml. contains( "type='virtiofs'" ) || domain_xml. contains( "driver type='virtiofs'" ) ,
672+ "Domain XML should contain virtiofs filesystem configuration"
673+ ) ;
674+
675+ // Verify that the filesystem has the correct tag
676+ assert ! (
677+ domain_xml. contains( "hoststorage" ) || domain_xml. contains( "dir='hoststorage'" ) ,
678+ "Domain XML should reference the hoststorage tag for container storage"
679+ ) ;
680+
681+ // Verify that the domain XML contains readonly element for virtiofs
682+ assert ! (
683+ domain_xml. contains( "<readonly/>" ) ,
684+ "Domain XML should contain readonly element for --bind-storage-ro"
685+ ) ;
686+
687+ // Check metadata for bind-storage-ro configuration
688+ if domain_xml. contains ( "bootc:bind-storage-ro" ) {
689+ assert ! (
690+ domain_xml. contains( "<bootc:bind-storage-ro>true</bootc:bind-storage-ro>" ) ,
691+ "Domain metadata should indicate bind-storage-ro is enabled"
692+ ) ;
693+ }
694+
695+ println ! ( "✓ Domain XML contains expected virtiofs configuration" ) ;
696+ println ! ( "✓ Container storage mount is configured as read-only" ) ;
697+ println ! ( "✓ hoststorage tag is present in filesystem configuration" ) ;
698+
699+ // Wait for VM to boot and SSH to become available
700+ if let Err ( e) = wait_for_ssh_available ( & bck, & domain_name, 180 ) {
701+ cleanup_domain ( & domain_name) ;
702+ panic ! ( "Failed to establish SSH connection: {}" , e) ;
703+ }
704+
705+ // Create mount point and mount virtiofs filesystem
706+ println ! ( "Creating mount point and mounting virtiofs filesystem..." ) ;
707+ let mount_setup = Command :: new ( "timeout" )
708+ . args ( [
709+ "30s" ,
710+ & bck,
711+ "libvirt" ,
712+ "ssh" ,
713+ & domain_name,
714+ "--" ,
715+ "sudo" ,
716+ "mkdir" ,
717+ "-p" ,
718+ "/run/virtiofs-mnt-hoststorage" ,
719+ ] )
720+ . output ( )
721+ . expect ( "Failed to create mount point" ) ;
722+
723+ if !mount_setup. status . success ( ) {
724+ let stderr = String :: from_utf8_lossy ( & mount_setup. stderr ) ;
725+ println ! ( "Warning: Failed to create mount point: {}" , stderr) ;
726+ }
727+
728+ let mount_cmd = Command :: new ( "timeout" )
729+ . args ( [
730+ "30s" ,
731+ & bck,
732+ "libvirt" ,
733+ "ssh" ,
734+ & domain_name,
735+ "--" ,
736+ "sudo" ,
737+ "mount" ,
738+ "-t" ,
739+ "virtiofs" ,
740+ "hoststorage" ,
741+ "/run/virtiofs-mnt-hoststorage" ,
742+ ] )
743+ . output ( )
744+ . expect ( "Failed to mount virtiofs" ) ;
745+
746+ if !mount_cmd. status . success ( ) {
747+ cleanup_domain ( & domain_name) ;
748+ let stderr = String :: from_utf8_lossy ( & mount_cmd. stderr ) ;
749+ panic ! ( "Failed to mount virtiofs filesystem: {}" , stderr) ;
750+ }
751+
752+ // Test SSH connection and verify container storage mount inside VM
753+ println ! ( "Testing SSH connection and checking container storage mount..." ) ;
754+ let st = Command :: new ( "timeout" )
755+ . args ( [
756+ "60s" ,
757+ & bck,
758+ "libvirt" ,
759+ "ssh" ,
760+ & domain_name,
761+ "--" ,
762+ "ls" ,
763+ "-la" ,
764+ "/run/virtiofs-mnt-hoststorage/overlay" ,
765+ ] )
766+ . status ( )
767+ . expect ( "Failed to run SSH command to check container storage" ) ;
768+
769+ assert ! ( st. success( ) ) ;
770+
771+ // Verify that the mount is read-only
772+ println ! ( "Verifying that the mount is read-only..." ) ;
773+ let ro_test_st = Command :: new ( "timeout" )
774+ . args ( [
775+ "30s" ,
776+ & bck,
777+ "libvirt" ,
778+ "ssh" ,
779+ & domain_name,
780+ "--" ,
781+ "touch" ,
782+ "/run/virtiofs-mnt-hoststorage/test-write" ,
783+ ] )
784+ . status ( )
785+ . expect ( "Failed to run SSH command to test read-only mount" ) ;
786+
787+ assert ! (
788+ !ro_test_st. success( ) ,
789+ "Mount should be read-only, but write operation succeeded"
790+ ) ;
791+ println ! ( "✓ Mount is correctly configured as read-only." ) ;
792+
793+ // Cleanup domain before completing test
794+ cleanup_domain ( & domain_name) ;
795+
796+ println ! ( "✓ --bind-storage-ro end-to-end test passed" ) ;
797+ }
798+
514799/// Test error handling for invalid configurations
515800pub fn test_libvirt_error_handling ( ) {
516801 let bck = get_bck_command ( ) . unwrap ( ) ;
0 commit comments