Skip to content

Commit 58bbcf0

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 <[email protected]>
1 parent 37d3b7a commit 58bbcf0

File tree

6 files changed

+286
-2
lines changed

6 files changed

+286
-2
lines changed

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ use color_eyre::{eyre::eyre, Result};
1111
use std::collections::HashMap;
1212
use uuid::Uuid;
1313

14+
/// Configuration for a virtiofs filesystem mount
15+
#[derive(Debug, Clone)]
16+
pub struct VirtiofsFilesystem {
17+
/// Host directory to share
18+
pub source_dir: String,
19+
/// Unique tag identifier for the filesystem
20+
pub tag: String,
21+
/// Whether the filesystem is read-only
22+
/// TODO: change libvirt to detect this
23+
#[allow(dead_code)]
24+
pub readonly: bool,
25+
}
26+
1427
/// Builder for creating libvirt domain XML configurations
1528
#[derive(Debug)]
1629
pub struct DomainBuilder {
@@ -24,6 +37,7 @@ pub struct DomainBuilder {
2437
kernel_args: Option<String>,
2538
metadata: HashMap<String, String>,
2639
qemu_args: Vec<String>,
40+
virtiofs_filesystems: Vec<VirtiofsFilesystem>,
2741
}
2842

2943
impl Default for DomainBuilder {
@@ -46,6 +60,7 @@ impl DomainBuilder {
4660
kernel_args: None,
4761
metadata: HashMap::new(),
4862
qemu_args: Vec::new(),
63+
virtiofs_filesystems: Vec::new(),
4964
}
5065
}
5166

@@ -103,6 +118,12 @@ impl DomainBuilder {
103118
self
104119
}
105120

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

161182
writer.end_element("os")?;
162183

184+
// Add memory backing for shared memory support (required for virtiofs)
185+
writer.start_element("memoryBacking", &[])?;
186+
writer.write_empty_element("source", &[("type", "memfd")])?;
187+
writer.write_empty_element("access", &[("mode", "shared")])?;
188+
writer.end_element("memoryBacking")?;
189+
163190
// Architecture-specific features
164191
arch_config.write_features(&mut writer)?;
165192

@@ -243,6 +270,15 @@ impl DomainBuilder {
243270
writer.end_element("video")?;
244271
}
245272

273+
// Virtiofs filesystems
274+
for filesystem in &self.virtiofs_filesystems {
275+
writer.start_element("filesystem", &[("type", "mount"), ("accessmode", "passthrough")])?;
276+
writer.write_empty_element("driver", &[("type", "virtiofs"), ("queue", "1024")])?;
277+
writer.write_empty_element("source", &[("dir", &filesystem.source_dir)])?;
278+
writer.write_empty_element("target", &[("dir", &filesystem.tag)])?;
279+
writer.end_element("filesystem")?;
280+
}
281+
246282
writer.end_element("devices")?;
247283

248284
// 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
use crate::xml_utils;
1718

@@ -59,6 +60,10 @@ pub struct LibvirtRunOpts {
5960
/// Automatically SSH into the VM after creation
6061
#[clap(long)]
6162
pub ssh: bool,
63+
64+
/// Mount host container storage (RO) at /run/virtiofs-mnt-hoststorage
65+
#[clap(long = "bind-storage-ro")]
66+
pub bind_storage_ro: bool,
6267
}
6368

6469
/// Execute the libvirt run command
@@ -374,7 +379,7 @@ fn create_libvirt_domain_from_disk(
374379
let memory = parse_memory_to_mb(&opts.memory.memory)?;
375380

376381
// Build domain XML using the existing DomainBuilder with bootc metadata and SSH keys
377-
let domain_xml = DomainBuilder::new()
382+
let mut domain_builder = DomainBuilder::new()
378383
.with_name(domain_name)
379384
.with_memory(memory.into())
380385
.with_vcpus(opts.cpus)
@@ -388,7 +393,33 @@ fn create_libvirt_domain_from_disk(
388393
.with_metadata("bootc:network", &opts.network)
389394
.with_metadata("bootc:ssh-generated", "true")
390395
.with_metadata("bootc:ssh-private-key-base64", &private_key_base64)
391-
.with_metadata("bootc:ssh-port", &ssh_port.to_string())
396+
.with_metadata("bootc:ssh-port", &ssh_port.to_string());
397+
398+
// Add container storage mount if requested
399+
if opts.bind_storage_ro {
400+
let storage_path = crate::utils::detect_container_storage_path()
401+
.context("Failed to detect container storage path.")?;
402+
crate::utils::validate_container_storage_path(&storage_path)
403+
.context("Container storage validation failed")?;
404+
405+
debug!(
406+
"Adding container storage from {} as hoststorage virtiofs mount",
407+
storage_path
408+
);
409+
410+
let virtiofs_fs = VirtiofsFilesystem {
411+
source_dir: storage_path.to_string(),
412+
tag: "hoststorage".to_string(),
413+
readonly: true,
414+
};
415+
416+
domain_builder = domain_builder
417+
.with_virtiofs_filesystem(virtiofs_fs)
418+
.with_metadata("bootc:bind-storage-ro", "true")
419+
.with_metadata("bootc:storage-path", storage_path.as_str());
420+
}
421+
422+
let domain_xml = domain_builder
392423
.with_qemu_args(vec![
393424
"-smbios".to_string(),
394425
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

docs/src/man/bcvk-libvirt-run.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ Run a bootable container as a persistent VM
6969

7070
Automatically SSH into the VM after creation
7171

72+
**--bind-storage-ro**
73+
74+
Mount host container storage (RO) at /run/virtiofs-mnt-hoststorage
75+
7276
<!-- END GENERATED OPTIONS -->
7377

7478
# EXAMPLES
@@ -93,6 +97,10 @@ Create a VM and automatically SSH into it:
9397

9498
bcvk libvirt run --name testvm --ssh quay.io/fedora/fedora-bootc:42
9599

100+
Create a VM with access to host container storage for bootc upgrade:
101+
102+
bcvk libvirt run --name upgrade-test --bind-storage-ro quay.io/fedora/fedora-bootc:42
103+
96104
Server management workflow:
97105

98106
# Create a persistent server VM

0 commit comments

Comments
 (0)