1 //! Helper to start a QEMU VM for single file restore.
2 use std
::fs
::{File, OpenOptions}
;
3 use std
::io
::prelude
::*;
4 use std
::os
::unix
::io
::AsRawFd
;
5 use std
::path
::PathBuf
;
6 use std
::time
::Duration
;
8 use anyhow
::{bail, format_err, Error}
;
11 use nix
::sys
::signal
::{kill, Signal}
;
14 use proxmox
::tools
::fs
::{create_path, file_read_string, make_tmp_file, CreateOptions}
;
15 use proxmox
::tools
::fd
::fd_change_cloexec
;
16 use proxmox_sys
::logrotate
::LogRotate
;
18 use pbs_client
::{VsockClient, DEFAULT_VSOCK_PORT}
;
20 use crate::{cpio, backup_user}
;
21 use super::SnapRestoreDetails
;
23 const PBS_VM_NAME
: &str = "pbs-restore-vm";
24 const MAX_CID_TRIES
: u64 = 32;
26 fn create_restore_log_dir() -> Result
<String
, Error
> {
27 let logpath
= format
!("{}/file-restore", pbs_buildcfg
::PROXMOX_BACKUP_LOG_DIR
);
29 proxmox_lang
::try_block
!({
30 let backup_user
= backup_user()?
;
31 let opts
= CreateOptions
::new()
32 .owner(backup_user
.uid
)
33 .group(backup_user
.gid
);
35 let opts_root
= CreateOptions
::new()
36 .owner(nix
::unistd
::ROOT
)
37 .group(nix
::unistd
::Gid
::from_raw(0));
39 create_path(pbs_buildcfg
::PROXMOX_BACKUP_LOG_DIR
, None
, Some(opts
))?
;
40 create_path(&logpath
, None
, Some(opts_root
))?
;
43 .map_err(|err
: Error
| format_err
!("unable to create file-restore log dir - {}", err
))?
;
48 fn validate_img_existance(debug
: bool
) -> Result
<(), Error
> {
49 let kernel
= PathBuf
::from(pbs_buildcfg
::PROXMOX_BACKUP_KERNEL_FN
);
50 let initramfs
= PathBuf
::from(if debug
{
51 pbs_buildcfg
::PROXMOX_BACKUP_INITRAMFS_DBG_FN
53 pbs_buildcfg
::PROXMOX_BACKUP_INITRAMFS_FN
55 if !kernel
.exists() || !initramfs
.exists() {
56 bail
!("cannot run file-restore VM: package 'proxmox-backup-restore-image' is not (correctly) installed");
61 pub fn try_kill_vm(pid
: i32) -> Result
<(), Error
> {
62 let pid
= Pid
::from_raw(pid
);
63 if let Ok(()) = kill(pid
, None
) {
64 // process is running (and we could kill it), check if it is actually ours
65 // (if it errors assume we raced with the process's death and ignore it)
66 if let Ok(cmdline
) = file_read_string(format
!("/proc/{}/cmdline", pid
)) {
67 if cmdline
.split('
\0'
).any(|a
| a
== PBS_VM_NAME
) {
68 // yes, it's ours, kill it brutally with SIGKILL, no reason to take
69 // any chances - in this state it's most likely broken anyway
70 if let Err(err
) = kill(pid
, Signal
::SIGKILL
) {
72 "reaping broken VM (pid {}) with SIGKILL failed: {}",
84 async
fn create_temp_initramfs(ticket
: &str, debug
: bool
) -> Result
<(File
, String
), Error
> {
85 use std
::ffi
::CString
;
88 let (tmp_file
, tmp_path
) =
89 make_tmp_file("/tmp/file-restore-qemu.initramfs.tmp", CreateOptions
::new())?
;
90 nix
::unistd
::unlink(&tmp_path
)?
;
91 fd_change_cloexec(tmp_file
.as_raw_fd(), false)?
;
93 let initramfs
= if debug
{
94 pbs_buildcfg
::PROXMOX_BACKUP_INITRAMFS_DBG_FN
96 pbs_buildcfg
::PROXMOX_BACKUP_INITRAMFS_FN
99 let mut f
= File
::from_std(tmp_file
);
100 let mut base
= File
::open(initramfs
).await?
;
102 tokio
::io
::copy(&mut base
, &mut f
).await?
;
104 let name
= CString
::new("ticket").unwrap();
110 (libc
::S_IFREG
| 0o400) as u16,
117 cpio
::append_trailer(&mut f
).await?
;
119 let tmp_file
= f
.into_std().await
;
120 let path
= format
!("/dev/fd/{}", &tmp_file
.as_raw_fd());
125 pub async
fn start_vm(
126 // u16 so we can do wrapping_add without going too high
128 details
: &SnapRestoreDetails
,
129 files
: impl Iterator
<Item
= String
>,
131 ) -> Result
<(i32, i32), Error
> {
132 if let Err(_
) = std
::env
::var("PBS_PASSWORD") {
133 bail
!("environment variable PBS_PASSWORD has to be set for QEMU VM restore");
136 let debug
= if let Ok(val
) = std
::env
::var("PBS_QEMU_DEBUG") {
142 validate_img_existance(debug
)?
;
145 let (mut pid_file
, pid_path
) = make_tmp_file("/tmp/file-restore-qemu.pid.tmp", CreateOptions
::new())?
;
146 nix
::unistd
::unlink(&pid_path
)?
;
147 fd_change_cloexec(pid_file
.as_raw_fd(), false)?
;
149 let (_ramfs_pid
, ramfs_path
) = create_temp_initramfs(ticket
, debug
).await?
;
151 let logpath
= create_restore_log_dir()?
;
152 let logfile
= &format
!("{}/qemu.log", logpath
);
153 let mut logrotate
= LogRotate
::new(logfile
, false, Some(16), None
)?
;
155 if let Err(err
) = logrotate
.do_rotate() {
156 eprintln
!("warning: logrotate for QEMU log file failed - {}", err
);
159 let mut logfd
= OpenOptions
::new()
163 fd_change_cloexec(logfd
.as_raw_fd(), false)?
;
165 // preface log file with start timestamp so one can see how long QEMU took to start
166 writeln
!(logfd
, "[{}] PBS file restore VM log", {
167 let now
= proxmox_time
::epoch_i64();
168 proxmox_time
::epoch_to_rfc3339(now
)?
174 "file,id=log,path=/dev/null,logfile=/dev/fd/{},logappend=on",
183 pbs_buildcfg
::PROXMOX_BACKUP_KERNEL_FN
,
188 "{} panic=1 zfs_arc_min=0 zfs_arc_max=0",
189 if debug { "debug" }
else { "quiet" }
193 &format
!("/dev/fd/{}", pid_file
.as_raw_fd()),
198 // Generate drive arguments for all fidx files in backup snapshot
199 let mut drives
= Vec
::new();
202 if !file
.ends_with(".img.fidx") {
205 drives
.push("-drive".to_owned());
206 let keyfile
= if let Some(ref keyfile
) = details
.keyfile
{
207 format
!(",,keyfile={}", keyfile
)
212 "file=pbs:repository={},,snapshot={},,archive={}{},read-only=on,if=none,id=drive{}",
213 details
.repo
, details
.snapshot
, file
, keyfile
, id
216 // a PCI bus can only support 32 devices, so add a new one every 32
217 let bus
= (id
/ 32) + 2;
219 drives
.push("-device".to_owned());
220 drives
.push(format
!("pci-bridge,id=bridge{},chassis_nr={}", bus
, bus
));
223 drives
.push("-device".to_owned());
224 // drive serial is used by VM to map .fidx files to /dev paths
225 let serial
= file
.strip_suffix(".img.fidx").unwrap_or(&file
);
227 "virtio-blk-pci,drive=drive{},serial={},bus=bridge{}",
236 // add more RAM if many drives are given
244 // Try starting QEMU in a loop to retry if we fail because of a bad 'cid' value
245 let mut attempts
= 0;
247 let mut qemu_cmd
= std
::process
::Command
::new("qemu-system-x86_64");
248 qemu_cmd
.args(base_args
.iter());
250 qemu_cmd
.arg(ram
.to_string());
251 qemu_cmd
.args(&drives
);
252 qemu_cmd
.arg("-device");
253 qemu_cmd
.arg(format
!(
254 "vhost-vsock-pci,guest-cid={},disable-legacy=on",
262 "socket,id=debugser,path=/run/proxmox-backup/file-restore-serial-{}.sock,server,nowait",
268 qemu_cmd
.args(debug_args
.iter());
271 qemu_cmd
.stdout(std
::process
::Stdio
::null());
272 qemu_cmd
.stderr(std
::process
::Stdio
::piped());
274 let res
= tokio
::task
::block_in_place(|| qemu_cmd
.spawn()?
.wait_with_output())?
;
276 if res
.status
.success() {
277 // at this point QEMU is already daemonized and running, so if anything fails we
278 // technically leave behind a zombie-VM... this shouldn't matter, as it will stop
279 // itself soon enough (timer), and the following operations are unlikely to fail
280 let mut pidstr
= String
::new();
281 pid_file
.read_to_string(&mut pidstr
)?
;
282 pid
= pidstr
.trim_end().parse().map_err(|err
| {
283 format_err
!("cannot parse PID returned by QEMU ('{}'): {}", &pidstr
, err
)
287 let out
= String
::from_utf8_lossy(&res
.stderr
);
288 if out
.contains("unable to set guest cid: Address already in use") {
290 if attempts
>= MAX_CID_TRIES
{
291 bail
!("CID '{}' in use, but max attempts reached, aborting", cid
);
293 // CID in use, try next higher one
294 eprintln
!("CID '{}' in use by other VM, attempting next one", cid
);
295 // skip special-meaning low values
296 cid
= cid
.wrapping_add(1).max(10);
299 bail
!("Starting VM failed. See output above for more information.");
304 // QEMU has started successfully, now wait for virtio socket to become ready
305 let pid_t
= Pid
::from_raw(pid
);
307 let client
= VsockClient
::new(cid
as i32, DEFAULT_VSOCK_PORT
, Some(ticket
.to_owned()));
309 time
::timeout(Duration
::from_secs(2), client
.get("api2/json/status", None
)).await
313 "Connect to '/run/proxmox-backup/file-restore-serial-{}.sock' for shell access",
317 return Ok((pid
, cid
as i32));
319 if kill(pid_t
, None
).is_err() { // check if QEMU process exited in between
320 bail
!("VM exited before connection could be established");
322 time
::sleep(Duration
::from_millis(200)).await
;
326 if let Err(err
) = try_kill_vm(pid
) {
327 eprintln
!("killing failed VM failed: {}", err
);
329 bail
!("starting VM timed out");