Merge pull request #59 from minrk/state-outside

move state outside envs
This commit is contained in:
Yuvi Panda
2018-07-19 10:33:24 -07:00
committed by GitHub
5 changed files with 231 additions and 13 deletions

View File

@@ -76,6 +76,13 @@ def install_miniconda(installer_path, prefix):
'-u', '-b', '-u', '-b',
'-p', prefix '-p', prefix
], stderr=subprocess.STDOUT) ], stderr=subprocess.STDOUT)
# fix permissions on initial install
# a few files have the wrong ownership and permissions initially
# when the installer is run as root
subprocess.check_call(
["chown", "-R", "{}:{}".format(os.getuid(), os.getgid()), prefix]
)
subprocess.check_call(["chmod", "-R", "o-w", prefix])
def pip_install(prefix, packages, editable=False): def pip_install(prefix, packages, editable=False):

View File

@@ -0,0 +1,206 @@
from contextlib import contextmanager
from concurrent.futures import ProcessPoolExecutor
from functools import partial
import grp
import os
import pwd
import subprocess
import sys
import pytest
ADMIN_GROUP = "jupyterhub-admins"
USER_GROUP = "jupyterhub-users"
INSTALL_PREFIX = os.environ.get("TLJH_INSTALL_PREFIX", "/opt/tljh")
HUB_PREFIX = os.path.join(INSTALL_PREFIX, "hub")
USER_PREFIX = os.path.join(INSTALL_PREFIX, "user")
STATE_DIR = os.path.join(INSTALL_PREFIX, "state")
@contextmanager
def noop():
"""no-op context manager
for parametrized tests
"""
yield
def setgroup(group):
"""Become the user nobody:group
Only call in a subprocess because there's no turning back
"""
gid = grp.getgrnam(group).gr_gid
uid = pwd.getpwnam("nobody").pw_uid
os.setgid(gid)
os.setuid(uid)
os.environ["HOME"] = "/tmp/test-home-%i-%i" % (uid, gid)
@pytest.mark.parametrize("group", [ADMIN_GROUP, USER_GROUP])
def test_groups_exist(group):
"""Verify that groups exist"""
grp.getgrnam(group)
def permissions_test(group, path, *, readable=None, writable=None, dirs_only=False):
"""Run a permissions test on all files in a path path"""
# start a subprocess and become nobody:group in the process
pool = ProcessPoolExecutor(1)
pool.submit(setgroup, group)
def access(path, flag):
"""Run access test in subproccess as nobody:group"""
return pool.submit(os.access, path, flag).result()
total_tested = 0
failures = []
# walk the directory and check permissions
for root, dirs, files in os.walk(path):
to_test = dirs
if not dirs_only:
to_test += files
total_tested += len(to_test)
for name in to_test:
path = os.path.join(root, name)
if os.path.islink(path):
# skip links
continue
st = os.lstat(path)
try:
user = pwd.getpwuid(st.st_uid).pw_name
except KeyError:
# uid may not exist
user = st.st_uid
try:
groupname = grp.getgrgid(st.st_gid).gr_name
except KeyError:
# gid may not exist
groupname = st.st_gid
stat_str = "{perm:04o} {user} {group}".format(
perm=st.st_mode, user=user, group=groupname
)
# check if the path should be writable
if writable is not None:
if access(path, os.W_OK) != writable:
failures.append(
"{} {} should {}be writable by {}".format(
stat_str, path, "" if writable else "not ", group
)
)
# check if the path should be readable
if readable is not None:
if access(path, os.R_OK) != readable:
failures.append(
"{} {} should {}be readable by {}".format(
stat_str, path, "" if readable else "not ", group
)
)
# verify that we actually tested some files
# (path typos)
assert total_tested > 0, "No files to test in %r" % path
# raise a nice summary of the failures:
if failures:
if len(failures) > 50:
failures = failures[:32] + ["...%i total" % len(failures)]
assert False, "\n".join(failures)
@pytest.mark.xfail(reason="admin-write permissions is not implemented")
def test_admin_writable():
permissions_test(ADMIN_GROUP, sys.prefix, writable=True, dirs_only=True)
@pytest.mark.parametrize("group", [ADMIN_GROUP, USER_GROUP])
def test_user_env_readable(group):
# every file in user env should be readable by everyone
permissions_test(group, USER_PREFIX, readable=True)
def test_nothing_user_writable():
# nothing in the install directory should be writable by users
permissions_test(USER_GROUP, INSTALL_PREFIX, writable=False)
@pytest.mark.parametrize(
"group, readwrite", [(ADMIN_GROUP, False), (USER_GROUP, False)]
)
def test_state_permissions(group, readwrite):
state_dir = os.path.abspath(os.path.join(sys.prefix, os.pardir, "state"))
permissions_test(group, state_dir, writable=readwrite, readable=readwrite)
# FIXME: admin-group should have install permissions
@pytest.mark.parametrize(
"group, allowed",
[
(USER_GROUP, False),
pytest.param(
ADMIN_GROUP,
True,
marks=pytest.mark.xfail(reason="admin-permissions not implemented"),
),
],
)
def test_pip_install(group, allowed):
if allowed:
context = noop()
else:
context = pytest.raises(subprocess.CalledProcessError)
python = os.path.join(USER_PREFIX, "bin", "python")
with context:
subprocess.check_call(
[python, "-m", "pip", "install", "--ignore-installed", "--no-deps", "flit"],
preexec_fn=partial(setgroup, group),
)
if allowed:
subprocess.check_call(
[python, "-m", "pip", "uninstall", "-y", "flit"],
preexec_fn=partial(setgroup, group),
)
@pytest.mark.parametrize(
"group, allowed",
[
(USER_GROUP, False),
pytest.param(
ADMIN_GROUP,
True,
marks=pytest.mark.xfail(reason="admin-permissions not implemented"),
),
],
)
def test_pip_upgrade(group, allowed):
if allowed:
context = noop()
pytest.skip("admin-install permissions is not implemented")
else:
context = pytest.raises(subprocess.CalledProcessError)
python = os.path.join(USER_PREFIX, "bin", "python")
with context:
subprocess.check_call(
[
python,
"-m",
"pip",
"install",
"--ignore-installed",
"--no-deps",
"testpath==0.3.0",
],
preexec_fn=partial(setgroup, group),
)
if allowed:
subprocess.check_call(
[python, "-m", "pip", "install", "--upgrade", "testpath"],
preexec_fn=partial(setgroup, group),
)

View File

@@ -1,18 +1,20 @@
import sys import argparse
import os import os
import tljh.systemd as systemd import secrets
import tljh.conda as conda import subprocess
import sys
import time
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.request import urlopen, URLError from urllib.request import urlopen, URLError
from tljh import user
import secrets
import argparse
import time
from ruamel.yaml import YAML from ruamel.yaml import YAML
from tljh import conda, systemd, user
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh') INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub') HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user')
STATE_DIR = os.path.join(INSTALL_PREFIX, 'state')
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
@@ -38,16 +40,16 @@ def ensure_jupyterhub_service(prefix):
systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params)) systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params))
systemd.reload_daemon() systemd.reload_daemon()
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
# Set up proxy / hub secret oken if it is not already setup # Set up proxy / hub secret oken if it is not already setup
# FIXME: Check umask here properly proxy_secret_path = os.path.join(STATE_DIR, 'configurable-http-proxy.secret')
proxy_secret_path = os.path.join(INSTALL_PREFIX, 'configurable-http-proxy.secret')
if not os.path.exists(proxy_secret_path): if not os.path.exists(proxy_secret_path):
with open(proxy_secret_path, 'w') as f: with open(proxy_secret_path, 'w') as f:
f.write('CONFIGPROXY_AUTH_TOKEN=' + secrets.token_hex(32)) f.write('CONFIGPROXY_AUTH_TOKEN=' + secrets.token_hex(32))
# If we are changing CONFIGPROXY_AUTH_TOKEN, restart configurable-http-proxy! # If we are changing CONFIGPROXY_AUTH_TOKEN, restart configurable-http-proxy!
systemd.restart_service('configurable-http-proxy') systemd.restart_service('configurable-http-proxy')
os.makedirs(os.path.join(INSTALL_PREFIX, 'hub', 'state'), mode=0o700, exist_ok=True)
# Start CHP if it has already not been started # Start CHP if it has already not been started
systemd.start_service('configurable-http-proxy') systemd.start_service('configurable-http-proxy')
# If JupyterHub is running, we want to restart it. # If JupyterHub is running, we want to restart it.
@@ -192,6 +194,9 @@ def main():
ensure_usergroups() ensure_usergroups()
ensure_user_environment(args.user_requirements_txt_url) ensure_user_environment(args.user_requirements_txt_url)
# Weird setuptools issue creates a few world-writable metadata files.
# Fix it:
subprocess.check_call(["chmod", "-R", "o-w", os.path.join(HUB_ENV_PREFIX, "pkgs")])
print("Setting up JupyterHub...") print("Setting up JupyterHub...")
ensure_jupyterhub_package(HUB_ENV_PREFIX) ensure_jupyterhub_package(HUB_ENV_PREFIX)

View File

@@ -14,7 +14,7 @@ PrivateTmp=yes
PrivateDevices=yes PrivateDevices=yes
ProtectKernelTunables=yes ProtectKernelTunables=yes
ProtectKernelModules=yes ProtectKernelModules=yes
EnvironmentFile={install_prefix}/configurable-http-proxy.secret EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
# Set PATH so env can find correct node # Set PATH so env can find correct node
Environment=PATH=$PATH:{install_prefix}/hub/bin Environment=PATH=$PATH:{install_prefix}/hub/bin
ExecStart={install_prefix}/hub/bin/configurable-http-proxy \ ExecStart={install_prefix}/hub/bin/configurable-http-proxy \

View File

@@ -10,14 +10,14 @@ User=root
Restart=always Restart=always
# jupyterhub process should have no access to home directories # jupyterhub process should have no access to home directories
ProtectHome=tmpfs ProtectHome=tmpfs
WorkingDirectory={install_prefix}/hub/state WorkingDirectory={install_prefix}/state
# Protect bits that are normally shared across the system # Protect bits that are normally shared across the system
PrivateTmp=yes PrivateTmp=yes
PrivateDevices=yes PrivateDevices=yes
ProtectKernelTunables=yes ProtectKernelTunables=yes
ProtectKernelModules=yes ProtectKernelModules=yes
# Source CONFIGPROXY_AUTH_TOKEN from here! # Source CONFIGPROXY_AUTH_TOKEN from here!
EnvironmentFile={install_prefix}/configurable-http-proxy.secret EnvironmentFile={install_prefix}/state/configurable-http-proxy.secret
Environment=TLJH_INSTALL_PREFIX={install_prefix} Environment=TLJH_INSTALL_PREFIX={install_prefix}
ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path} ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path}