* [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer
@ 2025-11-13 12:00 Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
this series adds a flag that marks certain api endpoints as unstable.
allowing for more fine grained control over when and what we declare to
be stable. it also adds an api viewer to pdm as a separate package that
is only suggested for now.
the code that dumps api definitions as json is also moved to proxmox-rs
and unified between pdm and pbs to reduce technical debt.
Changelog
---------
* add a patch that fixes a generation bug in pve by Wolfgang Bumiller.
this replaces a previous patch that worked around that bug in
proxmox-docgen.
* add a patch cleaning up some clippy lints in pve-api-types
* clean up some unused import in the docgen binary in pbs & pdm
proxmox:
Shannon Sterz (4):
router/api-macro: add unstable flag for ApiMethod
pve-api-types: fix clippy lints
docgen: add docgen crate
docgen: add support for the new stable flag
Wolfgang Bumiller (1):
pve-api-types: generate array objects
Cargo.toml | 1 +
proxmox-api-macro/src/api/method.rs | 9 +-
proxmox-docgen/Cargo.toml | 20 ++
proxmox-docgen/debian/changelog | 5 +
proxmox-docgen/debian/control | 36 ++++
proxmox-docgen/debian/copyright | 18 ++
proxmox-docgen/debian/debcargo.toml | 7 +
proxmox-docgen/src/lib.rs | 323 ++++++++++++++++++++++++++++
proxmox-router/src/router.rs | 10 +
pve-api-types/src/types/array.rs | 4 +-
pve-api-types/src/types/macros.rs | 147 ++++++++++++-
pve-api-types/src/types/mod.rs | 3 +
12 files changed, 578 insertions(+), 5 deletions(-)
create mode 100644 proxmox-docgen/Cargo.toml
create mode 100644 proxmox-docgen/debian/changelog
create mode 100644 proxmox-docgen/debian/control
create mode 100644 proxmox-docgen/debian/copyright
create mode 100644 proxmox-docgen/debian/debcargo.toml
create mode 100644 proxmox-docgen/src/lib.rs
proxmox-widget-toolkit:
Shannon Sterz (1):
api viewer: add support for endpoints that are marked as unstable
src/api-viewer/APIViewer.js | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
proxmox-backup:
Shannon Sterz (1):
docgen: use proxmox-rs docgen crate
Cargo.toml | 3 +
docs/api-viewer/index.html | 2 +
src/bin/docgen.rs | 326 ++-----------------------------------
3 files changed, 20 insertions(+), 311 deletions(-)
proxmox-datacenter-manager:
Shannon Sterz (2):
docgen: switch to proxmox-rs docgen crate
api-viewer: add an api-viewer package
Cargo.toml | 2 +
Makefile | 1 +
debian/control | 13 +
...xmox-datacenter-manager-api-viewer.install | 1 +
docs/api-viewer/Makefile | 27 ++
docs/api-viewer/index.html | 16 +
server/Cargo.toml | 1 +
server/src/bin/docgen.rs | 314 +-----------------
8 files changed, 65 insertions(+), 310 deletions(-)
create mode 100644 debian/proxmox-datacenter-manager-api-viewer.install
create mode 100644 docs/api-viewer/Makefile
create mode 100644 docs/api-viewer/index.html
Summary over all repositories:
24 files changed, 679 insertions(+), 628 deletions(-)
--
Generated by git-murpp 0.8.1
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 2/5] pve-api-types: generate array objects Shannon Sterz
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
this allows marking certain api endpoints as unstable. an unstable api
endpoint gives no guarantees that it will stay compatible between
different versions. programmers that make use of an unstable endpoint,
have to expect that anything is subject to change.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 9 ++++++++-
proxmox-router/src/router.rs | 10 ++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index d3d17447..68969735 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -245,6 +245,12 @@ pub fn handle_method(mut attribs: JSONObject, func: syn::ItemFn) -> Result<Token
.transpose()?
.unwrap_or(false);
+ let unstable: bool = attribs
+ .remove("unstable")
+ .map(TryFrom::try_from)
+ .transpose()?
+ .unwrap_or(false);
+
if !attribs.is_empty() {
error!(
attribs.span(),
@@ -329,7 +335,8 @@ pub fn handle_method(mut attribs: JSONObject, func: syn::ItemFn) -> Result<Token
#returns_schema_setter
#access_setter
.reload_timezone(#reload_timezone)
- .protected(#protected);
+ .protected(#protected)
+ .unstable(#unstable);
#default_consts
diff --git a/proxmox-router/src/router.rs b/proxmox-router/src/router.rs
index 732ced8b..f4207d2a 100644
--- a/proxmox-router/src/router.rs
+++ b/proxmox-router/src/router.rs
@@ -789,6 +789,8 @@ pub struct ApiMethod {
/// The protected flag indicates that the provides function should be forwarded
/// to the daemon running in privileged mode.
pub protected: bool,
+ /// Whether this method is still experimental or already stable.
+ pub unstable: bool,
/// This flag indicates that the provided method may change the local timezone, so the server
/// should do a tzset afterwards
pub reload_timezone: bool,
@@ -820,6 +822,7 @@ impl ApiMethod {
handler,
returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
+ unstable: false,
reload_timezone: false,
access: ApiAccess {
description: None,
@@ -838,6 +841,7 @@ impl ApiMethod {
handler: &DUMMY_HANDLER,
returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
+ unstable: false,
reload_timezone: false,
access: ApiAccess {
description: None,
@@ -858,6 +862,12 @@ impl ApiMethod {
self
}
+ pub const fn unstable(mut self, unstable: bool) -> Self {
+ self.unstable = unstable;
+
+ self
+ }
+
pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
self.reload_timezone = reload_timezone;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox v2 2/5] pve-api-types: generate array objects
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 3/5] pve-api-types: fix clippy lints Shannon Sterz
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
Previously the array objects had their *item* as schema, which is
wrong.
We now generate the object schema by pointing to the item schemas for
each field.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
[SS: minor clean up in commit message]
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
pve-api-types/src/types/macros.rs | 148 +++++++++++++++++++++++++++++-
pve-api-types/src/types/mod.rs | 3 +
2 files changed, 149 insertions(+), 2 deletions(-)
diff --git a/pve-api-types/src/types/macros.rs b/pve-api-types/src/types/macros.rs
index 32a04b66..e6116782 100644
--- a/pve-api-types/src/types/macros.rs
+++ b/pve-api-types/src/types/macros.rs
@@ -1,3 +1,124 @@
+use proxmox_schema::SchemaPropertyEntry;
+
+pub(crate) const __DIGIT_SPACE: usize = 4;
+
+/// Since our object schemas need lexicographically sorted names, we generate *those* indices
+/// separately.
+///
+/// The idea is as follows:
+/// - If we can attach a zero without going out of bounds, that's the next number.
+/// (1 => 10 => 100 => 1000, and so on)
+/// - Otherwise, repeat until we end up at zero (which is the end):
+/// - If the number does not end in a `9`, we can just increment. If we don't exceed the limit,
+/// return the number.
+/// (3 => 4, 134 => 135, 3850 => 3851, and so on)
+///
+/// - If it does end with a `9`, cut it off:
+/// (1299 => 129 => 12, 14399 => 1439 => 143)
+const fn next_lexicographical_number(mut at: usize, count: usize) -> Option<usize> {
+ // special case since `0 * 10` is still 0 ;-)
+ if at == 0 {
+ return Some(1);
+ }
+
+ let longer = at * 10;
+ if longer < count {
+ return Some(longer);
+ }
+
+ while at != 0 {
+ if at % 10 != 9 {
+ at += 1;
+ if at < count {
+ return Some(at);
+ }
+ }
+ at /= 10;
+ }
+
+ None
+}
+
+/// Equivalent to `write!(to, "{name}{index}")`.
+pub(crate) const fn write_name_index(to: &mut [u8], name: &'static str, mut index: usize) {
+ let name = name.as_bytes();
+ let mut len = 0;
+ while len != name.len() {
+ to[len] = name[len];
+ len += 1;
+ }
+ if index == 0 {
+ to[len] = b'0';
+ len += 1;
+ } else {
+ let mut digits = 0;
+ let mut copy = index;
+ while copy != 0 {
+ digits += 1;
+ copy /= 10;
+ }
+ len += digits;
+
+ let mut at = len - 1;
+ while index != 0 {
+ to[at] = b'0' + (index % 10) as u8;
+ index /= 10;
+ at -= 1;
+ }
+ }
+}
+
+/// Fill the buffer in `data` with `prefix0`, `prefix1`, `prefix2`, ... - but sorted
+/// lexicographically!
+pub(crate) const fn __fill_names<const N: usize>(prefix: &'static str, data: &mut [u8]) {
+ let unit_size = __DIGIT_SPACE + prefix.len();
+
+ let mut item = 0;
+ let mut sorted_index = Some(0);
+ while item != N {
+ let at = item * unit_size;
+
+ let (_, slot) = data.split_at_mut(at);
+ match sorted_index {
+ None => panic!("ran out of indices"),
+ Some(index) => {
+ write_name_index(slot, prefix, index);
+ sorted_index = next_lexicographical_number(index, N);
+ }
+ }
+
+ item += 1;
+ }
+}
+
+/// Assuming `data` is now an array of field names, perform the equivalent of:
+///
+/// `properties[N].0 = fields[N] foreach N;`
+pub(crate) const fn __fill_properties<const N: usize>(
+ prefix: &'static str,
+ mut data: &'static [u8],
+ properties: &mut [SchemaPropertyEntry; N],
+) {
+ let unit_size = __DIGIT_SPACE + prefix.len();
+ let mut item = 0;
+ while item != N {
+ let slot;
+ (slot, data) = data.split_at(unit_size);
+ let mut len = 0;
+ while len != unit_size && slot[len] != 0 {
+ len += 1;
+ }
+ let slot = slot.split_at(len).0;
+
+ match std::str::from_utf8(slot) {
+ Ok(field_name) => properties[item].0 = field_name,
+ Err(_) => panic!("non utf-8 field"),
+ }
+
+ item += 1;
+ }
+}
+
macro_rules! generate_array_field {
($type_name:ident [ $array_len:expr ] :
$doc:expr,
@@ -12,11 +133,34 @@ macro_rules! generate_array_field {
impl $type_name {
pub const MAX: usize = $array_len;
+
+ const ITEM_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_api_macro::json_schema! $api_def ;
+
+ const ARRAY_OBJECT_SCHEMA: Schema = const {
+ const BUFSIZE: usize = (stringify!($field_prefix).len() + $crate::types::__DIGIT_SPACE) * $array_len;
+
+ const NAMES: [u8; BUFSIZE] = const {
+ let mut buffer = [0u8; BUFSIZE];
+ $crate::types::__fill_names::<$array_len>(stringify!($field_prefix), &mut buffer);
+ buffer
+ };
+
+ const PROPERTIES: [::proxmox_schema::SchemaPropertyEntry; $array_len] = const {
+ let mut properties = [("", false, &$type_name::ITEM_SCHEMA); $array_len];
+ $crate::types::__fill_properties::<$array_len>(stringify!($field_prefix), &NAMES, &mut properties);
+ properties
+ };
+
+ ::proxmox_schema::ObjectSchema::new(
+ concat!("Container for the `", stringify!($field_prefix), "[N]` fields."),
+ &PROPERTIES,
+ ).schema()
+ };
}
impl ::proxmox_schema::ApiType for $type_name {
- const API_SCHEMA: ::proxmox_schema::Schema =
- ::proxmox_api_macro::json_schema! $api_def ;
+ const API_SCHEMA: ::proxmox_schema::Schema = Self::ARRAY_OBJECT_SCHEMA;
}
impl std::ops::Deref for $type_name {
diff --git a/pve-api-types/src/types/mod.rs b/pve-api-types/src/types/mod.rs
index fe52a169..66171e39 100644
--- a/pve-api-types/src/types/mod.rs
+++ b/pve-api-types/src/types/mod.rs
@@ -11,7 +11,10 @@ use serde_json::Value;
use proxmox_schema::{api, const_regex, ApiStringFormat, ApiType, Schema, StringSchema};
mod macros;
+pub(crate) use macros::__fill_names;
+pub(crate) use macros::__fill_properties;
use macros::generate_array_field;
+pub(crate) use macros::__DIGIT_SPACE;
pub mod array;
pub mod stringlist;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox v2 3/5] pve-api-types: fix clippy lints
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 2/5] pve-api-types: generate array objects Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 4/5] docgen: add docgen crate Shannon Sterz
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
by adding liftime parameters and removing an unnecessary assignment.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
pve-api-types/src/types/array.rs | 4 ++--
pve-api-types/src/types/macros.rs | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/pve-api-types/src/types/array.rs b/pve-api-types/src/types/array.rs
index d17fa992..6f468fbf 100644
--- a/pve-api-types/src/types/array.rs
+++ b/pve-api-types/src/types/array.rs
@@ -89,12 +89,12 @@ impl<T, const MAX: usize> ArrayMap<T, { MAX }> {
}
/// Iterator through `(index, &value)` pairs.
- pub fn iter(&self) -> btree_map::Iter<usize, T> {
+ pub fn iter(&self) -> btree_map::Iter<'_, usize, T> {
self.inner.iter()
}
/// Iterator through `(index, &mut value)` pairs.
- pub fn iter_mut(&mut self) -> btree_map::IterMut<usize, T> {
+ pub fn iter_mut(&mut self) -> btree_map::IterMut<'_, usize, T> {
self.inner.iter_mut()
}
diff --git a/pve-api-types/src/types/macros.rs b/pve-api-types/src/types/macros.rs
index e6116782..8e5c3f02 100644
--- a/pve-api-types/src/types/macros.rs
+++ b/pve-api-types/src/types/macros.rs
@@ -49,7 +49,6 @@ pub(crate) const fn write_name_index(to: &mut [u8], name: &'static str, mut inde
}
if index == 0 {
to[len] = b'0';
- len += 1;
} else {
let mut digits = 0;
let mut copy = index;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox v2 4/5] docgen: add docgen crate
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (2 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 3/5] pve-api-types: fix clippy lints Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 5/5] docgen: add support for the new stable flag Shannon Sterz
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
currently this is limited to dumping a `Router` into a `Value` which
can be serialized to JSON and interpreted by an api viewer.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 1 +
proxmox-docgen/Cargo.toml | 20 ++
proxmox-docgen/debian/changelog | 5 +
proxmox-docgen/debian/control | 36 ++++
proxmox-docgen/debian/copyright | 18 ++
proxmox-docgen/debian/debcargo.toml | 7 +
proxmox-docgen/src/lib.rs | 322 ++++++++++++++++++++++++++++
7 files changed, 409 insertions(+)
create mode 100644 proxmox-docgen/Cargo.toml
create mode 100644 proxmox-docgen/debian/changelog
create mode 100644 proxmox-docgen/debian/control
create mode 100644 proxmox-docgen/debian/copyright
create mode 100644 proxmox-docgen/debian/debcargo.toml
create mode 100644 proxmox-docgen/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index c9d734e6..9a4cf1b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ members = [
"proxmox-config-digest",
"proxmox-daemon",
"proxmox-dns-api",
+ "proxmox-docgen",
"proxmox-http",
"proxmox-http-error",
"proxmox-human-byte",
diff --git a/proxmox-docgen/Cargo.toml b/proxmox-docgen/Cargo.toml
new file mode 100644
index 00000000..84a515de
--- /dev/null
+++ b/proxmox-docgen/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "proxmox-docgen"
+description = "Library to easily create documentation from Rust sources for Proxmox projects."
+version = "1.0.0"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+serde_json.workspace = true
+
+proxmox-router.workspace = true
+proxmox-schema = { workspace = true }
+
+
diff --git a/proxmox-docgen/debian/changelog b/proxmox-docgen/debian/changelog
new file mode 100644
index 00000000..79b7e602
--- /dev/null
+++ b/proxmox-docgen/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-docgen (1.0.0-1) trixie; urgency=medium
+
+ * Initial packaging
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 10 Nov 2025 10:44:26 +0200
diff --git a/proxmox-docgen/debian/control b/proxmox-docgen/debian/control
new file mode 100644
index 00000000..4110e9e0
--- /dev/null
+++ b/proxmox-docgen/debian/control
@@ -0,0 +1,36 @@
+Source: rust-proxmox-docgen
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~) <!nocheck>,
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~) <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-docgen
+
+Package: librust-proxmox-docgen-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~),
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-docgen+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0.0+default-dev (= ${binary:Version})
+Description: Easily create documentation from Rust sources for Proxmox projects - Rust source code
+ Source code for Debianized Rust crate "proxmox-docgen"
diff --git a/proxmox-docgen/debian/copyright b/proxmox-docgen/debian/copyright
new file mode 100644
index 00000000..d6e3c304
--- /dev/null
+++ b/proxmox-docgen/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-docgen/debian/debcargo.toml b/proxmox-docgen/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-docgen/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-docgen/src/lib.rs b/proxmox-docgen/src/lib.rs
new file mode 100644
index 00000000..b3ef1ff8
--- /dev/null
+++ b/proxmox-docgen/src/lib.rs
@@ -0,0 +1,322 @@
+use serde_json::{json, Value};
+
+use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
+use proxmox_schema::format::get_property_string_type_text;
+use proxmox_schema::{ApiStringFormat, ObjectSchemaType, Schema};
+
+pub fn generate_api_tree(router: &Router, path: &str, privileges: &[(&str, u64)]) -> Value {
+ let mut data = dump_api_schema(router, path, privileges);
+ data["expanded"] = true.into();
+ data
+}
+
+fn dump_schema(schema: &Schema) -> Value {
+ let mut data;
+
+ match schema {
+ Schema::Null => {
+ data = json!({
+ "type": "null",
+ });
+ }
+ Schema::Boolean(boolean_schema) => {
+ data = json!({
+ "type": "boolean",
+ "description": boolean_schema.description,
+ });
+ if let Some(default) = boolean_schema.default {
+ data["default"] = default.into();
+ }
+ }
+ Schema::String(string_schema) => {
+ data = json!({
+ "type": "string",
+ "description": string_schema.description,
+ });
+ if let Some(default) = string_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(min_length) = string_schema.min_length {
+ data["minLength"] = min_length.into();
+ }
+ if let Some(max_length) = string_schema.max_length {
+ data["maxLength"] = max_length.into();
+ }
+ if let Some(type_text) = string_schema.type_text {
+ data["typetext"] = type_text.into();
+ }
+ match string_schema.format {
+ None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
+ Some(ApiStringFormat::Pattern(const_regex)) => {
+ data["pattern"] = format!("/{}/", const_regex.regex_string).into();
+ }
+ Some(ApiStringFormat::Enum(variants)) => {
+ let variants: Vec<String> =
+ variants.iter().map(|e| e.value.to_string()).collect();
+ data["enum"] = serde_json::to_value(variants).unwrap();
+ }
+ Some(ApiStringFormat::PropertyString(subschema)) => {
+ match subschema {
+ Schema::Object(_) | Schema::Array(_) => {
+ data["format"] = dump_schema(subschema);
+ data["typetext"] = get_property_string_type_text(subschema).into();
+ }
+ _ => { /* do nothing - should not happen */ }
+ };
+ }
+ }
+ }
+ Schema::Integer(integer_schema) => {
+ data = json!({
+ "type": "integer",
+ "description": integer_schema.description,
+ });
+ if let Some(default) = integer_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(minimum) = integer_schema.minimum {
+ data["minimum"] = minimum.into();
+ }
+ if let Some(maximum) = integer_schema.maximum {
+ data["maximum"] = maximum.into();
+ }
+ }
+ Schema::Number(number_schema) => {
+ data = json!({
+ "type": "number",
+ "description": number_schema.description,
+ });
+ if let Some(default) = number_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(minimum) = number_schema.minimum {
+ data["minimum"] = minimum.into();
+ }
+ if let Some(maximum) = number_schema.maximum {
+ data["maximum"] = maximum.into();
+ }
+ }
+ Schema::Object(object_schema) => {
+ data = dump_property_schema(object_schema);
+ data["type"] = "object".into();
+ if let Some(default_key) = object_schema.default_key {
+ data["default_key"] = default_key.into();
+ }
+ }
+ Schema::Array(array_schema) => {
+ data = json!({
+ "type": "array",
+ "description": array_schema.description,
+ "items": dump_schema(array_schema.items),
+ });
+ if let Some(min_length) = array_schema.min_length {
+ data["minLength"] = min_length.into();
+ }
+ if let Some(max_length) = array_schema.min_length {
+ data["maxLength"] = max_length.into();
+ }
+ }
+ Schema::AllOf(alloff_schema) => {
+ data = dump_property_schema(alloff_schema);
+ data["type"] = "object".into();
+ }
+ Schema::OneOf(schema) => {
+ let mut type_schema = dump_schema(schema.type_schema());
+ if schema.type_property_entry.1 {
+ type_schema["optional"] = true.into();
+ }
+ data = json!({
+ "type": "object",
+ "description": schema.description,
+ "typeProperty": schema.type_property(),
+ "typeSchema": type_schema,
+ });
+ let mut variants = Vec::with_capacity(schema.list.len());
+ for (title, variant) in schema.list {
+ let mut entry = dump_schema(variant);
+ entry["title"] = (*title).into();
+ variants.push(entry);
+ }
+ data["oneOf"] = variants.into();
+ }
+ };
+
+ data
+}
+
+fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
+ let mut properties = json!({});
+
+ for (prop, optional, schema) in param.properties() {
+ let mut property = dump_schema(schema);
+ if *optional {
+ property["optional"] = 1.into();
+ }
+ properties[prop] = property;
+ }
+
+ let data = json!({
+ "description": param.description(),
+ "additionalProperties": param.additional_properties(),
+ "properties": properties,
+ });
+
+ data
+}
+
+fn dump_api_permission(permission: &Permission, privileges: &[(&str, u64)]) -> Value {
+ match permission {
+ Permission::Superuser => json!({ "user": "root@pam" }),
+ Permission::User(user) => json!({ "user": user }),
+ Permission::Anybody => json!({ "user": "all" }),
+ Permission::World => json!({ "user": "world" }),
+ Permission::UserParam(param) => json!({ "userParam": param }),
+ Permission::Group(group) => json!({ "group": group }),
+ Permission::WithParam(param, sub_permission) => {
+ json!({
+ "withParam": {
+ "name": param,
+ "permissions": dump_api_permission(sub_permission, privileges),
+ },
+ })
+ }
+ Permission::Privilege(name, value, partial) => {
+ let mut privs = Vec::new();
+ for (name, v) in privileges {
+ if (value & v) != 0 {
+ privs.push(name.to_string());
+ }
+ }
+
+ json!({
+ "check": {
+ "path": name,
+ "privs": privs,
+ "partial": partial,
+ }
+ })
+ }
+ Permission::And(list) => {
+ let list: Vec<Value> = list
+ .iter()
+ .map(|p| dump_api_permission(p, privileges))
+ .collect();
+ json!({ "and": list })
+ }
+ Permission::Or(list) => {
+ let list: Vec<Value> = list
+ .iter()
+ .map(|p| dump_api_permission(p, privileges))
+ .collect();
+ json!({ "or": list })
+ }
+ }
+}
+
+fn dump_api_method_schema(
+ method: &str,
+ api_method: &ApiMethod,
+ privileges: &[(&str, u64)],
+) -> Value {
+ let mut data = json!({
+ "description": api_method.parameters.description(),
+ });
+
+ data["parameters"] = dump_property_schema(&api_method.parameters);
+
+ let mut returns = dump_schema(api_method.returns.schema);
+ if api_method.returns.optional {
+ returns["optional"] = 1.into();
+ }
+ data["returns"] = returns;
+
+ match api_method.access {
+ ApiAccess {
+ description: None,
+ permission: Permission::Superuser,
+ } => {
+ // no need to output default
+ }
+ ApiAccess {
+ description,
+ permission,
+ } => {
+ let mut permissions = dump_api_permission(permission, privileges);
+ if let Some(description) = description {
+ permissions["description"] = description.into();
+ }
+ data["permissions"] = permissions;
+ }
+ }
+
+ let mut method = method;
+
+ if let ApiHandler::AsyncHttp(_) = api_method.handler {
+ method = if method == "POST" { "UPLOAD" } else { method };
+ method = if method == "GET" { "DOWNLOAD" } else { method };
+ }
+
+ data["method"] = method.into();
+
+ data
+}
+
+fn dump_api_schema(router: &Router, path: &str, privileges: &[(&str, u64)]) -> Value {
+ let mut data = json!({});
+
+ let mut info = json!({});
+ if let Some(api_method) = router.get {
+ info["GET"] = dump_api_method_schema("GET", api_method, privileges);
+ }
+ if let Some(api_method) = router.post {
+ info["POST"] = dump_api_method_schema("POST", api_method, privileges);
+ }
+ if let Some(api_method) = router.put {
+ info["PUT"] = dump_api_method_schema("PUT", api_method, privileges);
+ }
+ if let Some(api_method) = router.delete {
+ info["DELETE"] = dump_api_method_schema("DELETE", api_method, privileges);
+ }
+
+ data["info"] = info;
+
+ match &router.subroute {
+ None => {
+ data["leaf"] = 1.into();
+ }
+ Some(SubRoute::MatchAll { router, param_name }) => {
+ let sub_path = if path == "." {
+ format!("/{{{param_name}}}")
+ } else {
+ format!("{path}/{{{param_name}}}")
+ };
+ let mut child = dump_api_schema(router, &sub_path, privileges);
+ child["path"] = sub_path.into();
+ child["text"] = format!("{{{param_name}}}").into();
+
+ let children = vec![child];
+ data["children"] = children.into();
+ data["leaf"] = 0.into();
+ }
+ Some(SubRoute::Map(dirmap)) => {
+ let mut children = Vec::new();
+
+ for (key, sub_router) in dirmap.iter() {
+ let sub_path = if path == "." {
+ format!("/{key}")
+ } else {
+ format!("{path}/{key}")
+ };
+ let mut child = dump_api_schema(sub_router, &sub_path, privileges);
+ child["path"] = sub_path.into();
+ child["text"] = key.to_string().into();
+ children.push(child);
+ }
+
+ data["children"] = children.into();
+ data["leaf"] = 0.into();
+ }
+ }
+
+ data
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox v2 5/5] docgen: add support for the new stable flag
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (3 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 4/5] docgen: add docgen crate Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH widget-toolkit v2 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
proxmox-docgen/src/lib.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/proxmox-docgen/src/lib.rs b/proxmox-docgen/src/lib.rs
index b3ef1ff8..67d502d5 100644
--- a/proxmox-docgen/src/lib.rs
+++ b/proxmox-docgen/src/lib.rs
@@ -229,6 +229,7 @@ fn dump_api_method_schema(
returns["optional"] = 1.into();
}
data["returns"] = returns;
+ data["unstable"] = api_method.unstable.into();
match api_method.access {
ApiAccess {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH widget-toolkit v2 1/1] api viewer: add support for endpoints that are marked as unstable
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (4 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 5/5] docgen: add support for the new stable flag Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate Shannon Sterz
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
display a warning when an endpoint is marked as unstable.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/api-viewer/APIViewer.js | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/api-viewer/APIViewer.js b/src/api-viewer/APIViewer.js
index 7f27e0d..1357741 100644
--- a/src/api-viewer/APIViewer.js
+++ b/src/api-viewer/APIViewer.js
@@ -210,7 +210,21 @@ Ext.onReady(function () {
usage += cliUsageRenderer(method, endpoint);
}
- let sections = [
+ let sections = [];
+
+ if (info.unstable) {
+ sections.push({
+ title: 'Unstable',
+ html: `<div class="proxmox-warning-row" style="padding: 10px;">
+ <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
+ This API endpoint is marked as unstable. All information on this
+ page is subject to change, including input parameters, return values
+ and permissions.
+ </div>`,
+ });
+ }
+
+ sections.concat([
{
title: 'Description',
html: Ext.htmlEncode(info.description),
@@ -221,7 +235,7 @@ Ext.onReady(function () {
html: usage,
bodyPadding: 10,
},
- ];
+ ]);
if (info.parameters && info.parameters.properties) {
let pstore = Ext.create('Ext.data.Store', {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (5 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH widget-toolkit v2 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to " Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package Shannon Sterz
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
this has the benefit of also handling the unstable flag correctly.
hence, the api viewer needs to add the css definitions to properly
render unstable endpoints.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 3 +
docs/api-viewer/index.html | 2 +
src/bin/docgen.rs | 326 ++-----------------------------------
3 files changed, 20 insertions(+), 311 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 28fdbf54..fbd16ccb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ proxmox-borrow = "1"
proxmox-compression = "1.0.1"
proxmox-config-digest = "1"
proxmox-daemon = "1"
+proxmox-docgen = "1"
proxmox-fuse = "1"
proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
@@ -216,6 +217,7 @@ proxmox-base64.workspace = true
proxmox-compression.workspace = true
proxmox-config-digest.workspace = true
proxmox-daemon.workspace = true
+proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
proxmox-human-byte.workspace = true
proxmox-io.workspace = true
@@ -275,6 +277,7 @@ proxmox-rrd-api-types.workspace = true
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
#proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
#proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
+#proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
#proxmox-http = { path = "../proxmox/proxmox-http" }
#proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
#proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/docs/api-viewer/index.html b/docs/api-viewer/index.html
index 72dae96a..28f5b3b7 100644
--- a/docs/api-viewer/index.html
+++ b/docs/api-viewer/index.html
@@ -6,6 +6,8 @@
<title>Proxmox Backup Server API Documentation</title>
<link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
+ <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
+ <link rel="stylesheet" type="text/css" href="/widgettoolkit/css/ext6-pmx.css" />
<link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/widgettoolkit/themes/theme-proxmox-dark.css" />
<script type="text/javascript" src="extjs/ext-all.js"></script>
<script type="text/javascript" src="apidoc.js"></script>
diff --git a/src/bin/docgen.rs b/src/bin/docgen.rs
index 674be945..04d2de29 100644
--- a/src/bin/docgen.rs
+++ b/src/bin/docgen.rs
@@ -1,9 +1,7 @@
use anyhow::{bail, Error};
-use serde_json::{json, Value};
-use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
-use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
-use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
+use proxmox_schema::format::dump_enum_properties;
+use proxmox_schema::ApiType;
use proxmox_section_config::dump_section_config;
use pbs_api_types::PRIVILEGES;
@@ -55,22 +53,30 @@ fn main() -> Result<(), Error> {
fn generate_api_tree() -> String {
let mut tree = Vec::new();
- let mut data = dump_api_schema(&api2::ROUTER, ".");
+ let mut data = proxmox_docgen::generate_api_tree(&api2::ROUTER, ".", PRIVILEGES);
data["path"] = "/".into();
// hack: add invisible space to sort as first entry
data["text"] = "​Management API (HTTP)".into();
- data["expanded"] = true.into();
-
tree.push(data);
- let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_");
+ let mut data = proxmox_docgen::generate_api_tree(
+ &api2::backup::BACKUP_API_ROUTER,
+ "/backup/_upgrade_",
+ PRIVILEGES,
+ );
data["path"] = "/backup/_upgrade_".into();
data["text"] = "Backup API (HTTP/2)".into();
+ data["expanded"] = false.into();
tree.push(data);
- let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_");
+ let mut data = proxmox_docgen::generate_api_tree(
+ &api2::reader::READER_API_ROUTER,
+ "/reader/_upgrade_",
+ PRIVILEGES,
+ );
data["path"] = "/reader/_upgrade_".into();
data["text"] = "Restore API (HTTP/2)".into();
+ data["expanded"] = false.into();
tree.push(data);
format!(
@@ -78,305 +84,3 @@ fn generate_api_tree() -> String {
serde_json::to_string_pretty(&tree).unwrap()
)
}
-
-pub fn dump_schema(schema: &Schema) -> Value {
- let mut data;
-
- match schema {
- Schema::Null => {
- data = json!({
- "type": "null",
- });
- }
- Schema::Boolean(boolean_schema) => {
- data = json!({
- "type": "boolean",
- "description": boolean_schema.description,
- });
- if let Some(default) = boolean_schema.default {
- data["default"] = default.into();
- }
- }
- Schema::String(string_schema) => {
- data = json!({
- "type": "string",
- "description": string_schema.description,
- });
- if let Some(default) = string_schema.default {
- data["default"] = default.into();
- }
- if let Some(min_length) = string_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = string_schema.max_length {
- data["maxLength"] = max_length.into();
- }
- if let Some(type_text) = string_schema.type_text {
- data["typetext"] = type_text.into();
- }
- match string_schema.format {
- None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
- Some(ApiStringFormat::Pattern(const_regex)) => {
- data["pattern"] = format!("/{}/", const_regex.regex_string).into();
- }
- Some(ApiStringFormat::Enum(variants)) => {
- let variants: Vec<String> =
- variants.iter().map(|e| e.value.to_string()).collect();
- data["enum"] = serde_json::to_value(variants).unwrap();
- }
- Some(ApiStringFormat::PropertyString(subschema)) => {
- match subschema {
- Schema::Object(_) | Schema::Array(_) => {
- data["format"] = dump_schema(subschema);
- data["typetext"] = get_property_string_type_text(subschema).into();
- }
- _ => { /* do nothing - should not happen */ }
- };
- }
- }
- // fixme: dump format
- }
- Schema::Integer(integer_schema) => {
- data = json!({
- "type": "integer",
- "description": integer_schema.description,
- });
- if let Some(default) = integer_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = integer_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = integer_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Number(number_schema) => {
- data = json!({
- "type": "number",
- "description": number_schema.description,
- });
- if let Some(default) = number_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = number_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = number_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Object(object_schema) => {
- data = dump_property_schema(object_schema);
- data["type"] = "object".into();
- if let Some(default_key) = object_schema.default_key {
- data["default_key"] = default_key.into();
- }
- }
- Schema::Array(array_schema) => {
- data = json!({
- "type": "array",
- "description": array_schema.description,
- "items": dump_schema(array_schema.items),
- });
- if let Some(min_length) = array_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = array_schema.min_length {
- data["maxLength"] = max_length.into();
- }
- }
- Schema::AllOf(alloff_schema) => {
- data = dump_property_schema(alloff_schema);
- data["type"] = "object".into();
- }
- Schema::OneOf(schema) => {
- let mut type_schema = dump_schema(schema.type_schema());
- if schema.type_property_entry.1 {
- type_schema["optional"] = true.into();
- }
- data = json!({
- "type": "object",
- "description": schema.description,
- "typeProperty": schema.type_property(),
- "typeSchema": type_schema,
- });
- let mut variants = Vec::with_capacity(schema.list.len());
- for (title, variant) in schema.list {
- let mut entry = dump_schema(variant);
- entry["title"] = (*title).into();
- variants.push(entry);
- }
- data["oneOf"] = variants.into();
- }
- };
-
- data
-}
-
-pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
- let mut properties = json!({});
-
- for (prop, optional, schema) in param.properties() {
- let mut property = dump_schema(schema);
- if *optional {
- property["optional"] = 1.into();
- }
- properties[prop] = property;
- }
-
- let data = json!({
- "description": param.description(),
- "additionalProperties": param.additional_properties(),
- "properties": properties,
- });
-
- data
-}
-
-fn dump_api_permission(permission: &Permission) -> Value {
- match permission {
- Permission::Superuser => json!({ "user": "root@pam" }),
- Permission::User(user) => json!({ "user": user }),
- Permission::Anybody => json!({ "user": "all" }),
- Permission::World => json!({ "user": "world" }),
- Permission::UserParam(param) => json!({ "userParam": param }),
- Permission::Group(group) => json!({ "group": group }),
- Permission::WithParam(param, sub_permission) => {
- json!({
- "withParam": {
- "name": param,
- "permissions": dump_api_permission(sub_permission),
- },
- })
- }
- Permission::Privilege(name, value, partial) => {
- let mut privs = Vec::new();
- for (name, v) in PRIVILEGES {
- if (value & v) != 0 {
- privs.push(name.to_string());
- }
- }
-
- json!({
- "check": {
- "path": name,
- "privs": privs,
- "partial": partial,
- }
- })
- }
- Permission::And(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "and": list })
- }
- Permission::Or(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "or": list })
- }
- }
-}
-
-fn dump_api_method_schema(method: &str, api_method: &ApiMethod) -> Value {
- let mut data = json!({
- "description": api_method.parameters.description(),
- });
-
- data["parameters"] = dump_property_schema(&api_method.parameters);
-
- let mut returns = dump_schema(api_method.returns.schema);
- if api_method.returns.optional {
- returns["optional"] = 1.into();
- }
- data["returns"] = returns;
-
- match api_method.access {
- ApiAccess {
- description: None,
- permission: Permission::Superuser,
- } => {
- // no need to output default
- }
- ApiAccess {
- description,
- permission,
- } => {
- let mut permissions = dump_api_permission(permission);
- if let Some(description) = description {
- permissions["description"] = description.into();
- }
- data["permissions"] = permissions;
- }
- }
-
- let mut method = method;
-
- if let ApiHandler::AsyncHttp(_) = api_method.handler {
- method = if method == "POST" { "UPLOAD" } else { method };
- method = if method == "GET" { "DOWNLOAD" } else { method };
- }
-
- data["method"] = method.into();
-
- data
-}
-
-pub fn dump_api_schema(router: &Router, path: &str) -> Value {
- let mut data = json!({});
-
- let mut info = json!({});
- if let Some(api_method) = router.get {
- info["GET"] = dump_api_method_schema("GET", api_method);
- }
- if let Some(api_method) = router.post {
- info["POST"] = dump_api_method_schema("POST", api_method);
- }
- if let Some(api_method) = router.put {
- info["PUT"] = dump_api_method_schema("PUT", api_method);
- }
- if let Some(api_method) = router.delete {
- info["DELETE"] = dump_api_method_schema("DELETE", api_method);
- }
-
- data["info"] = info;
-
- match &router.subroute {
- None => {
- data["leaf"] = 1.into();
- }
- Some(SubRoute::MatchAll { router, param_name }) => {
- let sub_path = if path == "." {
- format!("/{{{param_name}}}")
- } else {
- format!("{path}/{{{param_name}}}")
- };
- let mut child = dump_api_schema(router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = format!("{{{param_name}}}").into();
-
- let children = vec![child];
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- Some(SubRoute::Map(dirmap)) => {
- let mut children = Vec::new();
-
- for (key, sub_router) in dirmap.iter() {
- let sub_path = if path == "." {
- format!("/{key}")
- } else {
- format!("{path}/{key}")
- };
- let mut child = dump_api_schema(sub_router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = key.to_string().into();
- children.push(child);
- }
-
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- }
-
- data
-}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to proxmox-rs docgen crate
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (6 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package Shannon Sterz
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
and use pdm permissions not pbs ones.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 2 +
server/Cargo.toml | 1 +
server/src/bin/docgen.rs | 314 +--------------------------------------
3 files changed, 7 insertions(+), 310 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 3252ccb..f9a8872 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-auth-api = "1"
proxmox-base64 = "1"
proxmox-client = "1"
proxmox-daemon = "1"
+proxmox-docgen = "1"
proxmox-http = { version = "1", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
proxmox-io = "1.0.1" # tools and client use "tokio" feature
@@ -154,6 +155,7 @@ zstd = { version = "0.13" }
# proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
# proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
# proxmox-dns-api = { path = "../proxmox/proxmox-dns-api" }
+# proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
# proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
# proxmox-http = { path = "../proxmox/proxmox-http" }
# proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/server/Cargo.toml b/server/Cargo.toml
index b336f1a..6969549 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-async.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
proxmox-base64.workspace = true
proxmox-daemon.workspace = true
+proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
proxmox-lang.workspace = true
proxmox-ldap.workspace = true
diff --git a/server/src/bin/docgen.rs b/server/src/bin/docgen.rs
index cbf6fc0..85e05ec 100644
--- a/server/src/bin/docgen.rs
+++ b/server/src/bin/docgen.rs
@@ -1,12 +1,10 @@
use anyhow::{bail, Error};
-use serde_json::{json, Value};
-use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
-use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
-use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
+use proxmox_schema::format::dump_enum_properties;
+use proxmox_schema::ApiType;
use proxmox_section_config::{dump_section_config, typed::ApiSectionDataEntry};
-use pbs_api_types::PRIVILEGES;
+use pdm_api_types::PRIVILEGES;
use server::api;
@@ -45,12 +43,10 @@ fn main() -> Result<(), Error> {
fn generate_api_tree() -> String {
let mut tree = Vec::new();
- let mut data = dump_api_schema(&api::ROUTER, ".");
+ let mut data = proxmox_docgen::generate_api_tree(&api::ROUTER, ".", PRIVILEGES);
data["path"] = "/".into();
// hack: add invisible space to sort as first entry
data["text"] = "​Management API (HTTP)".into();
- data["expanded"] = true.into();
-
tree.push(data);
format!(
@@ -58,305 +54,3 @@ fn generate_api_tree() -> String {
serde_json::to_string_pretty(&tree).unwrap()
)
}
-
-pub fn dump_schema(schema: &Schema) -> Value {
- let mut data;
-
- match schema {
- Schema::Null => {
- data = json!({
- "type": "null",
- });
- }
- Schema::Boolean(boolean_schema) => {
- data = json!({
- "type": "boolean",
- "description": boolean_schema.description,
- });
- if let Some(default) = boolean_schema.default {
- data["default"] = default.into();
- }
- }
- Schema::String(string_schema) => {
- data = json!({
- "type": "string",
- "description": string_schema.description,
- });
- if let Some(default) = string_schema.default {
- data["default"] = default.into();
- }
- if let Some(min_length) = string_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = string_schema.max_length {
- data["maxLength"] = max_length.into();
- }
- if let Some(type_text) = string_schema.type_text {
- data["typetext"] = type_text.into();
- }
- match string_schema.format {
- None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
- Some(ApiStringFormat::Pattern(const_regex)) => {
- data["pattern"] = format!("/{}/", const_regex.regex_string).into();
- }
- Some(ApiStringFormat::Enum(variants)) => {
- let variants: Vec<String> =
- variants.iter().map(|e| e.value.to_string()).collect();
- data["enum"] = serde_json::to_value(variants).unwrap();
- }
- Some(ApiStringFormat::PropertyString(subschema)) => {
- match subschema {
- Schema::Object(_) | Schema::Array(_) => {
- data["format"] = dump_schema(subschema);
- data["typetext"] = get_property_string_type_text(subschema).into();
- }
- _ => { /* do nothing - should not happen */ }
- };
- }
- }
- // fixme: dump format
- }
- Schema::Integer(integer_schema) => {
- data = json!({
- "type": "integer",
- "description": integer_schema.description,
- });
- if let Some(default) = integer_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = integer_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = integer_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Number(number_schema) => {
- data = json!({
- "type": "number",
- "description": number_schema.description,
- });
- if let Some(default) = number_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = number_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = number_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Object(object_schema) => {
- data = dump_property_schema(object_schema);
- data["type"] = "object".into();
- if let Some(default_key) = object_schema.default_key {
- data["default_key"] = default_key.into();
- }
- }
- Schema::Array(array_schema) => {
- data = json!({
- "type": "array",
- "description": array_schema.description,
- "items": dump_schema(array_schema.items),
- });
- if let Some(min_length) = array_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = array_schema.min_length {
- data["maxLength"] = max_length.into();
- }
- }
- Schema::AllOf(alloff_schema) => {
- data = dump_property_schema(alloff_schema);
- data["type"] = "object".into();
- }
- Schema::OneOf(schema) => {
- let mut type_schema = dump_schema(schema.type_schema());
- if schema.type_property_entry.1 {
- type_schema["optional"] = true.into();
- }
- data = json!({
- "type": "object",
- "description": schema.description,
- "typeProperty": schema.type_property(),
- "typeSchema": type_schema,
- });
- let mut variants = Vec::with_capacity(schema.list.len());
- for (title, variant) in schema.list {
- let mut entry = dump_schema(variant);
- entry["title"] = (*title).into();
- variants.push(entry);
- }
- data["oneOf"] = variants.into();
- }
- };
-
- data
-}
-
-pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
- let mut properties = json!({});
-
- for (prop, optional, schema) in param.properties() {
- let mut property = dump_schema(schema);
- if *optional {
- property["optional"] = 1.into();
- }
- properties[prop] = property;
- }
-
- let data = json!({
- "description": param.description(),
- "additionalProperties": param.additional_properties(),
- "properties": properties,
- });
-
- data
-}
-
-fn dump_api_permission(permission: &Permission) -> Value {
- match permission {
- Permission::Superuser => json!({ "user": "root@pam" }),
- Permission::User(user) => json!({ "user": user }),
- Permission::Anybody => json!({ "user": "all" }),
- Permission::World => json!({ "user": "world" }),
- Permission::UserParam(param) => json!({ "userParam": param }),
- Permission::Group(group) => json!({ "group": group }),
- Permission::WithParam(param, sub_permission) => {
- json!({
- "withParam": {
- "name": param,
- "permissions": dump_api_permission(sub_permission),
- },
- })
- }
- Permission::Privilege(name, value, partial) => {
- let mut privs = Vec::new();
- for (name, v) in PRIVILEGES {
- if (value & v) != 0 {
- privs.push(name.to_string());
- }
- }
-
- json!({
- "check": {
- "path": name,
- "privs": privs,
- "partial": partial,
- }
- })
- }
- Permission::And(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "and": list })
- }
- Permission::Or(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "or": list })
- }
- }
-}
-
-fn dump_api_method_schema(method: &str, api_method: &ApiMethod) -> Value {
- let mut data = json!({
- "description": api_method.parameters.description(),
- });
-
- data["parameters"] = dump_property_schema(&api_method.parameters);
-
- let mut returns = dump_schema(api_method.returns.schema);
- if api_method.returns.optional {
- returns["optional"] = 1.into();
- }
- data["returns"] = returns;
-
- match api_method.access {
- ApiAccess {
- description: None,
- permission: Permission::Superuser,
- } => {
- // no need to output default
- }
- ApiAccess {
- description,
- permission,
- } => {
- let mut permissions = dump_api_permission(permission);
- if let Some(description) = description {
- permissions["description"] = description.into();
- }
- data["permissions"] = permissions;
- }
- }
-
- let mut method = method;
-
- if let ApiHandler::AsyncHttp(_) = api_method.handler {
- method = if method == "POST" { "UPLOAD" } else { method };
- method = if method == "GET" { "DOWNLOAD" } else { method };
- }
-
- data["method"] = method.into();
-
- data
-}
-
-pub fn dump_api_schema(router: &Router, path: &str) -> Value {
- let mut data = json!({});
-
- let mut info = json!({});
- if let Some(api_method) = router.get {
- info["GET"] = dump_api_method_schema("GET", api_method);
- }
- if let Some(api_method) = router.post {
- info["POST"] = dump_api_method_schema("POST", api_method);
- }
- if let Some(api_method) = router.put {
- info["PUT"] = dump_api_method_schema("PUT", api_method);
- }
- if let Some(api_method) = router.delete {
- info["DELETE"] = dump_api_method_schema("DELETE", api_method);
- }
-
- data["info"] = info;
-
- match &router.subroute {
- None => {
- data["leaf"] = 1.into();
- }
- Some(SubRoute::MatchAll { router, param_name }) => {
- let sub_path = if path == "." {
- format!("/{{{}}}", param_name)
- } else {
- format!("{}/{{{}}}", path, param_name)
- };
- let mut child = dump_api_schema(router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = format!("{{{}}}", param_name).into();
-
- let children = vec![child];
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- Some(SubRoute::Map(dirmap)) => {
- let mut children = Vec::new();
-
- for (key, sub_router) in dirmap.iter() {
- let sub_path = if path == "." {
- format!("/{}", key)
- } else {
- format!("{}/{}", path, key)
- };
- let mut child = dump_api_schema(sub_router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = key.to_string().into();
- children.push(child);
- }
-
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- }
-
- data
-}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
` (7 preceding siblings ...)
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to " Shannon Sterz
@ 2025-11-13 12:00 ` Shannon Sterz
8 siblings, 0 replies; 10+ messages in thread
From: Shannon Sterz @ 2025-11-13 12:00 UTC (permalink / raw)
To: pdm-devel
this can be installed as a suggested package and will then be served
at `https://$pdm-host::8443/docs/api-viewer/apidoc.js`.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Makefile | 1 +
debian/control | 13 +++++++++
...xmox-datacenter-manager-api-viewer.install | 1 +
docs/api-viewer/Makefile | 27 +++++++++++++++++++
docs/api-viewer/index.html | 16 +++++++++++
5 files changed, 58 insertions(+)
create mode 100644 debian/proxmox-datacenter-manager-api-viewer.install
create mode 100644 docs/api-viewer/Makefile
create mode 100644 docs/api-viewer/index.html
diff --git a/Makefile b/Makefile
index b2f794a..ca721eb 100644
--- a/Makefile
+++ b/Makefile
@@ -82,6 +82,7 @@ install: $(COMPILED_BINS) $(SHELL_COMPLETION_FILES)
$(foreach i,$(ZSH_COMPLETIONS), \
install -m644 $(COMPLETION_DIR)/$(i) $(DESTDIR)$(ZSHCOMPDIR)/ ;)
make -C services install
+ make -C docs/api-viewer install
$(COMPILED_BINS): .do-cargo-build
.do-cargo-build:
diff --git a/debian/control b/debian/control
index 19b13c8..ebe0923 100644
--- a/debian/control
+++ b/debian/control
@@ -125,6 +125,7 @@ Build-Depends: cargo:native,
librust-zstd-0.13+default-dev,
libstd-rust-dev,
libsystemd-dev,
+ proxmox-widget-toolkit-dev,
rustc:native,
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.6.1
@@ -143,6 +144,7 @@ Depends: proxmox-mini-journalreader,
${misc:Depends},
${shlibs:Depends},
Recommends: proxmox-datacenter-manager-client, proxmox-datacenter-manager-ui,
+Suggests: proxmox-datacenter-manager-api-viewer,
Description: Manage multiple Proxmox VE cluster and other Proxmox projects
This package provides the API daemons of the Proxmox Datacenter Manager (PDM)
which allows one to add multiple Proxmox VE and Proxmox Backup Server
@@ -155,3 +157,14 @@ Depends: ${misc:Depends}, ${shlibs:Depends},
Description: CLI Client for the Proxmox Datacenter Manager
This package provides the CLI client that can interface with a Proxmox
Datacenter Manager (PDM) instance.
+
+Package: proxmox-datacenter-manager-api-viewer
+Architecture: any
+Multi-Arch: allowed
+Depends: fonts-font-awesome,
+ libjs-extjs (>= 7~),
+ proxmox-widget-toolkit,
+ ${misc:Depends},
+ ${shlibs:Depends},
+Recommends: proxmox-datacenter-manager,
+Description: API Viewer for the Proxmox Datacenter Manager.
diff --git a/debian/proxmox-datacenter-manager-api-viewer.install b/debian/proxmox-datacenter-manager-api-viewer.install
new file mode 100644
index 0000000..0e4d9df
--- /dev/null
+++ b/debian/proxmox-datacenter-manager-api-viewer.install
@@ -0,0 +1 @@
+usr/share/doc/proxmox-datacenter-manager/html
diff --git a/docs/api-viewer/Makefile b/docs/api-viewer/Makefile
new file mode 100644
index 0000000..c64bfbb
--- /dev/null
+++ b/docs/api-viewer/Makefile
@@ -0,0 +1,27 @@
+DOCDIR = /usr/share/doc/proxmox-datacenter-manager
+
+API_VIEWER_SOURCES= \
+ index.html \
+ apidoc.js
+
+API_VIEWER_FILES := \
+ apidata.js \
+ /usr/share/javascript/proxmox-widget-toolkit-dev/APIViewer.js
+
+ifeq ($(BUILD_MODE), release)
+COMPILEDIR := ../../target/release
+else
+COMPILEDIR := ../../target/debug
+endif
+
+.PHONY: install
+install: ${API_VIEWER_SOURCES}
+ install -dm 0755 $(DESTDIR)$(DOCDIR)/html/api-viewer
+ install -m 0644 ${API_VIEWER_SOURCES} $(DESTDIR)$(DOCDIR)/html/api-viewer
+
+apidata.js: ${COMPILEDIR}/docgen
+ ${COMPILEDIR}/docgen apidata.js >$@
+
+apidoc.js: ${API_VIEWER_FILES}
+ cat ${API_VIEWER_FILES} >$@.tmp
+ mv $@.tmp $@
diff --git a/docs/api-viewer/index.html b/docs/api-viewer/index.html
new file mode 100644
index 0000000..2bf97c5
--- /dev/null
+++ b/docs/api-viewer/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+ <title>Proxmox Datacenter Manager API Documentation</title>
+
+ <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css">
+ <link rel="stylesheet" type="text/css" href="/proxmox-extjs-widget-toolkit/css/ext6-pmx.css" />
+ <link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/proxmox-extjs-widget-toolkit/themes/theme-proxmox-dark.css" />
+ <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
+ <script type="text/javascript" src="/extjs/ext-all.js"></script>
+ <script type="text/javascript" src="apidoc.js"></script>
+</head>
+<body></body>
+</html>
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2025-11-13 12:00 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-13 12:00 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v2 0/9] unstable flag and pdm api viewer Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 2/5] pve-api-types: generate array objects Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 3/5] pve-api-types: fix clippy lints Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 4/5] docgen: add docgen crate Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox v2 5/5] docgen: add support for the new stable flag Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH widget-toolkit v2 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] docgen: use proxmox-rs docgen crate Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 1/2] docgen: switch to " Shannon Sterz
2025-11-13 12:00 ` [pdm-devel] [PATCH datacenter-manager v2 2/2] api-viewer: add an api-viewer package Shannon Sterz
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.