From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [PATCH proxmox-offline-mirror] Add possibility to create named snapshots
Date: Thu, 08 May 2025 11:14:38 +0200 [thread overview]
Message-ID: <1746694526.an3gyo9wbw.astroid@yuna.none> (raw)
In-Reply-To: <20250506090926.71508-1-l.leahu-vladucu@proxmox.com>
On May 6, 2025 11:09 am, Laurențiu Leahu-Vlăducu wrote:
> In addition to the existing timed snapshots, it is now possible to
> create named snapshots. For some use cases, it makes sense to give
> snapshots a specific name, e.g. to specify the purpose of the snapshot
> (instead of a timestamp). This can be useful when deploying snapshots
> created for a certain purpose (for example, further testing).
>
> Partially fixes #6252
>
> Signed-off-by: Laurențiu Leahu-Vlăducu <l.leahu-vladucu@proxmox.com>
> ---
> src/bin/proxmox-offline-mirror-helper.rs | 6 +-
> src/bin/proxmox_offline_mirror_cmds/mirror.rs | 32 ++++++++---
> src/medium.rs | 4 +-
> src/mirror.rs | 56 +++++++++++++++----
> src/types.rs | 52 +++++++++++++----
> 5 files changed, 117 insertions(+), 33 deletions(-)
>
> diff --git a/src/bin/proxmox-offline-mirror-helper.rs b/src/bin/proxmox-offline-mirror-helper.rs
> index 7911d52..a00d26d 100644
> --- a/src/bin/proxmox-offline-mirror-helper.rs
> +++ b/src/bin/proxmox-offline-mirror-helper.rs
> @@ -151,8 +151,8 @@ async fn setup(_param: Value) -> Result<(), Error> {
> let selected_mirror = read_selection_from_tty("Select mirror", &mirrors, None)?;
> let snapshots: Vec<(Snapshot, String)> =
> medium::list_snapshots(mountpoint, selected_mirror)?
> - .into_iter()
> - .map(|s| (s, s.to_string()))
> + .iter()
> + .map(|s| (s.clone(), s.to_string()))
doesn't hurt too much, but could still be avoided by doing to_string()
first..
> .collect();
> if snapshots.is_empty() {
> println!("Mirror doesn't have any synced snapshots.");
> @@ -173,7 +173,7 @@ async fn setup(_param: Value) -> Result<(), Error> {
> selected_mirror.to_string(),
> (
> state.mirrors.get(*selected_mirror).unwrap(),
> - **selected_snapshot,
> + (**selected_snapshot).clone(),
> ),
> );
> }
> diff --git a/src/bin/proxmox_offline_mirror_cmds/mirror.rs b/src/bin/proxmox_offline_mirror_cmds/mirror.rs
> index 31a565e..7eba04d 100644
> --- a/src/bin/proxmox_offline_mirror_cmds/mirror.rs
> +++ b/src/bin/proxmox_offline_mirror_cmds/mirror.rs
> @@ -17,7 +17,7 @@ use proxmox_schema::api;
> use proxmox_offline_mirror::{
> config::{MirrorConfig, SubscriptionKey},
> mirror,
> - types::{MIRROR_ID_SCHEMA, Snapshot},
> + types::{MIRROR_ID_SCHEMA, NAMED_SNAPSHOT_SCHEMA, Snapshot},
> };
>
> use super::get_config_path;
> @@ -60,6 +60,10 @@ fn get_subscription_key(
> id: {
> schema: MIRROR_ID_SCHEMA,
> },
> + name: {
> + schema: NAMED_SNAPSHOT_SCHEMA,
> + optional: true,
> + },
> "dry-run": {
> type: bool,
> optional: true,
> @@ -73,6 +77,7 @@ fn get_subscription_key(
> async fn create_snapshot(
> config: Option<String>,
> id: String,
> + name: Option<String>,
> dry_run: bool,
> _param: Value,
> ) -> Result<(), Error> {
> @@ -83,12 +88,12 @@ async fn create_snapshot(
>
> let subscription = get_subscription_key(§ion_config, &config)?;
>
> - proxmox_offline_mirror::mirror::create_snapshot(
> - config,
> - &Snapshot::now(),
> - subscription,
> - dry_run,
> - )?;
> + let snapshot = match &name {
> + Some(name) => Snapshot::with_name(&name),
> + None => Snapshot::now(),
> + };
> +
> + proxmox_offline_mirror::mirror::create_snapshot(config, &snapshot, subscription, dry_run)?;
I think it would make more sense to treat the name as an extra "label",
rather than as a replacement for the timestamp..
because with the current approach:
- we lose the information when a snapshot was created, which is
important for ordering
- *anything* that matches the relaxed schema is now treated as snapshot,
including temporary dirs for named snapshots
if we attach the label *after* creating the snapshot (e.g., as a symlink
pointing at a snapshot dir?), we avoid those issues (and even get
additional benefits, like moving a label forward, having multiple labels
for a single snapshot, ..).
of course, that means handling dangling symlinks, and handling of those
symlinks in general in some parts..
>
> Ok(())
> }
> @@ -101,6 +106,10 @@ async fn create_snapshot(
> optional: true,
> description: "Path to mirroring config file.",
> },
> + name: {
> + schema: NAMED_SNAPSHOT_SCHEMA,
> + optional: true,
> + },
> "dry-run": {
> type: bool,
> optional: true,
> @@ -114,6 +123,7 @@ async fn create_snapshot(
> /// from original repository.
> async fn create_snapshots(
> config: Option<String>,
> + name: Option<String>,
> dry_run: bool,
> _param: Value,
> ) -> Result<(), Error> {
> @@ -135,9 +145,15 @@ async fn create_snapshots(
> continue;
> }
> };
> +
> + let snapshot = match &name {
> + Some(name) => Snapshot::with_name(&name),
> + None => Snapshot::now(),
> + };
> +
> let res = proxmox_offline_mirror::mirror::create_snapshot(
> mirror,
> - &Snapshot::now(),
> + &snapshot,
> subscription,
> dry_run,
this means that one of the main purposes of this "naming" gets quite
brittle - if a single mirror fails to be mirrored, the others will have
that name attached and it's thus "used"?
if we use the symlink approach, we also avoid this problem -> simply
snapshot all mirrors, and attach the label at the end if successful (or
make the behaviour configurable, depending on use case)
> );
> diff --git a/src/medium.rs b/src/medium.rs
> index bef6cc7..a2e2ee4 100644
> --- a/src/medium.rs
> +++ b/src/medium.rs
> @@ -18,7 +18,7 @@ use crate::{
> generate_repo_file_line,
> mirror::pool,
> pool::Pool,
> - types::{Diff, SNAPSHOT_REGEX, Snapshot},
> + types::{COMBINED_SNAPSHOT_REGEX, Diff, Snapshot},
> };
> #[derive(Clone, Debug, Serialize, Deserialize)]
> #[serde(rename_all = "kebab-case")]
> @@ -162,7 +162,7 @@ pub fn list_snapshots(medium_base: &Path, mirror: &str) -> Result<Vec<Snapshot>,
> proxmox_sys::fs::scandir(
> libc::AT_FDCWD,
> &mirror_base,
> - &SNAPSHOT_REGEX,
> + &COMBINED_SNAPSHOT_REGEX,
> |_l2_fd, snapshot, file_type| {
> if file_type != nix::dir::Type::Directory {
> return Ok(());
> diff --git a/src/mirror.rs b/src/mirror.rs
> index c88f81d..2aef691 100644
> --- a/src/mirror.rs
> +++ b/src/mirror.rs
> @@ -11,14 +11,14 @@ use globset::{Glob, GlobSet, GlobSetBuilder};
> use nix::libc;
> use proxmox_http::{HttpClient, HttpOptions, ProxyConfig, client::sync::Client};
> use proxmox_schema::{ApiType, Schema};
> -use proxmox_sys::fs::file_get_contents;
> +use proxmox_sys::fs::{CreateOptions, file_get_contents};
>
> use crate::{
> FetchResult, Progress,
> config::{MirrorConfig, SkipConfig, SubscriptionKey, WeakCryptoConfig},
> convert_repo_line,
> pool::Pool,
> - types::{Diff, SNAPSHOT_REGEX, Snapshot},
> + types::{COMBINED_SNAPSHOT_REGEX, Diff, Snapshot},
> };
>
> use proxmox_apt::deb822::{
> @@ -476,7 +476,7 @@ pub fn list_snapshots(config: &MirrorConfig) -> Result<Vec<Snapshot>, Error> {
> proxmox_sys::fs::scandir(
> libc::AT_FDCWD,
> &path,
> - &SNAPSHOT_REGEX,
> + &COMBINED_SNAPSHOT_REGEX,
> |_l2_fd, snapshot, file_type| {
> if file_type != nix::dir::Type::Directory {
> return Ok(());
> @@ -793,12 +793,6 @@ pub fn create_snapshot(
> None
> };
>
> - let mut config: ParsedMirrorConfig = config.try_into()?;
> - config.auth = auth;
this is unrelated?
> -
> - let prefix = format!("{snapshot}.tmp");
> - let prefix = Path::new(&prefix);
> -
> let mut progress = MirrorProgress {
> warnings: Vec::new(),
> skip_count: 0,
> @@ -807,6 +801,42 @@ pub fn create_snapshot(
> total: Progress::new(),
> };
>
> + let mut config: ParsedMirrorConfig = config.try_into()?;
> + config.auth = auth;
> +
> + let snapshot_relative_path = snapshot.to_string();
> + let snapshot_relative_path = Path::new(&snapshot_relative_path);
> + let snapshot_absolute_path = config.pool.get_path(snapshot_relative_path);
> +
> + if snapshot_absolute_path.is_ok_and(|path| path.exists()) {
> + if dry_run {
> + let msg = "WARNING: snapshot {snapshot} already exists! Continuing due to dry run...";
> + eprintln!("{msg}");
> + progress.warnings.push(msg.into());
> + } else {
> + bail!("Snapshot {snapshot} already exists!");
> + }
> + }
> +
> + let lockfile_path = config
> + .pool
> + .get_path(Path::new(&format!(".{snapshot}.lock")));
what does this new lock protect against? what is its scope? note that a
pool can be shared between mirrors..
> +
> + if let Err(lockfile_err) = lockfile_path {
> + bail!("cannot create lockfile for snapshot {snapshot}: {lockfile_err}");
> + }
> + let lockfile_path = lockfile_path.unwrap();
> +
> + let _snapshot_lock = proxmox_sys::fs::open_file_locked(
> + &lockfile_path,
> + std::time::Duration::new(10, 0),
> + true,
> + CreateOptions::default(),
> + )?;
> +
> + let prefix = format!("{snapshot}.tmp");
> + let prefix = Path::new(&prefix);
this might also already exist and should be checked..
> +
> let parse_release = |res: FetchResult, name: &str| -> Result<ReleaseFile, Error> {
> println!("Parsing {name}..");
> let parsed: ReleaseFile = res.data[..].try_into()?;
> @@ -1078,9 +1108,15 @@ pub fn create_snapshot(
> if !dry_run {
> println!("\nRotating temp. snapshot in-place: {prefix:?} -> \"{snapshot}\"");
> let locked = config.pool.lock()?;
> - locked.rename(prefix, Path::new(&format!("{snapshot}")))?;
> +
> + if snapshot_relative_path.exists() {
> + bail!("Snapshot {snapshot} already exists!");
> + }
this check is wrong, because it doesn't account for the current working
directory and the path being relative..
> + locked.rename(prefix, snapshot_relative_path)?;
> }
>
> + std::fs::remove_file(lockfile_path)?;
> +
> Ok(())
> }
>
> diff --git a/src/types.rs b/src/types.rs
> index 10dde65..49dc374 100644
> --- a/src/types.rs
> +++ b/src/types.rs
> @@ -1,6 +1,6 @@
> use std::{fmt::Display, path::PathBuf, str::FromStr};
>
> -use anyhow::Error;
> +use anyhow::{Error, bail};
> use proxmox_schema::{ApiStringFormat, Schema, StringSchema, api, const_regex};
> use proxmox_serde::{forward_deserialize_to_from_str, forward_serialize_to_display};
> use proxmox_time::{epoch_i64, epoch_to_rfc3339_utc, parse_rfc3339};
> @@ -32,6 +32,13 @@ pub const MEDIA_ID_SCHEMA: Schema = StringSchema::new("Medium name.")
> .max_length(32)
> .schema();
>
> +/// Schema for named snapshots
> +pub const NAMED_SNAPSHOT_SCHEMA: Schema = StringSchema::new("Custom name for snapshot.")
> + .format(&PROXMOX_SAFE_ID_FORMAT)
> + .min_length(3)
> + .max_length(32)
> + .schema();
> +
> #[rustfmt::skip]
> #[macro_export]
> macro_rules! PROXMOX_SUBSCRIPTION_KEY_REGEX_STR { () => { r"(?:pom-|pve\d+[a-z]-|pbs[a-z]-|pmg[a-z]-).*" }; }
> @@ -62,32 +69,51 @@ pub const PROXMOX_SERVER_ID_SCHEMA: Schema = StringSchema::new("Server ID.")
>
> #[rustfmt::skip]
> #[macro_export]
> -macro_rules! SNAPSHOT_RE { () => (r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z") }
> +macro_rules! TIMED_SNAPSHOT_RE { () => (r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z") }
> const_regex! {
> - pub(crate) SNAPSHOT_REGEX = concat!(r"^", SNAPSHOT_RE!() ,r"$");
> + pub(crate) TIMED_SNAPSHOT_REGEX = concat!(r"^", TIMED_SNAPSHOT_RE!() ,r"$");
> +}
> +
> +#[rustfmt::skip]
> +const_regex! {
> + pub(crate) COMBINED_SNAPSHOT_REGEX = concat!(r"^((", TIMED_SNAPSHOT_RE!() ,r")|", PROXMOX_SAFE_ID_REGEX_STR!(), ")$");
> +}
> +
> +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
> +enum SnapshotType {
> + TimedSnapshot(i64),
> + NamedSnapshot(String),
> }
>
> #[api(
> type: String,
> - format: &ApiStringFormat::Pattern(&SNAPSHOT_REGEX),
> )]
> -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
> +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
> /// Mirror snapshot
> -pub struct Snapshot(i64);
> +pub struct Snapshot(SnapshotType);
>
> forward_serialize_to_display!(Snapshot);
> forward_deserialize_to_from_str!(Snapshot);
>
> impl Snapshot {
> pub fn now() -> Self {
> - Self(epoch_i64())
> + Self(SnapshotType::TimedSnapshot(epoch_i64()))
> + }
> +
> + pub fn with_name(name: &str) -> Self {
> + Self(SnapshotType::NamedSnapshot(name.into()))
> }
> }
>
> impl Display for Snapshot {
> fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> - let formatted = epoch_to_rfc3339_utc(self.0).map_err(|_| std::fmt::Error)?;
> - f.write_str(&formatted)
> + match &self.0 {
> + SnapshotType::TimedSnapshot(time) => {
> + let formatted = epoch_to_rfc3339_utc(*time).map_err(|_| std::fmt::Error)?;
> + f.write_str(&formatted)
> + }
> + SnapshotType::NamedSnapshot(name) => f.write_str(&name),
> + }
> }
> }
>
> @@ -95,7 +121,13 @@ impl FromStr for Snapshot {
> type Err = Error;
>
> fn from_str(s: &str) -> Result<Self, Self::Err> {
> - Ok(Self(parse_rfc3339(s)?))
> + if TIMED_SNAPSHOT_REGEX.is_match(s) {
> + Ok(Self(SnapshotType::TimedSnapshot(parse_rfc3339(s)?)))
> + } else if PROXMOX_SAFE_ID_REGEX.is_match(s) {
> + Ok(Self(SnapshotType::NamedSnapshot(s.into())))
> + } else {
> + bail!("invalid snapshot name")
> + }
> }
> }
>
> --
> 2.39.5
>
>
>
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
>
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-05-08 9:14 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-05-06 9:09 Laurențiu Leahu-Vlăducu
2025-05-08 9:14 ` Fabian Grünbichler [this message]
2025-05-08 15:17 ` Laurențiu Leahu-Vlăducu
2025-05-09 7:33 ` Fabian Grünbichler
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=1746694526.an3gyo9wbw.astroid@yuna.none \
--to=f.gruenbichler@proxmox.com \
--cc=pve-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal