Skip to content

Commit 165323d

Browse files
committed
libvirt: add --bind-storage-ro support for bootc upgrades
Implement --bind-storage-ro flag for `bcvk libvirt run` to enable bootc upgrade workflows from persistent VMs by mounting host container storage read-only. Assisted-by: Claude Code Signed-off-by: Colin Walters <walters@verbum.org>
1 parent aae33b4 commit 165323d

6 files changed

Lines changed: 306 additions & 2 deletions

File tree

crates/integration-tests/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ fn main() {
204204
tests::libvirt_verb::test_libvirt_error_handling();
205205
Ok(())
206206
}),
207+
Trial::test("libvirt_bind_storage_ro", || {
208+
tests::libvirt_verb::test_libvirt_bind_storage_ro();
209+
Ok(())
210+
}),
207211
];
208212

209213
// Run the tests and exit with the result

crates/integration-tests/src/tests/libvirt_verb.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,189 @@ pub fn test_libvirt_vm_lifecycle() {
511511
println!("VM lifecycle test completed");
512512
}
513513

514+
/// Test container storage binding functionality end-to-end
515+
pub fn test_libvirt_bind_storage_ro() {
516+
let bck = get_bck_command().unwrap();
517+
let test_image = get_test_image();
518+
519+
// Generate unique domain name for this test
520+
let domain_name = format!(
521+
"test-bind-storage-{}",
522+
std::time::SystemTime::now()
523+
.duration_since(std::time::UNIX_EPOCH)
524+
.unwrap()
525+
.as_secs()
526+
);
527+
528+
println!("Testing --bind-storage-ro with domain: {}", domain_name);
529+
530+
// Cleanup any existing domain with this name
531+
let _ = Command::new("virsh")
532+
.args(&["destroy", &domain_name])
533+
.output();
534+
let _ = Command::new("virsh")
535+
.args(&["undefine", &domain_name])
536+
.output();
537+
538+
// Create domain with --bind-storage-ro flag
539+
println!("Creating libvirt domain with --bind-storage-ro...");
540+
let create_output = Command::new("timeout")
541+
.args([
542+
"300s", // 5 minute timeout for domain creation
543+
&bck,
544+
"libvirt",
545+
"run",
546+
"--name",
547+
&domain_name,
548+
"--bind-storage-ro",
549+
"--filesystem",
550+
"ext4",
551+
&test_image,
552+
])
553+
.output()
554+
.expect("Failed to run libvirt run with --bind-storage-ro");
555+
556+
let create_stdout = String::from_utf8_lossy(&create_output.stdout);
557+
let create_stderr = String::from_utf8_lossy(&create_output.stderr);
558+
559+
println!("Create stdout: {}", create_stdout);
560+
println!("Create stderr: {}", create_stderr);
561+
562+
if !create_output.status.success() {
563+
cleanup_domain(&domain_name);
564+
panic!(
565+
"Failed to create domain with --bind-storage-ro: {}",
566+
create_stderr
567+
);
568+
}
569+
570+
println!("Successfully created domain: {}", domain_name);
571+
572+
// Check that the domain was created with virtiofs filesystem
573+
println!("Checking domain XML for virtiofs filesystem...");
574+
let dumpxml_output = Command::new("virsh")
575+
.args(&["dumpxml", &domain_name])
576+
.output()
577+
.expect("Failed to dump domain XML");
578+
579+
if !dumpxml_output.status.success() {
580+
cleanup_domain(&domain_name);
581+
let stderr = String::from_utf8_lossy(&dumpxml_output.stderr);
582+
panic!("Failed to dump domain XML: {}", stderr);
583+
}
584+
585+
let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout);
586+
println!(
587+
"Domain XML snippet: {}",
588+
&domain_xml[..std::cmp::min(500, domain_xml.len())]
589+
);
590+
591+
// Verify that the domain XML contains virtiofs configuration
592+
assert!(
593+
domain_xml.contains("type='virtiofs'") || domain_xml.contains("driver type='virtiofs'"),
594+
"Domain XML should contain virtiofs filesystem configuration"
595+
);
596+
597+
// Verify that the filesystem has the correct tag
598+
assert!(
599+
domain_xml.contains("hoststorage") || domain_xml.contains("dir='hoststorage'"),
600+
"Domain XML should reference the hoststorage tag for container storage"
601+
);
602+
603+
// Verify readonly configuration is present
604+
assert!(
605+
domain_xml.contains("<readonly/>"),
606+
"Domain XML should have readonly configuration for container storage"
607+
);
608+
609+
// Check metadata for bind-storage-ro configuration
610+
if domain_xml.contains("bootc:bind-storage-ro") {
611+
assert!(
612+
domain_xml.contains("<bootc:bind-storage-ro>true</bootc:bind-storage-ro>"),
613+
"Domain metadata should indicate bind-storage-ro is enabled"
614+
);
615+
}
616+
617+
println!("✓ Domain XML contains expected virtiofs configuration");
618+
println!("✓ Container storage mount is configured as read-only");
619+
println!("✓ hoststorage tag is present in filesystem configuration");
620+
621+
// Wait for VM to boot and SSH to become available
622+
println!("Waiting for VM to boot and SSH to become available...");
623+
std::thread::sleep(std::time::Duration::from_secs(45));
624+
625+
// Create mount point and mount virtiofs filesystem
626+
println!("Creating mount point and mounting virtiofs filesystem...");
627+
let mount_setup = Command::new("timeout")
628+
.args([
629+
"30s",
630+
&bck,
631+
"libvirt",
632+
"ssh",
633+
&domain_name,
634+
"--",
635+
"sudo",
636+
"mkdir",
637+
"-p",
638+
"/run/virtiofs-mnt-hoststorage",
639+
])
640+
.output()
641+
.expect("Failed to create mount point");
642+
643+
if !mount_setup.status.success() {
644+
let stderr = String::from_utf8_lossy(&mount_setup.stderr);
645+
println!("Warning: Failed to create mount point: {}", stderr);
646+
}
647+
648+
let mount_cmd = Command::new("timeout")
649+
.args([
650+
"30s",
651+
&bck,
652+
"libvirt",
653+
"ssh",
654+
&domain_name,
655+
"--",
656+
"sudo",
657+
"mount",
658+
"-t",
659+
"virtiofs",
660+
"hoststorage",
661+
"/run/virtiofs-mnt-hoststorage",
662+
])
663+
.output()
664+
.expect("Failed to mount virtiofs");
665+
666+
if !mount_cmd.status.success() {
667+
cleanup_domain(&domain_name);
668+
let stderr = String::from_utf8_lossy(&mount_cmd.stderr);
669+
panic!("Failed to mount virtiofs filesystem: {}", stderr);
670+
}
671+
672+
// Test SSH connection and verify container storage mount inside VM
673+
println!("Testing SSH connection and checking container storage mount...");
674+
let st = Command::new("timeout")
675+
.args([
676+
"60s",
677+
&bck,
678+
"libvirt",
679+
"ssh",
680+
&domain_name,
681+
"--",
682+
"ls",
683+
"-la",
684+
"/run/virtiofs-mnt-hoststorage/overlay",
685+
])
686+
.status()
687+
.expect("Failed to run SSH command to check container storage");
688+
689+
assert!(st.success());
690+
691+
// Cleanup domain before completing test
692+
cleanup_domain(&domain_name);
693+
694+
println!("✓ --bind-storage-ro end-to-end test passed");
695+
}
696+
514697
/// Test error handling for invalid configurations
515698
pub fn test_libvirt_error_handling() {
516699
let bck = get_bck_command().unwrap();

crates/kit/src/libvirt/domain.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ use color_eyre::{eyre::eyre, Result};
1010
use std::collections::HashMap;
1111
use uuid::Uuid;
1212

13+
/// Configuration for a virtiofs filesystem mount
14+
#[derive(Debug, Clone)]
15+
pub struct VirtiofsFilesystem {
16+
/// Host directory to share
17+
pub source_dir: String,
18+
/// Unique tag identifier for the filesystem
19+
pub tag: String,
20+
/// Whether the filesystem is read-only
21+
pub readonly: bool,
22+
}
23+
1324
/// Builder for creating libvirt domain XML configurations
1425
#[derive(Debug)]
1526
pub struct DomainBuilder {
@@ -23,6 +34,7 @@ pub struct DomainBuilder {
2334
kernel_args: Option<String>,
2435
metadata: HashMap<String, String>,
2536
qemu_args: Vec<String>,
37+
virtiofs_filesystems: Vec<VirtiofsFilesystem>,
2638
}
2739

2840
impl Default for DomainBuilder {
@@ -45,6 +57,7 @@ impl DomainBuilder {
4557
kernel_args: None,
4658
metadata: HashMap::new(),
4759
qemu_args: Vec::new(),
60+
virtiofs_filesystems: Vec::new(),
4861
}
4962
}
5063

@@ -102,6 +115,12 @@ impl DomainBuilder {
102115
self
103116
}
104117

118+
/// Add a virtiofs filesystem mount
119+
pub fn with_virtiofs_filesystem(mut self, filesystem: VirtiofsFilesystem) -> Self {
120+
self.virtiofs_filesystems.push(filesystem);
121+
self
122+
}
123+
105124
/// Build the domain XML
106125
pub fn build_xml(self) -> Result<String> {
107126
let name = self.name.ok_or_else(|| eyre!("Domain name is required"))?;
@@ -165,6 +184,15 @@ impl DomainBuilder {
165184

166185
xml.push_str("\n </os>");
167186

187+
// Add memory backing for shared memory support (required for virtiofs)
188+
xml.push_str(
189+
r#"
190+
<memoryBacking>
191+
<source type="memfd"/>
192+
<access mode="shared"/>
193+
</memoryBacking>"#,
194+
);
195+
168196
// Architecture-specific features
169197
xml.push_str(arch_config.xml_features());
170198

@@ -277,6 +305,28 @@ impl DomainBuilder {
277305
));
278306
}
279307

308+
// Virtiofs filesystems
309+
for filesystem in &self.virtiofs_filesystems {
310+
xml.push_str(&format!(
311+
r#"
312+
<filesystem type="mount" accessmode="passthrough">
313+
<driver type="virtiofs" queue="1024"/>
314+
<source dir="{}"/>
315+
<target dir="{}"/>"#,
316+
filesystem.source_dir, filesystem.tag
317+
));
318+
if filesystem.readonly {
319+
xml.push_str(
320+
r#"
321+
<readonly/>"#,
322+
);
323+
}
324+
xml.push_str(
325+
r#"
326+
</filesystem>"#,
327+
);
328+
}
329+
280330
xml.push_str("\n </devices>");
281331

282332
// QEMU commandline section (if we have QEMU args)

crates/kit/src/libvirt/run.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::hash::{Hash, Hasher};
1212

1313
use crate::common_opts::MemoryOpts;
1414
use crate::domain_list::DomainLister;
15+
use crate::libvirt::domain::VirtiofsFilesystem;
1516
use crate::utils::parse_memory_to_mb;
1617

1718
/// Options for creating and running a bootable container VM
@@ -58,6 +59,10 @@ pub struct LibvirtRunOpts {
5859
/// Automatically SSH into the VM after creation
5960
#[clap(long)]
6061
pub ssh: bool,
62+
63+
/// Mount host container storage (RO) at /run/virtiofs-mnt-hoststorage
64+
#[clap(long = "bind-storage-ro")]
65+
pub bind_storage_ro: bool,
6166
}
6267

6368
/// Execute the libvirt run command
@@ -376,7 +381,7 @@ fn create_libvirt_domain_from_disk(
376381
let memory = parse_memory_to_mb(&opts.memory.memory)?;
377382

378383
// Build domain XML using the existing DomainBuilder with bootc metadata and SSH keys
379-
let domain_xml = DomainBuilder::new()
384+
let mut domain_builder = DomainBuilder::new()
380385
.with_name(domain_name)
381386
.with_memory(memory.into())
382387
.with_vcpus(opts.cpus)
@@ -390,7 +395,33 @@ fn create_libvirt_domain_from_disk(
390395
.with_metadata("bootc:network", &opts.network)
391396
.with_metadata("bootc:ssh-generated", "true")
392397
.with_metadata("bootc:ssh-private-key-base64", &private_key_base64)
393-
.with_metadata("bootc:ssh-port", &ssh_port.to_string())
398+
.with_metadata("bootc:ssh-port", &ssh_port.to_string());
399+
400+
// Add container storage mount if requested
401+
if opts.bind_storage_ro {
402+
let storage_path = crate::utils::detect_container_storage_path()
403+
.context("Failed to detect container storage path.")?;
404+
crate::utils::validate_container_storage_path(&storage_path)
405+
.context("Container storage validation failed")?;
406+
407+
debug!(
408+
"Adding container storage from {} as hoststorage virtiofs mount",
409+
storage_path
410+
);
411+
412+
let virtiofs_fs = VirtiofsFilesystem {
413+
source_dir: storage_path.to_string(),
414+
tag: "hoststorage".to_string(),
415+
readonly: true,
416+
};
417+
418+
domain_builder = domain_builder
419+
.with_virtiofs_filesystem(virtiofs_fs)
420+
.with_metadata("bootc:bind-storage-ro", "true")
421+
.with_metadata("bootc:storage-path", storage_path.as_str());
422+
}
423+
424+
let domain_xml = domain_builder
394425
.with_qemu_args(vec![
395426
"-smbios".to_string(),
396427
format!("type=11,value={}", smbios_cred),

docs/src/libvirt-run.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,34 @@ bcvk libvirt run \
131131
- Persistent development state
132132
- Host integration capabilities
133133

134+
### Container Storage Integration
135+
136+
```bash
137+
# Create VM with access to host container storage for bootc upgrades
138+
bcvk libvirt run \
139+
--name upgrade-test \
140+
--bind-storage-ro \
141+
--ssh \
142+
quay.io/fedora/fedora-bootc:42
143+
```
144+
145+
With this, a virtiofs mount named `hoststorage` is provisioned. There isn't
146+
yet automatic mounting, but you can inject code to do so that performs
147+
`mkdir /run/hoststorage && mount -t virtiofs hoststorage /run/hoststorage`.
148+
149+
Then on your host system after you've done a `podman build` that results in a new image `localhost/bootc`,
150+
in the guest system you can point bootc to use it via e.g.
151+
```
152+
env STORAGE_OPTS=additionalimagestore=/run/hoststorage bootc switch --transport containers-storage localhost/bootc
153+
```
154+
155+
You currently need to add the `STORAGE_OPTS` each time you invoke `bootc` - but there after e.g.
156+
```
157+
env STORAGE_OPTS=additionalimagestore=/run/hoststorage bootc upgrade
158+
```
159+
160+
will work.
161+
134162
## Resource Management Concepts
135163

136164
### CPU Allocation

0 commit comments

Comments
 (0)