2018-07-21 00:20:29 -07:00
|
|
|
"""Traefik installation and setup"""
|
|
|
|
|
import hashlib
|
|
|
|
|
import os
|
2020-05-29 17:59:20 +03:00
|
|
|
from glob import glob
|
2018-07-21 00:20:29 -07:00
|
|
|
|
2018-08-01 17:06:52 +02:00
|
|
|
from jinja2 import Template
|
2019-02-22 10:53:36 +01:00
|
|
|
from passlib.apache import HtpasswdFile
|
2019-05-19 22:29:18 -07:00
|
|
|
import backoff
|
2019-05-24 14:37:42 +03:00
|
|
|
import requests
|
2020-05-29 17:59:20 +03:00
|
|
|
import toml
|
2018-07-21 00:20:29 -07:00
|
|
|
|
2020-05-29 17:59:20 +03:00
|
|
|
from .config import CONFIG_DIR
|
2020-06-08 12:15:42 +03:00
|
|
|
from tljh.configurer import load_config, _merge_dictionaries
|
2018-07-21 00:20:29 -07:00
|
|
|
|
|
|
|
|
# FIXME: support more than one platform here
|
|
|
|
|
plat = "linux-amd64"
|
2019-10-01 11:00:28 +03:00
|
|
|
traefik_version = "1.7.18"
|
2018-07-21 00:20:29 -07:00
|
|
|
|
|
|
|
|
# record sha256 hashes for supported platforms here
|
|
|
|
|
checksums = {
|
2019-10-01 11:00:28 +03:00
|
|
|
"linux-amd64": "3c2d153d80890b6fc8875af9f8ced32c4d684e1eb5a46d9815337cb343dfd92e"
|
2018-07-21 00:20:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def checksum_file(path):
|
|
|
|
|
"""Compute the sha256 checksum of a path"""
|
|
|
|
|
hasher = hashlib.sha256()
|
|
|
|
|
with open(path, "rb") as f:
|
|
|
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
|
|
|
hasher.update(chunk)
|
|
|
|
|
return hasher.hexdigest()
|
|
|
|
|
|
2019-05-24 14:37:42 +03:00
|
|
|
def fatal_error(e):
|
|
|
|
|
# Retry only when connection is reset or we think we didn't download entire file
|
|
|
|
|
return str(e) != "ContentTooShort" and not isinstance(e, ConnectionResetError)
|
|
|
|
|
|
2019-05-19 22:29:18 -07:00
|
|
|
@backoff.on_exception(
|
|
|
|
|
backoff.expo,
|
2019-05-24 14:37:42 +03:00
|
|
|
Exception,
|
|
|
|
|
max_tries=2,
|
|
|
|
|
giveup=fatal_error
|
2019-05-19 22:29:18 -07:00
|
|
|
)
|
2018-07-21 00:20:29 -07:00
|
|
|
def ensure_traefik_binary(prefix):
|
|
|
|
|
"""Download and install the traefik binary"""
|
|
|
|
|
traefik_bin = os.path.join(prefix, "bin", "traefik")
|
|
|
|
|
if os.path.exists(traefik_bin):
|
|
|
|
|
checksum = checksum_file(traefik_bin)
|
|
|
|
|
if checksum == checksums[plat]:
|
|
|
|
|
# already have the right binary
|
|
|
|
|
# ensure permissions and we're done
|
|
|
|
|
os.chmod(traefik_bin, 0o755)
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
print(f"checksum mismatch on {traefik_bin}")
|
|
|
|
|
os.remove(traefik_bin)
|
|
|
|
|
|
|
|
|
|
traefik_url = (
|
|
|
|
|
"https://github.com/containous/traefik/releases"
|
|
|
|
|
f"/download/v{traefik_version}/traefik_{plat}"
|
|
|
|
|
)
|
|
|
|
|
print(f"Downloading traefik {traefik_version}...")
|
|
|
|
|
# download the file
|
2019-05-24 14:37:42 +03:00
|
|
|
response = requests.get(traefik_url)
|
|
|
|
|
if response.status_code == 206:
|
|
|
|
|
raise Exception("ContentTooShort")
|
|
|
|
|
with open(traefik_bin, 'wb') as f:
|
|
|
|
|
f.write(response.content)
|
2018-07-21 00:20:29 -07:00
|
|
|
os.chmod(traefik_bin, 0o755)
|
|
|
|
|
|
|
|
|
|
# verify that we got what we expected
|
|
|
|
|
checksum = checksum_file(traefik_bin)
|
|
|
|
|
if checksum != checksums[plat]:
|
|
|
|
|
raise IOError(f"Checksum failed {traefik_bin}: {checksum} != {checksums[plat]}")
|
|
|
|
|
|
|
|
|
|
|
2019-02-22 10:53:36 +01:00
|
|
|
def compute_basic_auth(username, password):
|
|
|
|
|
"""Generate hashed HTTP basic auth from traefik_api username+password"""
|
|
|
|
|
ht = HtpasswdFile()
|
|
|
|
|
# generate htpassword
|
|
|
|
|
ht.set_password(username, password)
|
|
|
|
|
hashed_password = str(ht.to_string()).split(":")[1][:-3]
|
|
|
|
|
return username + ":" + hashed_password
|
|
|
|
|
|
2020-06-08 11:41:22 +03:00
|
|
|
|
|
|
|
|
def load_extra_config(extra_config_dir):
|
2020-06-02 19:09:19 +03:00
|
|
|
extra_configs = sorted(glob(os.path.join(extra_config_dir, '*.toml')))
|
2020-06-02 11:38:32 +03:00
|
|
|
# Load the toml list of files into dicts and merge them
|
2020-06-08 11:41:22 +03:00
|
|
|
config = toml.load(extra_configs)
|
2020-05-29 17:59:20 +03:00
|
|
|
return config
|
2019-02-22 10:53:36 +01:00
|
|
|
|
2020-06-08 11:41:22 +03:00
|
|
|
|
2018-07-21 00:20:29 -07:00
|
|
|
def ensure_traefik_config(state_dir):
|
|
|
|
|
"""Render the traefik.toml config file"""
|
2020-06-02 11:38:32 +03:00
|
|
|
traefik_std_config_file = os.path.join(state_dir, "traefik.toml")
|
|
|
|
|
traefik_extra_config_dir = os.path.join(CONFIG_DIR, "traefik_config.d")
|
2020-06-18 17:44:12 +03:00
|
|
|
traefik_dynamic_config_dir = os.path.join(state_dir, "rules")
|
2020-06-02 11:38:32 +03:00
|
|
|
|
2018-07-21 00:20:29 -07:00
|
|
|
config = load_config()
|
2019-02-22 10:53:36 +01:00
|
|
|
config['traefik_api']['basic_auth'] = compute_basic_auth(
|
|
|
|
|
config['traefik_api']['username'],
|
|
|
|
|
config['traefik_api']['password'],
|
|
|
|
|
)
|
|
|
|
|
|
2018-07-21 00:20:29 -07:00
|
|
|
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
|
|
|
|
template = Template(f.read())
|
2020-06-08 11:41:22 +03:00
|
|
|
std_config = template.render(config)
|
2018-07-21 00:20:29 -07:00
|
|
|
https = config["https"]
|
|
|
|
|
letsencrypt = https["letsencrypt"]
|
|
|
|
|
tls = https["tls"]
|
|
|
|
|
# validate https config
|
|
|
|
|
if https["enabled"]:
|
|
|
|
|
if not tls["cert"] and not letsencrypt["email"]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"To enable https, you must set tls.cert+key or letsencrypt.email+domains"
|
|
|
|
|
)
|
|
|
|
|
if (letsencrypt["email"] and not letsencrypt["domains"]) or (
|
|
|
|
|
letsencrypt["domains"] and not letsencrypt["email"]
|
|
|
|
|
):
|
|
|
|
|
raise ValueError("Both email and domains must be set for letsencrypt")
|
2020-05-29 17:59:20 +03:00
|
|
|
|
2020-06-18 17:44:12 +03:00
|
|
|
# Ensure traefik extra static config dir exists and is private
|
2020-06-02 11:38:32 +03:00
|
|
|
os.makedirs(traefik_extra_config_dir, mode=0o700, exist_ok=True)
|
2020-05-29 17:59:20 +03:00
|
|
|
|
2020-06-18 17:44:12 +03:00
|
|
|
# Ensure traefik dynamic config dir exists and is private
|
|
|
|
|
os.makedirs(traefik_dynamic_config_dir, mode=0o700, exist_ok=True)
|
|
|
|
|
|
2020-06-08 11:41:22 +03:00
|
|
|
try:
|
|
|
|
|
# Load standard config file merge it with the extra config files into a dict
|
|
|
|
|
extra_config = load_extra_config(traefik_extra_config_dir)
|
|
|
|
|
new_toml = _merge_dictionaries(toml.loads(std_config), extra_config)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
new_toml = toml.loads(std_config)
|
2020-05-29 17:59:20 +03:00
|
|
|
|
2020-06-02 11:38:32 +03:00
|
|
|
# Dump the dict into a toml-formatted string and write it to file
|
|
|
|
|
with open(traefik_std_config_file, "w") as f:
|
2019-02-11 09:24:16 +02:00
|
|
|
os.fchmod(f.fileno(), 0o600)
|
2020-06-08 11:41:22 +03:00
|
|
|
toml.dump(new_toml, f)
|
2018-07-30 15:26:09 +02:00
|
|
|
|
2020-06-18 17:44:12 +03:00
|
|
|
with open(os.path.join(traefik_dynamic_config_dir, "rules.toml"), "w") as f:
|
2019-02-11 09:24:16 +02:00
|
|
|
os.fchmod(f.fileno(), 0o600)
|
2019-01-22 16:24:38 +02:00
|
|
|
|
2018-07-30 15:26:09 +02:00
|
|
|
# ensure acme.json exists and is private
|
|
|
|
|
with open(os.path.join(state_dir, "acme.json"), "a") as f:
|
|
|
|
|
os.fchmod(f.fileno(), 0o600)
|
|
|
|
|
|