public inbox for pbs-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal