From bb67c23918ba22be498537a29c01b696732d5b3b Mon Sep 17 00:00:00 2001
From: Magnus Ahltorp <map@kth.se>
Date: Mon, 3 Jul 2017 00:21:02 +0200
Subject: Automatic generation of config man page skeleton

---
 doc/Makefile                   |  10 ++-
 doc/catlfish-log.cfg.in.5.adoc |  95 +++++++++++++++++++++
 doc/catlfish-node.cfg.5.adoc   | 113 +++++++++++++++++++++++++
 tools/compileconfig.py         |  14 +++-
 tools/manpage.py               | 183 +++++++++++++++++++++++++++++++++++++++++
 tools/orderedtree.py           |  45 ++++++++++
 6 files changed, 457 insertions(+), 3 deletions(-)
 create mode 100644 doc/catlfish-log.cfg.in.5.adoc
 create mode 100644 doc/catlfish-node.cfg.5.adoc
 create mode 100644 tools/manpage.py
 create mode 100644 tools/orderedtree.py

diff --git a/doc/Makefile b/doc/Makefile
index b3de194..528b6d5 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,7 +1,12 @@
 MANDOCS = catlfish.1 genconfig.1
 RONN = ronn --warnings --organization="FIXME:\$$version"
 
-all: man html
+all: configman man html
+
+configman:
+	../tools/compileconfig.py --manpagedir=.
+	make catlfish-log.cfg.in.5
+	make catlfish-node.cfg.5
 
 man: $(MANDOCS)
 
@@ -10,6 +15,9 @@ html: $(addsuffix .html,$(MANDOCS))
 %: %.md
 	$(RONN) --roff $^
 
+%: %.adoc
+	a2x --doctype manpage --format manpage $^
+
 %.html: %.md
 	$(RONN) --html $^
 
diff --git a/doc/catlfish-log.cfg.in.5.adoc b/doc/catlfish-log.cfg.in.5.adoc
new file mode 100644
index 0000000..1cc912f
--- /dev/null
+++ b/doc/catlfish-log.cfg.in.5.adoc
@@ -0,0 +1,95 @@
+:man source:   Catlfish
+:man manual:   Catlfish Manual
+CATLFISH-LOG.CFG.IN(5)
+======================
+
+NAME
+----
+catlfish-log.cfg.in - catlfish log configuration
+
+OPTIONS
+-------
+   **apikeys**: (list of items)::
+//    write description here
+
+      **nodename**: __nodename__:::
+//       write description here
+
+      **publickey**: __key__:::
+          BASE64-encoded key
+
+   **backup-quorum-size**: __number-of-nodes__::
+       number of secondary merge nodes that need to have an entry before the entry is considered committed
+
+   **baseurl**: __url__::
+//    write description here
+
+   **cafingerprint**: __fingerprint__::
+//    write description here
+
+   **frontendnodes**: (list of items)::
+//    write description here
+
+      **address**: __ip-address__:::
+//       write description here
+
+      **name**: __nodename__:::
+//       write description here
+
+      **publicaddress**: __ip-address__:::
+//       write description here
+
+   **logpublickey**: __key__::
+//    write description here
+
+   **mergenodes**: (list of items)::
+//    write description here
+
+      **address**: __ip-address__:::
+//       write description here
+
+      **name**: __nodename__:::
+//       write description here
+
+   **mmd**: __seconds__::
+//    write description here
+
+   **primarymergenode**: __nodename__::
+//    write description here
+
+   **signingnodes**: (list of items)::
+//    write description here
+
+      **address**: __ip-address__:::
+//       write description here
+
+      **name**: __nodename__:::
+//       write description here
+
+   **statusservers**: (list of items)::
+//    write description here
+
+      **address**: __ip-address__:::
+//       write description here
+
+      **name**: __nodename__:::
+//       write description here
+
+      **publicaddress**: __ip-address__:::
+//       write description here
+
+   **storage-quorum-size**: __number-of-nodes__::
+//    write description here
+
+   **storagenodes**: (list of items)::
+//    write description here
+
+      **address**: __ip-address__:::
+//       write description here
+
+      **name**: __nodename__:::
+//       write description here
+
+   **version**: __version__::
+//    write description here
+
diff --git a/doc/catlfish-node.cfg.5.adoc b/doc/catlfish-node.cfg.5.adoc
new file mode 100644
index 0000000..fabef29
--- /dev/null
+++ b/doc/catlfish-node.cfg.5.adoc
@@ -0,0 +1,113 @@
+:man source:   Catlfish
+:man manual:   Catlfish Manual
+CATLFISH-NODE.CFG(5)
+====================
+
+NAME
+----
+catlfish-node.cfg - catlfish node configuration
+
+OPTIONS
+-------
+   **configurl**: __url__::
+//    write description here
+
+   **ctapiaddress**: __ip-address__::
+//    write description here
+
+   **dbbackend**: **permdb**|**fsdb**::
+//    write description here
+
+   **frontendaddress**: __ip-address__::
+//    write description here
+
+   **logadminkey**: __key__::
+//    write description here
+
+   **merge**: ::
+//    write description here
+
+      **backup-sendentries-chunksize**: __number-of-entries__:::
+//       write description here
+
+      **backup-sendlog-chunksize**: __number-of-entries__:::
+//       write description here
+
+      **backup-window-size**: __number-of-entries__:::
+//       write description here
+
+      **dist-sendentries-chunksize**: __number-of-entries__:::
+//       write description here
+
+      **dist-sendlog-chunksize**: __number-of-entries__:::
+//       write description here
+
+      **dist-window-size**: __number-of-entries__:::
+//       write description here
+
+      **min-delay**: __seconds__:::
+//       write description here
+
+   **mergeaddress**: __ip-address__::
+//    write description here
+
+   **nodename**: __nodename__::
+//    write description here
+
+   **paths**: ::
+//    write description here
+
+      **configdir**: __path__:::
+//       write description here
+
+      **db**: __path__:::
+//       write description here
+
+      **https_cacertfile**: __path__:::
+//       write description here
+
+      **https_certfile**: __path__:::
+//       write description here
+
+      **https_keyfile**: __path__:::
+//       write description here
+
+      **knownroots**: __path__:::
+//       write description here
+
+      **logprivatekey**: __path__:::
+//       write description here
+
+      **logpublickey**: __path__:::
+//       write description here
+
+      **mergedb**: __path__:::
+//       write description here
+
+      **privatekeys**: __path__:::
+//       write description here
+
+      **public_cacertfile**: __path__:::
+//       write description here
+
+      **publickeys**: __path__:::
+//       write description here
+
+      **verifycert_bin**: __path__:::
+//       write description here
+
+   **publichttpaddress**: __ip-address__::
+//    write description here
+
+   **ratelimits**: ::
+//    write description here
+
+      **add_chain**: __rate__:::
+//       write description here
+
+   **signingaddress**: __ip-address__::
+//    write description here
+
+   **storageaddress**: __ip-address__::
+//    write description here
+
diff --git a/tools/compileconfig.py b/tools/compileconfig.py
index 87d46c6..35ecb91 100755
--- a/tools/compileconfig.py
+++ b/tools/compileconfig.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Copyright (c) 2014-2016, NORDUnet A/S.
+# Copyright (c) 2014-2017, NORDUnet A/S.
 # See LICENSE for licensing information.
 
 import argparse
@@ -9,6 +9,7 @@ import readconfig
 import re
 import base64
 from datetime import datetime
+import manpage
 
 class Symbol(str):
     pass
@@ -561,6 +562,10 @@ def printnodenames(config):
 
     print " ".join(frontendnodenames|storagenodenames|signingnodenames|mergenodenames|statusservernodenames)
 
+def gen_manpage(manpagedir):
+    manpage.rewrite_manpage(manpagedir + "/catlfish-log.cfg.in.5.adoc", globalconfigschema, "Catlfish", "Catlfish Manual", "CATLFISH-LOG.CFG.IN(5)", "catlfish-log.cfg.in - catlfish log configuration")
+    manpage.rewrite_manpage(manpagedir + "/catlfish-node.cfg.5.adoc", localconfigschema, "Catlfish", "Catlfish Manual", "CATLFISH-NODE.CFG(5)", "catlfish-node.cfg - catlfish node configuration")
+
 localconfigschema = [
     ("nodename", "string", "nodename"),
     ("frontendaddress", "string", "ip address"),
@@ -623,14 +628,19 @@ globalconfigschema = [
 def main():
     parser = argparse.ArgumentParser(description="")
     parser.add_argument('--config', help="System configuration")
+    parser.add_argument("--manpagedir", metavar="file", help="Generate manpages to directory")
     parser.add_argument('--localconfig', help="Local configuration")
     parser.add_argument("--testmakefile", metavar="file", help="Generate makefile variables for test")
     parser.add_argument("--testshellvars", metavar="file", help="Generate shell variable file for test")
     parser.add_argument("--getnodenames", action='store_true', help="Get list of node names")
     args = parser.parse_args()
 
+    if args.manpagedir:
+        gen_manpage(args.manpagedir)
+        sys.exit(0)
+
     if not args.config:
-        print >>sys.stderr, "--config is required"
+        print >>sys.stderr, "either --config or --manpage is required"
         sys.exit(1)
 
     if args.testmakefile:
diff --git a/tools/manpage.py b/tools/manpage.py
new file mode 100644
index 0000000..1ea8753
--- /dev/null
+++ b/tools/manpage.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017, NORDUnet A/S.
+# See LICENSE for licensing information.
+
+import argparse
+import sys
+import readconfig
+import re
+import base64
+import shutil
+from datetime import datetime
+from orderedtree import TreeNode
+
+def level_is_list(schema):
+    if len(schema.keys()) != 1:
+        return False
+    return schema.keys()[0] == "[]"
+
+def traverse_schema_part(schema):
+    tree = TreeNode()
+    for k in sorted(schema.keys()):
+        schema_part = schema.get(k)
+        result = None
+        if isinstance(schema_part, tuple):
+            (lowleveldatatype, highleveldatatype) = schema_part
+            if isinstance(highleveldatatype, list):
+                formatted_datatype = "|".join(["**"+t+"**" for t in highleveldatatype])
+            else:
+                formatted_datatype = "__" + highleveldatatype.replace(" ", "-") + "__"
+            if k == "[]":
+                result = "list of " + formatted_datatype
+            else:
+                result = "**" + k + "**: " + formatted_datatype
+
+            tree.add(k, (result, []))
+
+        elif isinstance(schema_part, dict):
+            if k == "[]":
+                result = "list of items"
+            else:
+                if level_is_list(schema_part):
+                    formatted_datatype = "(list of items)"
+                    schema_part = schema_part["[]"]
+                else:
+                    formatted_datatype = ""
+                result = "**"+k+"**: " + formatted_datatype
+            subtree = traverse_schema_part(schema_part)
+            tree.add(k, (result, []), subtree=subtree)
+        else:
+            print >>sys.stderr, "unknown type", type(schema_part)
+            sys.exit(1)
+    return tree
+
+def traverse_schema(schema):
+    transformed_schema = readconfig.transform_schema(schema)
+    tree = traverse_schema_part(transformed_schema)
+    return tree
+
+def is_adoc_header(row):
+    return set(row.rstrip()) == set("=")
+
+def is_adoc_section(row):
+    return set(row.rstrip()) == set("-")
+
+def parse_manpage(filename):
+    f = open(filename)
+    header = []
+    for row in f:
+        if is_adoc_header(row):
+            break
+        header.append(row.rstrip("\n"))
+    section_name = None
+    section = []
+    sections = []
+    for row in f:
+        if is_adoc_section(row):
+            if section_name:
+                sections.append((section_name, section[:-1]))
+            section_name = section[-1]
+            section = []
+        else:
+            section.append(row.rstrip("\n"))
+    if section_name:
+        sections.append((section_name, section))
+    return (header, sections)
+
+def is_manpage_option(row):
+    return row.endswith("::")
+
+def extract_option_name(row):
+    if row == None:
+        return (None, None)
+    (name_part, _, _) = row.lstrip().partition(":")
+    depth = len(row) - len(row.rstrip(":")) - 2
+    return (name_part.replace("*", ""), depth)
+
+def parse_manpage_options(option_rows):
+    option_name_row = None
+    options = []
+    option = []
+    for row in option_rows:
+        if is_manpage_option(row):
+            options.append((option_name_row, option))
+            option_name_row = row
+            option = []
+        else:
+            option.append(row)
+    options.append((option_name_row, option))
+    return options
+
+def build_tree(l, key):
+    tree = TreeNode()
+    curpath = []
+    for e in l:
+        (k, depth) = key(e)
+        if depth > len(curpath):
+            print >>sys.stderr, "depth", depth, "from", e, "greater than length of curpath", curpath
+            sys.exit(1)
+        curpath = curpath[:depth]
+        tree.walk(curpath).add(k, e)
+        curpath.append(k)
+    return tree
+
+def transfer_tree(current, wanted):
+    for name in wanted.iterkeys():
+        if name in current and name != None:
+            transfer_tree(current[name], wanted[name])
+    if wanted.entry:
+        wanted.entry = (wanted.entry[0], current.entry[1])
+
+def print_tree(f, tree, depth=0):
+    if tree.entry:
+        (section, rows) = tree.entry
+        print >>f, "   " * depth + section + (depth+1) * ":"
+        has_content = False
+        for row in rows:
+            if row.strip():
+                has_content = True
+            print >>f, row
+        if not has_content:
+            print >>f, "// " + "   " * depth + "write description here"
+            print >>f, ""
+    for name in tree.iterkeys():
+        print_tree(f, tree[name], depth=depth+1)
+
+def rewrite_options(f, schema, options):
+    wanted = traverse_schema(schema)
+    current = build_tree(options, lambda e: extract_option_name(e[0]))
+    transfer_tree(current, wanted)
+    print_tree(f, wanted)
+
+def rewrite_manpage(filename, schema, man_source, man_manual, title, name):
+    try:
+        (header, sections) = parse_manpage(filename)
+    except IOError:
+        sections = []
+    sections_dict = dict(sections)
+    section_names = [e for e, _ in sections]
+    options = parse_manpage_options(sections_dict.get("OPTIONS", []))
+    sections_dict["NAME"] = [name]
+    if "NAME" not in section_names:
+        section_names.append("NAME")
+    if "OPTIONS" not in section_names:
+        section_names.append("OPTIONS")
+    f = open(filename + ".new", "w")
+    print >>f, ":man source:   " + man_source
+    print >>f, ":man manual:   " + man_manual
+    print >>f, title
+    print >>f, len(title) * "="
+    print >>f, ""
+    for section_name in section_names:
+        print >>f, section_name
+        print >>f, len(section_name) * "-"
+        if section_name == "OPTIONS":
+            rewrite_options(f, schema, options)
+            continue
+        for row in sections_dict[section_name]:
+            print >>f, row
+        print >>f, ""
+    f.close()
+    shutil.move(filename + ".new", filename)
+
diff --git a/tools/orderedtree.py b/tools/orderedtree.py
new file mode 100644
index 0000000..def8928
--- /dev/null
+++ b/tools/orderedtree.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017, NORDUnet A/S.
+# See LICENSE for licensing information.
+
+class OrderDict(dict):
+    def __init__(self):
+        self._order = []
+        dict.__init__({})
+    def __setitem__(self, key, value):
+        if key not in self:
+            self._order.append(key)
+        super(OrderDict, self).__setitem__(key, value)
+    def iterkeys(self):
+        return iter(self._order)
+
+class TreeNode():
+    def __init__(self):
+        self.entry = None
+        self._children = OrderDict()
+    def add(self, k, e, subtree=None):
+        if subtree != None:
+            self._children[k] = subtree
+        else:
+            self._children[k] = TreeNode()
+        self._children[k].entry = e
+    def __getitem__(self, key):
+        return self._children[key]
+    def iterkeys(self):
+        return self._children.iterkeys()
+    def __contains__(self, key):
+        return key in self._children
+    def walk(self, keys):
+        node = self
+        for k in keys:
+            node = node[k]
+        return node
+    def __str__(self):
+        s = str(self.entry) + "\n"
+        for k in self.iterkeys():
+            s += str(k) + ":\n"
+            for row in str(self._children[k]).split("\n"):
+                if row:
+                    s += "    " + row + "\n"
+        return s
-- 
cgit v1.1