all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pbs-devel] [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs
@ 2025-02-21  6:50 Dietmar Maurer
  2025-02-21  6:50 ` [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format Dietmar Maurer
  2025-02-21  9:10 ` [pbs-devel] applied: [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Thomas Lamprecht
  0 siblings, 2 replies; 3+ messages in thread
From: Dietmar Maurer @ 2025-02-21  6:50 UTC (permalink / raw)
  To: pbs-devel

Lines before commanmd groups are missing (i.e. see proxmox-backup-client command
syntax docs)

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 proxmox-router/src/cli/format.rs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/proxmox-router/src/cli/format.rs b/proxmox-router/src/cli/format.rs
index 84936336..95448aa9 100644
--- a/proxmox-router/src/cli/format.rs
+++ b/proxmox-router/src/cli/format.rs
@@ -415,6 +415,9 @@ fn generate_nested_usage_do<'cli>(
                 ));
             }
             CommandLineInterface::Nested(map) => {
+                if format == DocumentationFormat::ReST {
+                    usage.push_str("\n----\n\n");
+                }
                 usage.push_str(&generate_nested_usage_do(state, &new_prefix, map, format));
             }
         }
-- 
2.39.5


_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format
  2025-02-21  6:50 [pbs-devel] [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Dietmar Maurer
@ 2025-02-21  6:50 ` Dietmar Maurer
  2025-02-21  9:10 ` [pbs-devel] applied: [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Thomas Lamprecht
  1 sibling, 0 replies; 3+ messages in thread
From: Dietmar Maurer @ 2025-02-21  6:50 UTC (permalink / raw)
  To: pbs-devel

Try to keep code sparate from Rest formatting, so that we can easily
remove unnecessary parts later.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 proxmox-router/src/cli/command.rs |  18 +++
 proxmox-router/src/cli/format.rs  |  29 ++--
 proxmox-router/src/format.rs      |  28 ++--
 proxmox-schema/src/format.rs      | 213 ++++++++++++++++++++++++------
 proxmox-section-config/src/lib.rs |  43 +++++-
 5 files changed, 270 insertions(+), 61 deletions(-)

diff --git a/proxmox-router/src/cli/command.rs b/proxmox-router/src/cli/command.rs
index 01f64d19..64a28d92 100644
--- a/proxmox-router/src/cli/command.rs
+++ b/proxmox-router/src/cli/command.rs
@@ -406,6 +406,24 @@ where
             println!("{}", usage);
             std::process::exit(0);
         }
+
+        if args[0] == "printdoc-djot" {
+            let usage = match def {
+                CommandLineInterface::Simple(cli_cmd) => generate_usage_str_do(
+                    &prefix,
+                    cli_cmd,
+                    DocumentationFormat::Djot,
+                    "",
+                    &[],
+                    [].into_iter(),
+                ),
+                CommandLineInterface::Nested(map) => {
+                    generate_nested_usage(&prefix, map, DocumentationFormat::Djot)
+                }
+            };
+            println!("{}", usage);
+            std::process::exit(0);
+        }
     }
 
     (prefix, args)
diff --git a/proxmox-router/src/cli/format.rs b/proxmox-router/src/cli/format.rs
index 95448aa9..b449df1f 100644
--- a/proxmox-router/src/cli/format.rs
+++ b/proxmox-router/src/cli/format.rs
@@ -106,6 +106,7 @@ pub(crate) fn generate_usage_str_do<'cli>(
                 if is_array {
                     args.push('{');
                 }
+
                 args.push('<');
                 args.push_str(positional_arg);
                 args.push('>');
@@ -194,6 +195,10 @@ pub(crate) fn generate_usage_str_do<'cli>(
             "``{prefix}{args}{option_indicator}``\n\n{}",
             schema.description()
         ),
+        DocumentationFormat::Djot => format!(
+            "`{prefix}{args}{option_indicator}`\n\n{}",
+            schema.description()
+        ),
     };
 
     if !arg_descr.is_empty() {
@@ -217,7 +222,7 @@ pub(crate) fn generate_usage_str_do<'cli>(
         if done_hash.contains(name) {
             continue;
         }
-        if format == DocumentationFormat::ReST {
+        if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot {
             use std::fmt::Write as _;
 
             // In the ReST outputs we don't include the documentation for global options each time
@@ -233,8 +238,13 @@ pub(crate) fn generate_usage_str_do<'cli>(
             if !global_options.is_empty() {
                 global_options.push_str("\n\n");
             }
-
-            let _ = write!(global_options, "``--{name}``");
+            if format == DocumentationFormat::ReST {
+                let _ = write!(global_options, "``--{name}``");
+            } else if format == DocumentationFormat::Djot {
+                let _ = write!(global_options, "`--{name}`");
+            } else {
+                unreachable!();
+            }
         } else {
             // For the other ones we use the same generation method as for any other option:
 
@@ -376,8 +386,8 @@ fn generate_nested_usage_do<'cli>(
 
     let globals = state.describe_current(prefix, format);
     if !globals.is_empty() {
-        if format == DocumentationFormat::ReST {
-            usage.push_str("----\n\n");
+        if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot {
+            usage.push_str("\n----\n\n");
         }
         usage.push_str(&globals);
     }
@@ -402,8 +412,10 @@ fn generate_nested_usage_do<'cli>(
 
         match def.commands.get(cmd).unwrap() {
             CommandLineInterface::Simple(cli_cmd) => {
-                if !usage.is_empty() && format == DocumentationFormat::ReST {
-                    usage.push_str("----\n\n");
+                if !usage.is_empty()
+                    && (format == DocumentationFormat::ReST || format == DocumentationFormat::Djot)
+                {
+                    usage.push_str("\n----\n\n");
                 }
                 usage.push_str(&generate_usage_str_do(
                     &new_prefix,
@@ -415,9 +427,10 @@ fn generate_nested_usage_do<'cli>(
                 ));
             }
             CommandLineInterface::Nested(map) => {
-                if format == DocumentationFormat::ReST {
+                if format == DocumentationFormat::ReST || format == DocumentationFormat::Djot {
                     usage.push_str("\n----\n\n");
                 }
+
                 usage.push_str(&generate_nested_usage_do(state, &new_prefix, map, format));
             }
         }
diff --git a/proxmox-router/src/format.rs b/proxmox-router/src/format.rs
index 67568af0..8b2d4488 100644
--- a/proxmox-router/src/format.rs
+++ b/proxmox-router/src/format.rs
@@ -4,22 +4,28 @@ use std::io::Write;
 
 use anyhow::Error;
 
-use proxmox_schema::format::*;
+use proxmox_schema::format::{
+    dump_api_return_schema_rest, dump_properties_rest, wrap_text, ParameterDisplayStyle,
+};
 use proxmox_schema::ObjectSchemaType;
 
 #[cfg(feature = "server")]
 use crate::ApiHandler;
 use crate::ApiMethod;
 
-fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> {
+fn dump_method_definition_rest(
+    method: &str,
+    path: &str,
+    def: Option<&ApiMethod>,
+) -> Option<String> {
     let style = ParameterDisplayStyle::Config;
     match def {
         None => None,
         Some(api_method) => {
             let description = wrap_text("", "", api_method.parameters.description(), 80);
-            let param_descr = dump_properties(&api_method.parameters, "", style, &[]);
+            let param_descr = dump_properties_rest(&api_method.parameters, "", style, &[]);
 
-            let return_descr = dump_api_return_schema(&api_method.returns, style);
+            let return_descr = dump_api_return_schema_rest(&api_method.returns, style);
 
             #[cfg(feature = "server")]
             let mut method = method;
@@ -42,7 +48,7 @@ fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) ->
 }
 
 /// Generate ReST Documentation for a complete API defined by a ``Router``.
-pub fn dump_api(
+pub fn dump_api_rest(
     output: &mut dyn Write,
     router: &crate::Router,
     path: &str,
@@ -61,10 +67,10 @@ pub fn dump_api(
         Ok(())
     };
 
-    cond_print(dump_method_definition("GET", path, router.get))?;
-    cond_print(dump_method_definition("POST", path, router.post))?;
-    cond_print(dump_method_definition("PUT", path, router.put))?;
-    cond_print(dump_method_definition("DELETE", path, router.delete))?;
+    cond_print(dump_method_definition_rest("GET", path, router.get))?;
+    cond_print(dump_method_definition_rest("POST", path, router.post))?;
+    cond_print(dump_method_definition_rest("PUT", path, router.put))?;
+    cond_print(dump_method_definition_rest("DELETE", path, router.delete))?;
 
     match &router.subroute {
         None => return Ok(()),
@@ -74,7 +80,7 @@ pub fn dump_api(
             } else {
                 format!("{}/<{}>", path, param_name)
             };
-            dump_api(output, router, &sub_path, pos)?;
+            dump_api_rest(output, router, &sub_path, pos)?;
         }
         Some(SubRoute::Map(dirmap)) => {
             //let mut keys: Vec<&String> = map.keys().collect();
@@ -85,7 +91,7 @@ pub fn dump_api(
                 } else {
                     format!("{}/{}", path, key)
                 };
-                dump_api(output, sub_router, &sub_path, pos)?;
+                dump_api_rest(output, sub_router, &sub_path, pos)?;
             }
         }
     }
diff --git a/proxmox-schema/src/format.rs b/proxmox-schema/src/format.rs
index 080b0268..45f7cd9d 100644
--- a/proxmox-schema/src/format.rs
+++ b/proxmox-schema/src/format.rs
@@ -28,6 +28,8 @@ pub enum DocumentationFormat {
     Full,
     /// Like full, but in reStructuredText format.
     ReST,
+    /// Like full, but in Djot format.
+    Djot,
 }
 
 /// Line wrapping to form simple list of paragraphs.
@@ -135,7 +137,7 @@ fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String {
 }
 
 /// Generate ReST Documentation for object properties
-pub fn dump_properties(
+pub fn dump_properties_rest(
     param: &dyn ObjectSchemaType,
     indent: &str,
     style: ParameterDisplayStyle,
@@ -168,7 +170,7 @@ pub fn dump_properties(
             {
                 match sub_schema {
                     Schema::Object(object_schema) => {
-                        let sub_text = dump_properties(
+                        let sub_text = dump_properties_rest(
                             object_schema,
                             &next_indent,
                             ParameterDisplayStyle::ConfigSub,
@@ -218,6 +220,90 @@ pub fn dump_properties(
     res
 }
 
+/// Generate Djot Documentation for object properties
+pub fn dump_properties_djot(
+    param: &dyn ObjectSchemaType,
+    indent: &str,
+    style: ParameterDisplayStyle,
+    skip: &[&str],
+) -> String {
+    let mut res = String::new();
+    let next_indent = format!("  {indent}");
+
+    let mut required_list: Vec<String> = Vec::new();
+    let mut optional_list: Vec<String> = Vec::new();
+
+    for (prop, optional, schema) in param.properties() {
+        if skip.iter().any(|n| n == prop) {
+            continue;
+        }
+
+        let mut param_descr =
+            get_property_description(prop, schema, style, DocumentationFormat::Djot);
+
+        if !indent.is_empty() {
+            param_descr = format!("{indent}{param_descr}"); // indent first line
+            param_descr = param_descr.replace('\n', &format!("\n{indent}")); // indent rest
+        }
+
+        if style == ParameterDisplayStyle::Config {
+            if let Schema::String(StringSchema {
+                format: Some(ApiStringFormat::PropertyString(sub_schema)),
+                ..
+            }) = schema
+            {
+                match sub_schema {
+                    Schema::Object(object_schema) => {
+                        let sub_text = dump_properties_djot(
+                            object_schema,
+                            &next_indent,
+                            ParameterDisplayStyle::ConfigSub,
+                            &[],
+                        );
+                        if !sub_text.is_empty() {
+                            param_descr.push_str("\n\n");
+                        }
+                        param_descr.push_str(&sub_text);
+                    }
+                    Schema::Array(_) => {
+                        // do nothing - description should explain the list type
+                    }
+                    _ => unreachable!(),
+                }
+            }
+        }
+        if *optional {
+            optional_list.push(param_descr);
+        } else {
+            required_list.push(param_descr);
+        }
+    }
+
+    if !required_list.is_empty() {
+        if style != ParameterDisplayStyle::ConfigSub {
+            res.push_str("\n_Required properties:_\n\n");
+        }
+
+        for text in required_list {
+            res.push_str(&text);
+            res.push('\n');
+        }
+    }
+
+    if !optional_list.is_empty() {
+        if style != ParameterDisplayStyle::ConfigSub {
+            res.push_str("\n_Optional properties:_\n\n");
+        }
+
+        for text in optional_list {
+            res.push_str(&text);
+            res.push('\n');
+        }
+    }
+
+    res
+}
+
 /// Helper to format an object property, including name, type and description.
 pub fn get_property_description(
     name: &str,
@@ -269,42 +355,67 @@ pub fn get_property_description(
         None => String::from(descr),
     };
 
-    if format == DocumentationFormat::ReST {
-        let mut text = match style {
-            ParameterDisplayStyle::Config => {
-                // reST definition list format
-                format!("``{name}`` : ``{type_text}{default_text}``\n")
-            }
-            ParameterDisplayStyle::ConfigSub => {
-                // reST definition list format
-                format!("``{name}`` = ``{type_text}{default_text}``\n")
-            }
-            ParameterDisplayStyle::Arg => {
-                // reST option list format
-                format!("``--{name}`` ``{type_text}{default_text}``\n")
-            }
-            ParameterDisplayStyle::Fixed => {
-                format!("``<{name}>`` : ``{type_text}{default_text}``\n")
-            }
-        };
+    match format {
+        DocumentationFormat::ReST => {
+            let mut text = match style {
+                ParameterDisplayStyle::Config => {
+                    // reST definition list format
+                    format!("``{name}`` : ``{type_text}{default_text}``\n")
+                }
+                ParameterDisplayStyle::ConfigSub => {
+                    // reST definition list format
+                    format!("``{name}`` = ``{type_text}{default_text}``\n")
+                }
+                ParameterDisplayStyle::Arg => {
+                    // reST option list format
+                    format!("``--{name}`` ``{type_text}{default_text}``\n")
+                }
+                ParameterDisplayStyle::Fixed => {
+                    format!("``<{name}>`` : ``{type_text}{default_text}``\n")
+                }
+            };
 
-        text.push_str(&wrap_text("  ", "  ", &descr, 80));
+            text.push_str(&wrap_text("  ", "  ", &descr, 80));
 
-        text
-    } else {
-        let display_name = match style {
-            ParameterDisplayStyle::Config => format!("{name}:"),
-            ParameterDisplayStyle::ConfigSub => format!("{name}="),
-            ParameterDisplayStyle::Arg => format!("--{name}"),
-            ParameterDisplayStyle::Fixed => format!("<{name}>"),
-        };
+            text
+        }
+        DocumentationFormat::Djot => {
+            // Djot definition list format
+            let mut text = match style {
+                ParameterDisplayStyle::Config => {
+                    format!(": `{name} : {type_text}{default_text}`\n\n")
+                }
+                ParameterDisplayStyle::ConfigSub => {
+                    format!(": `{name} = {type_text}{default_text}`\n\n")
+                }
+                ParameterDisplayStyle::Arg => {
+                    format!(": `--{name} {type_text}{default_text}`\n\n")
+                }
+                ParameterDisplayStyle::Fixed => {
+                    format!(": `<{name}> : {type_text}{default_text}`\n\n")
+                }
+            };
 
-        let mut text = format!(" {display_name:-10} {type_text}{default_text}");
-        let indent = "             ";
-        text.push('\n');
-        text.push_str(&wrap_text(indent, indent, &descr, 80));
+            text.push_str(&wrap_text("  ", "  ", &descr, 80));
+            text.push_str("\n\n");
 
-        text
+            text
+        }
+        _ => {
+            let display_name = match style {
+                ParameterDisplayStyle::Config => format!("{name}:"),
+                ParameterDisplayStyle::ConfigSub => format!("{name}="),
+                ParameterDisplayStyle::Arg => format!("--{name}"),
+                ParameterDisplayStyle::Fixed => format!("<{name}>"),
+            };
+
+            let mut text = format!(" {display_name:-10} {type_text}{default_text}");
+            let indent = "             ";
+            text.push('\n');
+            text.push_str(&wrap_text(indent, indent, &descr, 80));
+
+            text
+        }
     }
 }
 
@@ -428,7 +539,7 @@ fn get_object_type_text(object_schema: &ObjectSchema) -> String {
 }
 
 /// Generate ReST Documentation for enumeration.
-pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> {
+pub fn dump_enum_properties_rest(schema: &Schema) -> Result<String, Error> {
     let mut res = String::new();
 
     if let Schema::String(StringSchema {
@@ -450,7 +561,31 @@ pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> {
     bail!("dump_enum_properties failed - not an enum");
 }
 
-pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String {
+/// Generate Djot Documentation for enumeration.
+pub fn dump_enum_properties_djot(schema: &Schema) -> Result<String, Error> {
+    let mut res = String::new();
+
+    if let Schema::String(StringSchema {
+        format: Some(ApiStringFormat::Enum(variants)),
+        ..
+    }) = schema
+    {
+        for item in variants.iter() {
+            use std::fmt::Write;
+
+            let _ = write!(res, ": `{}`\n\n", item.value);
+            let descr = wrap_text(" ", "  ", item.description, 80);
+            res.push_str(&descr);
+            res.push('\n');
+        }
+        return Ok(res);
+    }
+
+    bail!("dump_enum_properties failed - not an enum");
+}
+
+/// Generate ReST Documentation for return schema.
+pub fn dump_api_return_schema_rest(returns: &ReturnType, style: ParameterDisplayStyle) -> String {
     use std::fmt::Write;
 
     let schema = &returns.schema;
@@ -491,17 +626,17 @@ pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle
         Schema::Object(obj_schema) => {
             let description = wrap_text("", "", obj_schema.description, 80);
             res.push_str(&description);
-            res.push_str(&dump_properties(obj_schema, "", style, &[]));
+            res.push_str(&dump_properties_rest(obj_schema, "", style, &[]));
         }
         Schema::AllOf(all_of_schema) => {
             let description = wrap_text("", "", all_of_schema.description, 80);
             res.push_str(&description);
-            res.push_str(&dump_properties(all_of_schema, "", style, &[]));
+            res.push_str(&dump_properties_rest(all_of_schema, "", style, &[]));
         }
         Schema::OneOf(all_of_schema) => {
             let description = wrap_text("", "", all_of_schema.description, 80);
             res.push_str(&description);
-            res.push_str(&dump_properties(all_of_schema, "", style, &[]));
+            res.push_str(&dump_properties_rest(all_of_schema, "", style, &[]));
         }
     }
 
diff --git a/proxmox-section-config/src/lib.rs b/proxmox-section-config/src/lib.rs
index 1fc74e91..33f48677 100644
--- a/proxmox-section-config/src/lib.rs
+++ b/proxmox-section-config/src/lib.rs
@@ -30,7 +30,9 @@ use serde::ser::Serialize;
 use serde_json::{json, Value};
 
 use proxmox_lang::try_block;
-use proxmox_schema::format::{dump_properties, wrap_text, ParameterDisplayStyle};
+use proxmox_schema::format::{
+    dump_properties_djot, dump_properties_rest, wrap_text, ParameterDisplayStyle,
+};
 use proxmox_schema::*;
 
 pub mod typed;
@@ -1243,7 +1245,7 @@ sync: fail
 }
 
 /// Generate ReST Documentation for ``SectionConfig``
-pub fn dump_section_config(config: &SectionConfig) -> String {
+pub fn dump_section_config_rest(config: &SectionConfig) -> String {
     let mut res = String::new();
 
     let mut plugins: Vec<&String> = config.plugins().keys().collect();
@@ -1266,7 +1268,42 @@ pub fn dump_section_config(config: &SectionConfig) -> String {
             let _ = write!(res, "\n**Section type** \'``{name}``\':  {description}\n\n");
         }
 
-        res.push_str(&dump_properties(
+        res.push_str(&dump_properties_rest(
+            properties,
+            "",
+            ParameterDisplayStyle::Config,
+            &skip,
+        ));
+    }
+
+    res
+}
+
+/// Generate Djot Documentation for ``SectionConfig``
+pub fn dump_section_config_djot(config: &SectionConfig) -> String {
+    let mut res = String::new();
+
+    let mut plugins: Vec<&String> = config.plugins().keys().collect();
+    plugins.sort_unstable();
+
+    let plugin_count = config.plugins().len();
+
+    for name in plugins {
+        let plugin = config.plugins().get(name).unwrap();
+        let properties = plugin.properties();
+        let skip = match plugin.id_property() {
+            Some(id) => vec![id],
+            None => Vec::new(),
+        };
+
+        if plugin_count > 1 {
+            use std::fmt::Write as _;
+
+            let description = wrap_text("", "", properties.description(), 80);
+            let _ = write!(res, "\n*Section type* '`{name}`':  {description}\n\n");
+        }
+
+        res.push_str(&dump_properties_djot(
             properties,
             "",
             ParameterDisplayStyle::Config,
-- 
2.39.5


_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [pbs-devel] applied: [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs
  2025-02-21  6:50 [pbs-devel] [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Dietmar Maurer
  2025-02-21  6:50 ` [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format Dietmar Maurer
@ 2025-02-21  9:10 ` Thomas Lamprecht
  1 sibling, 0 replies; 3+ messages in thread
From: Thomas Lamprecht @ 2025-02-21  9:10 UTC (permalink / raw)
  To: Proxmox Backup Server development discussion, Dietmar Maurer

Am 21.02.25 um 07:50 schrieb Dietmar Maurer:
> Lines before commanmd groups are missing (i.e. see proxmox-backup-client command
> syntax docs)
> 
> Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
> ---
>  proxmox-router/src/cli/format.rs | 3 +++
>  1 file changed, 3 insertions(+)
> 
>

applied, thanks!


_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2025-02-21 10:58 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-21  6:50 [pbs-devel] [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Dietmar Maurer
2025-02-21  6:50 ` [pbs-devel] [PATCH proxmox 2/2] schema: support doc generation in Djot format Dietmar Maurer
2025-02-21  9:10 ` [pbs-devel] applied: [PATCH proxmox 1/2] router: docs: add horizontal line before nested command docs Thomas Lamprecht

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal