blob: ffc59d36e4b03bc3af736fc5fbc8064a4f2b5448 [file] [log] [blame]
#!/usr/bin/env python
import argparse as ap
import ConfigParser as cp
import json
import os
import re
import textwrap
import requests
import yaml
IBM_CLOUD_URL = "https://us-south.iaas.cloud.ibm.com/v1/"
IAM_URL = "https://iam.cloud.ibm.com/identity/token"
IBM_CLOUD_GENERATION = "2"
IBM_CLOUD_VERSION = "2019-08-09"
API_KEY = None
IAM_TOKEN = None
SESS = requests.session()
def tostr(obj):
ret = {}
for k, v in obj.items():
if isinstance(k, unicode):
k = k.encode("utf-8")
if isinstance(v, dict):
ret[k] = tostr(v)
elif isinstance(v, unicode):
ret[k] = v.encode("utf-8")
else:
ret[k] = v
return ret
def load_api_key():
global API_KEY
path = os.path.expanduser("~/.couchdb-infra-cm.cfg")
if not os.path.exists(path):
print "Missing config file: " + path
exit(1)
parser = cp.SafeConfigParser()
parser.read([path])
API_KEY = parser.get("ibmcloud", "api_key")
def load_iam_token():
global IAM_TOKEN
headers = {
"Accept": "application/json"
}
data = {
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
"apikey": API_KEY
}
resp = SESS.post(IAM_URL, headers=headers, data=data)
resp.raise_for_status()
body = resp.json()
IAM_TOKEN = body["token_type"] + " " + body["access_token"]
SESS.headers["Authorization"] = IAM_TOKEN
def init():
load_api_key()
load_iam_token()
def list_instances():
url = IBM_CLOUD_URL + "/instances"
params = {
"version": IBM_CLOUD_VERSION,
"generation": IBM_CLOUD_GENERATION,
"limit": 100
}
while url:
resp = SESS.get(url, params=params)
body = resp.json()
for instance in body["instances"]:
interface_url = instance["primary_network_interface"]["href"]
resp = SESS.get(interface_url, params=params)
instance["primary_network_interface"] = resp.json()
yield instance
url = body.get("next")
def load_bastion(bastions, instance):
if instance["status"] != "running":
return
name = instance["name"]
ip_addr = None
net_iface = instance["primary_network_interface"]
floating_ips = net_iface.get("floating_ips", [])
if not floating_ips:
print "Bastion is missing a public IP: %s" % name
exit(2)
ip_addr = floating_ips[0]["address"]
bastions[name] = {
"instance": {
"id": instance["id"],
"name": instance["name"],
"created_at": instance["created_at"],
"profile": instance["profile"]["name"],
"vpc": instance["vpc"]["name"],
"zone": instance["zone"]["name"],
"subnet": net_iface["subnet"]["name"]
},
"ip_addrs": {
"public": ip_addr,
"private": net_iface["primary_ipv4_address"]
},
"boot_volume": {
"device": instance["boot_volume_attachment"]["device"]["id"],
"name": instance["boot_volume_attachment"]["volume"]["name"]
},
"system": {
"arch": instance["vcpu"]["architecture"],
"num_cpus": instance["vcpu"]["count"],
"ram": instance["memory"]
}
}
def load_ci_agent(ci_agents, instance):
if instance["status"] != "running":
return
name = instance["name"]
net_iface = instance["primary_network_interface"]
ci_agents[name] = {
"instance": {
"id": instance["id"],
"name": instance["name"],
"created_at": instance["created_at"],
"profile": instance["profile"]["name"],
"vpc": instance["vpc"]["name"],
"zone": instance["zone"]["name"],
"subnet": net_iface["subnet"]["name"]
},
"ip_addrs": {
"bastion": None,
"public": None,
"private": net_iface["primary_ipv4_address"]
},
"boot_volume": {
"device": instance["boot_volume_attachment"]["device"]["id"],
"name": instance["boot_volume_attachment"]["volume"]["name"]
},
"system": {
"arch": instance["vcpu"]["architecture"],
"num_cpus": instance["vcpu"]["count"],
"ram": instance["memory"]
}
}
def assign_bastions(bastions, ci_agents):
subnets = {}
for (host, bastion) in bastions.items():
subnet = bastion["instance"]["subnet"]
ip_addr = bastion["ip_addrs"]["public"]
assert subnet not in subnets
subnets[subnet] = ip_addr
for (host, ci_agent) in ci_agents.items():
subnet = ci_agent["instance"]["subnet"]
assert subnet in subnets
ci_agent["ip_addrs"]["bastion"] = subnets[subnet]
def write_inventory(fname, bastions, ci_agents):
inventory = {"all": {
"children": {
"bastions": {
"hosts": bastions
},
"ci_agents": {
"hosts": ci_agents
}
}
}}
with open(fname, "w") as handle:
yaml.dump(tostr(inventory), stream=handle, default_flow_style=False)
def write_ssh_cfg(filename, bastions, ci_agents):
bastion_tmpl = textwrap.dedent("""\
Host {host}
Hostname {ip_addr}
User root
ForwardAgent yes
StrictHostKeyChecking no
ControlMaster auto
ControlPath /tmp/ansible-%r@%h:%p
ControlPersist 30m
""")
ci_agent_tmpl = textwrap.dedent("""\
Host {host}
Hostname {ip_addr}
User root
StrictHostKeyChecking no
ProxyCommand /usr/bin/ssh -F ./ssh.cfg -W %h:%p -q root@{bastion_ip}
ControlMaster auto
ControlPath /tmp/ansible-%r@%h:%p
ControlPersist 30m
""")
with open(filename, "w") as handle:
for host, info in sorted(bastions.items()):
args = {
"host": host,
"ip_addr": info["ip_addrs"]["public"]
}
entry = bastion_tmpl.format(**args)
handle.write(entry)
for host, info in sorted(ci_agents.items()):
args = {
"host": host,
"ip_addr": info["ip_addrs"]["private"],
"bastion_ip": info["ip_addrs"]["bastion"]
}
entry = ci_agent_tmpl.format(**args)
handle.write(entry)
def parse_args():
parser = ap.ArgumentParser(description="Inventory Generation")
parser.add_argument(
"--inventory",
default="production",
metavar="FILE",
type=str,
help="Inventory filename to write"
)
parser.add_argument(
"--ssh-cfg",
default="ssh.cfg",
metavar="FILE",
type=str,
help="SSH config filename to write"
)
return parser.parse_args()
def main():
args = parse_args()
init()
bastions = {}
ci_agents = {}
for instance in list_instances():
if instance["name"].startswith("couchdb-bastion"):
load_bastion(bastions, instance)
elif instance["name"].startswith("couchdb-worker"):
load_ci_agent(ci_agents, instance)
assign_bastions(bastions, ci_agents)
write_inventory(args.inventory, bastions, ci_agents)
write_ssh_cfg(args.ssh_cfg, bastions, ci_agents)
if __name__ == "__main__":
main()