Add the ability to add random unmanaged instances

The s390x host as such an example
diff --git a/README.md b/README.md
index 11719a2..1604aaa 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,11 @@
     crn = crn:v1:...
     instance_id = 123-abc...
 
+    [extra.<instancename>]
+    ip_addr = x.y.z.w
+    arch = s390x
+    num_cpus = 4
+    ram = 8
 
 `<environment>` is a tag used to differentiate multiple environments. It allows
 fetching instances from more than one IBM Cloud accounts. If `api_url` is
@@ -90,6 +95,8 @@
 `CRN: <crn>` header if provided. `instance_id` is used only by the `power`
 environment. (See `Power Instances` section for more details).
 
+`extra.<instancename>` can be an extra unmanaged manually added instance which
+is not discoverable via cloud.ibm.com with an API key.
 
 The `tools/gen-config` script can then be used to generate our `production`
 inventory and `ssh.cfg` configuration:
diff --git a/production b/production
index cbda855..3e374ad 100644
--- a/production
+++ b/production
@@ -35,7 +35,7 @@
             public: 158.175.161.155
           system:
             arch: power
-            num_cpu: 2
+            num_cpus: 2
             ram: 16
         couchdb-worker-x86-64-debian-dal-1-01:
           instance:
@@ -173,3 +173,15 @@
             arch: amd64
             num_cpus: 4
             ram: 8
+        couchdb01:
+          instance:
+            id: couchdb01
+            name: couchdb01
+            subnet: null
+          ip_addrs:
+            bastion: null
+            public: 148.100.113.138
+          system:
+            arch: s390x
+            num_cpus: 4
+            ram: 8
diff --git a/ssh.cfg b/ssh.cfg
index 7e80b5b..ee7098d 100644
--- a/ssh.cfg
+++ b/ssh.cfg
@@ -88,3 +88,12 @@
   ControlPath /tmp/ansible-%r@%h:%p
   ControlPersist 30m
 
+Host couchdb01
+  Hostname 148.100.113.138
+  User ubuntu
+  ForwardAgent yes
+  StrictHostKeyChecking no
+  ControlMaster auto
+  ControlPath /tmp/ansible-%r@%h:%p
+  ControlPersist 30m
+
diff --git a/tools/gen-config b/tools/gen-config
index b37cbf1..2af4933 100755
--- a/tools/gen-config
+++ b/tools/gen-config
@@ -17,6 +17,9 @@
 #
 ENV = {}
 
+# Hard-coded VMs managed by someone else without access to cloud.ibm.com
+EXTRA = {}
+
 IBM_CLOUD_URL = "https://us-south.iaas.cloud.ibm.com/v1"
 IAM_URL = "https://iam.cloud.ibm.com/identity/token"
 
@@ -38,6 +41,25 @@
     parser.read([path])
 
     for section in parser.sections():
+        if section.startswith("extra"):
+            split = section.split(".")
+            if len(split) != 2:
+                print(f"Invalid 'extra' section {section}")
+                exit(1)
+
+            (_, name) = split
+
+            EXTRA[name] = {
+                "name" : name,
+                "instance_id" : parser.get(section, "id", fallback=name),
+                "ip_addr": parser.get(section, "ip_addr"),
+                "system": {
+                    "arch": parser.get(section, "arch"),
+                    "num_cpus": int(parser.get(section, "num_cpus")),
+                    "ram": int(parser.get(section, "ram"))
+                }
+            }
+
         if not section.startswith("ibmcloud"):
             continue
 
@@ -97,6 +119,8 @@
             yield from list_x86_instances(env)
         if env == "power":
             yield from list_power_instances(env)
+    for inst  in EXTRA.values():
+        yield inst
 
 
 def list_x86_instances(env):
@@ -125,6 +149,7 @@
     resp = sess.get(url, params=params(env))
     instance = resp.json()
     instance["name"] = instance["serverName"]
+    instance["system"] = {"arch": "power"}
     yield instance
 
 
@@ -243,12 +268,32 @@
         },
         "system": {
             "arch": "power",
-            "num_cpu": instance["virtualCores"]["assigned"],
+            "num_cpus": instance["virtualCores"]["assigned"],
             "ram": instance["memory"]
         }
     }
 
 
+def load_s390x_ci_agent(ci_agents, instance):
+    name = instance["name"]
+    if name in ci_agents:
+        print(f"Duplicate ci_agent found {name}")
+        exit(2)
+
+    ci_agents[name] = {
+        "instance": {
+            "id": instance["instance_id"],
+            "name" : instance["name"],
+            "subnet": None
+        },
+        "ip_addrs": {
+            "bastion": None,
+            "public": instance["ip_addr"]
+        },
+        "system": instance["system"]
+    }
+
+
 def get_private_ip(instance):
     ip = instance["primary_network_interface"]["primary_ipv4_address"]
     if ip:
@@ -269,8 +314,8 @@
         assert subnet not in subnets
         subnets[subnet] = ip_addr
     for (host, ci_agent) in ci_agents.items():
-        if ci_agent["system"]["arch"] == "power":
-            # Power systems use an external IP without bastions
+        if ci_agent["system"]["arch"] in ["power", "s390x"]:
+            # Power & s390x an external IP without bastions
             continue
         subnet = ci_agent["instance"]["subnet"]
         assert subnet in subnets
@@ -326,8 +371,8 @@
             entry = bastion_tmpl.format(**args)
             handle.write(entry)
         for host, info in sorted(ci_agents.items()):
-            if info["system"]["arch"] == "power":
-                # Power systems use an external IP directly
+            if info["system"]["arch"] in ["power", "s390x"]:
+                # Power or s390x use an external IP directly
                 args = {
                     "user": "ubuntu",
                     "host": host,
@@ -377,8 +422,10 @@
             load_bastion(bastions, instance)
         elif instance["name"].startswith("couchdb-worker"):
             load_ci_agent(ci_agents, instance)
-        elif instance["name"].startswith("couchdb-ci"):
+        elif instance["system"]["arch"] == "power":
             load_power_ci_agent(ci_agents, instance)
+        elif instance["system"]["arch"] == "s390x":
+            load_s390x_ci_agent(ci_agents, instance)
 
     assign_bastions(bastions, ci_agents)