| #!/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": get_private_ip(instance) |
| }, |
| "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": get_private_ip(instance) |
| }, |
| "system": { |
| "arch": instance["vcpu"]["architecture"], |
| "num_cpus": instance["vcpu"]["count"], |
| "ram": instance["memory"] |
| } |
| } |
| |
| |
| def get_private_ip(instance): |
| ip = instance["primary_network_interface"]["primary_ipv4_address"] |
| if ip: |
| return ip |
| |
| for iface in instance["network_interfaces"]: |
| if iface.get("primary_ipv4_address"): |
| return iface["primary_ipv4_address"] |
| |
| raise Exception("Unable to locate a private IP address") |
| |
| |
| 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() |