From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager v3 10/12] subscription: add Adopt All bulk action
Date: Fri, 15 May 2026 09:43:20 +0200 [thread overview]
Message-ID: <20260515074623.766766-11-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20260515074623.766766-1-t.lamprecht@proxmox.com>
Add a server endpoint plus CLI / UI wiring for importing every
foreign live subscription in one transaction. The typical use case
is connecting an existing fleet of PVE/PBS nodes to PDM for the
first time: rather than clicking Adopt Key per-node, the operator
runs Adopt All once and the pool catches up with the deployed
subscriptions in a single call.
The candidate set is recomputed under the config lock, so a
parallel Assign / Adopt landing between the network read and the
lock cannot race-import a key that has just been bound. Candidates
are silently skipped on missing per-remote PRIV_RESOURCE_MODIFY,
on a conflicting pool state ((remote, node) target already bound,
or the live key already bound elsewhere), or on a remote-supplied
key or node name failing schema validation; the UI dialog
enumerates the same set.
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
New in v3.
cli/client/src/subscriptions.rs | 29 ++++
docs/subscription-registry.rst | 7 +
lib/pdm-api-types/src/subscription.rs | 17 ++
lib/pdm-client/src/lib.rs | 22 +++
server/src/api/subscriptions/mod.rs | 161 +++++++++++++++++-
ui/src/configuration/subscription_registry.rs | 160 +++++++++++++++++
6 files changed, 394 insertions(+), 2 deletions(-)
diff --git a/cli/client/src/subscriptions.rs b/cli/client/src/subscriptions.rs
index c9ba5e4c..469f0841 100644
--- a/cli/client/src/subscriptions.rs
+++ b/cli/client/src/subscriptions.rs
@@ -52,6 +52,7 @@ pub fn cli() -> CommandLineInterface {
"adopt-key",
CliCommand::new(&API_METHOD_ADOPT_KEY).arg_param(&["remote", "node"]),
)
+ .insert("adopt-all", CliCommand::new(&API_METHOD_ADOPT_ALL))
.into()
}
@@ -342,6 +343,34 @@ async fn adopt_key(
Ok(())
}
+#[api(
+ input: {
+ properties: {
+ digest: {
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ optional: true,
+ },
+ },
+ },
+)]
+/// Adopt every foreign live subscription into the pool in one transaction.
+///
+/// Walks all remotes the caller can audit, imports any (remote, node) with a live current key
+/// and no pool binding. Candidates the caller has no modify privilege on, or whose key is
+/// already bound elsewhere in the pool, are silently skipped.
+async fn adopt_all(digest: Option<String>) -> Result<(), Error> {
+ let digest = digest.map(ConfigDigest::from);
+ let adopted = client()?.subscription_adopt_all(digest).await?;
+ if adopted.is_empty() {
+ println!("No foreign live subscriptions to adopt.");
+ return Ok(());
+ }
+ println!("Adopted {} live subscription(s):", adopted.len());
+ for e in &adopted {
+ println!(" {}/{} -> {}", e.remote, e.node, e.key);
+ }
+ Ok(())
+}
#[api(
input: {
diff --git a/docs/subscription-registry.rst b/docs/subscription-registry.rst
index 4c31c9a6..6d599fe2 100644
--- a/docs/subscription-registry.rst
+++ b/docs/subscription-registry.rst
@@ -52,6 +52,13 @@ adoption are highlighted with a download hint icon in the Node Subscription Stat
the pool grid carries a hidden-by-default Source column distinguishing manually-added from
adopted entries, which can be enabled via the column picker if the distinction matters.
+The Adopt All action runs the same import across every remote the operator can audit in one
+transaction. Use it after first connecting an existing fleet of nodes to PDM so the pool
+catches up with the live subscriptions already deployed, without having to click through
+Adopt Key for each node. Candidates the operator has no modify privilege on, whose key is
+already bound elsewhere in the pool, whose (remote, node) target is already bound by another
+pool entry, or whose key or node name fails schema validation are skipped silently.
+
The proposed plan can be inspected before it is applied. Apply Pending walks the queue in
order; if any push or clear fails the remaining queue is kept intact for retry. Discard Pending
drops the plan without touching any remote.
diff --git a/lib/pdm-api-types/src/subscription.rs b/lib/pdm-api-types/src/subscription.rs
index 8a0a7977..df1fec1c 100644
--- a/lib/pdm-api-types/src/subscription.rs
+++ b/lib/pdm-api-types/src/subscription.rs
@@ -569,6 +569,23 @@ pub struct ClearPendingResult {
pub cleared: u32,
}
+#[api(
+ properties: {
+ "key": { schema: SUBSCRIPTION_KEY_SCHEMA },
+ },
+)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+/// One entry imported by the bulk Adopt-All endpoint.
+pub struct AdoptedEntry {
+ /// Remote the live subscription was running on.
+ pub remote: String,
+ /// Node within the remote.
+ pub node: String,
+ /// The adopted subscription key.
+ pub key: String,
+}
+
#[api]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 6c764c00..f03f6c40 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -1303,6 +1303,28 @@ impl<T: HttpApiClient> PdmClient<T> {
.nodata()
}
+ /// Adopt every foreign live subscription that the caller can modify, in one transaction.
+ /// Returns the list of `(remote, node, key)` tuples that were imported into the pool;
+ /// candidates the caller has no `PRIV_RESOURCE_MODIFY` on (or that fail validation, or that
+ /// are already bound elsewhere in the pool) are silently skipped. See the server endpoint
+ /// docs for the full skip rules.
+ pub async fn subscription_adopt_all(
+ &self,
+ digest: Option<ConfigDigest>,
+ ) -> Result<Vec<pdm_api_types::subscription::AdoptedEntry>, Error> {
+ #[derive(Serialize)]
+ struct AdoptAllArgs {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ digest: Option<ConfigDigest>,
+ }
+ Ok(self
+ .0
+ .post("/api2/extjs/subscriptions/adopt-all", &AdoptAllArgs { digest })
+ .await?
+ .expect_json()?
+ .data)
+ }
+
/// Queue a clear for the subscription on `remote`/`node`. Apply Pending later removes the
/// subscription from the node so the key can be reassigned elsewhere; Discard Pending
/// undoes the queueing without touching the remote. Returns `BAD_REQUEST` if no pool entry
diff --git a/server/src/api/subscriptions/mod.rs b/server/src/api/subscriptions/mod.rs
index cc46806c..a8f5cfc5 100644
--- a/server/src/api/subscriptions/mod.rs
+++ b/server/src/api/subscriptions/mod.rs
@@ -21,8 +21,8 @@ use proxmox_sortable_macro::sortable;
use pdm_api_types::remotes::{Remote, REMOTE_ID_SCHEMA};
use pdm_api_types::subscription::{
- pick_best_pve_socket_key, socket_count_from_key, AutoAssignProposal, ClearPendingResult,
- ProductType, ProposedAssignment, RemoteNodeStatus, SubscriptionKeyEntry,
+ pick_best_pve_socket_key, socket_count_from_key, AdoptedEntry, AutoAssignProposal,
+ ClearPendingResult, ProductType, ProposedAssignment, RemoteNodeStatus, SubscriptionKeyEntry,
SubscriptionKeySource, SubscriptionLevel, SUBSCRIPTION_KEY_SCHEMA,
};
use pdm_api_types::{
@@ -39,6 +39,7 @@ pub const ROUTER: Router = Router::new()
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
+ ("adopt-all", &Router::new().post(&API_METHOD_ADOPT_ALL)),
("adopt-key", &Router::new().post(&API_METHOD_ADOPT_KEY)),
(
"apply-pending",
@@ -1112,6 +1113,162 @@ async fn adopt_key(
Ok(())
}
+#[api(
+ input: {
+ properties: {
+ digest: {
+ type: ConfigDigest,
+ optional: true,
+ },
+ },
+ },
+ returns: {
+ type: Array,
+ description: "List of (remote, node, key) tuples that were adopted into the pool.",
+ items: { type: AdoptedEntry },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Adopt every foreign live subscription in one transaction.
+///
+/// Walks the node-status view (so only remotes the caller can audit are considered), collects
+/// every (remote, node) that has a live current key but no pool entry bound to it, and imports
+/// each one into the pool with source = `Adopted`. Candidates are skipped (not adopted, not an
+/// error) when:
+///
+/// - The caller has no `PRIV_RESOURCE_MODIFY` on the candidate's remote: an audit-only operator
+/// should not be able to materialise pool state for a remote they cannot manage.
+/// - The live key is already in the pool but bound elsewhere: leaving the rebind as a manual
+/// step keeps the bulk action from silently competing with a deliberate prior assignment.
+/// - The live key fails schema validation or its prefix is unknown: a buggy or malicious
+/// remote should not be able to inject garbage into the pool through a bulk shortcut.
+///
+/// Successfully-adopted entries are returned so the caller (CLI / UI) can summarise the outcome
+/// without needing a separate refresh round-trip.
+async fn adopt_all(
+ digest: Option<ConfigDigest>,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<AdoptedEntry>, Error> {
+ let auth_id: Authid = rpcenv
+ .get_auth_id()
+ .context("no authid available")?
+ .parse()?;
+
+ // Use a fresh node-status snapshot: a cached entry from minutes ago could miss a live
+ // subscription that was just installed on a remote, or vice-versa, claim a subscription
+ // that has since been removed. Adopting bogus or already-cleared keys would be a footgun.
+ let node_statuses = collect_node_status(FRESH_NODE_STATUS_MAX_AGE, rpcenv).await?;
+
+ // Lock + sync IO under spawn_blocking. The closure re-resolves the candidate set under the
+ // lock: a parallel admin's Assign / Adopt between the network read above and the lock
+ // acquisition here would otherwise let us race-import a key that has just been bound by
+ // them.
+ let (adopted, new_digest_opt) = tokio::task::spawn_blocking(
+ move || -> Result<(Vec<AdoptedEntry>, Option<ConfigDigest>), Error> {
+ let user_info = CachedUserInfo::new()?;
+ let _lock = pdm_config::subscriptions::lock_config()?;
+ let (mut config, config_digest) = pdm_config::subscriptions::config()?;
+ config_digest.detect_modification(digest.as_ref())?;
+
+ let mut adopted: Vec<AdoptedEntry> = Vec::new();
+ for n in &node_statuses {
+ let Some(current_key) = n.current_key.as_deref() else {
+ continue;
+ };
+ if n.assigned_key.is_some() {
+ continue;
+ }
+ if user_info.lookup_privs(&auth_id, &["resource", &n.remote])
+ & PRIV_RESOURCE_MODIFY
+ == 0
+ {
+ continue;
+ }
+ // Re-validate foreign node name: later interpolated into remote URL.
+ if NODE_SCHEMA.parse_simple_value(&n.node).is_err() {
+ warn!(
+ "skipping adopt-all candidate on {}/{}: node name fails schema",
+ n.remote, n.node,
+ );
+ continue;
+ }
+ // Re-check binding state under the lock - between the network read and here a
+ // parallel Adopt / Assign on the same target could have created a pool entry
+ // bound to (remote, node) that the cached node-status snapshot did not see.
+ let target_bound = config.iter().any(|(_, e)| {
+ e.remote.as_deref() == Some(n.remote.as_str())
+ && e.node.as_deref() == Some(n.node.as_str())
+ });
+ if target_bound {
+ continue;
+ }
+
+ if let Some(existing) = config.get_mut(current_key) {
+ if existing.remote.is_some() || existing.node.is_some() {
+ // Bound elsewhere: leave the rebind as an explicit operator decision.
+ continue;
+ }
+ existing.remote = Some(n.remote.clone());
+ existing.node = Some(n.node.clone());
+ } else {
+ if SUBSCRIPTION_KEY_SCHEMA
+ .parse_simple_value(current_key)
+ .is_err()
+ {
+ warn!(
+ "skipping adopt-all candidate on {}/{}: key '{}' fails schema",
+ n.remote,
+ n.node,
+ redact_key(current_key),
+ );
+ continue;
+ }
+ let Some(product_type) = ProductType::from_key(current_key) else {
+ warn!(
+ "skipping adopt-all candidate on {}/{}: unrecognised key prefix \
+ '{}'",
+ n.remote,
+ n.node,
+ redact_key(current_key),
+ );
+ continue;
+ };
+ let entry = SubscriptionKeyEntry {
+ key: current_key.to_string(),
+ product_type,
+ level: SubscriptionLevel::from_key(Some(current_key)),
+ source: SubscriptionKeySource::Adopted,
+ remote: Some(n.remote.clone()),
+ node: Some(n.node.clone()),
+ ..Default::default()
+ };
+ config.insert(current_key.to_string(), entry);
+ }
+ adopted.push(AdoptedEntry {
+ remote: n.remote.clone(),
+ node: n.node.clone(),
+ key: current_key.to_string(),
+ });
+ }
+
+ let new_digest = if adopted.is_empty() {
+ None
+ } else {
+ Some(pdm_config::subscriptions::save_config(&config)?)
+ };
+ Ok((adopted, new_digest))
+ },
+ )
+ .await??;
+
+ if let Some(new_digest) = new_digest_opt {
+ rpcenv["digest"] = new_digest.to_hex().into();
+ }
+ Ok(adopted)
+}
+
#[api(
input: {
properties: {
diff --git a/ui/src/configuration/subscription_registry.rs b/ui/src/configuration/subscription_registry.rs
index 7d79370b..b84ddb36 100644
--- a/ui/src/configuration/subscription_registry.rs
+++ b/ui/src/configuration/subscription_registry.rs
@@ -99,6 +99,14 @@ fn pending_badge(push_count: u32, clear_count: u32) -> Row {
row
}
+/// Row shape for the Adopt All preview table.
+#[derive(Clone, PartialEq)]
+struct AdoptCandidate {
+ remote: String,
+ node: String,
+ key: String,
+}
+
#[derive(Clone, Debug, PartialEq)]
enum NodeTreeEntry {
Root,
@@ -238,6 +246,8 @@ pub enum Msg {
/// Open the confirmation dialog for adopting the live subscription on the selected node
/// into the pool.
AdoptKeyForSelectedNode,
+ /// Open the confirmation dialog for adopting every foreign live subscription into the pool.
+ AdoptAllPreview,
}
#[derive(PartialEq)]
@@ -259,6 +269,12 @@ pub enum ViewState {
node: String,
current_key: String,
},
+ /// Pending confirmation to bulk-adopt every foreign live subscription. The candidate list
+ /// is captured at view-open time so the dialog body can show the operator exactly what
+ /// will be imported; the server re-computes the set under the lock at commit time.
+ ConfirmAdoptAll {
+ candidates: Vec<(String, String, String)>,
+ },
/// Assign a pool key to the given node. Opens from the right panel's Assign Key button.
AssignKeyToNode {
remote: String,
@@ -274,6 +290,7 @@ pub struct SubscriptionRegistryComp {
tree_store: TreeStore<NodeTreeEntry>,
tree_columns: Rc<Vec<DataTableHeader<NodeTreeEntry>>>,
proposal_columns: Rc<Vec<DataTableHeader<ProposedAssignment>>>,
+ adopt_columns: Rc<Vec<DataTableHeader<AdoptCandidate>>>,
node_selection: Selection,
last_node_data: Vec<RemoteNodeStatus>,
/// Canonical pool snapshot. Passed down to the key grid (display) and shared with the
@@ -458,6 +475,24 @@ impl SubscriptionRegistryComp {
.into(),
])
}
+
+ fn adopt_columns() -> Rc<Vec<DataTableHeader<AdoptCandidate>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Remote / Node"))
+ .flex(2)
+ .render(|c: &AdoptCandidate| format!("{} / {}", c.remote, c.node).into())
+ .into(),
+ DataTableColumn::new(tr!("Key"))
+ .flex(2)
+ .render(|c: &AdoptCandidate| {
+ Container::from_tag("span")
+ .class(pwt::css::FontStyle::LabelMedium)
+ .with_child(c.key.clone())
+ .into()
+ })
+ .into(),
+ ])
+ }
}
fn key_cell(n: &RemoteNodeStatus) -> Html {
@@ -542,6 +577,7 @@ impl LoadableComponent for SubscriptionRegistryComp {
tree_store: store.clone(),
tree_columns: Self::tree_columns(store),
proposal_columns: Self::proposal_columns(),
+ adopt_columns: Self::adopt_columns(),
node_selection,
last_node_data: Vec::new(),
pool_keys: Rc::new(Vec::new()),
@@ -692,6 +728,14 @@ impl LoadableComponent for SubscriptionRegistryComp {
current_key,
}));
}
+ Msg::AdoptAllPreview => {
+ let candidates = self.adopt_all_candidates();
+ if candidates.is_empty() {
+ return false;
+ }
+ ctx.link()
+ .change_view(Some(ViewState::ConfirmAdoptAll { candidates }));
+ }
}
true
}
@@ -699,6 +743,7 @@ impl LoadableComponent for SubscriptionRegistryComp {
fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
let link = ctx.link();
let (push_count, clear_count) = self.pending_counts();
+ let adopt_all_count = self.adopt_all_candidates().len();
let mut toolbar = Toolbar::new()
.border_bottom(true)
.with_child(
@@ -712,6 +757,18 @@ impl LoadableComponent for SubscriptionRegistryComp {
subscription, then queue it pending Apply."
)),
)
+ .with_child(
+ Tooltip::new(
+ Button::new(tr!("Adopt All"))
+ .icon_class("fa fa-download")
+ .disabled(adopt_all_count == 0)
+ .on_activate(link.callback(|_| Msg::AdoptAllPreview)),
+ )
+ .tip(tr!(
+ "Import every foreign live subscription that is not yet tracked by the \
+ pool. The remote is not contacted; only the pool config is updated."
+ )),
+ )
.with_spacer()
.with_child(
Tooltip::new(
@@ -860,6 +917,9 @@ impl LoadableComponent for SubscriptionRegistryComp {
ViewState::ConfirmAutoAssign(proposal) => {
Some(self.render_auto_assign_dialog(ctx, proposal))
}
+ ViewState::ConfirmAdoptAll { candidates } => {
+ Some(self.render_adopt_all_dialog(ctx, candidates))
+ }
ViewState::ConfirmAdoptKey {
remote,
node,
@@ -1175,6 +1235,25 @@ impl SubscriptionRegistryComp {
Some((n.remote.clone(), n.node.clone(), current_key))
}
+ /// Iterate the loaded node-status snapshot and return every `(remote, node, current_key)`
+ /// eligible for bulk Adopt-All (live key set, no pool binding). Used both for the toolbar
+ /// disabled gate and for the preview list in the confirm dialog; the authoritative set is
+ /// recomputed by the server under the lock at commit time, so this view is a hint, not a
+ /// contract.
+ fn adopt_all_candidates(&self) -> Vec<(String, String, String)> {
+ self.last_node_data
+ .iter()
+ .filter_map(|n| {
+ if n.assigned_key.is_some() {
+ return None;
+ }
+ n.current_key
+ .clone()
+ .map(|k| (n.remote.clone(), n.node.clone(), k))
+ })
+ .collect()
+ }
+
/// Returns `(remote, node, type, node_sockets)` for the right-panel Assign button:
/// selected row is a node, no assigned key in the pool yet, and no live active subscription.
/// Refusing earlier than the server keeps the button-disable affordance honest.
@@ -1250,4 +1329,85 @@ impl SubscriptionRegistryComp {
.with_child(body)
.into()
}
+
+ fn render_adopt_all_dialog(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ candidates: &[(String, String, String)],
+ ) -> Html {
+ use pwt::widget::Dialog;
+
+ let rows: Vec<AdoptCandidate> = candidates
+ .iter()
+ .map(|(r, n, k)| AdoptCandidate {
+ remote: r.clone(),
+ node: n.clone(),
+ key: k.clone(),
+ })
+ .collect();
+ let n = rows.len();
+ let store: Store<AdoptCandidate> = Store::with_extract_key(|c: &AdoptCandidate| {
+ format!("{}/{}", c.remote, c.node).into()
+ });
+ store.set_data(rows);
+
+ let link_close = ctx.link().clone();
+ let link_apply = ctx.link().clone();
+ let digest = self.pool_digest.clone();
+ let body = Column::new()
+ .class(Flex::Fill)
+ .class(Overflow::Hidden)
+ .min_height(0)
+ .padding(2)
+ .gap(2)
+ .min_width(600)
+ .with_child(Container::from_tag("p").with_child(tr!(
+ "The following {n} live subscription(s) will be imported into the pool; \
+ the remote is not contacted.",
+ n = n,
+ )))
+ .with_child(
+ DataTable::new(self.adopt_columns.clone(), store)
+ .striped(true)
+ .class(FlexFit)
+ .min_height(140),
+ )
+ .with_child(
+ Row::new()
+ .class(JustifyContent::FlexEnd)
+ .gap(2)
+ .padding_top(2)
+ .with_child(
+ Button::new(tr!("Cancel"))
+ .on_activate(move |_| link_close.change_view(None)),
+ )
+ .with_child(Button::new(tr!("Adopt")).on_activate(move |_| {
+ let link = link_apply.clone();
+ let digest = digest.clone();
+ link.clone().spawn(async move {
+ let digest = digest.map(pdm_client::ConfigDigest::from);
+ if let Err(err) =
+ crate::pdm_client().subscription_adopt_all(digest).await
+ {
+ link.show_error(tr!("Adopt All"), err.to_string(), true);
+ }
+ link.change_view(None);
+ link.send_reload();
+ });
+ })),
+ );
+
+ Dialog::new(tr!("Adopt All"))
+ .resizable(true)
+ .width(700)
+ .min_width(500)
+ .min_height(300)
+ .max_height("80vh")
+ .on_close({
+ let link = ctx.link().clone();
+ move |_| link.change_view(None)
+ })
+ .with_child(body)
+ .into()
+ }
}
--
2.47.3
next prev parent reply other threads:[~2026-05-15 7:46 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-15 7:43 [PATCH datacenter-manager v3 00/12] subscription key pool registry Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 01/12] api types: subscription level: render full names Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 02/12] pdm-client: add wait_for_local_task helper Thomas Lamprecht
2026-05-15 15:21 ` Wolfgang Bumiller
2026-05-15 7:43 ` [PATCH datacenter-manager v3 03/12] subscription: pool: add data model and config layer Thomas Lamprecht
2026-05-15 15:21 ` Wolfgang Bumiller
2026-05-15 7:43 ` [PATCH datacenter-manager v3 04/12] subscription: api: add key pool and node status endpoints Thomas Lamprecht
2026-05-15 15:21 ` Wolfgang Bumiller
2026-05-15 7:43 ` [PATCH datacenter-manager v3 05/12] ui: registry: add view with key pool and node status Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 06/12] cli: client: add subscription key pool management subcommands Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 07/12] docs: add subscription registry chapter Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 08/12] subscription: add Clear Key action and per-node revert Thomas Lamprecht
2026-05-15 7:43 ` [PATCH datacenter-manager v3 09/12] subscription: add Adopt Key action for foreign live subscriptions Thomas Lamprecht
2026-05-15 7:43 ` Thomas Lamprecht [this message]
2026-05-15 7:43 ` [PATCH datacenter-manager v3 11/12] subscription: add Check Subscription action Thomas Lamprecht
2026-05-15 7:43 ` [RFC PATCH datacenter-manager v3 12/12] ui: registry: add Add-and-Assign wizard from Assign Key dialog Thomas Lamprecht
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=20260515074623.766766-11-t.lamprecht@proxmox.com \
--to=t.lamprecht@proxmox.com \
--cc=pdm-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