tof-voters: begin tool for voter membership

The TOF voting requires looking at Gerrit data to determine who is
eligible to vote.  Start a tool that can be used to dump and analyze
this data.

First sub-command added is a method to dump all Gerrit data after a
certain date (typically the cut-off for the previous election).

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I02b22c9dcb81b570ac3872d7c38f838d61690c83
diff --git a/tof-voters/libvoters/__init__.py b/tof-voters/libvoters/__init__.py
new file mode 100644
index 0000000..a93a4bf
--- /dev/null
+++ b/tof-voters/libvoters/__init__.py
@@ -0,0 +1 @@
+#!/usr/bin/python3
diff --git a/tof-voters/libvoters/entry_point.py b/tof-voters/libvoters/entry_point.py
new file mode 100644
index 0000000..e36d06e
--- /dev/null
+++ b/tof-voters/libvoters/entry_point.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python3
+
+import argparse
+from importlib import import_module
+from typing import List
+
+subcommands = ["dump-gerrit"]
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Obtain TOF voter metrics")
+    parser.add_argument(
+        "--data-directory",
+        "-d",
+        help="Data directory (default 'data')",
+        dest="dir",
+        default="data",
+    )
+
+    subparser = parser.add_subparsers(help="Available subcommands")
+
+    commands = []
+    for c in subcommands:
+        commands.append(
+            import_module("libvoters.subcmd." + c).subcmd(subparser)  # type: ignore
+        )
+
+    args = parser.parse_args()
+
+    if "cmd" not in args:
+        print("Missing subcommand!")
+        return 1
+
+    return int(args.cmd.run(args))
diff --git a/tof-voters/libvoters/subcmd/__init__.py b/tof-voters/libvoters/subcmd/__init__.py
new file mode 100644
index 0000000..a93a4bf
--- /dev/null
+++ b/tof-voters/libvoters/subcmd/__init__.py
@@ -0,0 +1 @@
+#!/usr/bin/python3
diff --git a/tof-voters/libvoters/subcmd/dump-gerrit.py b/tof-voters/libvoters/subcmd/dump-gerrit.py
new file mode 100644
index 0000000..455f0e6
--- /dev/null
+++ b/tof-voters/libvoters/subcmd/dump-gerrit.py
@@ -0,0 +1,63 @@
+#!/usr/bin/python3
+
+import argparse
+import json
+import os
+from sh import ssh  # type: ignore
+
+
+class subcmd:
+    def __init__(self, parser: argparse._SubParsersAction) -> None:
+        p = parser.add_parser(
+            "dump-gerrit", help="Dump commit data from Gerrit"
+        )
+        p.add_argument(
+            "--server",
+            "-s",
+            help="Gerrit server SSH alias (default=openbmc.gerrit)",
+            default="openbmc.gerrit",
+        )
+        p.add_argument(
+            "--after",
+            "-a",
+            help="Timestamp for Gerrit 'after:' directive (ex. YYYY-MM-DD)",
+            required=True,
+        )
+        p.set_defaults(cmd=self)
+
+    def run(self, args: argparse.Namespace) -> int:
+        data_path: str = args.dir
+
+        if os.path.exists(data_path) and not os.path.isdir(data_path):
+            print(f"Path {data_path} exists but is not a directory.")
+            return 1
+
+        if not os.path.exists(data_path):
+            os.mkdir(data_path)
+
+        query = list(
+            ssh(
+                args.server,
+                "gerrit",
+                "query",
+                "--format=json",
+                "--patch-sets",
+                "--comments",
+                "--files",
+                "--no-limit",
+                f"after:{args.after} AND delta:>=10",
+            )
+        )[
+            0:-1
+        ]  # The last result from Gerrit is a query stat result.
+
+        for change in query:
+            data = json.loads(change)
+            formatted_data = json.dumps(data, indent=4)
+
+            with open(
+                os.path.join(data_path, str(data["number"]) + ".json"),
+                "w",
+            ) as file:
+                file.write(formatted_data)
+        return 0