From 3ee387cd3bdf40cf1ed42e9477a3d86c18537796 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 11 Feb 2019 09:24:16 +0200 Subject: [PATCH] Fixed some issues --- integration-tests/requirements.txt | 1 + integration-tests/test_proxy.py | 9 +++++++-- tests/test_config.py | 7 +++++-- tests/test_configurer.py | 24 ++++++++++++++++++++++ tljh/config.py | 18 ++++++++++++++--- tljh/configurer.py | 27 ++++++++++++++++++++++++- tljh/jupyterhub_config.py | 6 +++--- tljh/systemd.py | 32 +----------------------------- tljh/traefik.py | 4 ++-- tljh/traefik.toml.tpl | 8 +++++--- 10 files changed, 89 insertions(+), 47 deletions(-) diff --git a/integration-tests/requirements.txt b/integration-tests/requirements.txt index 271c563..91f56ca 100644 --- a/integration-tests/requirements.txt +++ b/integration-tests/requirements.txt @@ -1,3 +1,4 @@ pytest pytest-asyncio +passlib git+https://github.com/yuvipanda/hubtraf.git \ No newline at end of file diff --git a/integration-tests/test_proxy.py b/integration-tests/test_proxy.py index a8e114d..1ee236f 100644 --- a/integration-tests/test_proxy.py +++ b/integration-tests/test_proxy.py @@ -53,8 +53,13 @@ def test_manual_https(preserve_config): # verify that our certificate was loaded by traefik assert server_cert == file_cert - # verify that we can still connect to the hub - r = requests.get("https://127.0.0.1/hub/api", verify=False) + for i in range(5): + time.sleep(i) + # verify that we can still connect to the hub + r = requests.get("https://127.0.0.1/hub/api", verify=False) + if r.status_code == 200: + break; + r.raise_for_status() # cleanup diff --git a/tests/test_config.py b/tests/test_config.py index 7b3dda8..811e010 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -119,16 +119,19 @@ def test_remove_from_config_error(): def test_reload_hub(): with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch( 'tljh.systemd.check_service_active' - ) as check_active, mock.patch('tljh.systemd.check_hub_ready') as check_ready: + ) as check_active, mock.patch('tljh.config.check_hub_ready') as check_ready: config.reload_component('hub') assert restart_service.called_with('jupyterhub') assert check_active.called_with('jupyterhub') def test_reload_proxy(tljh_dir): - with mock.patch('tljh.systemd.restart_service') as restart_service: + with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch( + 'tljh.systemd.check_service_active' + ) as check_active: config.reload_component('proxy') assert restart_service.called_with('traefik') + assert check_active.called_with('traefik') assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml')) diff --git a/tests/test_configurer.py b/tests/test_configurer.py index 59dd6ec..14ce953 100644 --- a/tests/test_configurer.py +++ b/tests/test_configurer.py @@ -159,3 +159,27 @@ def test_auth_github(): assert c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator' assert c.GitHubOAuthenticator.client_id == 'something' assert c.GitHubOAuthenticator.client_secret == 'something-else' + + +def test_auth_api_default(): + """ + Test default traefik api authentication settings with no overrides + """ + c = apply_mock_config({}) + + assert c.TraefikTomlProxy.traefik_api_username == 'api_admin' + assert c.TraefikTomlProxy.traefik_api_password == 'admin' + + +def test_set_auth_api(): + """ + Test setting per traefik api credentials + """ + c = apply_mock_config({ + 'auth_api': { + 'username': 'some_user', + 'password': '1234' + } + }) + assert c.TraefikTomlProxy.traefik_api_username == 'some_user' + assert c.TraefikTomlProxy.traefik_api_password == '1234' \ No newline at end of file diff --git a/tljh/config.py b/tljh/config.py index c322421..bdc879b 100644 --- a/tljh/config.py +++ b/tljh/config.py @@ -173,6 +173,14 @@ def remove_config_value(config_path, key_path, value): with open(config_path, 'w') as f: yaml.dump(config, f) +def check_hub_ready(): + import requests + + try: + r = requests.get('http://127.0.0.1:80') + return r.status_code == 200 + except: + return False def reload_component(component): """ @@ -182,17 +190,21 @@ def reload_component(component): """ # import here to avoid circular imports from tljh import systemd, traefik + import time + if component == 'hub': systemd.restart_service('jupyterhub') # Ensure hub is back up while not systemd.check_service_active('jupyterhub'): - asyncio.sleep(1) - while not systemd.check_hub_ready(): - asyncio.sleep(1) + time.sleep(1) + while not check_hub_ready(): + time.sleep(1) print('Hub reload with new configuration complete') elif component == 'proxy': traefik.ensure_traefik_config(STATE_DIR) systemd.restart_service('traefik') + while not systemd.check_service_active('traefik'): + time.sleep(1) print('Proxy reload with new configuration complete') diff --git a/tljh/configurer.py b/tljh/configurer.py index e8526d9..a99e88a 100644 --- a/tljh/configurer.py +++ b/tljh/configurer.py @@ -46,12 +46,18 @@ default = { 'domains': [], }, }, + 'auth_api': { + 'ip': "127.0.0.1", + 'port': 8099, + 'username': 'api_admin', + 'password': 'admin', + 'basic_auth': '' + }, 'user_environment': { 'default_app': 'classic', }, } - def load_config(config_file=CONFIG_FILE): """Load the current config as a dictionary @@ -62,6 +68,8 @@ def load_config(config_file=CONFIG_FILE): config_overrides = yaml.load(f) else: config_overrides = {} + + generate_traefik_api_credentials() return _merge_dictionaries(dict(default), config_overrides) @@ -76,6 +84,7 @@ def apply_config(config_overrides, c): update_limits(c, tljh_config) update_user_environment(c, tljh_config) update_user_account_config(c, tljh_config) + update_auth_api(c, tljh_config) def set_if_not_none(parent, key, value): @@ -85,6 +94,14 @@ def set_if_not_none(parent, key, value): if value is not None: setattr(parent, key, value) +def generate_traefik_api_credentials(): + from passlib.apache import HtpasswdFile + + ht = HtpasswdFile() + ht.set_password(default['auth_api']['username'], default['auth_api']['password']) + traefik_api_hashed_password = str(ht.to_string()).split(":")[1][:-3] + default['auth_api']['basic_auth'] = default['auth_api']['username'] + ":" + traefik_api_hashed_password + def update_auth(c, config): """ @@ -149,6 +166,14 @@ def update_user_account_config(c, config): c.SystemdSpawner.username_template = 'jupyter-{USERNAME}' +def update_auth_api(c, config): + """ + Set traefik api endpoint credentials + """ + c.TraefikTomlProxy.traefik_api_username = config['auth_api']['username'] + c.TraefikTomlProxy.traefik_api_password = config['auth_api']['password'] + + def _merge_dictionaries(a, b, path=None, update=True): """ Merge two dictionaries recursively. diff --git a/tljh/jupyterhub_config.py b/tljh/jupyterhub_config.py index da1999d..61be82e 100644 --- a/tljh/jupyterhub_config.py +++ b/tljh/jupyterhub_config.py @@ -44,9 +44,9 @@ c.JupyterHub.cleanup_servers = False c.JupyterHub.hub_port = 15001 c.TraefikTomlProxy.should_start = False -c.TraefikTomlProxy.traefik_api_password = "admin" -c.TraefikTomlProxy.traefik_api_username = "api_admin" -c.TraefikTomlProxy.toml_dynamic_config_file = "/opt/tljh/state/rules.toml" + +dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules.toml') +c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path c.JupyterHub.proxy_class = TraefikTomlProxy c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')] diff --git a/tljh/systemd.py b/tljh/systemd.py index 44b10d0..53fa965 100644 --- a/tljh/systemd.py +++ b/tljh/systemd.py @@ -84,34 +84,4 @@ def check_service_active(name): ], check=True) return True except subprocess.CalledProcessError: - return False - - -def check_hub_ready(): - """ - Check if the hub is ready - """ - - try: - last_restart = subprocess.check_output([ - 'systemctl', - 'show', - 'jupyterhub', - '-p', - 'ActiveEnterTimestamp' - ]).decode().strip() - - last_restart = " ".join(last_restart.split(" ")[-3:-1]) - - out = subprocess.check_output([ - 'journalctl', - '-u', - 'jupyterhub', - '--since', - last_restart - ]) - - if "JupyterHub is now running at" in out.decode(): - return True - except subprocess.CalledProcessError: - return False + return False \ No newline at end of file diff --git a/tljh/traefik.py b/tljh/traefik.py index 1230b47..5758d09 100644 --- a/tljh/traefik.py +++ b/tljh/traefik.py @@ -75,11 +75,11 @@ def ensure_traefik_config(state_dir): ): raise ValueError("Both email and domains must be set for letsencrypt") with open(os.path.join(state_dir, "traefik.toml"), "w") as f: - os.fchmod(f.fileno(), 0o744) + os.fchmod(f.fileno(), 0o600) f.write(new_toml) with open(os.path.join(state_dir, "rules.toml"), "w") as f: - os.fchmod(f.fileno(), 0o744) + os.fchmod(f.fileno(), 0o600) # ensure acme.json exists and is private with open(os.path.join(state_dir, "acme.json"), "a") as f: diff --git a/tljh/traefik.toml.tpl b/tljh/traefik.toml.tpl index 9ed4625..3e53326 100644 --- a/tljh/traefik.toml.tpl +++ b/tljh/traefik.toml.tpl @@ -41,9 +41,11 @@ idleTimeout = "10m0s" {% endif %} {% endif %} [entryPoints.auth_api] - address = ":8099" + address = ":{{auth_api['port']}}" + [entryPoints.auth_api.whiteList] + sourceRange = ['{{auth_api['ip']}}'] [entryPoints.auth_api.auth.basic] - users = ["api_admin:$apr1$eS/j3kum$q/X2khsIEG/bBGsteP.x./"] + users = ['{{auth_api['basic_auth']}}'] [wss] protocol = "http" @@ -67,5 +69,5 @@ entryPoint = "https" {% endif %} [file] -filename = "/opt/tljh/state/rules.toml" +filename = "rules.toml" watch = true