public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Filip Schauer <f.schauer@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH v4 vma-to-pbs 5/6] Add support for streaming the VMA file via stdin
Date: Tue,  5 Mar 2024 14:56:44 +0100	[thread overview]
Message-ID: <20240305135645.96347-6-f.schauer@proxmox.com> (raw)
In-Reply-To: <20240305135645.96347-1-f.schauer@proxmox.com>

This allows the user to stream a compressed VMA file directly to a PBS
without the need to extract it to an intermediate .vma file.

Example usage:

zstd -d --stdout vzdump.vma.zstd | vma-to-pbs \
    --repository <auth_id@host:port:datastore> \
    --vmid 123 \
    --password_file pbs_password

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 src/main.rs    | 232 +--------------------------------
 src/vma.rs     | 270 ++++++++++++++------------------------
 src/vma2pbs.rs | 348 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 455 insertions(+), 395 deletions(-)
 create mode 100644 src/vma2pbs.rs

diff --git a/src/main.rs b/src/main.rs
index b58387b..dc8171e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,235 +1,14 @@
 extern crate anyhow;
 extern crate clap;
-extern crate proxmox_backup_qemu;
-extern crate proxmox_io;
 extern crate proxmox_sys;
-extern crate scopeguard;
 
-use std::env;
-use std::ffi::{c_char, CStr, CString};
-use std::ptr;
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use anyhow::{anyhow, Context, Result};
+use anyhow::Result;
 use clap::{command, Arg, ArgAction};
-use proxmox_backup_qemu::*;
 use proxmox_sys::linux::tty;
-use scopeguard::defer;
 
 mod vma;
-use vma::*;
-
-fn backup_vma_to_pbs(
-    vma_file_path: String,
-    pbs_repository: String,
-    backup_id: String,
-    pbs_password: String,
-    keyfile: Option<String>,
-    key_password: Option<String>,
-    master_keyfile: Option<String>,
-    fingerprint: String,
-    compress: bool,
-    encrypt: bool,
-) -> Result<()> {
-    println!("VMA input file: {}", vma_file_path);
-    println!("PBS repository: {}", pbs_repository);
-    println!("PBS fingerprint: {}", fingerprint);
-    println!("compress: {}", compress);
-    println!("encrypt: {}", encrypt);
-
-    let backup_time = SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .unwrap()
-        .as_secs();
-    println!("backup time: {}", backup_time);
-
-    let mut pbs_err: *mut c_char = ptr::null_mut();
-
-    let pbs_repository_cstr = CString::new(pbs_repository).unwrap();
-    let backup_id_cstr = CString::new(backup_id).unwrap();
-    let pbs_password_cstr = CString::new(pbs_password).unwrap();
-    let fingerprint_cstr = CString::new(fingerprint).unwrap();
-    let keyfile_cstr = keyfile.map(|v| CString::new(v).unwrap());
-    let keyfile_ptr = keyfile_cstr.map(|v| v.as_ptr()).unwrap_or(ptr::null());
-    let key_password_cstr = key_password.map(|v| CString::new(v).unwrap());
-    let key_password_ptr = key_password_cstr.map(|v| v.as_ptr()).unwrap_or(ptr::null());
-    let master_keyfile_cstr = master_keyfile.map(|v| CString::new(v).unwrap());
-    let master_keyfile_ptr = master_keyfile_cstr
-        .map(|v| v.as_ptr())
-        .unwrap_or(ptr::null());
-
-    let pbs = proxmox_backup_new_ns(
-        pbs_repository_cstr.as_ptr(),
-        ptr::null(),
-        backup_id_cstr.as_ptr(),
-        backup_time,
-        PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE,
-        pbs_password_cstr.as_ptr(),
-        keyfile_ptr,
-        key_password_ptr,
-        master_keyfile_ptr,
-        true,
-        false,
-        fingerprint_cstr.as_ptr(),
-        &mut pbs_err,
-    );
-
-    defer! {
-        proxmox_backup_disconnect(pbs);
-    }
-
-    if pbs == ptr::null_mut() {
-        unsafe {
-            let pbs_err_cstr = CStr::from_ptr(pbs_err);
-            return Err(anyhow!("proxmox_backup_new_ns failed: {pbs_err_cstr:?}"));
-        }
-    }
-
-    let connect_result = proxmox_backup_connect(pbs, &mut pbs_err);
-
-    if connect_result < 0 {
-        unsafe {
-            let pbs_err_cstr = CStr::from_ptr(pbs_err);
-            return Err(anyhow!("proxmox_backup_connect failed: {pbs_err_cstr:?}"));
-        }
-    }
-
-    let mut vma_reader = VmaReader::new(&vma_file_path)?;
-
-    // Handle configs
-    let configs = vma_reader.get_configs();
-    for (config_name, config_data) in configs {
-        println!("CFG: size: {} name: {}", config_data.len(), config_name);
-
-        let config_name_cstr = CString::new(config_name).unwrap();
-
-        if proxmox_backup_add_config(
-            pbs,
-            config_name_cstr.as_ptr(),
-            config_data.as_ptr(),
-            config_data.len() as u64,
-            &mut pbs_err,
-        ) < 0
-        {
-            unsafe {
-                let pbs_err_cstr = CStr::from_ptr(pbs_err);
-                return Err(anyhow!(
-                    "proxmox_backup_add_config failed: {pbs_err_cstr:?}"
-                ));
-            }
-        }
-    }
-
-    // Handle block devices
-    for device_id in 0..255 {
-        let device_name = match vma_reader.get_device_name(device_id) {
-            Some(x) => x,
-            None => {
-                continue;
-            }
-        };
-
-        let device_size = match vma_reader.get_device_size(device_id) {
-            Some(x) => x,
-            None => {
-                continue;
-            }
-        };
-
-        println!(
-            "DEV: dev_id={} size: {} devname: {}",
-            device_id, device_size, device_name
-        );
-
-        let device_name_cstr = CString::new(device_name).unwrap();
-        let pbs_device_id = proxmox_backup_register_image(
-            pbs,
-            device_name_cstr.as_ptr(),
-            device_size,
-            false,
-            &mut pbs_err,
-        );
-
-        if pbs_device_id < 0 {
-            unsafe {
-                let pbs_err_cstr = CStr::from_ptr(pbs_err);
-                return Err(anyhow!(
-                    "proxmox_backup_register_image failed: {pbs_err_cstr:?}"
-                ));
-            }
-        }
-
-        let mut image_chunk_buffer =
-            proxmox_io::boxed::zeroed(PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE as usize);
-        let mut bytes_transferred = 0;
-
-        while bytes_transferred < device_size {
-            let bytes_left = device_size - bytes_transferred;
-            let chunk_size = bytes_left.min(PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE);
-            println!(
-                "Uploading dev_id: {} offset: {:#0X} - {:#0X}",
-                device_id,
-                bytes_transferred,
-                bytes_transferred + chunk_size
-            );
-
-            let is_zero_chunk = vma_reader
-                .read_device_contents(
-                    device_id,
-                    &mut image_chunk_buffer[0..chunk_size as usize],
-                    bytes_transferred,
-                )
-                .with_context(|| {
-                    format!(
-                        "read {} bytes at offset {} from disk {} from VMA file",
-                        chunk_size, bytes_transferred, device_id
-                    )
-                })?;
-
-            let write_data_result = proxmox_backup_write_data(
-                pbs,
-                pbs_device_id as u8,
-                if is_zero_chunk {
-                    ptr::null()
-                } else {
-                    image_chunk_buffer.as_ptr()
-                },
-                bytes_transferred,
-                chunk_size,
-                &mut pbs_err,
-            );
-
-            if write_data_result < 0 {
-                unsafe {
-                    let pbs_err_cstr = CStr::from_ptr(pbs_err);
-                    return Err(anyhow!(
-                        "proxmox_backup_write_data failed: {pbs_err_cstr:?}"
-                    ));
-                }
-            }
-
-            bytes_transferred += chunk_size;
-        }
-
-        if proxmox_backup_close_image(pbs, pbs_device_id as u8, &mut pbs_err) < 0 {
-            unsafe {
-                let pbs_err_cstr = CStr::from_ptr(pbs_err);
-                return Err(anyhow!(
-                    "proxmox_backup_close_image failed: {pbs_err_cstr:?}"
-                ));
-            }
-        }
-    }
-
-    if proxmox_backup_finish(pbs, &mut pbs_err) < 0 {
-        unsafe {
-            let pbs_err_cstr = CStr::from_ptr(pbs_err);
-            return Err(anyhow!("proxmox_backup_finish failed: {pbs_err_cstr:?}"));
-        }
-    }
-
-    Ok(())
-}
+mod vma2pbs;
+use vma2pbs::backup_vma_to_pbs;
 
 fn main() -> Result<()> {
     let matches = command!()
@@ -307,7 +86,8 @@ fn main() -> Result<()> {
     let compress = matches.get_flag("compress");
     let encrypt = matches.get_flag("encrypt");
 
-    let vma_file_path = matches.get_one::<String>("vma_file").unwrap().to_string();
+    let vma_file_path = matches.get_one::<String>("vma_file");
+
     let password_file = matches.get_one::<String>("password_file");
 
     let pbs_password = match password_file {
@@ -331,7 +111,7 @@ fn main() -> Result<()> {
     };
 
     backup_vma_to_pbs(
-        vma_file_path,
+        vma_file_path.cloned(),
         pbs_repository,
         vmid,
         pbs_password,
diff --git a/src/vma.rs b/src/vma.rs
index 5ec3822..de3045d 100644
--- a/src/vma.rs
+++ b/src/vma.rs
@@ -2,10 +2,9 @@ extern crate anyhow;
 extern crate md5;
 
 use std::collections::HashMap;
-use std::fs::File;
-use std::io::{Read, Seek, SeekFrom};
+use std::io::Read;
 use std::mem::size_of;
-use std::{cmp, str};
+use std::str;
 
 use anyhow::{anyhow, Result};
 use bincode::Options;
@@ -45,6 +44,8 @@ struct VmaHeader {
     reserved1: [u8; 4],
     #[serde(with = "BigArray")]
     pub dev_info: [VmaDeviceInfoHeader; VMA_MAX_DEVICES],
+    #[serde(skip_deserializing, skip_serializing)]
+    blob_buffer: Vec<u8>,
 }
 
 #[repr(C)]
@@ -68,52 +69,35 @@ struct VmaExtentHeader {
     pub blockinfo: [VmaBlockInfo; VMA_BLOCKS_PER_EXTENT],
 }
 
-#[derive(Clone)]
-struct VmaBlockIndexEntry {
-    pub cluster_file_offset: u64,
-    pub mask: u16,
-}
-
 pub struct VmaReader {
-    vma_file: File,
+    vma_file: Box<dyn Read>,
     vma_header: VmaHeader,
     configs: HashMap<String, String>,
-    block_index: Vec<Vec<VmaBlockIndexEntry>>,
-    blocks_are_indexed: bool,
 }
 
 impl VmaReader {
-    pub fn new(vma_file_path: &str) -> Result<Self> {
-        let mut vma_file = match File::open(vma_file_path) {
-            Err(why) => return Err(anyhow!("couldn't open {}: {}", vma_file_path, why)),
-            Ok(file) => file,
-        };
-
-        let vma_header = Self::read_header(&mut vma_file).unwrap();
-        let configs = Self::read_blob_buffer(&mut vma_file, &vma_header).unwrap();
-        let block_index: Vec<Vec<VmaBlockIndexEntry>> = (0..256).map(|_| Vec::new()).collect();
+    pub fn new(mut vma_file: impl Read + 'static) -> Result<Self> {
+        let vma_header = Self::read_header(&mut vma_file)?;
+        let configs = Self::read_configs(&vma_header)?;
 
         let instance = Self {
-            vma_file,
+            vma_file: Box::new(vma_file),
             vma_header,
             configs,
-            block_index,
-            blocks_are_indexed: false,
         };
 
         Ok(instance)
     }
 
-    fn read_header(vma_file: &mut File) -> Result<VmaHeader> {
-        let mut buffer = Vec::with_capacity(size_of::<VmaHeader>());
-        buffer.resize(size_of::<VmaHeader>(), 0);
+    fn read_header(vma_file: &mut impl Read) -> Result<VmaHeader> {
+        let mut buffer = vec![0; 12288];
         vma_file.read_exact(&mut buffer)?;
 
         let bincode_options = bincode::DefaultOptions::new()
             .with_fixint_encoding()
             .with_big_endian();
 
-        let vma_header: VmaHeader = bincode_options.deserialize(&buffer)?;
+        let mut vma_header: VmaHeader = bincode_options.deserialize(&buffer)?;
 
         if vma_header.magic != [b'V', b'M', b'A', 0] {
             return Err(anyhow!("Invalid magic number"));
@@ -124,7 +108,7 @@ impl VmaReader {
         }
 
         buffer.resize(vma_header.header_size as usize, 0);
-        vma_file.read_exact(&mut buffer[size_of::<VmaHeader>()..])?;
+        vma_file.read_exact(&mut buffer[12288..])?;
 
         // Fill the MD5 sum field with zeros to compute the MD5 sum
         buffer[32..48].fill(0);
@@ -134,89 +118,77 @@ impl VmaReader {
             return Err(anyhow!("Wrong VMA header checksum"));
         }
 
-        return Ok(vma_header);
-    }
-
-    fn read_string_from_file(vma_file: &mut File, file_offset: u64) -> Result<String> {
-        let mut size_bytes = [0u8; 2];
-        vma_file.seek(SeekFrom::Start(file_offset))?;
-        vma_file.read_exact(&mut size_bytes)?;
-        let size = u16::from_le_bytes(size_bytes) as usize;
-        let mut string_bytes = Vec::with_capacity(size - 1);
-        string_bytes.resize(size - 1, 0);
-        vma_file.read_exact(&mut string_bytes)?;
-        let string = str::from_utf8(&string_bytes)?;
+        let blob_buffer = &buffer[12288..vma_header.header_size as usize];
+        vma_header.blob_buffer = blob_buffer.to_vec();
 
-        return Ok(string.to_string());
+        Ok(vma_header)
     }
 
-    fn read_blob_buffer(
-        vma_file: &mut File,
-        vma_header: &VmaHeader,
-    ) -> Result<HashMap<String, String>> {
+    fn read_configs(vma_header: &VmaHeader) -> Result<HashMap<String, String>> {
         let mut configs = HashMap::new();
 
         for i in 0..VMA_MAX_CONFIGS {
-            let config_name_offset = vma_header.config_names[i];
-            let config_data_offset = vma_header.config_data[i];
+            let config_name_offset = vma_header.config_names[i] as usize;
+            let config_data_offset = vma_header.config_data[i] as usize;
 
             if config_name_offset == 0 || config_data_offset == 0 {
                 continue;
             }
 
-            let config_name_file_offset =
-                (vma_header.blob_buffer_offset + config_name_offset) as u64;
-            let config_data_file_offset =
-                (vma_header.blob_buffer_offset + config_data_offset) as u64;
-            let config_name = Self::read_string_from_file(vma_file, config_name_file_offset)?;
-            let config_data = Self::read_string_from_file(vma_file, config_data_file_offset)?;
-
-            configs.insert(String::from(config_name), String::from(config_data));
+            let config_name_size_bytes =
+                vma_header.blob_buffer[config_name_offset..config_name_offset + 2].try_into()?;
+            let config_name_size = u16::from_le_bytes(config_name_size_bytes) as usize;
+            let config_name_bytes = &vma_header.blob_buffer
+                [config_name_offset + 2..config_name_offset + 1 + config_name_size];
+            let config_name = str::from_utf8(config_name_bytes);
+            let config_data_size_bytes =
+                vma_header.blob_buffer[config_data_offset..config_data_offset + 2].try_into()?;
+            let config_data_size = u16::from_le_bytes(config_data_size_bytes) as usize;
+            let config_data_bytes = &vma_header.blob_buffer
+                [config_data_offset + 2..config_data_offset + 1 + config_data_size];
+            let config_data = str::from_utf8(config_data_bytes);
+
+            configs.insert(String::from(config_name?), String::from(config_data?));
         }
 
-        return Ok(configs);
+        Ok(configs)
     }
 
     pub fn get_configs(&self) -> HashMap<String, String> {
-        return self.configs.clone();
+        self.configs.clone()
     }
 
-    pub fn get_device_name(&mut self, device_id: usize) -> Option<String> {
-        if device_id >= VMA_MAX_DEVICES {
-            return None;
-        }
-
-        let device_name_offset = self.vma_header.dev_info[device_id].device_name_offset;
+    pub fn get_device_name(&self, device_id: u8) -> Option<String> {
+        let device_name_offset =
+            self.vma_header.dev_info[device_id as usize].device_name_offset as usize;
 
         if device_name_offset == 0 {
             return None;
         }
 
-        let device_name_file_offset =
-            (self.vma_header.blob_buffer_offset + device_name_offset) as u64;
-        let device_name =
-            Self::read_string_from_file(&mut self.vma_file, device_name_file_offset).unwrap();
+        let size_bytes = self.vma_header.blob_buffer[device_name_offset..device_name_offset + 2]
+            .try_into()
+            .unwrap();
+        let size = u16::from_le_bytes(size_bytes) as usize;
+        let device_name_bytes =
+            &self.vma_header.blob_buffer[device_name_offset + 2..device_name_offset + 1 + size];
+        let device_name = str::from_utf8(device_name_bytes);
 
-        return Some(device_name.to_string());
+        Some(String::from(device_name.unwrap()))
     }
 
-    pub fn get_device_size(&self, device_id: usize) -> Option<u64> {
-        if device_id >= VMA_MAX_DEVICES {
-            return None;
-        }
-
-        let dev_info = &self.vma_header.dev_info[device_id];
+    pub fn get_device_size(&self, device_id: u8) -> Option<u64> {
+        let dev_info = &self.vma_header.dev_info[device_id as usize];
 
         if dev_info.device_name_offset == 0 {
             return None;
         }
 
-        return Some(dev_info.device_size);
+        Some(dev_info.device_size)
     }
 
-    fn read_extent_header(vma_file: &mut File) -> Result<VmaExtentHeader> {
-        let mut buffer = Vec::with_capacity(size_of::<VmaExtentHeader>());
-        buffer.resize(size_of::<VmaExtentHeader>(), 0);
+    fn read_extent_header(mut vma_file: impl Read) -> Result<VmaExtentHeader> {
+        let mut buffer = vec![0; size_of::<VmaExtentHeader>()];
         vma_file.read_exact(&mut buffer)?;
 
         let bincode_options = bincode::DefaultOptions::new()
@@ -237,113 +209,73 @@ impl VmaReader {
             return Err(anyhow!("Wrong VMA extent header checksum"));
         }
 
-        return Ok(vma_extent_header);
+        Ok(vma_extent_header)
     }
 
-    fn index_device_clusters(&mut self) -> Result<()> {
-        for device_id in 0..255 {
-            let device_size = match self.get_device_size(device_id) {
-                Some(x) => x,
-                None => {
-                    continue;
-                }
-            };
+    fn restore_extent<F>(&mut self, func: F) -> Result<()>
+    where
+        F: Fn(u8, u64, Option<Vec<u8>>) -> Result<()>,
+    {
+        let vma_extent_header = Self::read_extent_header(&mut self.vma_file)?;
 
-            let device_cluster_count = (device_size + 4096 * 16 - 1) / (4096 * 16);
+        for i in 0..VMA_BLOCKS_PER_EXTENT {
+            let blockinfo = &vma_extent_header.blockinfo[i];
 
-            let block_index_entry_placeholder = VmaBlockIndexEntry {
-                cluster_file_offset: 0,
-                mask: 0,
-            };
-
-            self.block_index[device_id]
-                .resize(device_cluster_count as usize, block_index_entry_placeholder);
-        }
+            if blockinfo.dev_id == 0 {
+                continue;
+            }
 
-        let mut file_offset = self.vma_header.header_size as u64;
-        let vma_file_size = self.vma_file.metadata()?.len();
+            let image_offset = 4096 * 16 * blockinfo.cluster_num as u64;
+            let is_zero = blockinfo.mask == 0;
 
-        while file_offset < vma_file_size {
-            self.vma_file.seek(SeekFrom::Start(file_offset))?;
-            let vma_extent_header = Self::read_extent_header(&mut self.vma_file)?;
-            file_offset += size_of::<VmaExtentHeader>() as u64;
+            let image_chunk_buffer = if is_zero {
+                None
+            } else {
+                let mut image_chunk_buffer = vec![0; 4096 * 16];
 
-            for i in 0..VMA_BLOCKS_PER_EXTENT {
-                let blockinfo = &vma_extent_header.blockinfo[i];
+                for j in 0..16 {
+                    let block_mask = ((blockinfo.mask >> j) & 1) == 1;
 
-                if blockinfo.dev_id == 0 {
-                    continue;
+                    if block_mask {
+                        self.vma_file.read_exact(
+                            &mut image_chunk_buffer[4096 * j as usize..4096 * (j + 1) as usize],
+                        )?;
+                    } else {
+                        image_chunk_buffer[4096 * j as usize..4096 * (j + 1) as usize].fill(0);
+                    }
                 }
 
-                let block_index_entry = VmaBlockIndexEntry {
-                    cluster_file_offset: file_offset,
-                    mask: blockinfo.mask,
-                };
+                Some(image_chunk_buffer)
+            };
 
-                self.block_index[blockinfo.dev_id as usize][blockinfo.cluster_num as usize] =
-                    block_index_entry;
-                file_offset += blockinfo.mask.count_ones() as u64 * 4096;
-            }
+            func(blockinfo.dev_id, image_offset, image_chunk_buffer)?;
         }
 
-        self.blocks_are_indexed = true;
-
-        return Ok(());
+        Ok(())
     }
 
-    pub fn read_device_contents(
-        &mut self,
-        device_id: usize,
-        buffer: &mut [u8],
-        offset: u64,
-    ) -> Result<bool> {
-        if device_id >= VMA_MAX_DEVICES {
-            return Err(anyhow!("invalid device id {}", device_id));
-        }
-
-        if offset % (4096 * 16) != 0 {
-            return Err(anyhow!("offset is not aligned to 65536"));
-        }
-
-        // Make sure that the device clusters are already indexed
-        if !self.blocks_are_indexed {
-            self.index_device_clusters()?;
-        }
-
-        let this_device_block_index = &self.block_index[device_id];
-        let length = cmp::min(
-            buffer.len(),
-            this_device_block_index.len() * 4096 * 16 - offset as usize,
-        );
-        let mut buffer_offset = 0;
-        let mut buffer_is_zero = true;
-
-        while buffer_offset < length {
-            let block_index_entry =
-                &this_device_block_index[(offset as usize + buffer_offset) / (4096 * 16)];
-            self.vma_file
-                .seek(SeekFrom::Start(block_index_entry.cluster_file_offset))?;
-
-            for i in 0..16 {
-                if buffer_offset >= length {
-                    break;
-                }
-
-                let block_buffer_end = buffer_offset + cmp::min(length - buffer_offset, 4096);
-                let block_mask = ((block_index_entry.mask >> i) & 1) == 1;
-
-                if block_mask {
-                    self.vma_file
-                        .read_exact(&mut buffer[buffer_offset..block_buffer_end])?;
-                    buffer_is_zero = false;
-                } else {
-                    buffer[buffer_offset..block_buffer_end].fill(0);
-                }
-
-                buffer_offset += 4096;
+    pub fn restore<F>(&mut self, func: F) -> Result<()>
+    where
+        F: Fn(u8, u64, Option<Vec<u8>>) -> Result<()>,
+    {
+        loop {
+            match self.restore_extent(&func) {
+                Ok(()) => {}
+                Err(e) => match e.downcast_ref::<std::io::Error>() {
+                    Some(ioerr) => {
+                        if ioerr.kind() == std::io::ErrorKind::UnexpectedEof {
+                            break;
+                        } else {
+                            return Err(anyhow!("Failed to read VMA file: {}", ioerr));
+                        }
+                    }
+                    _ => {
+                        return Err(anyhow!("{}", e));
+                    }
+                },
             }
         }
 
-        return Ok(buffer_is_zero);
+        Ok(())
     }
 }
diff --git a/src/vma2pbs.rs b/src/vma2pbs.rs
new file mode 100644
index 0000000..2451bff
--- /dev/null
+++ b/src/vma2pbs.rs
@@ -0,0 +1,348 @@
+extern crate anyhow;
+extern crate proxmox_backup_qemu;
+extern crate scopeguard;
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::ffi::{c_char, CStr, CString};
+use std::fs::File;
+use std::io::{stdin, BufRead, BufReader};
+use std::ptr;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use anyhow::{anyhow, Result};
+use proxmox_backup_qemu::*;
+use scopeguard::defer;
+
+use crate::vma::*;
+
+const VMA_CLUSTER_SIZE: usize = 65536;
+
+pub fn backup_vma_to_pbs(
+    vma_file_path: Option<String>,
+    pbs_repository: String,
+    backup_id: String,
+    pbs_password: String,
+    keyfile: Option<String>,
+    key_password: Option<String>,
+    master_keyfile: Option<String>,
+    fingerprint: String,
+    compress: bool,
+    encrypt: bool,
+) -> Result<()> {
+    println!("PBS repository: {}", pbs_repository);
+    println!("PBS fingerprint: {}", fingerprint);
+    println!("compress: {}", compress);
+    println!("encrypt: {}", encrypt);
+
+    let backup_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
+    println!("backup time: {}", backup_time);
+
+    let mut pbs_err: *mut c_char = ptr::null_mut();
+
+    let pbs_repository_cstr = CString::new(pbs_repository)?;
+    let backup_id_cstr = CString::new(backup_id)?;
+    let pbs_password_cstr = CString::new(pbs_password)?;
+    let fingerprint_cstr = CString::new(fingerprint)?;
+    let keyfile_cstr = keyfile.map(|v| CString::new(v).unwrap());
+    let keyfile_ptr = keyfile_cstr.map(|v| v.as_ptr()).unwrap_or(ptr::null());
+    let key_password_cstr = key_password.map(|v| CString::new(v).unwrap());
+    let key_password_ptr = key_password_cstr.map(|v| v.as_ptr()).unwrap_or(ptr::null());
+    let master_keyfile_cstr = master_keyfile.map(|v| CString::new(v).unwrap());
+    let master_keyfile_ptr = master_keyfile_cstr
+        .map(|v| v.as_ptr())
+        .unwrap_or(ptr::null());
+
+    let pbs = proxmox_backup_new_ns(
+        pbs_repository_cstr.as_ptr(),
+        ptr::null(),
+        backup_id_cstr.as_ptr(),
+        backup_time,
+        PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE,
+        pbs_password_cstr.as_ptr(),
+        keyfile_ptr,
+        key_password_ptr,
+        master_keyfile_ptr,
+        true,
+        false,
+        fingerprint_cstr.as_ptr(),
+        &mut pbs_err,
+    );
+
+    defer! {
+        proxmox_backup_disconnect(pbs);
+    }
+
+    if pbs.is_null() {
+        unsafe {
+            let pbs_err_cstr = CStr::from_ptr(pbs_err);
+            return Err(anyhow!("proxmox_backup_new_ns failed: {pbs_err_cstr:?}"));
+        }
+    }
+
+    let connect_result = proxmox_backup_connect(pbs, &mut pbs_err);
+
+    if connect_result < 0 {
+        unsafe {
+            let pbs_err_cstr = CStr::from_ptr(pbs_err);
+            return Err(anyhow!("proxmox_backup_connect failed: {pbs_err_cstr:?}"));
+        }
+    }
+
+    println!("Connected to Proxmox Backup Server");
+
+    let vma_file: Box<dyn BufRead> = match vma_file_path {
+        Some(vma_file_path) => match File::open(vma_file_path) {
+            Err(why) => return Err(anyhow!("Couldn't open file: {}", why)),
+            Ok(file) => Box::new(BufReader::new(file)),
+        },
+        None => Box::new(BufReader::new(stdin())),
+    };
+    let mut vma_reader = VmaReader::new(vma_file)?;
+
+    let start_transfer_time = SystemTime::now();
+
+    // Handle configs
+    let configs = vma_reader.get_configs();
+    for (config_name, config_data) in configs {
+        println!("CFG: size: {} name: {}", config_data.len(), config_name);
+
+        let config_name_cstr = CString::new(config_name)?;
+
+        if proxmox_backup_add_config(
+            pbs,
+            config_name_cstr.as_ptr(),
+            config_data.as_ptr(),
+            config_data.len() as u64,
+            &mut pbs_err,
+        ) < 0
+        {
+            unsafe {
+                let pbs_err_cstr = CStr::from_ptr(pbs_err);
+                return Err(anyhow!(
+                    "proxmox_backup_add_config failed: {pbs_err_cstr:?}"
+                ));
+            }
+        }
+    }
+
+    let mut pbs_device_id_map = [-1i32; 256];
+    let mut device_sizes = [0u64; 256];
+
+    // Handle block devices
+    for device_id in 0..255 {
+        let device_name = match vma_reader.get_device_name(device_id) {
+            Some(x) => x,
+            None => continue,
+        };
+
+        let device_size = match vma_reader.get_device_size(device_id) {
+            Some(x) => x,
+            None => continue,
+        };
+
+        device_sizes[device_id as usize] = device_size;
+
+        println!(
+            "DEV: dev_id={} size: {} devname: {}",
+            device_id, device_size, device_name
+        );
+
+        let device_name_cstr = CString::new(device_name)?;
+        let pbs_device_id = proxmox_backup_register_image(
+            pbs,
+            device_name_cstr.as_ptr(),
+            device_size,
+            false,
+            &mut pbs_err,
+        );
+
+        if pbs_device_id < 0 {
+            unsafe {
+                let pbs_err_cstr = CStr::from_ptr(pbs_err);
+                return Err(anyhow!(
+                    "proxmox_backup_register_image failed: {pbs_err_cstr:?}"
+                ));
+            }
+        }
+
+        pbs_device_id_map[device_id as usize] = pbs_device_id;
+    }
+
+    struct ImageChunk {
+        sub_chunks: HashMap<u8, Option<Vec<u8>>>,
+        mask: u64,
+        non_zero_mask: u64,
+    }
+
+    let images_chunks: RefCell<HashMap<u8, HashMap<u64, ImageChunk>>> =
+        RefCell::new(HashMap::new());
+
+    vma_reader.restore(|dev_id, offset, buffer| {
+        let pbs_device_id = pbs_device_id_map[dev_id as usize];
+
+        if pbs_device_id < 0 {
+            return Err(anyhow!(
+                "Reference to unknown device id {} in VMA file",
+                dev_id
+            ));
+        }
+
+        let mut images_chunks = images_chunks.borrow_mut();
+        let image_chunks = images_chunks.get_mut(&dev_id);
+        let pbs_chunk_offset =
+            PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE * (offset / PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE);
+        let sub_chunk_index =
+            ((offset % PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE) / VMA_CLUSTER_SIZE as u64) as u8;
+
+        let pbs_chunk_size =
+            PROXMOX_BACKUP_DEFAULT_CHUNK_SIZE.min(device_sizes[dev_id as usize] - pbs_chunk_offset);
+
+        let pbs_upload_chunk = |pbs_chunk_buffer: Option<&[u8]>| {
+            println!(
+                "Uploading dev_id: {} offset: {:#0X} - {:#0X}",
+                dev_id,
+                pbs_chunk_offset,
+                pbs_chunk_offset + pbs_chunk_size,
+            );
+
+            let mut pbs_err: *mut c_char = ptr::null_mut();
+
+            let write_data_result = proxmox_backup_write_data(
+                pbs,
+                pbs_device_id as u8,
+                match pbs_chunk_buffer {
+                    Some(pbs_chunk_buffer) => pbs_chunk_buffer.as_ptr(),
+                    None => ptr::null(),
+                },
+                pbs_chunk_offset,
+                pbs_chunk_size,
+                &mut pbs_err,
+            );
+
+            if write_data_result < 0 {
+                unsafe {
+                    let pbs_err_cstr = CStr::from_ptr(pbs_err);
+                    return Err(anyhow!(
+                        "proxmox_backup_write_data failed: {pbs_err_cstr:?}"
+                    ));
+                }
+            }
+
+            Ok(())
+        };
+
+        let insert_image_chunk = |image_chunks: &mut HashMap<u64, ImageChunk>,
+                                  buffer: Option<Vec<u8>>| {
+            let mut sub_chunks: HashMap<u8, Option<Vec<u8>>> = HashMap::new();
+            let mask = 1 << sub_chunk_index;
+            let non_zero_mask = buffer.is_some() as u64;
+            sub_chunks.insert(sub_chunk_index, buffer);
+
+            let image_chunk = ImageChunk {
+                sub_chunks,
+                mask,
+                non_zero_mask,
+            };
+
+            image_chunks.insert(pbs_chunk_offset, image_chunk);
+        };
+
+        match image_chunks {
+            Some(image_chunks) => {
+                let image_chunk = image_chunks.get_mut(&pbs_chunk_offset);
+
+                match image_chunk {
+                    Some(image_chunk) => {
+                        image_chunk.mask |= 1 << sub_chunk_index;
+                        image_chunk.non_zero_mask |= (buffer.is_some() as u64) << sub_chunk_index;
+                        image_chunk.sub_chunks.insert(sub_chunk_index, buffer);
+
+                        let sub_chunk_count =
+                            ((pbs_chunk_size + 65535) / VMA_CLUSTER_SIZE as u64) as u8;
+                        let pbs_chunk_mask = 1_u64
+                            .checked_shl(sub_chunk_count.into())
+                            .unwrap_or(0)
+                            .wrapping_sub(1);
+
+                        if image_chunk.mask == pbs_chunk_mask {
+                            if image_chunk.non_zero_mask == 0 {
+                                pbs_upload_chunk(None)?;
+                            } else {
+                                let mut pbs_chunk_buffer =
+                                    proxmox_io::boxed::zeroed(pbs_chunk_size as usize);
+
+                                for i in 0..sub_chunk_count {
+                                    let sub_chunk = image_chunk.sub_chunks.get(&i).unwrap();
+                                    let a = i as usize * VMA_CLUSTER_SIZE;
+                                    let b = (a + VMA_CLUSTER_SIZE).min(pbs_chunk_size as usize);
+
+                                    match sub_chunk {
+                                        Some(sub_chunk) => {
+                                            pbs_chunk_buffer[a..b]
+                                                .copy_from_slice(&sub_chunk[0..b - a]);
+                                        }
+                                        None => pbs_chunk_buffer[a..b].fill(0),
+                                    }
+                                }
+
+                                pbs_upload_chunk(Some(&*pbs_chunk_buffer))?;
+                            }
+
+                            image_chunks.remove(&pbs_chunk_offset);
+                        }
+                    }
+                    None => {
+                        if pbs_chunk_size <= VMA_CLUSTER_SIZE as u64 {
+                            pbs_upload_chunk(buffer.as_deref())?;
+                        } else {
+                            insert_image_chunk(image_chunks, buffer);
+                        }
+                    }
+                }
+            }
+            None => {
+                if pbs_chunk_size <= VMA_CLUSTER_SIZE as u64 {
+                    pbs_upload_chunk(buffer.as_deref())?;
+                } else {
+                    let mut image_chunks: HashMap<u64, ImageChunk> = HashMap::new();
+                    insert_image_chunk(&mut image_chunks, buffer);
+                    images_chunks.insert(dev_id, image_chunks);
+                }
+            }
+        }
+
+        Ok(())
+    })?;
+
+    for pbs_device_id in pbs_device_id_map.iter().take(255) {
+        if *pbs_device_id < 0 {
+            continue;
+        }
+
+        if proxmox_backup_close_image(pbs, *pbs_device_id as u8, &mut pbs_err) < 0 {
+            unsafe {
+                let pbs_err_cstr = CStr::from_ptr(pbs_err);
+                return Err(anyhow!(
+                    "proxmox_backup_close_image failed: {pbs_err_cstr:?}"
+                ));
+            }
+        }
+    }
+
+    if proxmox_backup_finish(pbs, &mut pbs_err) < 0 {
+        unsafe {
+            let pbs_err_cstr = CStr::from_ptr(pbs_err);
+            return Err(anyhow!("proxmox_backup_finish failed: {pbs_err_cstr:?}"));
+        }
+    }
+
+    println!(
+        "Backup finished within {}ms",
+        SystemTime::now()
+            .duration_since(start_transfer_time)?
+            .as_millis()
+    );
+
+    Ok(())
+}
-- 
2.39.2





  parent reply	other threads:[~2024-03-05 13:57 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-03-05 13:56 [pbs-devel] [PATCH v4 vma-to-pbs 0/6] Implement vma-to-pbs tool Filip Schauer
2024-03-05 13:56 ` [pbs-devel] [PATCH v4 vma-to-pbs 1/6] Initial commit Filip Schauer
2024-03-06 15:49   ` Max Carrara
2024-03-05 13:56 ` [pbs-devel] [PATCH v4 vma-to-pbs 2/6] cargo fmt Filip Schauer
2024-03-05 13:56 ` [pbs-devel] [PATCH v4 vma-to-pbs 3/6] bump proxmox-backup-qemu Filip Schauer
2024-03-05 13:56 ` [pbs-devel] [PATCH v4 vma-to-pbs 4/6] Add the ability to provide credentials via files Filip Schauer
2024-03-06 15:49   ` Max Carrara
2024-03-05 13:56 ` Filip Schauer [this message]
2024-03-06 15:49   ` [pbs-devel] [PATCH v4 vma-to-pbs 5/6] Add support for streaming the VMA file via stdin Max Carrara
2024-03-12 14:04     ` Filip Schauer
2024-03-05 13:56 ` [pbs-devel] [PATCH v4 vma-to-pbs 6/6] Add a fallback for the --fingerprint argument Filip Schauer
2024-03-06 15:49 ` [pbs-devel] [PATCH v4 vma-to-pbs 0/6] Implement vma-to-pbs tool Max Carrara
2024-03-06 15:57   ` Wolfgang Bumiller
2024-03-20 14:18 ` Filip Schauer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240305135645.96347-6-f.schauer@proxmox.com \
    --to=f.schauer@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal