netboot: script to configure a remote host from toml and netboot a kernel

Signed-off-by: Andrew Jeffery <andrew@aj.id.au>
diff --git a/netboot/README.md b/netboot/README.md
new file mode 100644
index 0000000..717f156
--- /dev/null
+++ b/netboot/README.md
@@ -0,0 +1,28 @@
+Netboot a remote OpenBMC host via telnet (and hopefully at some stage, ssh).
+
+To configure, edit `${HOME}/.config/obmc-scripts/netboot` to configure a
+machine:
+
+```
+[foo]
+platform = "bar"
+user = "baz"
+password = "quux"
+
+    # telnet serial server
+    [foo.console]
+    host = "1.2.3.4"
+    port = 5678
+    
+    [foo.u-boot]
+    commands = [
+        "setenv bootfile fitImage",
+        "tftp",
+    ]
+```
+
+Then netboot your machine:
+
+```
+$ ./netboot foo
+```
diff --git a/netboot/netboot b/netboot/netboot
new file mode 100755
index 0000000..3c941a0
--- /dev/null
+++ b/netboot/netboot
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+import argparse
+import sys
+import time
+import toml
+from os import path
+from telnetlib import Telnet
+from types import MethodType
+from xdg import BaseDirectory
+
+def expect_or_raise(conn, patterns, timeout=None):
+    i, m, d = conn.expect([bytes(p, 'ascii') for p in patterns], timeout)
+    if i == -1:
+        msg = "Match failed, expected '%s', got '%s'" % (str(patterns), d)
+        print(msg, file=sys.stderr)
+        raise ValueError
+    return i, m, d
+
+def encode_and_write(conn, comm="", sep="\n"):
+    # Slow down the writes to help poor ol' serial-over-telnet
+    for c in comm + sep:
+        conn.write(bytes(c, 'ascii'))
+        time.sleep(0.01)
+
+def init_telnet(host, port=0, timeout=None):
+    conn = Telnet(host, port, timeout)
+    conn.encode_and_write = MethodType(encode_and_write, conn)
+    conn.expect_or_raise = MethodType(expect_or_raise, conn)
+    return conn
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("machine")
+    args = parser.parse_args()
+
+    confbase = BaseDirectory.save_config_path("obmc-scripts")
+    conffile = path.join(confbase, "netboot")
+    if not path.exists(conffile):
+        print("Missing configuration file: %s" % (conffile))
+        sys.exit(1)
+
+    conf = toml.load(conffile)
+    mach = conf[args.machine]
+    console = mach["console"]
+    conn = init_telnet(console["host"], console["port"])
+
+    try:
+        conn.encode_and_write()
+        i, m, d = conn.expect_or_raise([
+            "%s login:" % (mach["platform"]),
+            "root@%s:.*#" % (mach["platform"]),
+            "ast#",
+        ], 5)
+
+        if i != 2:
+            if i == 0:
+                conn.encode_and_write(mach["user"])
+                conn.read_until(b"Password:")
+                conn.encode_and_write(mach["password"])
+                conn.expect_or_raise(["root@%s:.*#" % (mach["platform"])])
+
+            conn.encode_and_write("reboot")
+
+            conn.expect_or_raise(["Hit any key to stop autoboot"])
+            conn.encode_and_write()
+
+        for comm in mach["u-boot"]["commands"]:
+            conn.encode_and_write(comm)
+            if "tftp" in comm:
+                i, m, d = conn.expect_or_raise([
+                    r"Bytes transferred = \d+ \([0-9a-f]+ hex\)",
+                    "Not retrying...",
+                    r"[*]{3} ERROR:",
+                ])
+                if i == 1 or i == 2:
+                    print("Error detected, exiting", file=sys.stderr)
+                    return
+
+        conn.encode_and_write("printenv set_bootargs")
+        i, m, d = conn.expect_or_raise([
+            "set_bootargs=.*$",
+            "## Error: \"set_bootargs\" not defined",
+        ], 1)
+        if i == 0:
+            conn.encode_and_write("run set_bootargs")
+            conn.read_until(b"ast#")
+
+        conn.encode_and_write("bootm")
+        conn.read_until(b"Starting kernel")
+    finally:
+        conn.close()
+
+if __name__ == "__main__":
+    main()