From 06576d8b126c090b7d47a1912bea39f22a4784e3 Mon Sep 17 00:00:00 2001 From: Josh VanderLinden Date: Sat, 19 Jul 2014 22:22:41 -0600 Subject: [PATCH 1/5] PEP-8, Python 3, and documentation --- main.py | 76 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/main.py b/main.py index 339d065..a1d6739 100755 --- a/main.py +++ b/main.py @@ -1,42 +1,60 @@ #!/usr/bin/python -import etcd -from jinja2 import Environment, PackageLoader -import os from subprocess import call +import os import sys import time +from jinja2 import Environment, PackageLoader +import etcd + + env = Environment(loader=PackageLoader('haproxy', 'templates')) -POLL_TIMEOUT=5 +POLL_TIMEOUT = 5 + def get_etcd_addr(): - if "ETCD_HOST" not in os.environ: - print "ETCD_HOST not set" - sys.exit(1) + """ + Determine the host and port that etcd should be available on using the + `ETCD_HOST` environment variable.. + + :returns: + A 2-tuple with the hostname/IP and the numeric TCP port at which etcd + can be reached. + + """ - etcd_host = os.environ["ETCD_HOST"] + etcd_host = os.environ.get("ETCD_HOST", None) if not etcd_host: - print "ETCD_HOST not set" + print("ETCD_HOST not set") sys.exit(1) - port = 4001 - host = etcd_host + host, port = etcd_host, 4001 + if ":" in host: + host, port = host.split(":") - if ":" in etcd_host: - host, port = etcd_host.split(":") + return host, int(port) - return host, port def get_services(): + """ + Find all services which have been published to etcd and have exposed a + port. + + :returns: + A dictionary of dictionaries keyed on service name. The inner + dictionary includes the TCP port that the service uses, along with a + list of IP:port values that refer to containers which have exposed the + service port (thereby acting as backend services). + + """ host, port = get_etcd_addr() client = etcd.Client(host=host, port=int(port)) - backends = client.read('/backends', recursive = True) + backends = client.read('/backends', recursive=True) services = {} for i in backends.children: - if i.key[1:].count("/") != 2: continue @@ -45,14 +63,29 @@ def get_services(): if container == "port": endpoints["port"] = i.value continue + endpoints["backends"].append(dict(name=container, addr=i.value)) + return services + def generate_config(services): + """ + Generate a configuration file for haproxy and save it to disk. + + It is expected that the results of :py:func:`get_services` will be passed + to this function. + + :param dict services: + A dictionary of dictionaries, keyed on service name. + + """ + template = env.get_template('haproxy.cfg.tmpl') with open("/etc/haproxy.cfg", "w") as f: f.write(template.render(services=services)) + if __name__ == "__main__": current_services = {} while True: @@ -63,16 +96,15 @@ def generate_config(services): time.sleep(POLL_TIMEOUT) continue - print "config changed. reload haproxy" + print("config changed. reload haproxy") generate_config(services) ret = call(["./reload-haproxy.sh"]) if ret != 0: - print "reloading haproxy returned: ", ret + print("reloading haproxy returned: ", ret) time.sleep(POLL_TIMEOUT) continue current_services = services + except Exception as e: + print("Error:", e) - except Exception, e: - print "Error:", e - - time.sleep(POLL_TIMEOUT) \ No newline at end of file + time.sleep(POLL_TIMEOUT) From 0f8ef80556b15fa7d20b73210ea9b768759c1dc3 Mon Sep 17 00:00:00 2001 From: Josh VanderLinden Date: Sat, 19 Jul 2014 22:57:53 -0600 Subject: [PATCH 2/5] Fairly large refactor... - more flexible shebang - restart haproxy directly rather than using shell script - check that haproxy is installed and on the PATH before running - more docs - connect to etcd once - created main() function - simplified main procedure --- main.py | 103 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/main.py b/main.py index a1d6739..69632f0 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ -#!/usr/bin/python +#!/usr/bin/env python +from distutils.spawn import find_executable from subprocess import call import os import sys @@ -11,6 +12,8 @@ env = Environment(loader=PackageLoader('haproxy', 'templates')) POLL_TIMEOUT = 5 +HAPROXY_CONFIG = '/etc/haproxy.cfg' +HAPROXY_PID = '/var/run/haproxy.pid' def get_etcd_addr(): @@ -22,6 +25,9 @@ def get_etcd_addr(): A 2-tuple with the hostname/IP and the numeric TCP port at which etcd can be reached. + :raises SystemExit: + If the `ETCD_HOST` environment variable is not defined or is empty. + """ etcd_host = os.environ.get("ETCD_HOST", None) @@ -36,11 +42,32 @@ def get_etcd_addr(): return host, int(port) -def get_services(): +def get_haproxy_path(): + """ + Return the absolute path to the `haproxy` executable. + + :raises SystemExit: + If haproxy cannot be found on the PATH. + + """ + + path = find_executable('haproxy') + if not path: + print('haproxy was not found on your PATH, and it must be installed ' + 'to use this script') + sys.exit(1) + + return path + + +def get_services(client): """ Find all services which have been published to etcd and have exposed a port. + :param etcd.Client client: + A handle to an etcd server. + :returns: A dictionary of dictionaries keyed on service name. The inner dictionary includes the TCP port that the service uses, along with a @@ -49,8 +76,7 @@ def get_services(): """ - host, port = get_etcd_addr() - client = etcd.Client(host=host, port=int(port)) + # TODO: handle severed connection, etc backends = client.read('/backends', recursive=True) services = {} @@ -82,29 +108,66 @@ def generate_config(services): """ template = env.get_template('haproxy.cfg.tmpl') - with open("/etc/haproxy.cfg", "w") as f: + with open(HAPROXY_CONFIG, "w") as f: f.write(template.render(services=services)) -if __name__ == "__main__": +def restart_haproxy(): + """ + Restart haproxy. + + :returns: + ``True`` when haproxy appears to have restarted successfully, ``False`` + otherwise. + + """ + + path = get_haproxy_path() + cmd = '{haproxy} -f {cfg} -p {pid} -sf $(cat {pid})'.format( + haproxy=path, + cfg=HAPROXY_CONFIG, + pid=HAPROXY_PID, + ) + + # TODO: shell=True is yucky... read in PID rather than using the cat + return call(cmd, shell=True) == 0 + + +def main(): + """ + Periodically poll etcd for the list of available services running in docker + containers. When a new service becomes available or a service disappears, + update the configuration for haproxy and restart it. + + """ + + # check for haproxy and etcd config before getting into the real code + get_haproxy_path() + host, port = get_etcd_addr() + + client = None current_services = {} - while True: - try: - services = get_services() - if not services or services == current_services: - time.sleep(POLL_TIMEOUT) - continue + while True: + if client is None: + # TODO: connection error handling + client = etcd.Client(host=host, port=port) + services = get_services(client) + if services != current_services: print("config changed. reload haproxy") generate_config(services) - ret = call(["./reload-haproxy.sh"]) - if ret != 0: - print("reloading haproxy returned: ", ret) - time.sleep(POLL_TIMEOUT) - continue - current_services = services - except Exception as e: - print("Error:", e) + + if restart_haproxy(): + current_services = services + else: + print("failed to restart haproxy!") time.sleep(POLL_TIMEOUT) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + pass From 6ba0360b487a7134a1c18cdfe760c73220ed955b Mon Sep 17 00:00:00 2001 From: Josh VanderLinden Date: Sat, 19 Jul 2014 23:26:01 -0600 Subject: [PATCH 3/5] Removed script to reload haproxy --- reload-haproxy.sh | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 reload-haproxy.sh diff --git a/reload-haproxy.sh b/reload-haproxy.sh deleted file mode 100755 index 851ba78..0000000 --- a/reload-haproxy.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -/usr/local/sbin/haproxy -f /etc/haproxy.cfg -p /var/run/haproxy.pid -sf $(cat /var/run/haproxy.pid) From 0313071191d91947464331ed1e786b4ca768afd2 Mon Sep 17 00:00:00 2001 From: Josh VanderLinden Date: Sun, 20 Jul 2014 00:04:14 -0600 Subject: [PATCH 4/5] Handle private repos --- main.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 69632f0..77dd2e4 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +from collections import defaultdict from distutils.spawn import find_executable from subprocess import call import os @@ -78,20 +79,28 @@ def get_services(client): # TODO: handle severed connection, etc backends = client.read('/backends', recursive=True) - services = {} + services = defaultdict(lambda: { + 'port': None, + 'backends': [] + }) for i in backends.children: - if i.key[1:].count("/") != 2: + if i.key[1:].count("/") < 2: continue - ignore, service, container = i.key[1:].split("/") - endpoints = services.setdefault(service, dict(port="", backends=[])) + ignore, service, container = i.key[1:].rsplit("/", 2) + endpoints = services[service] if container == "port": endpoints["port"] = i.value continue endpoints["backends"].append(dict(name=container, addr=i.value)) + # filter out services with no "port" value in etcd + for svc, data in tuple(services.items()): + if data['port'] is None: + services.pop(svc) + return services From a59180570d9f6e48443ac7b1d9eff94cb51db6f9 Mon Sep 17 00:00:00 2001 From: Casey Harford Date: Tue, 4 Nov 2014 12:34:37 -0800 Subject: [PATCH 5/5] implementd sort, added a dependency to dockerifle --- Dockerfile | 2 +- main.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2aef6ba..fd7faf7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:14.04 RUN apt-get update -RUN apt-get install -y wget make gcc binutils python-pip python-dev libssl-dev +RUN apt-get install -y wget make gcc binutils python-pip python-dev libssl-dev libffi-dev WORKDIR /root diff --git a/main.py b/main.py index 77dd2e4..ca30d91 100755 --- a/main.py +++ b/main.py @@ -101,6 +101,9 @@ def get_services(client): if data['port'] is None: services.pop(svc) + for service in services: + services[service]["backends"].sort() + return services @@ -163,8 +166,10 @@ def main(): client = etcd.Client(host=host, port=port) services = get_services(client) + if services != current_services: print("config changed. reload haproxy") + generate_config(services) if restart_haproxy():