use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use std::{fs, io};
use tracing::{info, warn};
use crate::client::Client;
use crate::error::Result;
use crate::state::{SnapshotRow, StateDb, Status};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SnapshotInfo {
pub id: String,
pub vm_id: String,
pub name: Option<String>,
pub disk_path: PathBuf,
pub disk_bytes: u64,
pub created_at: SystemTime,
}
impl From<SnapshotRow> for SnapshotInfo {
fn from(row: SnapshotRow) -> Self {
Self {
id: row.id,
vm_id: row.vm_id,
name: row.name,
disk_path: PathBuf::from(&row.disk_path),
disk_bytes: row.disk_bytes,
created_at: row.created_at,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct SnapshotManager {
db: Arc<StateDb>,
snapshots_dir: PathBuf,
}
impl SnapshotManager {
pub(crate) fn new(db: Arc<StateDb>, data_dir: &Path) -> io::Result<Self> {
let snapshots_dir = data_dir.join("snapshots");
fs::create_dir_all(&snapshots_dir)?;
Ok(Self { db, snapshots_dir })
}
pub(crate) async fn create(
&self,
vm_id: &str,
vm_status: Status,
overlay_path: &Path,
client: &Client,
name: Option<&str>,
) -> Result<SnapshotInfo> {
let snapshot_id = crate::state::gen_id();
let dest = self.snapshots_dir.join(format!("{snapshot_id}.qcow2"));
let quiesced = try_quiesce(vm_id, vm_status, client).await;
let src = overlay_path.to_path_buf();
let dst = dest.clone();
let disk_bytes =
tokio::task::spawn_blocking(move || -> io::Result<u64> { fs::copy(&src, &dst) })
.await
.map_err(io::Error::other)??;
if quiesced {
client.thaw().await.ok();
}
let row = SnapshotRow {
id: snapshot_id.clone(),
vm_id: vm_id.to_owned(),
name: name.map(ToOwned::to_owned),
disk_path: dest.to_string_lossy().into_owned(),
disk_bytes,
created_at: SystemTime::now(),
};
self.db.insert_snapshot(&row)?;
info!(vm_id, snapshot_id = %snapshot_id, bytes = disk_bytes, "snapshot created");
Ok(SnapshotInfo::from(row))
}
pub(crate) fn list(&self, vm_id: &str) -> Result<Vec<SnapshotInfo>> {
Ok(self
.db
.list_snapshots(vm_id)?
.into_iter()
.map(SnapshotInfo::from)
.collect())
}
pub(crate) fn delete(&self, snapshot_id: &str) -> Result<()> {
let snap = self.db.get_snapshot(snapshot_id)?;
fs::remove_file(&snap.disk_path).ok();
self.db.delete_snapshot(snapshot_id)?;
info!(snapshot_id, "snapshot deleted");
Ok(())
}
}
async fn try_quiesce(vm_id: &str, status: Status, client: &Client) -> bool {
if status != Status::Running {
return false;
}
match client.quiesce().await {
Ok(n) => {
info!(vm_id, frozen = n, "filesystems quiesced for snapshot");
true
}
Err(e) => {
warn!(vm_id, error = %e, "quiesce failed, snapshot may be inconsistent");
false
}
}
}