class Haproxy(object): # envvar envvar_default_ssl_cert = os.getenv("DEFAULT_SSL_CERT") or os.getenv("SSL_CERT") envvar_extra_ssl_certs = os.getenv("EXTRA_SSL_CERTS") envvar_default_ca_cert = os.getenv("CA_CERT") envvar_maxconn = os.getenv("MAXCONN", "4096") envvar_mode = os.getenv("MODE", "http") envvar_option = os.getenv("OPTION", "redispatch, httplog, dontlognull, forwardfor").split(",") envvar_rsyslog_destnation = os.getenv("RSYSLOG_DESTINATION", "127.0.0.1") envvar_ssl_bind_ciphers = os.getenv("SSL_BIND_CIPHERS") envvar_ssl_bind_options = os.getenv("SSL_BIND_OPTIONS") envvar_stats_auth = os.getenv("STATS_AUTH", "stats:stats") envvar_stats_port = os.getenv("STATS_PORT", "1936") envvar_timeout = os.getenv("TIMEOUT", "http-request 5s, connect 5000, client 50000, server 50000").split(",") envvar_health_check = os.getenv("HEALTH_CHECK", "check inter 2000 rise 2 fall 3") envvar_extra_global_settings = os.getenv("EXTRA_GLOBAL_SETTINGS") envvar_extra_default_settings = os.getenv("EXTRA_DEFAULT_SETTINGS") envvar_extra_bind_settings = os.getenv("EXTRA_BIND_SETTINGS") envvar_http_basic_auth = os.getenv("HTTP_BASIC_AUTH") envvar_monitor_uri = os.getenv("MONITOR_URI") envvar_monitor_port = os.getenv("MONITOR_PORT") # envvar overwritable envvar_balance = os.getenv("BALANCE", "roundrobin") # const var const_cert_dir = "/certs/" const_cacert_dir = "/cacerts/" const_config_file = "/haproxy.cfg" const_command = ["/usr/sbin/haproxy", "-f", const_config_file, "-db", "-q"] const_api_retry = 10 # seconds # class var cls_container_uri = os.getenv("TUTUM_CONTAINER_API_URI") cls_service_uri = os.getenv("TUTUM_SERVICE_API_URI") cls_tutum_auth = os.getenv("TUTUM_AUTH") cls_linked_services = [] cls_cfg = None cls_haproxy_process = None cls_certs = [] cls_service_name_match = re.compile(r"(.+)_\d+$") cls_linked_container_object_cache = {} def __init__(self): Haproxy.extra_bind_settings = Haproxy._parse_extra_bind_settings(Haproxy.envvar_extra_bind_settings) self.ssl = None self.ssl_updated = False self.routes_added = [] self.require_default_route = False if Haproxy.cls_container_uri and Haproxy.cls_service_uri and Haproxy.cls_tutum_auth: haproxy_container = self.fetch_tutum_obj(Haproxy.cls_container_uri) links = {} for link in haproxy_container.linked_to_container: linked_container_uri = link["to_container"] linked_container_name = link["name"].upper().replace("-", "_") linked_container_service_name = linked_container_name match = Haproxy.cls_service_name_match.match(linked_container_name) if match: linked_container_service_name = match.group(1) links[linked_container_uri] = { "container_name": linked_container_name, "container_uri": linked_container_uri, "service_name": linked_container_service_name, "endpoints": link["endpoints"], } new_container_object_uris = filter(lambda x: x not in Haproxy.cls_linked_container_object_cache, links) pool = ThreadPool(processes=10) new_container_objects = pool.map(Haproxy.fetch_tutum_obj, new_container_object_uris) for i, container_uri in enumerate(new_container_object_uris): Haproxy.cls_linked_container_object_cache[container_uri] = new_container_objects[i] linked_containers = [ Haproxy.cls_linked_container_object_cache[link["to_container"]] for link in haproxy_container.linked_to_container ] for linked_container in linked_containers: linked_container_uri = linked_container.resource_uri linked_container_service_uri = linked_container.service linked_container_name = linked_container.name.upper().replace("-", "_") linked_container_envvars = {} for envvar in linked_container.container_envvars: if "_ENV_" not in envvar["key"]: linked_container_envvars["%s_ENV_%s" % (linked_container_name, envvar["key"])] = envvar["value"] links[linked_container_uri]["service_uri"] = linked_container_service_uri links[linked_container_uri]["container_envvars"] = linked_container_envvars linked_services = [] for link in links.itervalues(): if link["service_uri"] not in linked_services: linked_services.append(link["service_uri"]) logger.info( "Service links: %s", ", ".join( sorted( set( [ "%s(%s)" % (link.get("service_name"), parse_uuid_from_resource_uri(link.get("service_uri"))) for link in links.itervalues() ] ) ) ), ) logger.info( "Container links: %s", ", ".join( sorted( set( [ "%s(%s)" % (link.get("container_name"), parse_uuid_from_resource_uri(link.get("container_uri"))) for link in links.itervalues() ] ) ) ), ) Haproxy.cls_linked_services = linked_services self.specs = Specs(links) else: logger.info("Loading HAProxy definition from environment variables") Haproxy.cls_linked_services = None Haproxy.specs = Specs() def update(self): cfg_dict = OrderedDict() self._config_ssl() cfg_dict.update(self._config_global_defaults()) for cfg in self._config_tcp(): cfg_dict.update(cfg) cfg_dict.update(self._config_frontend()) cfg_dict.update(self._config_backend()) cfg = self._prettify(cfg_dict) if Haproxy.cls_service_uri and Haproxy.cls_container_uri and Haproxy.cls_tutum_auth: if Haproxy.cls_cfg != cfg: if not Haproxy.cls_cfg: logger.info("HAProxy configuration:\n%s" % cfg) else: logger.info("HAProxy configuration is updated:\n%s" % cfg) Haproxy.cls_cfg = cfg if self._save_conf(): self._run() elif self.ssl_updated: self._run() else: logger.info("HAProxy configuration remains unchanged") logger.info("===========END===========") else: logger.info("HAProxy configuration:\n%s" % cfg) Haproxy.cls_cfg = cfg self._save_conf() logger.info("Launching HAProxy") p = subprocess.Popen(self.const_command) logger.info("HAProxy has been launched(PID: %s)", str(p.pid)) logger.info("===========END===========") p.wait() def _run(self): def _wait_pid(p): if p: pid = p.pid p.wait() logger.info("HAProxy(PID:%s) has been terminated" % str(pid)) if Haproxy.cls_haproxy_process: # Reload haproxy logger.info("Reloading HAProxy") process = subprocess.Popen(self.const_command + ["-sf", str(Haproxy.cls_haproxy_process.pid)]) old_process = Haproxy.cls_haproxy_process thread.start_new_thread(_wait_pid, (old_process,)) Haproxy.cls_haproxy_process = process logger.info("HAProxy has been reloaded(PID: %s)", str(Haproxy.cls_haproxy_process.pid)) else: # Launch haproxy logger.info("Launching HAProxy") Haproxy.cls_haproxy_process = subprocess.Popen(self.const_command) logger.info("HAProxy has been launched(PID: %s)", str(Haproxy.cls_haproxy_process.pid)) @staticmethod def _prettify(cfg): text = "" for section, contents in cfg.items(): text += "%s\n" % section for content in contents: text += " %s\n" % content return text.strip() def _config_ssl(self): certs = [] cacerts = [] if self.envvar_default_ssl_cert: certs.append(self.envvar_default_ssl_cert) if self.envvar_default_ca_cert: cacerts.append(self.envvar_default_ca_cert) certs.extend(self.get_extra_ssl_certs()) certs.extend(self.specs.get_default_ssl_cert()) certs.extend(self.specs.get_ssl_cert()) if certs: if set(certs) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(certs) self.ssl_updated = True self._save_certs(certs) self.ssl = "ssl crt /certs/" if cacerts: if set(cacerts) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(cacerts) self.ssl_updated = True self._save_ca_certs(cacerts) self.ssl += " ca-file /cacerts/cert0.pem verify required" def get_extra_ssl_certs(self): extra_certs = [] if self.envvar_extra_ssl_certs: for cert_name in self.envvar_extra_ssl_certs.split(): extra_certs.append(os.getenv(cert_name)) return extra_certs def _save_certs(self, certs): try: if not os.path.exists(self.const_cert_dir): os.makedirs(self.const_cert_dir) except Exception as e: logger.error(e) for index, cert in enumerate(certs): cert_filename = "%scert%d.pem" % (self.const_cert_dir, index) try: with open(cert_filename, "w") as f: f.write(cert.replace("\\n", "\n")) except Exception as e: logger.error(e) logger.info("SSL certificates are updated") def _save_ca_certs(self, certs): try: if not os.path.exists(self.const_cacert_dir): os.makedirs(self.const_cacert_dir) except Exception as e: logger.error(e) for index, cert in enumerate(certs): cert_filename = "%scert%d.pem" % (self.const_cacert_dir, index) try: with open(cert_filename, "w") as f: f.write(cert.replace("\\n", "\n")) except Exception as e: logger.error(e) logger.info("CA certificates are updated") def _save_conf(self): try: with open(self.const_config_file, "w") as f: f.write(Haproxy.cls_cfg) return True except Exception as e: logger.error(e) return False @classmethod def _config_global_defaults(cls): cfg = OrderedDict() cfg["global"] = [ "log %s local0" % cls.envvar_rsyslog_destnation, "log %s local1 notice" % cls.envvar_rsyslog_destnation, "log-send-hostname", "maxconn %s" % cls.envvar_maxconn, "pidfile /var/run/haproxy.pid", "user haproxy", "group haproxy", "daemon", "stats socket /var/run/haproxy.stats level admin", ] cfg["defaults"] = ["balance %s" % cls.envvar_balance, "log global", "mode %s" % cls.envvar_mode] bind = " ".join([cls.envvar_stats_port, cls.extra_bind_settings.get(cls.envvar_stats_port, "")]) cfg["listen stats"] = [ "bind :%s" % bind.strip(), "mode http", "stats enable", "timeout connect 10s", "timeout client 1m", "timeout server 1m", "stats hide-version", "stats realm Haproxy\ Statistics", "stats uri /", "stats auth %s" % cls.envvar_stats_auth, ] for opt in cls.envvar_option: if opt: cfg["defaults"].append("option %s" % opt.strip()) for t in cls.envvar_timeout: if t: cfg["defaults"].append("timeout %s" % t.strip()) if cls.envvar_ssl_bind_options: cfg["global"].append("ssl-default-bind-options %s" % cls.envvar_ssl_bind_options) if cls.envvar_ssl_bind_ciphers: cfg["global"].append("ssl-default-bind-ciphers %s" % cls.envvar_ssl_bind_ciphers) if Haproxy.envvar_extra_default_settings: settings = re.split(r"(?<!\\),", Haproxy.envvar_extra_default_settings) for setting in settings: if setting.strip(): cfg["defaults"].append(setting.strip().replace("\,", ",")) if Haproxy.envvar_extra_global_settings: settings = re.split(r"(?<!\\),", Haproxy.envvar_extra_global_settings) for setting in settings: if setting.strip(): cfg["global"].append(setting.strip().replace("\,", ",")) if Haproxy.envvar_http_basic_auth: auth_list = re.split(r"(?<!\\),", Haproxy.envvar_http_basic_auth) userlist = [] for auth in auth_list: if auth.strip(): terms = auth.strip().split(":", 1) if len(terms) == 2: username = terms[0].replace("\,", ",") password = terms[1].replace("\,", ",") userlist.append("user %s insecure-password %s" % (username, password)) if userlist: cfg["userlist haproxy_userlist"] = userlist return cfg def _config_tcp(self): cfgs = [] if not self._get_service_attr("tcp_ports"): return cfgs ports = [] for service_alias in self.specs.get_service_aliases(): tcp_ports = self._get_service_attr("tcp_ports", service_alias) if tcp_ports: ports.extend(tcp_ports) for port in set(ports): cfg = OrderedDict() ssl = False port_num = port if port.lower().endswith("/ssl"): port_num = port[:-4] if self.ssl: ssl = True bind = " ".join([port_num, self.extra_bind_settings.get(port_num, "")]) if ssl: bind = " ".join([bind.strip(), self.ssl]) listen = ["bind :%s" % bind.strip(), "mode tcp"] for _service_alias, routes in self.specs.get_routes().iteritems(): tcp_ports = self._get_service_attr("tcp_ports", _service_alias) if tcp_ports and port in tcp_ports: for route in routes: if route["port"] == port_num: tcp_route = ["server %s %s:%s" % (route["container_name"], route["addr"], route["port"])] health_check = self._get_service_attr("health_check", _service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check tcp_route.append(health_check) listen.append(" ".join(tcp_route)) self.routes_added.append(route) options = self._get_service_attr("option", service_alias) if options: for option in options: listen.append("option %s" % option) extra_settings = self._get_service_attr("extra_settings", service_alias) if extra_settings: settings = re.split(r"(?<!\\),", extra_settings) for setting in settings: if setting.strip(): listen.append(setting.strip().replace("\,", ",")) cfg["listen port_%s" % port_num] = listen cfgs.append(cfg) return cfgs def _config_frontend(self): monitor_uri_configured = False cfg = OrderedDict() if self.specs.get_vhosts(): frontends_dict = {} rule_counter = 0 for vhost in self.specs.get_vhosts(): rule_counter += 1 port = vhost["port"] # initialize bind clause for each port if port not in frontends_dict: ssl = False for v in self.specs.get_vhosts(): if v["port"] == port: scheme = v["scheme"].lower() if scheme in ["https", "wss"] and self.ssl: ssl = True break bind = " ".join([port, self.extra_bind_settings.get(port, "")]) if ssl: bind = " ".join([bind.strip(), self.ssl]) frontends_dict[port] = ["bind :%s" % bind] if ssl: frontends_dict[port].append("reqadd X-Forwarded-Proto:\ https") # add websocket acl rule frontends_dict[port].append("acl is_websocket hdr(Upgrade) -i WebSocket") # add monitor uri if port == Haproxy.envvar_monitor_port and Haproxy.envvar_monitor_uri: frontends_dict[port].append("monitor-uri %s" % Haproxy.envvar_monitor_uri) monitor_uri_configured = True acl_rule = [] # calculate virtual host rule host_rules = [] host = vhost["host"].strip("/") if "*" in host: host_rules.append( "acl host_rule_%d hdr_reg(host) -i ^%s$" % (rule_counter, host.replace(".", "\.").replace("*", ".*")) ) host_rules.append( "acl host_rule_%d_port hdr_reg(host) -i ^%s:%s$" % (rule_counter, host.replace(".", "\.").replace("*", ".*"), port) ) elif host: host_rules.append("acl host_rule_%d hdr(host) -i %s" % (rule_counter, host)) host_rules.append("acl host_rule_%d_port hdr(host) -i %s:%s" % (rule_counter, host, port)) acl_rule.extend(host_rules) # calculate virtual path rules path_rules = [] path = vhost["path"].strip() if "*" in path: path_rules.append( "acl path_rule_%d path_reg -i ^%s$" % (rule_counter, path.replace(".", "\.").replace("*", ".*")) ) elif path: path_rules.append("acl path_rule_%d path -i %s" % (rule_counter, path)) acl_rule.extend(path_rules) if vhost["scheme"].lower() in ["ws", "wss"]: acl_condition = "is_websocket" else: acl_condition = "" if path_rules: acl_condition = " ".join([acl_condition, "path_rule_%d" % rule_counter]) if host_rules: acl_condition_1 = ("%s host_rule_%d" % (acl_condition, rule_counter)).strip() acl_condition_2 = ("%s host_rule_%d_port" % (acl_condition, rule_counter)).strip() acl_condition = " or ".join([acl_condition_1, acl_condition_2]) if acl_condition: use_backend = "use_backend SERVICE_%s if %s" % (vhost["service_alias"], acl_condition) acl_rule.append(use_backend) frontends_dict[port].extend(acl_rule) for port, frontend in frontends_dict.iteritems(): cfg["frontend port_%s" % port] = frontend else: all_routes = [] for routes in self.specs.get_routes().itervalues(): all_routes.extend(routes) if len(self.routes_added) < len(all_routes): self.require_default_route = True if self.require_default_route: frontend = [("bind :80 %s" % self.extra_bind_settings.get("80", "")).strip()] if self.ssl and self: frontend.append(("bind :443 %s %s" % (self.ssl, self.extra_bind_settings.get("443", ""))).strip()) frontend.append("reqadd X-Forwarded-Proto:\ https") if Haproxy.envvar_monitor_uri and ( Haproxy.envvar_monitor_port == "80" or Haproxy.envvar_monitor_port == "443" ): frontend.append("monitor-uri %s" % Haproxy.envvar_monitor_uri) monitor_uri_configured = True frontend.append("maxconn %s" % Haproxy.envvar_maxconn) frontend.append("default_backend default_service") cfg["frontend default_frontend"] = frontend if not monitor_uri_configured and Haproxy.envvar_monitor_port and Haproxy.envvar_monitor_uri: cfg["frontend monitor"] = [ "bind :%s" % Haproxy.envvar_monitor_port, "monitor-uri %s" % Haproxy.envvar_monitor_uri, ] return cfg def _config_backend(self): cfg = OrderedDict() if not self.specs.get_vhosts(): services_aliases = [None] else: services_aliases = self.specs.get_service_aliases() for service_alias in services_aliases: backend = [] is_sticky = False # Add http-service-close option for websocket backend for v in self.specs.get_vhosts(): if service_alias == v["service_alias"]: if v["scheme"].lower() in ["ws", "wss"]: backend.append("option http-server-close") break # To add an entry to backend section: append to backend # To add items to a route: append to route_setting balance = self._get_service_attr("balance", service_alias) if balance: backend.append("balance %s" % balance) appsession = self._get_service_attr("appsession", service_alias) if appsession: backend.append("appsession %s" % appsession) is_sticky = True cookie = self._get_service_attr("cookie", service_alias) if cookie: backend.append("cookie %s" % cookie) is_sticky = True force_ssl = self._get_service_attr("force_ssl", service_alias) if force_ssl: backend.append("redirect scheme https code 301 if !{ ssl_fc }") http_check = self._get_service_attr("http_check", service_alias) if http_check: backend.append("option httpchk %s" % http_check) hsts_max_age = self._get_service_attr("hsts_max_age", service_alias) if hsts_max_age: backend.append("rspadd Strict-Transport-Security:\ max-age=%s;\ includeSubDomains" % hsts_max_age) gzip_compression_type = self._get_service_attr("gzip_compression_type", service_alias) if gzip_compression_type: backend.append("compression algo gzip") backend.append("compression type %s" % gzip_compression_type) options = self._get_service_attr("option", service_alias) if options: for option in options: backend.append("option %s" % option) extra_settings = self._get_service_attr("extra_settings", service_alias) if extra_settings: settings = re.split(r"(?<!\\),", extra_settings) for setting in settings: if setting.strip(): backend.append(setting.strip().replace("\,", ",")) if Haproxy.envvar_http_basic_auth: backend.append("acl need_auth http_auth(haproxy_userlist)") backend.append("http-request auth realm haproxy_basic_auth if !need_auth") for _service_alias, routes in self.specs.get_routes().iteritems(): if not service_alias or _service_alias == service_alias: for route in routes: # avoid adding those tcp routes adding http backends if route in self.routes_added: continue backend_route = ["server %s %s:%s" % (route["container_name"], route["addr"], route["port"])] if is_sticky: backend_route.append("cookie %s" % route["container_name"]) health_check = self._get_service_attr("health_check", service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check backend_route.append(health_check) backend.append(" ".join(backend_route)) if not service_alias: if self.require_default_route: cfg["backend default_service"] = backend else: if self._get_service_attr("virtual_host", service_alias): cfg["backend SERVICE_%s" % service_alias] = backend else: cfg["backend default_service"] = backend return cfg def _get_service_attr(self, attr_name, service_alias=None): # service is None, when there is no virtual host is set if service_alias: try: return self.specs.get_details()[service_alias][attr_name] except: return None else: # Randomly pick a None value from the linked service for _service_alias in self.specs.get_details().iterkeys(): if self.specs.get_details()[_service_alias][attr_name]: return self.specs.get_details()[_service_alias][attr_name] return None @classmethod def fetch_tutum_obj(cls, uri): if not uri: return None while True: try: obj = tutum.Utils.fetch_by_resource_uri(uri) break except Exception as e: logger.error(e) time.sleep(cls.const_api_retry) return obj @staticmethod def _parse_extra_bind_settings(extra_bind_settings): bind_dict = {} if extra_bind_settings: settings = re.split(r"(?<!\\),", extra_bind_settings) for setting in settings: term = setting.split(":", 1) if len(term) == 2: bind_dict[term[0].strip()] = term[1].strip() return bind_dict
class Haproxy(object): cls_linked_services = [] cls_cfg = None cls_process = None cls_certs = [] cls_service_name_match = re.compile(r"(.+)_\d+$") LINKED_CONTAINER_CACHE = {} def __init__(self, msg=""): logger.info("==========BEGIN==========") if msg: logger.info(msg) self.ssl_bind_string = None self.ssl_updated = False self.routes_added = [] self.require_default_route = False self._initialize() def _initialize(self): if HAPROXY_CONTAINER_URI and HAPROXY_SERVICE_URI and API_AUTH: haproxy_container = fetch_remote_obj(HAPROXY_CONTAINER_URI) haproxy_links = InitHelper.get_links_from_haproxy(haproxy_container.linked_to_container) new_added_container_uris = InitHelper.get_new_added_link_uri(Haproxy.LINKED_CONTAINER_CACHE, haproxy_links) new_added_containers = InitHelper.get_container_object_from_uri(new_added_container_uris) InitHelper.update_container_cache(Haproxy.LINKED_CONTAINER_CACHE, new_added_container_uris, new_added_containers) linked_containers = InitHelper.get_linked_containers(Haproxy.LINKED_CONTAINER_CACHE, haproxy_container.linked_to_container) InitHelper.update_haproxy_links(haproxy_links, linked_containers) logger.info("Service links: %s", ", ".join(InitHelper.get_service_links_str(haproxy_links))) logger.info("Container links: %s", ", ".join(InitHelper.get_container_links_str(haproxy_links))) Haproxy.cls_linked_services = InitHelper.get_linked_services(haproxy_links) self.specs = Specs(haproxy_links) else: logger.info("Loading HAProxy definition from environment variables") Haproxy.cls_linked_services = None Haproxy.specs = Specs() def update(self): self._config_ssl() cfg_dict = OrderedDict() cfg_dict.update(self._config_global_section()) cfg_dict.update(self._config_defaults_section()) cfg_dict.update(self._config_stats_section()) cfg_dict.update(self._config_userlist_section(HTTP_BASIC_AUTH)) cfg_dict.update(self._config_tcp_sections()) cfg_dict.update(self._config_frontend_sections()) cfg_dict.update(self._config_backend_sections()) cfg = prettify(cfg_dict) self._update_haproxy(cfg) def _update_haproxy(self, cfg): if HAPROXY_SERVICE_URI and HAPROXY_CONTAINER_URI and API_AUTH: if Haproxy.cls_cfg != cfg: logger.info("HAProxy configuration:\n%s" % cfg) Haproxy.cls_cfg = cfg if save_to_file(HAPROXY_CONFIG_FILE, cfg): Haproxy.cls_process = UpdateHelper.run_reload(Haproxy.cls_process) elif self.ssl_updated: logger.info("SSL certificates have been changed") Haproxy.cls_process = UpdateHelper.run_reload(Haproxy.cls_process) else: logger.info("HAProxy configuration remains unchanged") logger.info("===========END===========") else: logger.info("HAProxy configuration:\n%s" % cfg) save_to_file(HAPROXY_CONFIG_FILE, cfg) UpdateHelper.run_once() def _config_ssl(self): ssl_bind_string = "" ssl_bind_string += self._config_ssl_certs() ssl_bind_string += self._config_ssl_cacerts() if ssl_bind_string: self.ssl_bind_string = ssl_bind_string def _config_ssl_certs(self): ssl_bind_string = "" certs = [] if DEFAULT_SSL_CERT: certs.append(DEFAULT_SSL_CERT) certs.extend(SslHelper.get_extra_ssl_certs(EXTRA_SSL_CERT)) certs.extend(self.specs.get_default_ssl_cert()) certs.extend(self.specs.get_ssl_cert()) if certs: if set(certs) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(certs) self.ssl_updated = True SslHelper.save_certs(CERT_DIR, certs) logger.info("SSL certificates are updated") ssl_bind_string = "ssl crt /certs/" return ssl_bind_string def _config_ssl_cacerts(self): ssl_bind_string = "" cacerts = [] if DEFAULT_CA_CERT: cacerts.append(DEFAULT_CA_CERT) if cacerts: if set(cacerts) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(cacerts) self.ssl_updated = True SslHelper.save_certs(CACERT_DIR, cacerts) logger.info("SSL CA certificates are updated") ssl_bind_string = " ca-file /cacerts/cert0.pem verify required" return ssl_bind_string @staticmethod def _config_global_section(): cfg = OrderedDict() statements = ["log %s local0" % RSYSLOG_DESTINATION, "log %s local1 notice" % RSYSLOG_DESTINATION, "log-send-hostname", "maxconn %s" % MAXCONN, "pidfile /var/run/haproxy.pid", "user haproxy", "group haproxy", "daemon", "stats socket /var/run/haproxy.stats level admin"] statements.extend(ConfigHelper.config_ssl_bind_options(SSL_BIND_OPTIONS)) statements.extend(ConfigHelper.config_ssl_bind_ciphers(SSL_BIND_CIPHERS)) statements.extend(ConfigHelper.config_extra_settings(EXTRA_GLOBAL_SETTINGS)) cfg["global"] = statements return cfg @staticmethod def _config_stats_section(): cfg = OrderedDict() bind = " ".join([STATS_PORT, EXTRA_BIND_SETTINGS.get(STATS_PORT, "")]) cfg["listen stats"] = ["bind :%s" % bind.strip(), "mode http", "stats enable", "timeout connect 10s", "timeout client 1m", "timeout server 1m", "stats hide-version", "stats realm Haproxy\ Statistics", "stats uri /", "stats auth %s" % STATS_AUTH] return cfg @staticmethod def _config_defaults_section(): cfg = OrderedDict() statements = ["balance %s" % BALANCE, "log global", "mode %s" % MODE] statements.extend(ConfigHelper.config_option(OPTION)) statements.extend(ConfigHelper.config_timeout(TIMEOUT)) statements.extend(ConfigHelper.config_extra_settings(EXTRA_DEFAULT_SETTINGS)) cfg["defaults"] = statements return cfg @staticmethod def _config_userlist_section(basic_auth): cfg = OrderedDict() if basic_auth: auth_list = re.split(r'(?<!\\),', basic_auth) userlist = [] for auth in auth_list: if auth.strip(): terms = auth.strip().split(":", 1) if len(terms) == 2: username = terms[0].replace("\,", ",") password = terms[1].replace("\,", ",") userlist.append("user %s insecure-password %s" % (username, password)) if userlist: cfg["userlist haproxy_userlist"] = userlist return cfg def _config_tcp_sections(self): details = self.specs.get_details() services_aliases = self.specs.get_service_aliases() cfg = OrderedDict() if not get_service_attribute(details, "tcp_ports"): return cfg tcp_ports = TcpHelper.get_tcp_port_list(details, services_aliases) for tcp_port in set(tcp_ports): tcp_section, port_num = self.get_tcp_section(details, services_aliases, tcp_port) cfg["listen port_%s" % port_num] = tcp_section return cfg def get_tcp_section(self, details, services_aliases, tcp_port): tcp_section = [] enable_ssl, port_num = TcpHelper.parse_port_string(tcp_port, self.ssl_bind_string) bind_string = get_bind_string(enable_ssl, port_num, self.ssl_bind_string, EXTRA_BIND_SETTINGS) tcp_routes, self.routes_added = TcpHelper.get_tcp_routes(details, self.specs.get_routes(), tcp_port, port_num) services = TcpHelper.get_service_aliases_given_tcp_port(details, services_aliases, tcp_port) balance = TcpHelper.get_tcp_balance(details) options = TcpHelper.get_tcp_options(details, services) extra_settings = TcpHelper.get_tcp_extra_settings(details, services) tcp_section.append("bind :%s" % bind_string.strip()) tcp_section.append("mode tcp") tcp_section.extend(balance) tcp_section.extend(options) tcp_section.extend(extra_settings) tcp_section.extend(tcp_routes) return tcp_section, port_num def _config_frontend_sections(self): vhosts = self.specs.get_vhosts() ssl_bind_string = self.ssl_bind_string monitor_uri_configured = False if vhosts: cfg, monitor_uri_configured = FrontendHelper.config_frontend_with_virtual_host(vhosts, ssl_bind_string) else: self.require_default_route = FrontendHelper.check_require_default_route(self.specs.get_routes(), self.routes_added) if self.require_default_route: cfg, monitor_uri_configured = FrontendHelper.config_default_frontend(ssl_bind_string) else: cfg = OrderedDict() cfg.update(FrontendHelper.config_monitor_frontend(monitor_uri_configured)) return cfg def _config_backend_sections(self): details = self.specs.get_details() routes = self.specs.get_routes() vhosts = self.specs.get_vhosts() cfg = OrderedDict() if not self.specs.get_vhosts(): services_aliases = [None] else: services_aliases = self.specs.get_service_aliases() for service_alias in services_aliases: backend = BackendHelper.get_backend_section(details, routes, vhosts, service_alias, self.routes_added) if BackendHelper.check_backend_has_routes(backend): if not service_alias: if self.require_default_route: cfg["backend default_service"] = backend else: if get_service_attribute(details, "virtual_host", service_alias): cfg["backend SERVICE_%s" % service_alias] = backend else: cfg["backend default_service"] = backend return cfg
class Haproxy(object): # envvar envvar_default_ssl_cert = os.getenv("DEFAULT_SSL_CERT") or os.getenv("SSL_CERT") envvar_maxconn = os.getenv("MAXCONN", "4096") envvar_mode = os.getenv("MODE", "http") envvar_option = os.getenv("OPTION", "redispatch, httplog, dontlognull, forwardfor").split(",") envvar_rsyslog_destnation = os.getenv("RSYSLOG_DESTINATION", "127.0.0.1") envvar_ssl_bind_ciphers = os.getenv("SSL_BIND_CIPHERS") envvar_ssl_bind_options = os.getenv("SSL_BIND_OPTIONS") envvar_stats_auth = os.getenv("STATS_AUTH", "stats:stats") envvar_stats_port = os.getenv("STATS_PORT", "1936") envvar_timeout = os.getenv("TIMEOUT", "connect 5000, client 50000, server 50000").split(",") envvar_health_check = os.getenv("HEALTH_CHECK", "check inter 2000 rise 2 fall 3") # envvar overwritable envvar_balance = os.getenv("BALANCE", "roundrobin") # const var const_cert_dir = "/certs/" const_config_file = "/haproxy.cfg" const_command = ['/usr/sbin/haproxy', '-f', const_config_file, '-db', '-q'] const_api_retry = 10 # seconds # class var cls_container_uri = os.getenv("TUTUM_CONTAINER_API_URI") cls_service_uri = os.getenv("TUTUM_SERVICE_API_URI") cls_tutum_auth = os.getenv("TUTUM_AUTH") cls_linked_services = None cls_cfg = None cls_haproxy_process = None cls_certs = [] def __init__(self): self.ssl = None self.ssl_updated = False self.routes_added = [] self.require_default_route = False if Haproxy.cls_container_uri and Haproxy.cls_service_uri and Haproxy.cls_tutum_auth: logger.info("Loading HAProxy definition through REST API") container = self.fetch_tutum_obj(Haproxy.cls_container_uri) service = self.fetch_tutum_obj(Haproxy.cls_service_uri) Haproxy.cls_linked_services = [srv.get("to_service") for srv in service.linked_to_service] self.specs = Specs(container, service) else: logger.info("Loading HAProxy definition from environment variables") Haproxy.cls_linked_services = None Haproxy.specs = Specs() def update(self): cfg_dict = OrderedDict() self._config_ssl() cfg_dict.update(self._config_default()) for cfg in self._config_tcp(): cfg_dict.update(cfg) cfg_dict.update(self._config_frontend()) cfg_dict.update(self._config_backend()) cfg = self._prettify(cfg_dict) if Haproxy.cls_service_uri and Haproxy.cls_container_uri and Haproxy.cls_tutum_auth: if Haproxy.cls_cfg != cfg: if not Haproxy.cls_cfg: logger.info("HAProxy configuration:\n%s" % cfg) else: logger.info("HAProxy configuration is updated:\n%s" % cfg) Haproxy.cls_cfg = cfg if self._save_conf(): self._run() elif self.ssl_updated: self._run() else: logger.info("HAProxy configuration remains unchanged") else: logger.info("HAProxy configuration:\n%s" % cfg) Haproxy.cls_cfg = cfg self._save_conf() logger.info("Launching HAProxy") p = subprocess.Popen(self.const_command) p.wait() def _run(self): if Haproxy.cls_haproxy_process: # Reload haproxy logger.info("Reloading HAProxy") process = subprocess.Popen(self.const_command + ["-sf", str(Haproxy.cls_haproxy_process.pid)]) Haproxy.cls_haproxy_process.wait() Haproxy.cls_haproxy_process = process logger.info("HAProxy has been reloaded\n******************************") else: # Launch haproxy logger.info("Launching HAProxy\n******************************") Haproxy.cls_haproxy_process = subprocess.Popen(self.const_command) @staticmethod def _prettify(cfg): text = "" for section, contents in cfg.items(): text += "%s\n" % section for content in contents: text += " %s\n" % content return text.strip() def _config_ssl(self): certs = [] if self.envvar_default_ssl_cert: certs.append(self.envvar_default_ssl_cert) certs.extend(self.specs.get_default_ssl_cert()) certs.extend(self.specs.get_ssl_cert()) if certs: if set(certs) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(certs) self.ssl_updated = True self._save_certs(certs) self.ssl = "ssl crt /certs/" def _save_certs(self, certs): try: if not os.path.exists(self.const_cert_dir): os.makedirs(self.const_cert_dir) except Exception as e: logger.error(e) for index, cert in enumerate(certs): cert_filename = "%scert%d.pem" % (self.const_cert_dir, index) try: with open(cert_filename, 'w') as f: f.write(cert.replace("\\n", '\n')) except Exception as e: logger.error(e) logger.info("SSL certificates are updated") def _save_conf(self): try: with open(self.const_config_file, 'w') as f: f.write(Haproxy.cls_cfg) return True except Exception as e: logger.error(e) return False @classmethod def _config_default(cls): cfg = OrderedDict({ "global": ["log %s local0" % cls.envvar_rsyslog_destnation, "log %s local1 notice" % cls.envvar_rsyslog_destnation, "log-send-hostname", "maxconn %s" % cls.envvar_maxconn, "tune.ssl.default-dh-param 2048", "pidfile /var/run/haproxy.pid", "user haproxy", "group haproxy", "daemon", "stats socket /var/run/haproxy.stats level admin"], "listen stats": ["bind :%s" % cls.envvar_stats_port, "mode http", "stats enable", "timeout connect 10s", "timeout client 1m", "timeout server 1m", "stats hide-version", "stats realm Haproxy\ Statistics", "stats uri /", "stats auth %s" % cls.envvar_stats_auth], "defaults": ["balance %s" % cls.envvar_balance, "log global", "mode %s" % cls.envvar_mode]}) for opt in cls.envvar_option: if opt: cfg["defaults"].append("option %s" % opt.strip()) for t in cls.envvar_timeout: if t: cfg["defaults"].append("timeout %s" % t.strip()) if cls.envvar_ssl_bind_options: cfg["global"].append("ssl-default-bind-options %s" % cls.envvar_ssl_bind_options) if cls.envvar_ssl_bind_ciphers: cfg["global"].append("ssl-default-bind-ciphers %s" % cls.envvar_ssl_bind_ciphers) return cfg def _config_tcp(self): cfgs = [] if not self._get_service_attr("tcp_ports"): return cfgs ports = [] for service_alias in self.specs.service_aliases: tcp_ports = self._get_service_attr("tcp_ports", service_alias) if tcp_ports: ports.extend(tcp_ports) for port in set(ports): cfg = OrderedDict() ssl = False port_num = port if port.lower().endswith("/ssl"): port_num = port[:-4] if self.ssl: ssl = True if ssl: listen = ["bind :%s %s" % (port_num, self.ssl), "mode tcp"] else: listen = ["bind :%s" % port, "mode tcp"] for _service_alias, routes in self.specs.get_routes().iteritems(): tcp_ports = self._get_service_attr("tcp_ports", _service_alias) if tcp_ports and port in tcp_ports: for route in routes: if route["port"] == port_num: tcp_route = ["server %s %s:%s" % (route["container_name"], route["addr"], route["port"])] health_check = self._get_service_attr("health_check", _service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check tcp_route.append(health_check) listen.append(" ".join(tcp_route)) self.routes_added.append(route) cfg["listen port_%s" % port_num] = listen cfgs.append(cfg) return cfgs def _config_frontend(self): cfg = OrderedDict() if self.specs.get_vhosts(): frontends_dict = {} rule_counter = 0 for vhost in self.specs.get_vhosts(): rule_counter += 1 port = vhost["port"] # initialize bind clause for each port if port not in frontends_dict: ssl = False for v in self.specs.get_vhosts(): if v["port"] == port: scheme = v["scheme"].lower() if scheme in ["https", "wss"] and self.ssl: ssl = True break if ssl: frontends_dict[port] = ["bind :%s %s" % (port, self.ssl), "reqadd X-Forwarded-Proto:\ https"] else: frontends_dict[port] = ["bind :%s" % port] # add websocket acl rule frontends_dict[port].append("acl is_websocket hdr(Upgrade) -i WebSocket") # calculate virtual host rule host_acl = ["acl", "host_rule_%d" % rule_counter] host = vhost["host"].strip("/") if host == "*": pass elif "*" in host: host_acl.append("hdr_reg(host) -i %s" % host.replace(".", "\.").replace("*", ".*")) elif host.startswith("*"): host_acl.append("hdr_end(host) -i %s" % host[1:]) elif host.endswith("*"): host_acl.append("hdr_beg(host) -i %s" % host[:-1]) elif host: host_acl.append("hdr_dom(host) -i %s" % host) # calculate virtual path rules path_acl = ["acl", "path_rule_%d" % rule_counter] path = vhost["path"].strip() if "*" in path[1:-1]: path_acl.append("path_reg -i %s" % path.replace(".", "\.").replace("*", ".*")) elif path.startswith("*"): path_acl.append("path_end -i %s" % path[1:]) elif path.endswith("*"): path_acl.append("path_beg -i %s" % path[:-1]) elif path: path_acl.append("path -i %s" % path) if len(host_acl) > 2 or len(path_acl): service_alias = vhost["service_alias"] if len(host_acl) > 2 and len(path_acl) > 2: acl_condition = "host_rule_%d path_rule_%d" % (rule_counter, rule_counter) if vhost["scheme"].lower() in ["ws", "wss"]: acl_condition += " is_websocket" acl_rule = [" ".join(host_acl), " ".join(path_acl), "use_backend SERVICE_%s if %s" % (service_alias, acl_condition)] elif len(host_acl) > 2: acl_condition = "host_rule_%d" % rule_counter acl_rule = [" ".join(host_acl), "use_backend SERVICE_%s if %s" % (service_alias, acl_condition)] elif len(path_acl) > 2: acl_condition = "path_rule_%d" % rule_counter acl_rule = [" ".join(path_acl), "use_backend SERVICE_%s if %s" % (service_alias, acl_condition)] frontends_dict[port].extend(acl_rule) for port, frontend in frontends_dict.iteritems(): cfg["frontend port_%s" % port] = frontend else: all_routes = [] for routes in self.specs.get_routes().itervalues(): all_routes.extend(routes) if len(self.routes_added) < len(all_routes): self.require_default_route = True if self.require_default_route: frontend = ["bind :80"] if self.ssl and self: frontend.append("bind :443 %s" % self.ssl) frontend.append("reqadd X-Forwarded-Proto:\ https") frontend.append("default_backend default_service") cfg["frontend default_frontend"] = frontend return cfg def _config_backend(self): cfg = OrderedDict() if not self.specs.get_vhosts(): services_aliases = [None] else: services_aliases = self.specs.service_aliases for service_alias in services_aliases: backend = [] is_sticky = False # Add http-service-close option for websocket backend for v in self.specs.get_vhosts(): if service_alias == v["service_alias"]: if v["scheme"].lower() in ["ws", "wss"]: backend.append("option http-server-close") break # To add an entry to backend section: append to backend # To add items to a route: append to route_setting balance = self._get_service_attr("balance", service_alias) if balance: backend.append("balance %s" % balance) appsession = self._get_service_attr("appsession", service_alias) if appsession: backend.append("appsession %s" % appsession) is_sticky = True cookie = self._get_service_attr("cookie", service_alias) if cookie: backend.append("cookie %s" % cookie) is_sticky = True force_ssl = self._get_service_attr("force_ssl", service_alias) if force_ssl: backend.append("redirect scheme https code 301 if !{ ssl_fc }") http_check = self._get_service_attr("http_check", service_alias) if http_check: backend.append("option httpchk %s" % http_check) hsts_max_age = self._get_service_attr("hsts_max_age", service_alias) if hsts_max_age: backend.append("rspadd Strict-Transport-Security:\ max-age=%s;\ includeSubDomains" % hsts_max_age) gzip_compression_type = self._get_service_attr('gzip_compression_type', service_alias) if gzip_compression_type: backend.append("compression algo gzip") backend.append("compression type %s" % gzip_compression_type) for _service_alias, routes in self.specs.get_routes().iteritems(): if not service_alias or _service_alias == service_alias: for route in routes: # avoid adding those tcp routes adding http backends if route in self.routes_added: continue backend_route = ["server %s %s:%s" % (route["container_name"], route["addr"], route["port"])] if is_sticky: backend_route.append("cookie %s" % route["container_name"]) health_check = self._get_service_attr("health_check", service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check backend_route.append(health_check) backend.append(" ".join(backend_route)) if not service_alias: if self.require_default_route: cfg["backend default_service"] = sorted(backend) else: if self._get_service_attr("virtual_host", service_alias): cfg["backend SERVICE_%s" % service_alias] = sorted(backend) else: cfg["backend default_service"] = sorted(backend) return cfg def _get_service_attr(self, attr_name, service_alias=None): # service is None, when there is no virtual host is set if service_alias: try: return self.specs.get_details()[service_alias][attr_name] except: return None else: # Randomly pick a None value from the linked service for _service_alias in self.specs.get_details().iterkeys(): if self.specs.get_details()[_service_alias][attr_name]: return self.specs.get_details()[_service_alias][attr_name] return None @classmethod def fetch_tutum_obj(cls, uri): if not uri: return None while True: try: obj = tutum.Utils.fetch_by_resource_uri(uri) break except Exception as e: logger.error(e) time.sleep(cls.const_api_retry) return obj
class Haproxy(object): # envvar envvar_default_ssl_cert = os.getenv("DEFAULT_SSL_CERT") or os.getenv( "SSL_CERT") envvar_default_ca_cert = os.getenv("CA_CERT") envvar_maxconn = os.getenv("MAXCONN", "4096") envvar_mode = os.getenv("MODE", "http") envvar_option = os.getenv( "OPTION", "redispatch, httplog, dontlognull, forwardfor").split(",") envvar_rsyslog_destnation = os.getenv("RSYSLOG_DESTINATION", "127.0.0.1") envvar_ssl_bind_ciphers = os.getenv("SSL_BIND_CIPHERS") envvar_ssl_bind_options = os.getenv("SSL_BIND_OPTIONS") envvar_stats_auth = os.getenv("STATS_AUTH", "stats:stats") envvar_stats_port = os.getenv("STATS_PORT", "1936") envvar_timeout = os.getenv( "TIMEOUT", "connect 5000, client 50000, server 50000").split(",") envvar_health_check = os.getenv("HEALTH_CHECK", "check inter 2000 rise 2 fall 3") envvar_extra_global_settings = os.getenv("EXTRA_GLOBAL_SETTINGS") envvar_extra_default_settings = os.getenv("EXTRA_DEFAULT_SETTINGS") envvar_extra_bind_settings = os.getenv("EXTRA_BIND_SETTINGS") envvar_http_basic_auth = os.getenv("HTTP_BASIC_AUTH") envvar_monitor_uri = os.getenv("MONITOR_URI") envvar_monitor_port = os.getenv("MONITOR_PORT") # envvar overwritable envvar_balance = os.getenv("BALANCE", "roundrobin") # const var const_cert_dir = "/certs/" const_cacert_dir = "/cacerts/" const_config_file = "/haproxy.cfg" const_command = ['/usr/sbin/haproxy', '-f', const_config_file, '-db', '-q'] const_api_retry = 10 # seconds # class var cls_container_uri = os.getenv("TUTUM_CONTAINER_API_URI") cls_service_uri = os.getenv("TUTUM_SERVICE_API_URI") cls_tutum_auth = os.getenv("TUTUM_AUTH") cls_linked_services = None cls_cfg = None cls_haproxy_process = None cls_certs = [] def __init__(self): Haproxy.extra_bind_settings = Haproxy._parse_extra_bind_settings( Haproxy.envvar_extra_bind_settings) self.ssl = None self.ssl_updated = False self.routes_added = [] self.require_default_route = False if Haproxy.cls_container_uri and Haproxy.cls_service_uri and Haproxy.cls_tutum_auth: container = self.fetch_tutum_obj(Haproxy.cls_container_uri) service = self.fetch_tutum_obj(Haproxy.cls_service_uri) Haproxy.cls_linked_services = [ srv.get("to_service") for srv in service.linked_to_service ] logger.info( "Current links: %s", ", ".join([ "%s(%s)" % (srv.get("name"), parse_uuid_from_resource_uri(srv.get("to_service"))) for srv in service.linked_to_service ])) self.specs = Specs(container, service) else: logger.info( "Loading HAProxy definition from environment variables") Haproxy.cls_linked_services = None Haproxy.specs = Specs() def update(self): cfg_dict = OrderedDict() self._config_ssl() cfg_dict.update(self._config_global_defaults()) for cfg in self._config_tcp(): cfg_dict.update(cfg) cfg_dict.update(self._config_frontend()) cfg_dict.update(self._config_backend()) cfg = self._prettify(cfg_dict) if Haproxy.cls_service_uri and Haproxy.cls_container_uri and Haproxy.cls_tutum_auth: if Haproxy.cls_cfg != cfg: if not Haproxy.cls_cfg: logger.info("HAProxy configuration:\n%s" % cfg) else: logger.info("HAProxy configuration is updated:\n%s" % cfg) Haproxy.cls_cfg = cfg if self._save_conf(): self._run() elif self.ssl_updated: self._run() else: logger.info("HAProxy configuration remains unchanged") logger.info("===========END===========") else: logger.info("HAProxy configuration:\n%s" % cfg) Haproxy.cls_cfg = cfg self._save_conf() logger.info("Launching HAProxy") p = subprocess.Popen(self.const_command) logger.info("HAProxy has been launched(PID: %s)", str(p.pid)) logger.info("===========END===========") p.wait() def _run(self): def _wait_pid(p): if p: pid = p.pid p.wait() logger.info("HAProxy(PID:%s) has been terminated" % str(pid)) if Haproxy.cls_haproxy_process: # Reload haproxy logger.info("Reloading HAProxy") process = subprocess.Popen( self.const_command + ["-sf", str(Haproxy.cls_haproxy_process.pid)]) old_process = Haproxy.cls_haproxy_process thread.start_new_thread(_wait_pid, (old_process, )) Haproxy.cls_haproxy_process = process logger.info("HAProxy has been reloaded(PID: %s)", str(Haproxy.cls_haproxy_process.pid)) else: # Launch haproxy logger.info("Launching HAProxy") Haproxy.cls_haproxy_process = subprocess.Popen(self.const_command) logger.info("HAProxy has been launched(PID: %s)", str(Haproxy.cls_haproxy_process.pid)) @staticmethod def _prettify(cfg): text = "" for section, contents in cfg.items(): text += "%s\n" % section for content in contents: text += " %s\n" % content return text.strip() def _config_ssl(self): certs = [] cacerts = [] if self.envvar_default_ssl_cert: certs.append(self.envvar_default_ssl_cert) if self.envvar_default_ca_cert: cacerts.append(self.envvar_default_ca_cert) certs.extend(self.specs.get_default_ssl_cert()) certs.extend(self.specs.get_ssl_cert()) if certs: if set(certs) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(certs) self.ssl_updated = True self._save_certs(certs) self.ssl = "ssl crt /certs/" if cacerts: if set(cacerts) != set(Haproxy.cls_certs): Haproxy.cls_certs = copy.copy(cacerts) self.ssl_updated = True self._save_ca_certs(cacerts) self.ssl += " ca-file /cacerts/cert0.pem verify required" def _save_certs(self, certs): try: if not os.path.exists(self.const_cert_dir): os.makedirs(self.const_cert_dir) except Exception as e: logger.error(e) for index, cert in enumerate(certs): cert_filename = "%scert%d.pem" % (self.const_cert_dir, index) try: with open(cert_filename, 'w') as f: f.write(cert.replace("\\n", '\n')) except Exception as e: logger.error(e) logger.info("SSL certificates are updated") def _save_ca_certs(self, certs): try: if not os.path.exists(self.const_cacert_dir): os.makedirs(self.const_cacert_dir) except Exception as e: logger.error(e) for index, cert in enumerate(certs): cert_filename = "%scert%d.pem" % (self.const_cacert_dir, index) try: with open(cert_filename, 'w') as f: f.write(cert.replace("\\n", '\n')) except Exception as e: logger.error(e) logger.info("CA certificates are updated") def _save_conf(self): try: with open(self.const_config_file, 'w') as f: f.write(Haproxy.cls_cfg) return True except Exception as e: logger.error(e) return False @classmethod def _config_global_defaults(cls): cfg = OrderedDict() cfg["global"] = [ "log %s local0" % cls.envvar_rsyslog_destnation, "log %s local1 notice" % cls.envvar_rsyslog_destnation, "log-send-hostname", "maxconn %s" % cls.envvar_maxconn, "pidfile /var/run/haproxy.pid", "user haproxy", "group haproxy", "daemon", "stats socket /var/run/haproxy.stats level admin" ] cfg["defaults"] = [ "balance %s" % cls.envvar_balance, "log global", "mode %s" % cls.envvar_mode ] bind = " ".join([ cls.envvar_stats_port, cls.extra_bind_settings.get(cls.envvar_stats_port, "") ]) cfg["listen stats"] = [ "bind :%s" % bind.strip(), "mode http", "stats enable", "timeout connect 10s", "timeout client 1m", "timeout server 1m", "stats hide-version", "stats realm Haproxy\ Statistics", "stats uri /", "stats auth %s" % cls.envvar_stats_auth ] for opt in cls.envvar_option: if opt: cfg["defaults"].append("option %s" % opt.strip()) for t in cls.envvar_timeout: if t: cfg["defaults"].append("timeout %s" % t.strip()) if cls.envvar_ssl_bind_options: cfg["global"].append("ssl-default-bind-options %s" % cls.envvar_ssl_bind_options) if cls.envvar_ssl_bind_ciphers: cfg["global"].append("ssl-default-bind-ciphers %s" % cls.envvar_ssl_bind_ciphers) if Haproxy.envvar_extra_default_settings: settings = re.split(r'(?<!\\),', Haproxy.envvar_extra_default_settings) for setting in settings: if setting.strip(): cfg["defaults"].append(setting.strip().replace("\,", ",")) if Haproxy.envvar_extra_global_settings: settings = re.split(r'(?<!\\),', Haproxy.envvar_extra_global_settings) for setting in settings: if setting.strip(): cfg["global"].append(setting.strip().replace("\,", ",")) if Haproxy.envvar_http_basic_auth: auth_list = re.split(r'(?<!\\),', Haproxy.envvar_http_basic_auth) userlist = [] for auth in auth_list: if auth.strip(): terms = auth.strip().split(":", 1) if len(terms) == 2: username = terms[0].replace("\,", ",") password = terms[1].replace("\,", ",") userlist.append("user %s insecure-password %s" % (username, password)) if userlist: cfg["userlist haproxy_userlist"] = userlist return cfg def _config_tcp(self): cfgs = [] if not self._get_service_attr("tcp_ports"): return cfgs ports = [] for service_alias in self.specs.get_service_aliases(): tcp_ports = self._get_service_attr("tcp_ports", service_alias) if tcp_ports: ports.extend(tcp_ports) for port in set(ports): cfg = OrderedDict() ssl = False port_num = port if port.lower().endswith("/ssl"): port_num = port[:-4] if self.ssl: ssl = True bind = " ".join( [port_num, self.extra_bind_settings.get(port_num, "")]) if ssl: bind = " ".join([bind.strip(), self.ssl]) listen = ["bind :%s" % bind.strip(), "mode tcp"] for _service_alias, routes in self.specs.get_routes().iteritems(): tcp_ports = self._get_service_attr("tcp_ports", _service_alias) if tcp_ports and port in tcp_ports: for route in routes: if route["port"] == port_num: tcp_route = [ "server %s %s:%s" % (route["container_name"], route["addr"], route["port"]) ] health_check = self._get_service_attr( "health_check", _service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check tcp_route.append(health_check) listen.append(" ".join(tcp_route)) self.routes_added.append(route) options = self._get_service_attr('option', service_alias) if options: for option in options: listen.append("option %s" % option) extra_settings = self._get_service_attr('extra_settings', service_alias) if extra_settings: settings = re.split(r'(?<!\\),', extra_settings) for setting in settings: if setting.strip(): listen.append(setting.strip().replace("\,", ",")) cfg["listen port_%s" % port_num] = listen cfgs.append(cfg) return cfgs def _config_frontend(self): monitor_uri_configured = False cfg = OrderedDict() if self.specs.get_vhosts(): frontends_dict = {} rule_counter = 0 for vhost in self.specs.get_vhosts(): rule_counter += 1 port = vhost["port"] # initialize bind clause for each port if port not in frontends_dict: ssl = False for v in self.specs.get_vhosts(): if v["port"] == port: scheme = v["scheme"].lower() if scheme in ["https", "wss"] and self.ssl: ssl = True break bind = " ".join( [port, self.extra_bind_settings.get(port, "")]) if ssl: bind = " ".join([bind.strip(), self.ssl]) frontends_dict[port] = ["bind :%s" % bind] if ssl: frontends_dict[port].append( "reqadd X-Forwarded-Proto:\ https") # add websocket acl rule frontends_dict[port].append( "acl is_websocket hdr(Upgrade) -i WebSocket") # add monitor uri if port == Haproxy.envvar_monitor_port and Haproxy.envvar_monitor_uri: frontends_dict[port].append("monitor-uri %s" % Haproxy.envvar_monitor_uri) monitor_uri_configured = True acl_rule = [] # calculate virtual host rule host_rules = [] host = vhost["host"].strip("/") if host == "*": pass elif "*" in host: host_rules.append( "acl host_rule_%d hdr_reg(host) -i ^%s$" % (rule_counter, host.replace(".", "\.").replace( "*", ".*"))) host_rules.append( "acl host_rule_%d_port hdr_reg(host) -i ^%s:%s$" % (rule_counter, host.replace(".", "\.").replace( "*", ".*"), port)) elif host: host_rules.append("acl host_rule_%d hdr(host) -i %s" % (rule_counter, host)) host_rules.append( "acl host_rule_%d_port hdr(host) -i %s:%s" % (rule_counter, host, port)) acl_rule.extend(host_rules) # calculate virtual path rules path_rules = [] path = vhost["path"].strip() if "*" in path: path_rules.append("acl path_rule_%d path_reg -i ^%s$" % (rule_counter, path.replace( ".", "\.").replace("*", ".*"))) elif path: path_rules.append("acl path_rule_%d path -i %s" % (rule_counter, path)) acl_rule.extend(path_rules) if vhost["scheme"].lower() in ["ws", "wss"]: acl_condition = "is_websocket" else: acl_condition = "" if path_rules: acl_condition = " ".join( [acl_condition, "path_rule_%d" % rule_counter]) if host_rules: acl_condition_1 = ("%s host_rule_%d" % (acl_condition, rule_counter)).strip() acl_condition_2 = ("%s host_rule_%d_port" % (acl_condition, rule_counter)).strip() acl_condition = " or ".join( [acl_condition_1, acl_condition_2]) if acl_condition: use_backend = "use_backend SERVICE_%s if %s" % ( vhost["service_alias"], acl_condition) acl_rule.append(use_backend) frontends_dict[port].extend(acl_rule) for port, frontend in frontends_dict.iteritems(): cfg["frontend port_%s" % port] = frontend else: all_routes = [] for routes in self.specs.get_routes().itervalues(): all_routes.extend(routes) if len(self.routes_added) < len(all_routes): self.require_default_route = True if self.require_default_route: frontend = [("bind :80 %s" % self.extra_bind_settings.get('80', "")).strip()] if self.ssl and self: frontend.append( ("bind :443 %s %s" % (self.ssl, self.extra_bind_settings.get('443', ""))).strip()) frontend.append("reqadd X-Forwarded-Proto:\ https") if Haproxy.envvar_monitor_uri and ( Haproxy.envvar_monitor_port == '80' or Haproxy.envvar_monitor_port == '443'): frontend.append("monitor-uri %s" % Haproxy.envvar_monitor_uri) monitor_uri_configured = True frontend.append("default_backend default_service") cfg["frontend default_frontend"] = frontend if not monitor_uri_configured and Haproxy.envvar_monitor_port and Haproxy.envvar_monitor_uri: cfg["frontend monitor"] = [ "bind :%s" % Haproxy.envvar_monitor_port, "monitor-uri %s" % Haproxy.envvar_monitor_uri ] return cfg def _config_backend(self): cfg = OrderedDict() if not self.specs.get_vhosts(): services_aliases = [None] else: services_aliases = self.specs.get_service_aliases() for service_alias in services_aliases: backend = [] is_sticky = False # Add http-service-close option for websocket backend for v in self.specs.get_vhosts(): if service_alias == v["service_alias"]: if v["scheme"].lower() in ["ws", "wss"]: backend.append("option http-server-close") break # To add an entry to backend section: append to backend # To add items to a route: append to route_setting balance = self._get_service_attr("balance", service_alias) if balance: backend.append("balance %s" % balance) appsession = self._get_service_attr("appsession", service_alias) if appsession: backend.append("appsession %s" % appsession) is_sticky = True cookie = self._get_service_attr("cookie", service_alias) if cookie: backend.append("cookie %s" % cookie) is_sticky = True force_ssl = self._get_service_attr("force_ssl", service_alias) if force_ssl: backend.append("redirect scheme https code 301 if !{ ssl_fc }") http_check = self._get_service_attr("http_check", service_alias) if http_check: backend.append("option httpchk %s" % http_check) hsts_max_age = self._get_service_attr("hsts_max_age", service_alias) if hsts_max_age: backend.append( "rspadd Strict-Transport-Security:\ max-age=%s;\ includeSubDomains" % hsts_max_age) gzip_compression_type = self._get_service_attr( 'gzip_compression_type', service_alias) if gzip_compression_type: backend.append("compression algo gzip") backend.append("compression type %s" % gzip_compression_type) options = self._get_service_attr('option', service_alias) if options: for option in options: backend.append("option %s" % option) extra_settings = self._get_service_attr('extra_settings', service_alias) if extra_settings: settings = re.split(r'(?<!\\),', extra_settings) for setting in settings: if setting.strip(): backend.append(setting.strip().replace("\,", ",")) if Haproxy.envvar_http_basic_auth: backend.append("acl need_auth http_auth(haproxy_userlist)") backend.append( "http-request auth realm haproxy_basic_auth if !need_auth") for _service_alias, routes in self.specs.get_routes().iteritems(): if not service_alias or _service_alias == service_alias: for route in routes: # avoid adding those tcp routes adding http backends if route in self.routes_added: continue backend_route = [ "server %s %s:%s" % (route["container_name"], route["addr"], route["port"]) ] if is_sticky: backend_route.append("cookie %s" % route["container_name"]) health_check = self._get_service_attr( "health_check", service_alias) health_check = health_check if health_check else Haproxy.envvar_health_check backend_route.append(health_check) backend.append(" ".join(backend_route)) if not service_alias: if self.require_default_route: cfg["backend default_service"] = sorted(backend) else: if self._get_service_attr("virtual_host", service_alias): cfg["backend SERVICE_%s" % service_alias] = sorted(backend) else: cfg["backend default_service"] = sorted(backend) return cfg def _get_service_attr(self, attr_name, service_alias=None): # service is None, when there is no virtual host is set if service_alias: try: return self.specs.get_details()[service_alias][attr_name] except: return None else: # Randomly pick a None value from the linked service for _service_alias in self.specs.get_details().iterkeys(): if self.specs.get_details()[_service_alias][attr_name]: return self.specs.get_details()[_service_alias][attr_name] return None @classmethod def fetch_tutum_obj(cls, uri): if not uri: return None while True: try: obj = tutum.Utils.fetch_by_resource_uri(uri) break except Exception as e: logger.error(e) time.sleep(cls.const_api_retry) return obj @staticmethod def _parse_extra_bind_settings(extra_bind_settings): bind_dict = {} if extra_bind_settings: settings = re.split(r'(?<!\\),', extra_bind_settings) for setting in settings: term = setting.split(":", 1) if len(term) == 2: bind_dict[term[0].strip()] = term[1].strip() return bind_dict