From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2 3/9] api: add subscription endpoints
Date: Mon, 1 Dec 2025 14:53:03 +0100 [thread overview]
Message-ID: <20251201135318.2983539-6-d.csapak@proxmox.com> (raw)
In-Reply-To: <20251201135318.2983539-1-d.csapak@proxmox.com>
From: Fabian Grünbichler <f.gruenbichler@proxmox.com>
for the PDM system itself, by proxy of how many of the remote nodes have valid
subscriptions above a certain level.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
server/src/api/nodes/mod.rs | 2 +
server/src/api/nodes/subscription.rs | 194 +++++++++++++++++++++++++++
server/src/api/resources.rs | 2 +-
3 files changed, 197 insertions(+), 1 deletion(-)
create mode 100644 server/src/api/nodes/subscription.rs
diff --git a/server/src/api/nodes/mod.rs b/server/src/api/nodes/mod.rs
index a0fe14ab..3a795d65 100644
--- a/server/src/api/nodes/mod.rs
+++ b/server/src/api/nodes/mod.rs
@@ -11,6 +11,7 @@ pub mod journal;
pub mod network;
pub mod rrddata;
pub mod status;
+pub mod subscription;
pub mod syslog;
pub mod tasks;
pub mod termproxy;
@@ -45,6 +46,7 @@ pub const SUBDIRS: SubdirMap = &sorted!([
("journal", &journal::ROUTER),
("network", &network::ROUTER),
("rrdata", &rrddata::ROUTER),
+ ("subscription", &subscription::ROUTER),
("status", &status::ROUTER),
("syslog", &syslog::ROUTER),
("tasks", &tasks::ROUTER),
diff --git a/server/src/api/nodes/subscription.rs b/server/src/api/nodes/subscription.rs
new file mode 100644
index 00000000..d9b44d59
--- /dev/null
+++ b/server/src/api/nodes/subscription.rs
@@ -0,0 +1,194 @@
+use std::collections::HashMap;
+
+use anyhow::{bail, Error};
+
+use proxmox_router::{Permission, Router};
+use proxmox_schema::api;
+use proxmox_schema::api_types::NODE_SCHEMA;
+use proxmox_subscription::files::update_apt_auth;
+use proxmox_subscription::{SubscriptionInfo, SubscriptionStatus};
+use proxmox_sys::fs::CreateOptions;
+
+use pdm_api_types::remotes::RemoteType;
+use pdm_api_types::subscription::{
+ NodeSubscriptionInfo, SubscriptionLevel, SubscriptionStatistics,
+};
+use pdm_api_types::PRIV_SYS_MODIFY;
+
+use crate::api::resources::get_subscription_info_for_remote;
+
+const PRODUCT_URL: &str = "https://www.proxmox.com/en/proxmox-datacenter-manager/pricing";
+const APT_AUTH_FN: &str = "/etc/apt/auth.conf.d/pdm.conf";
+const APT_AUTH_URL: &str = "enterprise.proxmox.com/debian/pdm";
+
+// minimum ratio of nodes with active subscriptions
+const SUBSCRIPTION_THRESHOLD: f64 = 0.9;
+// max ratio of nodes with community subscriptions, among nodes with subscriptions
+const COMMUNITY_THRESHOLD: f64 = 0.4;
+
+fn apt_auth_file_opts() -> CreateOptions {
+ let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
+ CreateOptions::new().perm(mode).owner(nix::unistd::ROOT)
+}
+
+async fn get_all_subscription_infos(
+) -> Result<HashMap<String, (RemoteType, HashMap<String, Option<NodeSubscriptionInfo>>)>, Error> {
+ let (remotes_config, _digest) = pdm_config::remotes::config()?;
+
+ let mut subscription_info = HashMap::new();
+ for (remote_name, remote) in remotes_config.iter() {
+ match get_subscription_info_for_remote(remote, 24 * 60 * 60).await {
+ Ok(info) => {
+ subscription_info.insert(remote_name.to_string(), (remote.ty, info));
+ }
+ Err(err) => {
+ log::debug!("Failed to get subscription info for remote {remote_name} - {err}");
+ subscription_info.insert(remote_name.to_string(), (remote.ty, HashMap::new()));
+ }
+ }
+ }
+ Ok(subscription_info)
+}
+
+fn count_subscriptions(
+ subscription_infos: &HashMap<
+ String,
+ (RemoteType, HashMap<String, Option<NodeSubscriptionInfo>>),
+ >,
+) -> SubscriptionStatistics {
+ let mut stats = SubscriptionStatistics::default();
+ for (_remote, (_remote_type, remote_infos)) in subscription_infos.iter() {
+ if remote_infos.is_empty() {
+ // count remotes without info as at least one node
+ stats.total_nodes += 1;
+ continue;
+ }
+ for (_node, node_info) in remote_infos.iter() {
+ stats.total_nodes += 1;
+ if let Some(info) = node_info {
+ if info.status == SubscriptionStatus::Active {
+ stats.active_subscriptions += 1;
+ if info.level == SubscriptionLevel::Community {
+ stats.community += 1;
+ }
+ }
+ }
+ }
+ }
+ stats
+}
+
+fn check_counts(stats: SubscriptionStatistics) -> Result<(), Error> {
+ let subscribed_ratio = stats.active_subscriptions as f64 / stats.total_nodes as f64;
+ let community_ratio = stats.community as f64 / stats.active_subscriptions as f64;
+
+ if subscribed_ratio > SUBSCRIPTION_THRESHOLD {
+ if community_ratio < COMMUNITY_THRESHOLD {
+ return Ok(());
+ } else {
+ bail!("Too many remote nodes with community level subscription!");
+ }
+ } else {
+ bail!("Too many remote nodes without active subscription!");
+ }
+}
+
+#[api(
+ access: { permission: &Permission::Anybody, },
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ type: SubscriptionInfo,
+ }
+)]
+/// Return subscription status
+pub async fn get_subscription() -> Result<SubscriptionInfo, Error> {
+ let infos = get_all_subscription_infos().await?;
+
+ let stats = count_subscriptions(&infos);
+
+ if let Err(err) = check_counts(stats) {
+ Ok(SubscriptionInfo {
+ status: SubscriptionStatus::Invalid,
+ message: Some(format!("{err}")),
+ serverid: None,
+ url: Some(PRODUCT_URL.into()),
+ ..Default::default()
+ })
+ } else {
+ Ok(SubscriptionInfo {
+ status: SubscriptionStatus::Active,
+ url: Some(PRODUCT_URL.into()),
+ ..Default::default()
+ })
+ }
+}
+
+#[api(
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ protected: true,
+ access: {
+ permission: &Permission::Privilege(&["system"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// Update subscription information
+pub async fn check_subscription() -> Result<(), Error> {
+ let infos = get_all_subscription_infos().await?;
+ let stats = count_subscriptions(&infos);
+
+ if let Err(err) = check_counts(stats) {
+ update_apt_auth(APT_AUTH_FN, apt_auth_file_opts(), APT_AUTH_URL, None, None)?;
+ return Err(err);
+ }
+
+ let mut found = false;
+ 'outer: for (remote, (remote_type, remote_info)) in infos.iter() {
+ if *remote_type != RemoteType::Pve || *remote_type != RemoteType::Pbs {
+ continue;
+ }
+ for (node, node_info) in remote_info.iter() {
+ if let Some(info) = node_info {
+ if info.status == SubscriptionStatus::Active
+ && info.level >= SubscriptionLevel::Basic
+ && info.key.is_some()
+ && info.serverid.is_some()
+ {
+ log::info!("Using subscription of node '{node}' of remote '{remote}' for enterprise repository access");
+ update_apt_auth(
+ APT_AUTH_FN,
+ apt_auth_file_opts(),
+ APT_AUTH_URL,
+ info.key.clone(),
+ info.serverid.clone(),
+ )?;
+ found = true;
+ break 'outer;
+ }
+ }
+ }
+ }
+
+ if !found {
+ log::warn!(
+ "No valid Basic+ subscription found for configuring enterprise repository access.."
+ );
+ update_apt_auth(APT_AUTH_FN, apt_auth_file_opts(), APT_AUTH_URL, None, None)?;
+ }
+
+ Ok(())
+}
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_SUBSCRIPTION)
+ .post(&API_METHOD_CHECK_SUBSCRIPTION);
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 4beaa542..adab021a 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -767,7 +767,7 @@ static SUBSCRIPTION_CACHE: LazyLock<RwLock<HashMap<String, CachedSubscriptionSta
///
/// If recent enough cached data is available, it is returned
/// instead of calling out to the remote.
-async fn get_subscription_info_for_remote(
+pub async fn get_subscription_info_for_remote(
remote: &Remote,
max_age: u64,
) -> Result<HashMap<String, Option<NodeSubscriptionInfo>>, Error> {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-12-01 13:53 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-01 13:52 [pdm-devel] [PATCH datacenter-manager/yew-comp v2 00/11] add subscription checks to apt repository updates & login Dominik Csapak
2025-12-01 13:52 ` [pdm-devel] [PATCH yew-comp v2 1/2] subscription: refactor api subscription check for showing the alert Dominik Csapak
2025-12-01 19:02 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH yew-comp v2 2/2] apt package manager: add optional subscription check on 'Refresh' button Dominik Csapak
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 1/9] subscription: add serverid field to node subscription info Dominik Csapak
2025-12-01 23:01 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 2/9] api types: add new SubscriptionStatistics Dominik Csapak
2025-12-01 23:01 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` Dominik Csapak [this message]
2025-12-01 23:01 ` [pdm-devel] applied: [PATCH datacenter-manager v2 3/9] api: add subscription endpoints Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 4/9] server: api: subscription: fix permission check Dominik Csapak
2025-12-01 23:01 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 5/9] server: api: pve/pbs: node: add subscription api call Dominik Csapak
2025-12-01 23:01 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 6/9] ui: pve/pbs: updates: add subscription_url Dominik Csapak
2025-12-01 23:01 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 7/9] ui: login: enable subscription check Dominik Csapak
2025-12-01 23:02 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 8/9] ui: refactor check_subscription into lib Dominik Csapak
2025-12-01 23:02 ` [pdm-devel] applied: " Thomas Lamprecht
2025-12-01 13:53 ` [pdm-devel] [PATCH datacenter-manager v2 9/9] ui: remote updates: add subscription check on 'Refresh all' Dominik Csapak
2025-12-01 23:02 ` [pdm-devel] applied: " 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=20251201135318.2983539-6-d.csapak@proxmox.com \
--to=d.csapak@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