arcbox_computer_runtime/rootfs/
inject.rs1use super::RootfsBuilder;
4use anyhow::{Context, Result, bail};
5use std::path::Path;
6#[cfg(target_os = "linux")]
7use std::{path::PathBuf, sync::Arc};
8
9impl RootfsBuilder {
10 pub async fn inject_vm_agent(&self, image: &Path) -> crate::error::Result<()> {
15 let builder = Self::new(self.paths.clone(), self.block_tools.clone());
16 let image = image.to_owned();
17 let (reply, completed) = tokio::sync::oneshot::channel();
18 tokio::spawn(async move {
19 let result = builder.inject_agent(&image).await;
20 if let Err(Err(error)) = reply.send(result) {
21 tracing::error!(?error, "cancelled vm-agent injection failed");
22 }
23 });
24 completed
25 .await
26 .context("vm-agent injection task stopped")
27 .and_then(std::convert::identity)
28 .map_err(super::rootfs_err)
29 }
30
31 #[cfg(target_os = "linux")]
41 pub(super) async fn inject_agent(&self, image: &Path) -> Result<()> {
42 if !self.paths.vm_agent.exists() {
43 bail!("vm-agent not found at {}", self.paths.vm_agent.display());
44 }
45
46 let loop_dev = self
49 .attach_loop(image)
50 .await
51 .context("failed to attach ext4 image for vm-agent injection")?;
52 let mount_dir =
53 std::env::temp_dir().join(format!("arcbox-inject-{}", uuid::Uuid::new_v4()));
54 let staged = match tokio::fs::create_dir_all(&mount_dir).await {
55 Ok(()) => mount_ext4(&loop_dev, &mount_dir)
56 .await
57 .with_context(|| format!("mount {loop_dev} for vm-agent injection")),
58 Err(e) => Err(e).context("failed to create injection mount dir"),
59 };
60 if let Err(error) = staged {
61 let mut failures = Vec::new();
63 self.detach_loop(&loop_dev)
64 .await
65 .push_failure(&mut failures);
66 remove_mount_dir(&mount_dir)
67 .await
68 .push_failure(&mut failures);
69 return finish_cleanup(Err(error), failures);
70 }
71
72 let injected = self.write_agent_into(&mount_dir).await;
73
74 let mut failures = Vec::new();
77 unmount(&mount_dir).await.push_failure(&mut failures);
78 self.detach_loop(&loop_dev)
79 .await
80 .push_failure(&mut failures);
81 remove_mount_dir(&mount_dir)
82 .await
83 .push_failure(&mut failures);
84
85 finish_cleanup(injected, failures)
86 }
87
88 #[cfg(target_os = "linux")]
91 async fn write_agent_into(&self, mount_dir: &Path) -> Result<()> {
92 let sbin = mount_dir.join("sbin");
93 let dest = mount_dir.join(super::VM_AGENT_PATH.trim_start_matches('/'));
94 tokio::fs::create_dir_all(&sbin)
95 .await
96 .context("failed to create /sbin in rootfs")?;
97 tokio::fs::copy(&self.paths.vm_agent, &dest)
98 .await
99 .context("failed to copy vm-agent into rootfs")?;
100 tokio::fs::set_permissions(&dest, std::os::unix::fs::PermissionsExt::from_mode(0o755))
101 .await
102 .context("failed to chmod vm-agent in rootfs")?;
103
104 let etc = mount_dir.join("etc");
109 tokio::fs::create_dir_all(&etc)
110 .await
111 .context("create /etc in rootfs")?;
112 let resolv = etc.join("resolv.conf");
113 match tokio::fs::remove_file(&resolv).await {
114 Ok(()) => {}
115 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
116 Err(error) => return Err(error).context("replace /etc/resolv.conf in rootfs"),
117 }
118 tokio::fs::symlink("../run/resolv.conf", &resolv)
119 .await
120 .context("link /etc/resolv.conf into the run tmpfs")?;
121 Ok(())
122 }
123
124 #[cfg(not(target_os = "linux"))]
127 pub(super) async fn inject_agent(&self, _image: &Path) -> Result<()> {
128 bail!("vm-agent injection needs a Linux loop mount")
129 }
130
131 #[cfg(target_os = "linux")]
133 async fn attach_loop(&self, image: &Path) -> Result<String> {
134 let tools = Arc::clone(&self.block_tools);
135 let image = PathBuf::from(image);
136 tokio::task::spawn_blocking(move || tools.attach_loop(&image, false))
137 .await
138 .context("loop attach task panicked")?
139 .map_err(anyhow::Error::from)
140 }
141
142 #[cfg(target_os = "linux")]
148 async fn detach_loop(&self, loop_dev: &str) -> Result<()> {
149 let tools = Arc::clone(&self.block_tools);
150 let dev = loop_dev.to_owned();
151 tokio::task::spawn_blocking(move || tools.detach_loop(&dev))
152 .await
153 .context("loop detach task panicked")?
154 .with_context(|| format!("detach {loop_dev}"))
155 }
156}
157
158#[cfg(target_os = "linux")]
159async fn remove_mount_dir(dir: &Path) -> Result<()> {
160 match tokio::fs::remove_dir(dir).await {
161 Ok(()) => Ok(()),
162 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
164 Err(error) => Err(error).with_context(|| format!("remove mount dir {}", dir.display())),
165 }
166}
167
168#[cfg(target_os = "linux")]
169fn finish_cleanup(injected: Result<()>, failures: Vec<String>) -> Result<()> {
170 if failures.is_empty() {
171 return injected;
172 }
173 let cleanup = format!(
174 "vm-agent injection left resources behind: {}",
175 failures.join("; ")
176 );
177 tracing::error!(error = %cleanup, "vm-agent injection cleanup failed");
178 match injected {
179 Ok(()) => Err(anyhow::anyhow!(cleanup)),
180 Err(error) => Err(error.context(cleanup)),
181 }
182}
183
184#[cfg(target_os = "linux")]
187async fn mount_ext4(loop_dev: &str, dir: &Path) -> Result<()> {
188 use nix::mount::{MsFlags, mount};
189
190 let dev = loop_dev.to_owned();
191 let dir = dir.to_path_buf();
192 tokio::task::spawn_blocking(move || {
193 mount(
194 Some(dev.as_str()),
195 &dir,
196 Some("ext4"),
197 MsFlags::empty(),
198 None::<&str>,
199 )
200 })
201 .await
202 .context("mount task panicked")?
203 .map_err(anyhow::Error::from)
204}
205
206#[cfg(target_os = "linux")]
210async fn unmount(dir: &Path) -> Result<()> {
211 use nix::mount::{MntFlags, umount, umount2};
212
213 let target = dir.to_path_buf();
214 tokio::task::spawn_blocking(move || match umount(&target) {
215 Ok(()) => Ok(()),
216 Err(busy) => match umount2(&target, MntFlags::MNT_DETACH) {
220 Ok(()) => bail!(
221 "umount {}: {busy} (lazily detached instead)",
222 target.display()
223 ),
224 Err(lazy) => bail!("umount {}: {busy}; lazy unmount: {lazy}", target.display()),
225 },
226 })
227 .await
228 .context("umount task panicked")?
229}
230
231#[cfg(target_os = "linux")]
233trait PushFailure {
234 fn push_failure(self, failures: &mut Vec<String>);
235}
236
237#[cfg(target_os = "linux")]
238impl PushFailure for Result<()> {
239 fn push_failure(self, failures: &mut Vec<String>) {
240 if let Err(e) = self {
241 failures.push(format!("{e:#}"));
242 }
243 }
244}
245
246#[cfg(all(test, target_os = "linux"))]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn cleanup_preserves_the_injection_error_and_every_release_failure() {
252 let error = finish_cleanup(
253 Err(anyhow::anyhow!("agent copy failed")),
254 vec!["unmount failed".into(), "loop detach failed".into()],
255 )
256 .unwrap_err();
257 let details = format!("{error:#}");
258 assert!(details.contains("agent copy failed"), "{details}");
259 assert!(details.contains("unmount failed"), "{details}");
260 assert!(details.contains("loop detach failed"), "{details}");
261 }
262
263 #[test]
264 fn unreceived_injection_cleanup_errors_are_logged() {
265 if !super::super::tests::isolated_log_test(
266 "rootfs::inject::tests::unreceived_injection_cleanup_errors_are_logged",
267 ) {
268 return;
269 }
270 let log = tempfile::NamedTempFile::new().unwrap();
271 let writer = log.reopen().unwrap();
272 let subscriber = tracing_subscriber::fmt()
273 .with_ansi(false)
274 .without_time()
275 .with_writer(move || writer.try_clone().unwrap())
276 .finish();
277 tracing::subscriber::with_default(subscriber, || {
278 let result = finish_cleanup(Ok(()), vec!["loop detach failed".into()]);
279 let (reply, completed) = tokio::sync::oneshot::channel();
280 reply.send(result).unwrap();
281 drop(completed);
282 });
283 let logged = std::fs::read_to_string(log.path()).unwrap();
284 assert!(logged.contains("loop detach failed"), "{logged}");
285 }
286}