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}
;
16 use pbs_client
::{VsockClient, DEFAULT_VSOCK_PORT}
;
18 use proxmox_backup
::backup
::backup_user
;
19 use proxmox_backup
::tools
;
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
);
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 tools
::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();
105 tools
::cpio
::append_file(
110 (libc
::S_IFREG
| 0o400) as u16,
117 tools
::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 tools
::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
= tools
::logrotate
::LogRotate
::new(logfile
, false)
154 .ok_or_else(|| format_err
!("could not get QEMU log file names"))?
;
156 if let Err(err
) = logrotate
.do_rotate(CreateOptions
::default(), Some(16)) {
157 eprintln
!("warning: logrotate for QEMU log file failed - {}", err
);
160 let mut logfd
= OpenOptions
::new()
164 tools
::fd_change_cloexec(logfd
.as_raw_fd(), false)?
;
166 // preface log file with start timestamp so one can see how long QEMU took to start
167 writeln
!(logfd
, "[{}] PBS file restore VM log", {
168 let now
= proxmox
::tools
::time
::epoch_i64();
169 proxmox
::tools
::time
::epoch_to_rfc3339(now
)?
175 "file,id=log,path=/dev/null,logfile=/dev/fd/{},logappend=on",
184 pbs_buildcfg
::PROXMOX_BACKUP_KERNEL_FN
,
189 "{} panic=1 zfs_arc_min=0 zfs_arc_max=0",
190 if debug { "debug" }
else { "quiet" }
194 &format
!("/dev/fd/{}", pid_file
.as_raw_fd()),
199 // Generate drive arguments for all fidx files in backup snapshot
200 let mut drives
= Vec
::new();
203 if !file
.ends_with(".img.fidx") {
206 drives
.push("-drive".to_owned());
207 let keyfile
= if let Some(ref keyfile
) = details
.keyfile
{
208 format
!(",,keyfile={}", keyfile
)
213 "file=pbs:repository={},,snapshot={},,archive={}{},read-only=on,if=none,id=drive{}",
214 details
.repo
, details
.snapshot
, file
, keyfile
, id
217 // a PCI bus can only support 32 devices, so add a new one every 32
218 let bus
= (id
/ 32) + 2;
220 drives
.push("-device".to_owned());
221 drives
.push(format
!("pci-bridge,id=bridge{},chassis_nr={}", bus
, bus
));
224 drives
.push("-device".to_owned());
225 // drive serial is used by VM to map .fidx files to /dev paths
226 let serial
= file
.strip_suffix(".img.fidx").unwrap_or(&file
);
228 "virtio-blk-pci,drive=drive{},serial={},bus=bridge{}",
237 // add more RAM if many drives are given
245 // Try starting QEMU in a loop to retry if we fail because of a bad 'cid' value
246 let mut attempts
= 0;
248 let mut qemu_cmd
= std
::process
::Command
::new("qemu-system-x86_64");
249 qemu_cmd
.args(base_args
.iter());
251 qemu_cmd
.arg(ram
.to_string());
252 qemu_cmd
.args(&drives
);
253 qemu_cmd
.arg("-device");
254 qemu_cmd
.arg(format
!(
255 "vhost-vsock-pci,guest-cid={},disable-legacy=on",
263 "socket,id=debugser,path=/run/proxmox-backup/file-restore-serial-{}.sock,server,nowait",
269 qemu_cmd
.args(debug_args
.iter());
272 qemu_cmd
.stdout(std
::process
::Stdio
::null());
273 qemu_cmd
.stderr(std
::process
::Stdio
::piped());
275 let res
= tokio
::task
::block_in_place(|| qemu_cmd
.spawn()?
.wait_with_output())?
;
277 if res
.status
.success() {
278 // at this point QEMU is already daemonized and running, so if anything fails we
279 // technically leave behind a zombie-VM... this shouldn't matter, as it will stop
280 // itself soon enough (timer), and the following operations are unlikely to fail
281 let mut pidstr
= String
::new();
282 pid_file
.read_to_string(&mut pidstr
)?
;
283 pid
= pidstr
.trim_end().parse().map_err(|err
| {
284 format_err
!("cannot parse PID returned by QEMU ('{}'): {}", &pidstr
, err
)
288 let out
= String
::from_utf8_lossy(&res
.stderr
);
289 if out
.contains("unable to set guest cid: Address already in use") {
291 if attempts
>= MAX_CID_TRIES
{
292 bail
!("CID '{}' in use, but max attempts reached, aborting", cid
);
294 // CID in use, try next higher one
295 eprintln
!("CID '{}' in use by other VM, attempting next one", cid
);
296 // skip special-meaning low values
297 cid
= cid
.wrapping_add(1).max(10);
300 bail
!("Starting VM failed. See output above for more information.");
305 // QEMU has started successfully, now wait for virtio socket to become ready
306 let pid_t
= Pid
::from_raw(pid
);
308 let client
= VsockClient
::new(cid
as i32, DEFAULT_VSOCK_PORT
, Some(ticket
.to_owned()));
310 time
::timeout(Duration
::from_secs(2), client
.get("api2/json/status", None
)).await
314 "Connect to '/run/proxmox-backup/file-restore-serial-{}.sock' for shell access",
318 return Ok((pid
, cid
as i32));
320 if kill(pid_t
, None
).is_err() { // check if QEMU process exited in between
321 bail
!("VM exited before connection could be established");
323 time
::sleep(Duration
::from_millis(200)).await
;
327 if let Err(err
) = try_kill_vm(pid
) {
328 eprintln
!("killing failed VM failed: {}", err
);
330 bail
!("starting VM timed out");