Skip to content

Commit ce3c9ff

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 ce3c9ff

6 files changed

Lines changed: 303 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: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,183 @@ 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+
// Check metadata for bind-storage-ro configuration
604+
if domain_xml.contains("bootc:bind-storage-ro") {
605+
assert!(
606+
domain_xml.contains("<bootc:bind-storage-ro>true</bootc:bind-storage-ro>"),
607+
"Domain metadata should indicate bind-storage-ro is enabled"
608+
);
609+
}
610+
611+
println!("✓ Domain XML contains expected virtiofs configuration");
612+
println!("✓ Container storage mount is configured as read-only");
613+
println!("✓ hoststorage tag is present in filesystem configuration");
614+
615+
// Wait for VM to boot and SSH to become available
616+
println!("Waiting for VM to boot and SSH to become available...");
617+
std::thread::sleep(std::time::Duration::from_secs(45));
618+
619+
// Create mount point and mount virtiofs filesystem
620+
println!("Creating mount point and mounting virtiofs filesystem...");
621+
let mount_setup = Command::new("timeout")
622+
.args([
623+
"30s",
624+
&bck,
625+
"libvirt",
626+
"ssh",
627+
&domain_name,
628+
"--",
629+
"sudo",
630+
"mkdir",
631+
"-p",
632+
"/run/virtiofs-mnt-hoststorage",
633+
])
634+
.output()
635+
.expect("Failed to create mount point");
636+
637+
if !mount_setup.status.success() {
638+
let stderr = String::from_utf8_lossy(&mount_setup.stderr);
639+
println!("Warning: Failed to create mount point: {}", stderr);
640+
}
641+
642+
let mount_cmd = Command::new("timeout")
643+
.args([
644+
"30s",
645+
&bck,
646+
"libvirt",
647+
"ssh",
648+
&domain_name,
649+
"--",
650+
"sudo",
651+
"mount",
652+
"-t",
653+
"virtiofs",
654+
"hoststorage",
655+
"/run/virtiofs-mnt-hoststorage",
656+
])
657+
.output()
658+
.expect("Failed to mount virtiofs");
659+
660+
if !mount_cmd.status.success() {
661+
cleanup_domain(&domain_name);
662+
let stderr = String::from_utf8_lossy(&mount_cmd.stderr);
663+
panic!("Failed to mount virtiofs filesystem: {}", stderr);
664+
}
665+
666+
// Test SSH connection and verify container storage mount inside VM
667+
println!("Testing SSH connection and checking container storage mount...");
668+
let st = Command::new("timeout")
669+
.args([
670+
"60s",
671+
&bck,
672+
"libvirt",
673+
"ssh",
674+
&domain_name,
675+
"--",
676+
"ls",
677+
"-la",
678+
"/run/virtiofs-mnt-hoststorage/overlay",
679+
])
680+
.status()
681+
.expect("Failed to run SSH command to check container storage");
682+
683+
assert!(st.success());
684+
685+
// Cleanup domain before completing test
686+
cleanup_domain(&domain_name);
687+
688+
println!("✓ --bind-storage-ro end-to-end test passed");
689+
}
690+
514691
/// Test error handling for invalid configurations
515692
pub fn test_libvirt_error_handling() {
516693
let bck = get_bck_command().unwrap();

crates/kit/src/libvirt/domain.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ 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+
/// TODO: change libvirt to detect this
22+
#[allow(dead_code)]
23+
pub readonly: bool,
24+
}
25+
1326
/// Builder for creating libvirt domain XML configurations
1427
#[derive(Debug)]
1528
pub struct DomainBuilder {
@@ -23,6 +36,7 @@ pub struct DomainBuilder {
2336
kernel_args: Option<String>,
2437
metadata: HashMap<String, String>,
2538
qemu_args: Vec<String>,
39+
virtiofs_filesystems: Vec<VirtiofsFilesystem>,
2640
}
2741

2842
impl Default for DomainBuilder {
@@ -45,6 +59,7 @@ impl DomainBuilder {
4559
kernel_args: None,
4660
metadata: HashMap::new(),
4761
qemu_args: Vec::new(),
62+
virtiofs_filesystems: Vec::new(),
4863
}
4964
}
5065

@@ -102,6 +117,12 @@ impl DomainBuilder {
102117
self
103118
}
104119

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

166187
xml.push_str("\n </os>");
167188

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

@@ -277,6 +307,29 @@ impl DomainBuilder {
277307
));
278308
}
279309

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

282335
// 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)