mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #59 from minrk/state-outside
move state outside envs
This commit is contained in:
@@ -76,6 +76,13 @@ def install_miniconda(installer_path, prefix):
|
||||
'-u', '-b',
|
||||
'-p', prefix
|
||||
], 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):
|
||||
|
||||
206
integration-tests/test_install.py
Normal file
206
integration-tests/test_install.py
Normal 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),
|
||||
)
|
||||
@@ -1,18 +1,20 @@
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import tljh.systemd as systemd
|
||||
import tljh.conda as conda
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen, URLError
|
||||
from tljh import user
|
||||
import secrets
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from tljh import conda, systemd, user
|
||||
|
||||
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
|
||||
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
|
||||
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__))
|
||||
|
||||
@@ -38,16 +40,16 @@ def ensure_jupyterhub_service(prefix):
|
||||
systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params))
|
||||
systemd.reload_daemon()
|
||||
|
||||
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
||||
|
||||
# Set up proxy / hub secret oken if it is not already setup
|
||||
# FIXME: Check umask here properly
|
||||
proxy_secret_path = os.path.join(INSTALL_PREFIX, 'configurable-http-proxy.secret')
|
||||
proxy_secret_path = os.path.join(STATE_DIR, 'configurable-http-proxy.secret')
|
||||
if not os.path.exists(proxy_secret_path):
|
||||
with open(proxy_secret_path, 'w') as f:
|
||||
f.write('CONFIGPROXY_AUTH_TOKEN=' + secrets.token_hex(32))
|
||||
# If we are changing CONFIGPROXY_AUTH_TOKEN, restart 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
|
||||
systemd.start_service('configurable-http-proxy')
|
||||
# If JupyterHub is running, we want to restart it.
|
||||
@@ -192,6 +194,9 @@ def main():
|
||||
|
||||
ensure_usergroups()
|
||||
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...")
|
||||
ensure_jupyterhub_package(HUB_ENV_PREFIX)
|
||||
|
||||
@@ -14,7 +14,7 @@ PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=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
|
||||
Environment=PATH=$PATH:{install_prefix}/hub/bin
|
||||
ExecStart={install_prefix}/hub/bin/configurable-http-proxy \
|
||||
|
||||
@@ -10,14 +10,14 @@ User=root
|
||||
Restart=always
|
||||
# jupyterhub process should have no access to home directories
|
||||
ProtectHome=tmpfs
|
||||
WorkingDirectory={install_prefix}/hub/state
|
||||
WorkingDirectory={install_prefix}/state
|
||||
# Protect bits that are normally shared across the system
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
# 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}
|
||||
ExecStart={python_interpreter_path} -m jupyterhub.app -f {jupyterhub_config_path}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user