mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
pre-commit: run black with string normalization
This commit is contained in:
106
.github/integration-test.py
vendored
106
.github/integration-test.py
vendored
@@ -10,7 +10,7 @@ def build_systemd_image(image_name, source_path, build_args=None):
|
|||||||
|
|
||||||
Built image is tagged with image_name
|
Built image is tagged with image_name
|
||||||
"""
|
"""
|
||||||
cmd = ['docker', 'build', f'-t={image_name}', source_path]
|
cmd = ["docker", "build", f"-t={image_name}", source_path]
|
||||||
if build_args:
|
if build_args:
|
||||||
cmd.extend([f"--build-arg={ba}" for ba in build_args])
|
cmd.extend([f"--build-arg={ba}" for ba in build_args])
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
@@ -25,20 +25,20 @@ def run_systemd_image(image_name, container_name, bootstrap_pip_spec):
|
|||||||
Container named container_name will be started.
|
Container named container_name will be started.
|
||||||
"""
|
"""
|
||||||
cmd = [
|
cmd = [
|
||||||
'docker',
|
"docker",
|
||||||
'run',
|
"run",
|
||||||
'--privileged',
|
"--privileged",
|
||||||
'--mount=type=bind,source=/sys/fs/cgroup,target=/sys/fs/cgroup',
|
"--mount=type=bind,source=/sys/fs/cgroup,target=/sys/fs/cgroup",
|
||||||
'--detach',
|
"--detach",
|
||||||
f'--name={container_name}',
|
f"--name={container_name}",
|
||||||
# A bit less than 1GB to ensure TLJH runs on 1GB VMs.
|
# A bit less than 1GB to ensure TLJH runs on 1GB VMs.
|
||||||
# If this is changed all docs references to the required memory must be changed too.
|
# If this is changed all docs references to the required memory must be changed too.
|
||||||
'--memory=900m',
|
"--memory=900m",
|
||||||
]
|
]
|
||||||
|
|
||||||
if bootstrap_pip_spec:
|
if bootstrap_pip_spec:
|
||||||
cmd.append('-e')
|
cmd.append("-e")
|
||||||
cmd.append(f'TLJH_BOOTSTRAP_PIP_SPEC={bootstrap_pip_spec}')
|
cmd.append(f"TLJH_BOOTSTRAP_PIP_SPEC={bootstrap_pip_spec}")
|
||||||
|
|
||||||
cmd.append(image_name)
|
cmd.append(image_name)
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ def stop_container(container_name):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
['docker', 'inspect', container_name], stderr=subprocess.STDOUT
|
["docker", "inspect", container_name], stderr=subprocess.STDOUT
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
# No such container exists, nothing to do
|
# No such container exists, nothing to do
|
||||||
return
|
return
|
||||||
subprocess.check_call(['docker', 'rm', '-f', container_name])
|
subprocess.check_call(["docker", "rm", "-f", container_name])
|
||||||
|
|
||||||
|
|
||||||
def run_container_command(container_name, cmd):
|
def run_container_command(container_name, cmd):
|
||||||
@@ -72,7 +72,7 @@ def copy_to_container(container_name, src_path, dest_path):
|
|||||||
"""
|
"""
|
||||||
Copy files from src_path to dest_path inside container_name
|
Copy files from src_path to dest_path inside container_name
|
||||||
"""
|
"""
|
||||||
subprocess.check_call(['docker', 'cp', src_path, f'{container_name}:{dest_path}'])
|
subprocess.check_call(["docker", "cp", src_path, f"{container_name}:{dest_path}"])
|
||||||
|
|
||||||
|
|
||||||
def run_test(
|
def run_test(
|
||||||
@@ -86,38 +86,38 @@ def run_test(
|
|||||||
|
|
||||||
source_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
source_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
copy_to_container(test_name, os.path.join(source_path, 'bootstrap/.'), '/srv/src')
|
copy_to_container(test_name, os.path.join(source_path, "bootstrap/."), "/srv/src")
|
||||||
copy_to_container(
|
copy_to_container(
|
||||||
test_name, os.path.join(source_path, 'integration-tests/'), '/srv/src'
|
test_name, os.path.join(source_path, "integration-tests/"), "/srv/src"
|
||||||
)
|
)
|
||||||
|
|
||||||
# These logs can be very relevant to debug a container startup failure
|
# These logs can be very relevant to debug a container startup failure
|
||||||
print(f"--- Start of logs from the container: {test_name}")
|
print(f"--- Start of logs from the container: {test_name}")
|
||||||
print(subprocess.check_output(['docker', 'logs', test_name]).decode())
|
print(subprocess.check_output(["docker", "logs", test_name]).decode())
|
||||||
print(f"--- End of logs from the container: {test_name}")
|
print(f"--- End of logs from the container: {test_name}")
|
||||||
|
|
||||||
# Install TLJH from the default branch first to test upgrades
|
# Install TLJH from the default branch first to test upgrades
|
||||||
if upgrade:
|
if upgrade:
|
||||||
run_container_command(
|
run_container_command(
|
||||||
test_name, 'curl -L https://tljh.jupyter.org/bootstrap.py | python3 -'
|
test_name, "curl -L https://tljh.jupyter.org/bootstrap.py | python3 -"
|
||||||
)
|
)
|
||||||
|
|
||||||
run_container_command(test_name, f'python3 /srv/src/bootstrap.py {installer_args}')
|
run_container_command(test_name, f"python3 /srv/src/bootstrap.py {installer_args}")
|
||||||
|
|
||||||
# Install pkgs from requirements in hub's pip, where
|
# Install pkgs from requirements in hub's pip, where
|
||||||
# the bootstrap script installed the others
|
# the bootstrap script installed the others
|
||||||
run_container_command(
|
run_container_command(
|
||||||
test_name,
|
test_name,
|
||||||
'/opt/tljh/hub/bin/python3 -m pip install -r /srv/src/integration-tests/requirements.txt',
|
"/opt/tljh/hub/bin/python3 -m pip install -r /srv/src/integration-tests/requirements.txt",
|
||||||
)
|
)
|
||||||
run_container_command(
|
run_container_command(
|
||||||
test_name,
|
test_name,
|
||||||
# We abort pytest after two failures as a compromise between wanting to
|
# We abort pytest after two failures as a compromise between wanting to
|
||||||
# avoid a flood of logs while still understanding if multiple tests
|
# avoid a flood of logs while still understanding if multiple tests
|
||||||
# would fail.
|
# would fail.
|
||||||
'/opt/tljh/hub/bin/python3 -m pytest --verbose --maxfail=2 --color=yes --durations=10 --capture=no {}'.format(
|
"/opt/tljh/hub/bin/python3 -m pytest --verbose --maxfail=2 --color=yes --durations=10 --capture=no {}".format(
|
||||||
' '.join(
|
" ".join(
|
||||||
[os.path.join('/srv/src/integration-tests/', f) for f in test_files]
|
[os.path.join("/srv/src/integration-tests/", f) for f in test_files]
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -127,53 +127,53 @@ def show_logs(container_name):
|
|||||||
"""
|
"""
|
||||||
Print logs from inside container to stdout
|
Print logs from inside container to stdout
|
||||||
"""
|
"""
|
||||||
run_container_command(container_name, 'journalctl --no-pager')
|
run_container_command(container_name, "journalctl --no-pager")
|
||||||
run_container_command(
|
run_container_command(
|
||||||
container_name, 'systemctl --no-pager status jupyterhub traefik'
|
container_name, "systemctl --no-pager status jupyterhub traefik"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argparser = argparse.ArgumentParser()
|
argparser = argparse.ArgumentParser()
|
||||||
subparsers = argparser.add_subparsers(dest='action')
|
subparsers = argparser.add_subparsers(dest="action")
|
||||||
|
|
||||||
build_image_parser = subparsers.add_parser('build-image')
|
build_image_parser = subparsers.add_parser("build-image")
|
||||||
build_image_parser.add_argument(
|
build_image_parser.add_argument(
|
||||||
"--build-arg",
|
"--build-arg",
|
||||||
action="append",
|
action="append",
|
||||||
dest="build_args",
|
dest="build_args",
|
||||||
)
|
)
|
||||||
|
|
||||||
subparsers.add_parser('stop-container').add_argument('container_name')
|
subparsers.add_parser("stop-container").add_argument("container_name")
|
||||||
|
|
||||||
subparsers.add_parser('start-container').add_argument('container_name')
|
subparsers.add_parser("start-container").add_argument("container_name")
|
||||||
|
|
||||||
run_parser = subparsers.add_parser('run')
|
run_parser = subparsers.add_parser("run")
|
||||||
run_parser.add_argument('container_name')
|
run_parser.add_argument("container_name")
|
||||||
run_parser.add_argument('command')
|
run_parser.add_argument("command")
|
||||||
|
|
||||||
copy_parser = subparsers.add_parser('copy')
|
copy_parser = subparsers.add_parser("copy")
|
||||||
copy_parser.add_argument('container_name')
|
copy_parser.add_argument("container_name")
|
||||||
copy_parser.add_argument('src')
|
copy_parser.add_argument("src")
|
||||||
copy_parser.add_argument('dest')
|
copy_parser.add_argument("dest")
|
||||||
|
|
||||||
run_test_parser = subparsers.add_parser('run-test')
|
run_test_parser = subparsers.add_parser("run-test")
|
||||||
run_test_parser.add_argument('--installer-args', default='')
|
run_test_parser.add_argument("--installer-args", default="")
|
||||||
run_test_parser.add_argument('--upgrade', action='store_true')
|
run_test_parser.add_argument("--upgrade", action="store_true")
|
||||||
run_test_parser.add_argument(
|
run_test_parser.add_argument(
|
||||||
'--bootstrap-pip-spec', nargs='?', default="", type=str
|
"--bootstrap-pip-spec", nargs="?", default="", type=str
|
||||||
)
|
)
|
||||||
run_test_parser.add_argument('test_name')
|
run_test_parser.add_argument("test_name")
|
||||||
run_test_parser.add_argument('test_files', nargs='+')
|
run_test_parser.add_argument("test_files", nargs="+")
|
||||||
|
|
||||||
show_logs_parser = subparsers.add_parser('show-logs')
|
show_logs_parser = subparsers.add_parser("show-logs")
|
||||||
show_logs_parser.add_argument('container_name')
|
show_logs_parser.add_argument("container_name")
|
||||||
|
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
|
|
||||||
image_name = 'tljh-systemd'
|
image_name = "tljh-systemd"
|
||||||
|
|
||||||
if args.action == 'run-test':
|
if args.action == "run-test":
|
||||||
run_test(
|
run_test(
|
||||||
image_name,
|
image_name,
|
||||||
args.test_name,
|
args.test_name,
|
||||||
@@ -182,19 +182,19 @@ def main():
|
|||||||
args.upgrade,
|
args.upgrade,
|
||||||
args.installer_args,
|
args.installer_args,
|
||||||
)
|
)
|
||||||
elif args.action == 'show-logs':
|
elif args.action == "show-logs":
|
||||||
show_logs(args.container_name)
|
show_logs(args.container_name)
|
||||||
elif args.action == 'run':
|
elif args.action == "run":
|
||||||
run_container_command(args.container_name, args.command)
|
run_container_command(args.container_name, args.command)
|
||||||
elif args.action == 'copy':
|
elif args.action == "copy":
|
||||||
copy_to_container(args.container_name, args.src, args.dest)
|
copy_to_container(args.container_name, args.src, args.dest)
|
||||||
elif args.action == 'start-container':
|
elif args.action == "start-container":
|
||||||
run_systemd_image(image_name, args.container_name, args.bootstrap_pip_spec)
|
run_systemd_image(image_name, args.container_name, args.bootstrap_pip_spec)
|
||||||
elif args.action == 'stop-container':
|
elif args.action == "stop-container":
|
||||||
stop_container(args.container_name)
|
stop_container(args.container_name)
|
||||||
elif args.action == 'build-image':
|
elif args.action == "build-image":
|
||||||
build_systemd_image(image_name, 'integration-tests', args.build_args)
|
build_systemd_image(image_name, "integration-tests", args.build_args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -144,15 +144,15 @@ def run_subprocess(cmd, *args, **kwargs):
|
|||||||
In TLJH, this sends successful output to the installer log,
|
In TLJH, this sends successful output to the installer log,
|
||||||
and failed output directly to the user's screen
|
and failed output directly to the user's screen
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('tljh')
|
logger = logging.getLogger("tljh")
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
||||||
)
|
)
|
||||||
printable_command = ' '.join(cmd)
|
printable_command = " ".join(cmd)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
# Our process failed! Show output to the user
|
# Our process failed! Show output to the user
|
||||||
logger.error(
|
logger.error(
|
||||||
'Ran {command} with exit code {code}'.format(
|
"Ran {command} with exit code {code}".format(
|
||||||
command=printable_command, code=proc.returncode
|
command=printable_command, code=proc.returncode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -161,7 +161,7 @@ def run_subprocess(cmd, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
# This goes into installer.log
|
# This goes into installer.log
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Ran {command} with exit code {code}'.format(
|
"Ran {command} with exit code {code}".format(
|
||||||
command=printable_command, code=proc.returncode
|
command=printable_command, code=proc.returncode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -197,13 +197,13 @@ def ensure_host_system_can_install_tljh():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Require Ubuntu 18.04+
|
# Require Ubuntu 18.04+
|
||||||
distro = get_os_release_variable('ID')
|
distro = get_os_release_variable("ID")
|
||||||
version = float(get_os_release_variable('VERSION_ID'))
|
version = float(get_os_release_variable("VERSION_ID"))
|
||||||
if distro != 'ubuntu':
|
if distro != "ubuntu":
|
||||||
print('The Littlest JupyterHub currently supports Ubuntu Linux only')
|
print("The Littlest JupyterHub currently supports Ubuntu Linux only")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif float(version) < 18.04:
|
elif float(version) < 18.04:
|
||||||
print('The Littlest JupyterHub requires Ubuntu 18.04 or higher')
|
print("The Littlest JupyterHub requires Ubuntu 18.04 or higher")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Require Python 3.6+
|
# Require Python 3.6+
|
||||||
@@ -212,10 +212,10 @@ def ensure_host_system_can_install_tljh():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Require systemd (systemctl is a part of systemd)
|
# Require systemd (systemctl is a part of systemd)
|
||||||
if not shutil.which('systemd') or not shutil.which('systemctl'):
|
if not shutil.which("systemd") or not shutil.which("systemctl"):
|
||||||
print("Systemd is required to run TLJH")
|
print("Systemd is required to run TLJH")
|
||||||
# Provide additional information about running in docker containers
|
# Provide additional information about running in docker containers
|
||||||
if os.path.exists('/.dockerenv'):
|
if os.path.exists("/.dockerenv"):
|
||||||
print("Running inside a docker container without systemd isn't supported")
|
print("Running inside a docker container without systemd isn't supported")
|
||||||
print(
|
print(
|
||||||
"We recommend against running a production TLJH instance inside a docker container"
|
"We recommend against running a production TLJH instance inside a docker container"
|
||||||
@@ -233,9 +233,9 @@ class ProgressPageRequestHandler(SimpleHTTPRequestHandler):
|
|||||||
logs = log_file.read()
|
logs = log_file.read()
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'text/plain; charset=utf-8')
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(logs.encode('utf-8'))
|
self.wfile.write(logs.encode("utf-8"))
|
||||||
elif self.path == "/index.html":
|
elif self.path == "/index.html":
|
||||||
self.path = "/var/run/index.html"
|
self.path = "/var/run/index.html"
|
||||||
return SimpleHTTPRequestHandler.do_GET(self)
|
return SimpleHTTPRequestHandler.do_GET(self)
|
||||||
@@ -244,7 +244,7 @@ class ProgressPageRequestHandler(SimpleHTTPRequestHandler):
|
|||||||
return SimpleHTTPRequestHandler.do_GET(self)
|
return SimpleHTTPRequestHandler.do_GET(self)
|
||||||
elif self.path == "/":
|
elif self.path == "/":
|
||||||
self.send_response(302)
|
self.send_response(302)
|
||||||
self.send_header('Location', '/index.html')
|
self.send_header("Location", "/index.html")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
else:
|
else:
|
||||||
SimpleHTTPRequestHandler.send_error(self, code=403)
|
SimpleHTTPRequestHandler.send_error(self, code=403)
|
||||||
@@ -262,10 +262,10 @@ def main():
|
|||||||
ensure_host_system_can_install_tljh()
|
ensure_host_system_can_install_tljh()
|
||||||
|
|
||||||
# Various related constants
|
# Various related constants
|
||||||
install_prefix = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
|
install_prefix = os.environ.get("TLJH_INSTALL_PREFIX", "/opt/tljh")
|
||||||
hub_prefix = os.path.join(install_prefix, 'hub')
|
hub_prefix = os.path.join(install_prefix, "hub")
|
||||||
python_bin = os.path.join(hub_prefix, 'bin', 'python3')
|
python_bin = os.path.join(hub_prefix, "bin", "python3")
|
||||||
pip_bin = os.path.join(hub_prefix, 'bin', 'pip')
|
pip_bin = os.path.join(hub_prefix, "bin", "pip")
|
||||||
initial_setup = not os.path.exists(python_bin)
|
initial_setup = not os.path.exists(python_bin)
|
||||||
|
|
||||||
# Attempt to start a web server to serve a progress page reporting
|
# Attempt to start a web server to serve a progress page reporting
|
||||||
@@ -306,28 +306,28 @@ def main():
|
|||||||
|
|
||||||
# Set up logging to print to a file and to stderr
|
# Set up logging to print to a file and to stderr
|
||||||
os.makedirs(install_prefix, exist_ok=True)
|
os.makedirs(install_prefix, exist_ok=True)
|
||||||
file_logger_path = os.path.join(install_prefix, 'installer.log')
|
file_logger_path = os.path.join(install_prefix, "installer.log")
|
||||||
file_logger = logging.FileHandler(file_logger_path)
|
file_logger = logging.FileHandler(file_logger_path)
|
||||||
# installer.log should be readable only by root
|
# installer.log should be readable only by root
|
||||||
os.chmod(file_logger_path, 0o500)
|
os.chmod(file_logger_path, 0o500)
|
||||||
|
|
||||||
file_logger.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
|
file_logger.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||||
file_logger.setLevel(logging.DEBUG)
|
file_logger.setLevel(logging.DEBUG)
|
||||||
logger.addHandler(file_logger)
|
logger.addHandler(file_logger)
|
||||||
|
|
||||||
stderr_logger = logging.StreamHandler()
|
stderr_logger = logging.StreamHandler()
|
||||||
stderr_logger.setFormatter(logging.Formatter('%(message)s'))
|
stderr_logger.setFormatter(logging.Formatter("%(message)s"))
|
||||||
stderr_logger.setLevel(logging.INFO)
|
stderr_logger.setLevel(logging.INFO)
|
||||||
logger.addHandler(stderr_logger)
|
logger.addHandler(stderr_logger)
|
||||||
|
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
if not initial_setup:
|
if not initial_setup:
|
||||||
logger.info('Existing TLJH installation detected, upgrading...')
|
logger.info("Existing TLJH installation detected, upgrading...")
|
||||||
else:
|
else:
|
||||||
logger.info('Existing TLJH installation not detected, installing...')
|
logger.info("Existing TLJH installation not detected, installing...")
|
||||||
logger.info('Setting up hub environment...')
|
logger.info("Setting up hub environment...")
|
||||||
logger.info('Installing Python, venv, pip, and git via apt-get...')
|
logger.info("Installing Python, venv, pip, and git via apt-get...")
|
||||||
|
|
||||||
# In some very minimal base VM images, it looks like the "universe" apt
|
# In some very minimal base VM images, it looks like the "universe" apt
|
||||||
# package repository is disabled by default, causing bootstrapping to
|
# package repository is disabled by default, causing bootstrapping to
|
||||||
@@ -340,56 +340,56 @@ def main():
|
|||||||
#
|
#
|
||||||
apt_get_adjusted_env = os.environ.copy()
|
apt_get_adjusted_env = os.environ.copy()
|
||||||
apt_get_adjusted_env["DEBIAN_FRONTEND"] = "noninteractive"
|
apt_get_adjusted_env["DEBIAN_FRONTEND"] = "noninteractive"
|
||||||
run_subprocess(['apt-get', 'update'])
|
run_subprocess(["apt-get", "update"])
|
||||||
run_subprocess(
|
run_subprocess(
|
||||||
['apt-get', 'install', '--yes', 'software-properties-common'],
|
["apt-get", "install", "--yes", "software-properties-common"],
|
||||||
env=apt_get_adjusted_env,
|
env=apt_get_adjusted_env,
|
||||||
)
|
)
|
||||||
run_subprocess(['add-apt-repository', 'universe', '--yes'])
|
run_subprocess(["add-apt-repository", "universe", "--yes"])
|
||||||
run_subprocess(['apt-get', 'update'])
|
run_subprocess(["apt-get", "update"])
|
||||||
run_subprocess(
|
run_subprocess(
|
||||||
[
|
[
|
||||||
'apt-get',
|
"apt-get",
|
||||||
'install',
|
"install",
|
||||||
'--yes',
|
"--yes",
|
||||||
'python3',
|
"python3",
|
||||||
'python3-venv',
|
"python3-venv",
|
||||||
'python3-pip',
|
"python3-pip",
|
||||||
'git',
|
"git",
|
||||||
],
|
],
|
||||||
env=apt_get_adjusted_env,
|
env=apt_get_adjusted_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info('Setting up virtual environment at {}'.format(hub_prefix))
|
logger.info("Setting up virtual environment at {}".format(hub_prefix))
|
||||||
os.makedirs(hub_prefix, exist_ok=True)
|
os.makedirs(hub_prefix, exist_ok=True)
|
||||||
run_subprocess(['python3', '-m', 'venv', hub_prefix])
|
run_subprocess(["python3", "-m", "venv", hub_prefix])
|
||||||
|
|
||||||
# Upgrade pip
|
# Upgrade pip
|
||||||
# Keep pip version pinning in sync with the one in unit-test.yml!
|
# Keep pip version pinning in sync with the one in unit-test.yml!
|
||||||
# See changelog at https://pip.pypa.io/en/latest/news/#changelog
|
# See changelog at https://pip.pypa.io/en/latest/news/#changelog
|
||||||
logger.info('Upgrading pip...')
|
logger.info("Upgrading pip...")
|
||||||
run_subprocess([pip_bin, 'install', '--upgrade', 'pip==21.3.*'])
|
run_subprocess([pip_bin, "install", "--upgrade", "pip==21.3.*"])
|
||||||
|
|
||||||
# Install/upgrade TLJH installer
|
# Install/upgrade TLJH installer
|
||||||
tljh_install_cmd = [pip_bin, 'install', '--upgrade']
|
tljh_install_cmd = [pip_bin, "install", "--upgrade"]
|
||||||
if os.environ.get('TLJH_BOOTSTRAP_DEV', 'no') == 'yes':
|
if os.environ.get("TLJH_BOOTSTRAP_DEV", "no") == "yes":
|
||||||
tljh_install_cmd.append('--editable')
|
tljh_install_cmd.append("--editable")
|
||||||
tljh_install_cmd.append(
|
tljh_install_cmd.append(
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
'TLJH_BOOTSTRAP_PIP_SPEC',
|
"TLJH_BOOTSTRAP_PIP_SPEC",
|
||||||
'git+https://github.com/jupyterhub/the-littlest-jupyterhub.git',
|
"git+https://github.com/jupyterhub/the-littlest-jupyterhub.git",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if initial_setup:
|
if initial_setup:
|
||||||
logger.info('Installing TLJH installer...')
|
logger.info("Installing TLJH installer...")
|
||||||
else:
|
else:
|
||||||
logger.info('Upgrading TLJH installer...')
|
logger.info("Upgrading TLJH installer...")
|
||||||
run_subprocess(tljh_install_cmd)
|
run_subprocess(tljh_install_cmd)
|
||||||
|
|
||||||
# Run TLJH installer
|
# Run TLJH installer
|
||||||
logger.info('Running TLJH installer...')
|
logger.info("Running TLJH installer...")
|
||||||
os.execv(python_bin, [python_bin, '-m', 'tljh.installer'] + tljh_installer_flags)
|
os.execv(python_bin, [python_bin, "-m", "tljh.installer"] + tljh_installer_flags)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
40
docs/conf.py
40
docs/conf.py
@@ -1,21 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
source_suffix = ['.rst']
|
source_suffix = [".rst"]
|
||||||
|
|
||||||
project = 'The Littlest JupyterHub'
|
project = "The Littlest JupyterHub"
|
||||||
copyright = '2018, JupyterHub Team'
|
copyright = "2018, JupyterHub Team"
|
||||||
author = 'JupyterHub Team'
|
author = "JupyterHub Team"
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
version = ''
|
version = ""
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = 'v0.1'
|
release = "v0.1"
|
||||||
|
|
||||||
# Enable MathJax for Math
|
# Enable MathJax for Math
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.mathjax',
|
"sphinx.ext.mathjax",
|
||||||
'sphinx.ext.intersphinx',
|
"sphinx.ext.intersphinx",
|
||||||
'sphinx_copybutton',
|
"sphinx_copybutton",
|
||||||
]
|
]
|
||||||
|
|
||||||
# The root toctree document.
|
# The root toctree document.
|
||||||
@@ -25,30 +25,30 @@ root_doc = master_doc = "index"
|
|||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path .
|
# This pattern also affects html_static_path and html_extra_path .
|
||||||
exclude_patterns = [
|
exclude_patterns = [
|
||||||
'_build',
|
"_build",
|
||||||
'Thumbs.db',
|
"Thumbs.db",
|
||||||
'.DS_Store',
|
".DS_Store",
|
||||||
'install/custom.rst',
|
"install/custom.rst",
|
||||||
]
|
]
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'sphinx': ('http://www.sphinx-doc.org/en/master/', None),
|
"sphinx": ("http://www.sphinx-doc.org/en/master/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
intersphinx_cache_limit = 90 # days
|
intersphinx_cache_limit = 90 # days
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = "sphinx"
|
||||||
|
|
||||||
html_theme = 'pydata_sphinx_theme'
|
html_theme = "pydata_sphinx_theme"
|
||||||
|
|
||||||
html_logo = 'images/logo/logo.png'
|
html_logo = "images/logo/logo.png"
|
||||||
html_favicon = 'images/logo/favicon.ico'
|
html_favicon = "images/logo/favicon.ico"
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
# Do this only if _static exists, otherwise this will error
|
# Do this only if _static exists, otherwise this will error
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
if os.path.exists(os.path.join(here, '_static')):
|
if os.path.exists(os.path.join(here, "_static")):
|
||||||
html_static_path = ['_static']
|
html_static_path = ["_static"]
|
||||||
|
|||||||
@@ -7,49 +7,49 @@ from tljh.hooks import hookimpl
|
|||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_extra_user_conda_packages():
|
def tljh_extra_user_conda_packages():
|
||||||
return [
|
return [
|
||||||
'hypothesis',
|
"hypothesis",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_extra_user_pip_packages():
|
def tljh_extra_user_pip_packages():
|
||||||
return [
|
return [
|
||||||
'django',
|
"django",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_extra_hub_pip_packages():
|
def tljh_extra_hub_pip_packages():
|
||||||
return [
|
return [
|
||||||
'there',
|
"there",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_extra_apt_packages():
|
def tljh_extra_apt_packages():
|
||||||
return [
|
return [
|
||||||
'sl',
|
"sl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_config_post_install(config):
|
def tljh_config_post_install(config):
|
||||||
# Put an arbitrary marker we can test for
|
# Put an arbitrary marker we can test for
|
||||||
config['simplest_plugin'] = {'present': True}
|
config["simplest_plugin"] = {"present": True}
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_custom_jupyterhub_config(c):
|
def tljh_custom_jupyterhub_config(c):
|
||||||
c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'
|
c.JupyterHub.authenticator_class = "tmpauthenticator.TmpAuthenticator"
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_post_install():
|
def tljh_post_install():
|
||||||
with open('test_post_install', 'w') as f:
|
with open("test_post_install", "w") as f:
|
||||||
f.write('123456789')
|
f.write("123456789")
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def tljh_new_user_create(username):
|
def tljh_new_user_create(username):
|
||||||
with open('test_new_user_create', 'w') as f:
|
with open("test_new_user_create", "w") as f:
|
||||||
f.write(username)
|
f.write(username)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async def test_admin_login():
|
|||||||
Test if the admin that was added during install can login with
|
Test if the admin that was added during install can login with
|
||||||
the password provided.
|
the password provided.
|
||||||
"""
|
"""
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = "admin"
|
username = "admin"
|
||||||
password = "admin"
|
password = "admin"
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ async def test_unsuccessful_login(username, password):
|
|||||||
"""
|
"""
|
||||||
Ensure nobody but the admin that was added during install can login
|
Ensure nobody but the admin that was added during install can login
|
||||||
"""
|
"""
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
user_logged_in = await u.login()
|
user_logged_in = await u.login()
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ def test_serverextensions():
|
|||||||
"""
|
"""
|
||||||
# jupyter-serverextension writes to stdout and stderr weirdly
|
# jupyter-serverextension writes to stdout and stderr weirdly
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
['/opt/tljh/user/bin/jupyter-serverextension', 'list', '--sys-prefix'],
|
["/opt/tljh/user/bin/jupyter-serverextension", "list", "--sys-prefix"],
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'jupyterlab 3.',
|
"jupyterlab 3.",
|
||||||
'nbgitpuller 1.',
|
"nbgitpuller 1.",
|
||||||
'nteract_on_jupyter 2.1.',
|
"nteract_on_jupyter 2.1.",
|
||||||
'jupyter_resource_usage',
|
"jupyter_resource_usage",
|
||||||
]
|
]
|
||||||
|
|
||||||
for e in extensions:
|
for e in extensions:
|
||||||
@@ -28,21 +28,21 @@ def test_nbextensions():
|
|||||||
"""
|
"""
|
||||||
# jupyter-nbextension writes to stdout and stderr weirdly
|
# jupyter-nbextension writes to stdout and stderr weirdly
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
['/opt/tljh/user/bin/jupyter-nbextension', 'list', '--sys-prefix'],
|
["/opt/tljh/user/bin/jupyter-nbextension", "list", "--sys-prefix"],
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'jupyter_resource_usage/main',
|
"jupyter_resource_usage/main",
|
||||||
# This is what ipywidgets nbextension is called
|
# This is what ipywidgets nbextension is called
|
||||||
'jupyter-js-widgets/extension',
|
"jupyter-js-widgets/extension",
|
||||||
]
|
]
|
||||||
|
|
||||||
for e in extensions:
|
for e in extensions:
|
||||||
assert f'{e} \x1b[32m enabled \x1b[0m' in proc.stdout.decode()
|
assert f"{e} \x1b[32m enabled \x1b[0m" in proc.stdout.decode()
|
||||||
|
|
||||||
# Ensure we have 'OK' messages in our stdout, to make sure everything is importable
|
# Ensure we have 'OK' messages in our stdout, to make sure everything is importable
|
||||||
assert proc.stderr.decode() == ' - Validating: \x1b[32mOK\x1b[0m\n' * len(
|
assert proc.stderr.decode() == " - Validating: \x1b[32mOK\x1b[0m\n" * len(
|
||||||
extensions
|
extensions
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ from tljh.normalize import generate_system_username
|
|||||||
|
|
||||||
# Use sudo to invoke it, since this is how users invoke it.
|
# Use sudo to invoke it, since this is how users invoke it.
|
||||||
# This catches issues with PATH
|
# This catches issues with PATH
|
||||||
TLJH_CONFIG_PATH = ['sudo', 'tljh-config']
|
TLJH_CONFIG_PATH = ["sudo", "tljh-config"]
|
||||||
|
|
||||||
|
|
||||||
def test_hub_up():
|
def test_hub_up():
|
||||||
r = requests.get('http://127.0.0.1')
|
r = requests.get("http://127.0.0.1")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
@@ -30,32 +30,32 @@ async def test_user_code_execute():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
await u.start_kernel()
|
await u.start_kernel()
|
||||||
await u.assert_code_output("5 * 4", "20", 5, 5)
|
await u.assert_code_output("5 * 4", "20", 5, 5)
|
||||||
|
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
assert pwd.getpwnam(f'jupyter-{username}') is not None
|
assert pwd.getpwnam(f"jupyter-{username}") is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -73,7 +73,7 @@ async def test_user_server_started_with_custom_base_url():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -81,18 +81,18 @@ async def test_user_server_started_with_custom_base_url():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'base_url', base_url
|
*TLJH_CONFIG_PATH, "set", "base_url", base_url
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@ async def test_user_server_started_with_custom_base_url():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'unset', 'base_url'
|
*TLJH_CONFIG_PATH, "unset", "base_url"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,14 +120,14 @@ async def test_user_admin_add():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -135,26 +135,26 @@ async def test_user_admin_add():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'add-item', 'users.admin', username
|
*TLJH_CONFIG_PATH, "add-item", "users.admin", username
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
assert pwd.getpwnam(f'jupyter-{username}') is not None
|
assert pwd.getpwnam(f"jupyter-{username}") is not None
|
||||||
|
|
||||||
# Assert that the user has admin rights
|
# Assert that the user has admin rights
|
||||||
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
|
assert f"jupyter-{username}" in grp.getgrnam("jupyterhub-admins").gr_mem
|
||||||
|
|
||||||
|
|
||||||
# FIXME: Make this test pass
|
# FIXME: Make this test pass
|
||||||
@@ -168,14 +168,14 @@ async def test_user_admin_remove():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -183,39 +183,39 @@ async def test_user_admin_remove():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'add-item', 'users.admin', username
|
*TLJH_CONFIG_PATH, "add-item", "users.admin", username
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
assert pwd.getpwnam(f'jupyter-{username}') is not None
|
assert pwd.getpwnam(f"jupyter-{username}") is not None
|
||||||
|
|
||||||
# Assert that the user has admin rights
|
# Assert that the user has admin rights
|
||||||
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
|
assert f"jupyter-{username}" in grp.getgrnam("jupyterhub-admins").gr_mem
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'remove-item', 'users.admin', username
|
*TLJH_CONFIG_PATH, "remove-item", "users.admin", username
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ async def test_user_admin_remove():
|
|||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
# Assert that the user does *not* have admin rights
|
# Assert that the user does *not* have admin rights
|
||||||
assert f'jupyter-{username}' not in grp.getgrnam('jupyterhub-admins').gr_mem
|
assert f"jupyter-{username}" not in grp.getgrnam("jupyterhub-admins").gr_mem
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -233,37 +233,37 @@ async def test_long_username():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(32)
|
username = secrets.token_hex(32)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
system_username = generate_system_username(f'jupyter-{username}')
|
system_username = generate_system_username(f"jupyter-{username}")
|
||||||
assert pwd.getpwnam(system_username) is not None
|
assert pwd.getpwnam(system_username) is not None
|
||||||
|
|
||||||
await u.stop_server()
|
await u.stop_server()
|
||||||
except:
|
except:
|
||||||
# If we have any errors, print jupyterhub logs before exiting
|
# If we have any errors, print jupyterhub logs before exiting
|
||||||
subprocess.check_call(['journalctl', '-u', 'jupyterhub', '--no-pager'])
|
subprocess.check_call(["journalctl", "-u", "jupyterhub", "--no-pager"])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -274,17 +274,17 @@ async def test_user_group_adding():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
groups = {"somegroup": [username]}
|
groups = {"somegroup": [username]}
|
||||||
# Create the group we want to add the user to
|
# Create the group we want to add the user to
|
||||||
system('groupadd somegroup')
|
system("groupadd somegroup")
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -293,8 +293,8 @@ async def test_user_group_adding():
|
|||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH,
|
*TLJH_CONFIG_PATH,
|
||||||
'add-item',
|
"add-item",
|
||||||
'users.extra_user_groups.somegroup',
|
"users.extra_user_groups.somegroup",
|
||||||
username,
|
username,
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
@@ -302,28 +302,28 @@ async def test_user_group_adding():
|
|||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
|
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
system_username = generate_system_username(f'jupyter-{username}')
|
system_username = generate_system_username(f"jupyter-{username}")
|
||||||
assert pwd.getpwnam(system_username) is not None
|
assert pwd.getpwnam(system_username) is not None
|
||||||
|
|
||||||
# Assert that the user was added to the specified group
|
# Assert that the user was added to the specified group
|
||||||
assert f'jupyter-{username}' in grp.getgrnam('somegroup').gr_mem
|
assert f"jupyter-{username}" in grp.getgrnam("somegroup").gr_mem
|
||||||
|
|
||||||
await u.stop_server()
|
await u.stop_server()
|
||||||
# Delete the group
|
# Delete the group
|
||||||
system('groupdel somegroup')
|
system("groupdel somegroup")
|
||||||
except:
|
except:
|
||||||
# If we have any errors, print jupyterhub logs before exiting
|
# If we have any errors, print jupyterhub logs before exiting
|
||||||
subprocess.check_call(['journalctl', '-u', 'jupyterhub', '--no-pager'])
|
subprocess.check_call(["journalctl", "-u", "jupyterhub", "--no-pager"])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -335,14 +335,14 @@ async def test_idle_server_culled():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -351,7 +351,7 @@ async def test_idle_server_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.every', "10"
|
*TLJH_CONFIG_PATH, "set", "services.cull.every", "10"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -360,7 +360,7 @@ async def test_idle_server_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.users', "True"
|
*TLJH_CONFIG_PATH, "set", "services.cull.users", "True"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -369,36 +369,36 @@ async def test_idle_server_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.max_age', "60"
|
*TLJH_CONFIG_PATH, "set", "services.cull.max_age", "60"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
# Start user's server
|
# Start user's server
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
assert pwd.getpwnam(f'jupyter-{username}') is not None
|
assert pwd.getpwnam(f"jupyter-{username}") is not None
|
||||||
|
|
||||||
# Check that we can get to the user's server
|
# Check that we can get to the user's server
|
||||||
r = await u.session.get(
|
r = await u.session.get(
|
||||||
u.hub_url / 'hub/api/users' / username,
|
u.hub_url / "hub/api/users" / username,
|
||||||
headers={'Referer': str(u.hub_url / 'hub/')},
|
headers={"Referer": str(u.hub_url / "hub/")},
|
||||||
)
|
)
|
||||||
assert r.status == 200
|
assert r.status == 200
|
||||||
|
|
||||||
async def _check_culling_done():
|
async def _check_culling_done():
|
||||||
# Check that after 60s, the user and server have been culled and are not reacheable anymore
|
# Check that after 60s, the user and server have been culled and are not reacheable anymore
|
||||||
r = await u.session.get(
|
r = await u.session.get(
|
||||||
u.hub_url / 'hub/api/users' / username,
|
u.hub_url / "hub/api/users" / username,
|
||||||
headers={'Referer': str(u.hub_url / 'hub/')},
|
headers={"Referer": str(u.hub_url / "hub/")},
|
||||||
)
|
)
|
||||||
print(r.status)
|
print(r.status)
|
||||||
return r.status == 403
|
return r.status == 403
|
||||||
@@ -418,14 +418,14 @@ async def test_active_server_not_culled():
|
|||||||
"""
|
"""
|
||||||
# This *must* be localhost, not an IP
|
# This *must* be localhost, not an IP
|
||||||
# aiohttp throws away cookies if we are connecting to an IP!
|
# aiohttp throws away cookies if we are connecting to an IP!
|
||||||
hub_url = 'http://localhost'
|
hub_url = "http://localhost"
|
||||||
username = secrets.token_hex(8)
|
username = secrets.token_hex(8)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummy'
|
*TLJH_CONFIG_PATH, "set", "auth.type", "dummy"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -434,7 +434,7 @@ async def test_active_server_not_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.every', "10"
|
*TLJH_CONFIG_PATH, "set", "services.cull.every", "10"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -443,7 +443,7 @@ async def test_active_server_not_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.users', "True"
|
*TLJH_CONFIG_PATH, "set", "services.cull.users", "True"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
@@ -452,36 +452,36 @@ async def test_active_server_not_culled():
|
|||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(
|
await asyncio.create_subprocess_exec(
|
||||||
*TLJH_CONFIG_PATH, 'set', 'services.cull.max_age', "60"
|
*TLJH_CONFIG_PATH, "set", "services.cull.max_age", "60"
|
||||||
)
|
)
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
0
|
0
|
||||||
== await (
|
== await (
|
||||||
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')
|
await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, "reload")
|
||||||
).wait()
|
).wait()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with User(username, hub_url, partial(login_dummy, password='')) as u:
|
async with User(username, hub_url, partial(login_dummy, password="")) as u:
|
||||||
await u.login()
|
await u.login()
|
||||||
# Start user's server
|
# Start user's server
|
||||||
await u.ensure_server_simulate()
|
await u.ensure_server_simulate()
|
||||||
# Assert that the user exists
|
# Assert that the user exists
|
||||||
assert pwd.getpwnam(f'jupyter-{username}') is not None
|
assert pwd.getpwnam(f"jupyter-{username}") is not None
|
||||||
|
|
||||||
# Check that we can get to the user's server
|
# Check that we can get to the user's server
|
||||||
r = await u.session.get(
|
r = await u.session.get(
|
||||||
u.hub_url / 'hub/api/users' / username,
|
u.hub_url / "hub/api/users" / username,
|
||||||
headers={'Referer': str(u.hub_url / 'hub/')},
|
headers={"Referer": str(u.hub_url / "hub/")},
|
||||||
)
|
)
|
||||||
assert r.status == 200
|
assert r.status == 200
|
||||||
|
|
||||||
async def _check_culling_done():
|
async def _check_culling_done():
|
||||||
# Check that after 30s, we can still reach the user's server
|
# Check that after 30s, we can still reach the user's server
|
||||||
r = await u.session.get(
|
r = await u.session.get(
|
||||||
u.hub_url / 'hub/api/users' / username,
|
u.hub_url / "hub/api/users" / username,
|
||||||
headers={'Referer': str(u.hub_url / 'hub/')},
|
headers={"Referer": str(u.hub_url / "hub/")},
|
||||||
)
|
)
|
||||||
print(r.status)
|
print(r.status)
|
||||||
return r.status != 200
|
return r.status != 200
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ def test_admin_writable():
|
|||||||
|
|
||||||
def test_installer_log_readable():
|
def test_installer_log_readable():
|
||||||
# Test that installer.log is owned by root, and not readable by anyone else
|
# Test that installer.log is owned by root, and not readable by anyone else
|
||||||
file_stat = os.stat('/opt/tljh/installer.log')
|
file_stat = os.stat("/opt/tljh/installer.log")
|
||||||
assert file_stat.st_uid == 0
|
assert file_stat.st_uid == 0
|
||||||
assert file_stat.st_mode == 0o100500
|
assert file_stat.st_mode == 0o100500
|
||||||
|
|
||||||
@@ -234,4 +234,4 @@ def test_symlinks():
|
|||||||
"""
|
"""
|
||||||
Test we symlink tljh-config to /usr/local/bin
|
Test we symlink tljh-config to /usr/local/bin
|
||||||
"""
|
"""
|
||||||
assert os.path.exists('/usr/bin/tljh-config')
|
assert os.path.exists("/usr/bin/tljh-config")
|
||||||
|
|||||||
@@ -9,30 +9,30 @@ import subprocess
|
|||||||
from tljh.config import CONFIG_FILE, USER_ENV_PREFIX, HUB_ENV_PREFIX
|
from tljh.config import CONFIG_FILE, USER_ENV_PREFIX, HUB_ENV_PREFIX
|
||||||
from tljh import user
|
from tljh import user
|
||||||
|
|
||||||
yaml = YAML(typ='rt')
|
yaml = YAML(typ="rt")
|
||||||
|
|
||||||
|
|
||||||
def test_apt_packages():
|
def test_apt_packages():
|
||||||
"""
|
"""
|
||||||
Test extra apt packages are installed
|
Test extra apt packages are installed
|
||||||
"""
|
"""
|
||||||
assert os.path.exists('/usr/games/sl')
|
assert os.path.exists("/usr/games/sl")
|
||||||
|
|
||||||
|
|
||||||
def test_pip_packages():
|
def test_pip_packages():
|
||||||
"""
|
"""
|
||||||
Test extra user & hub pip packages are installed
|
Test extra user & hub pip packages are installed
|
||||||
"""
|
"""
|
||||||
subprocess.check_call([f'{USER_ENV_PREFIX}/bin/python3', '-c', 'import django'])
|
subprocess.check_call([f"{USER_ENV_PREFIX}/bin/python3", "-c", "import django"])
|
||||||
|
|
||||||
subprocess.check_call([f'{HUB_ENV_PREFIX}/bin/python3', '-c', 'import there'])
|
subprocess.check_call([f"{HUB_ENV_PREFIX}/bin/python3", "-c", "import there"])
|
||||||
|
|
||||||
|
|
||||||
def test_conda_packages():
|
def test_conda_packages():
|
||||||
"""
|
"""
|
||||||
Test extra user conda packages are installed
|
Test extra user conda packages are installed
|
||||||
"""
|
"""
|
||||||
subprocess.check_call([f'{USER_ENV_PREFIX}/bin/python3', '-c', 'import hypothesis'])
|
subprocess.check_call([f"{USER_ENV_PREFIX}/bin/python3", "-c", "import hypothesis"])
|
||||||
|
|
||||||
|
|
||||||
def test_config_hook():
|
def test_config_hook():
|
||||||
@@ -42,16 +42,16 @@ def test_config_hook():
|
|||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
data = yaml.load(f)
|
data = yaml.load(f)
|
||||||
|
|
||||||
assert data['simplest_plugin']['present']
|
assert data["simplest_plugin"]["present"]
|
||||||
|
|
||||||
|
|
||||||
def test_jupyterhub_config_hook():
|
def test_jupyterhub_config_hook():
|
||||||
"""
|
"""
|
||||||
Test that tmpauthenticator is enabled by our custom config plugin
|
Test that tmpauthenticator is enabled by our custom config plugin
|
||||||
"""
|
"""
|
||||||
resp = requests.get('http://localhost/hub/tmplogin', allow_redirects=False)
|
resp = requests.get("http://localhost/hub/tmplogin", allow_redirects=False)
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert resp.headers['Location'] == '/hub/spawn'
|
assert resp.headers["Location"] == "/hub/spawn"
|
||||||
|
|
||||||
|
|
||||||
def test_post_install_hook():
|
def test_post_install_hook():
|
||||||
|
|||||||
34
setup.py
34
setup.py
@@ -1,28 +1,28 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='the-littlest-jupyterhub',
|
name="the-littlest-jupyterhub",
|
||||||
version='0.1',
|
version="0.1",
|
||||||
description='A small JupyterHub distribution',
|
description="A small JupyterHub distribution",
|
||||||
url='https://github.com/jupyterhub/the-littlest-jupyterhub',
|
url="https://github.com/jupyterhub/the-littlest-jupyterhub",
|
||||||
author='Jupyter Development Team',
|
author="Jupyter Development Team",
|
||||||
author_email='jupyter@googlegroups.com',
|
author_email="jupyter@googlegroups.com",
|
||||||
license='3 Clause BSD',
|
license="3 Clause BSD",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ruamel.yaml==0.17.*',
|
"ruamel.yaml==0.17.*",
|
||||||
'jinja2',
|
"jinja2",
|
||||||
'pluggy==1.*',
|
"pluggy==1.*",
|
||||||
'passlib',
|
"passlib",
|
||||||
'backoff',
|
"backoff",
|
||||||
'requests',
|
"requests",
|
||||||
'bcrypt',
|
"bcrypt",
|
||||||
'jupyterhub-traefik-proxy==0.3.*',
|
"jupyterhub-traefik-proxy==0.3.*",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'tljh-config = tljh.config:main',
|
"tljh-config = tljh.config:main",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def tljh_dir(tmpdir):
|
|||||||
reload(tljh)
|
reload(tljh)
|
||||||
for name in dir(tljh):
|
for name in dir(tljh):
|
||||||
mod = getattr(tljh, name)
|
mod = getattr(tljh, name)
|
||||||
if isinstance(mod, types.ModuleType) and mod.__name__.startswith('tljh.'):
|
if isinstance(mod, types.ModuleType) and mod.__name__.startswith("tljh."):
|
||||||
reload(mod)
|
reload(mod)
|
||||||
assert tljh.config.INSTALL_PREFIX == tljh_dir
|
assert tljh.config.INSTALL_PREFIX == tljh_dir
|
||||||
os.makedirs(tljh.config.STATE_DIR)
|
os.makedirs(tljh.config.STATE_DIR)
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope="module")
|
||||||
def prefix():
|
def prefix():
|
||||||
"""
|
"""
|
||||||
Provide a temporary directory with a mambaforge conda environment
|
Provide a temporary directory with a mambaforge conda environment
|
||||||
"""
|
"""
|
||||||
# see https://github.com/conda-forge/miniforge/releases
|
# see https://github.com/conda-forge/miniforge/releases
|
||||||
mambaforge_version = '4.10.3-7'
|
mambaforge_version = "4.10.3-7"
|
||||||
if os.uname().machine == 'aarch64':
|
if os.uname().machine == "aarch64":
|
||||||
installer_sha256 = (
|
installer_sha256 = (
|
||||||
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
||||||
)
|
)
|
||||||
elif os.uname().machine == 'x86_64':
|
elif os.uname().machine == "x86_64":
|
||||||
installer_sha256 = (
|
installer_sha256 = (
|
||||||
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ def prefix():
|
|||||||
installer_url, installer_sha256
|
installer_url, installer_sha256
|
||||||
) as installer_path:
|
) as installer_path:
|
||||||
conda.install_miniconda(installer_path, tmpdir)
|
conda.install_miniconda(installer_path, tmpdir)
|
||||||
conda.ensure_conda_packages(tmpdir, ['conda==4.10.3'])
|
conda.ensure_conda_packages(tmpdir, ["conda==4.10.3"])
|
||||||
yield tmpdir
|
yield tmpdir
|
||||||
|
|
||||||
|
|
||||||
@@ -39,29 +39,29 @@ def test_ensure_packages(prefix):
|
|||||||
"""
|
"""
|
||||||
Test installing packages in conda environment
|
Test installing packages in conda environment
|
||||||
"""
|
"""
|
||||||
conda.ensure_conda_packages(prefix, ['numpy'])
|
conda.ensure_conda_packages(prefix, ["numpy"])
|
||||||
# Throws an error if this fails
|
# Throws an error if this fails
|
||||||
subprocess.check_call([os.path.join(prefix, 'bin', 'python'), '-c', 'import numpy'])
|
subprocess.check_call([os.path.join(prefix, "bin", "python"), "-c", "import numpy"])
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_pip_packages(prefix):
|
def test_ensure_pip_packages(prefix):
|
||||||
"""
|
"""
|
||||||
Test installing pip packages in conda environment
|
Test installing pip packages in conda environment
|
||||||
"""
|
"""
|
||||||
conda.ensure_conda_packages(prefix, ['pip'])
|
conda.ensure_conda_packages(prefix, ["pip"])
|
||||||
conda.ensure_pip_packages(prefix, ['numpy'])
|
conda.ensure_pip_packages(prefix, ["numpy"])
|
||||||
# Throws an error if this fails
|
# Throws an error if this fails
|
||||||
subprocess.check_call([os.path.join(prefix, 'bin', 'python'), '-c', 'import numpy'])
|
subprocess.check_call([os.path.join(prefix, "bin", "python"), "-c", "import numpy"])
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_pip_requirements(prefix):
|
def test_ensure_pip_requirements(prefix):
|
||||||
"""
|
"""
|
||||||
Test installing pip packages with requirements.txt in conda environment
|
Test installing pip packages with requirements.txt in conda environment
|
||||||
"""
|
"""
|
||||||
conda.ensure_conda_packages(prefix, ['pip'])
|
conda.ensure_conda_packages(prefix, ["pip"])
|
||||||
with tempfile.NamedTemporaryFile() as f:
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
# Sample small package to test
|
# Sample small package to test
|
||||||
f.write(b'there')
|
f.write(b"there")
|
||||||
f.flush()
|
f.flush()
|
||||||
conda.ensure_pip_requirements(prefix, f.name)
|
conda.ensure_pip_requirements(prefix, f.name)
|
||||||
subprocess.check_call([os.path.join(prefix, 'bin', 'python'), '-c', 'import there'])
|
subprocess.check_call([os.path.join(prefix, "bin", "python"), "-c", "import there"])
|
||||||
|
|||||||
@@ -14,25 +14,25 @@ from tljh import config, configurer
|
|||||||
def test_set_no_mutate():
|
def test_set_no_mutate():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(conf, 'a.b', 'c')
|
new_conf = config.set_item_in_config(conf, "a.b", "c")
|
||||||
assert new_conf['a']['b'] == 'c'
|
assert new_conf["a"]["b"] == "c"
|
||||||
assert conf == {}
|
assert conf == {}
|
||||||
|
|
||||||
|
|
||||||
def test_set_one_level():
|
def test_set_one_level():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(conf, 'a', 'b')
|
new_conf = config.set_item_in_config(conf, "a", "b")
|
||||||
assert new_conf['a'] == 'b'
|
assert new_conf["a"] == "b"
|
||||||
|
|
||||||
|
|
||||||
def test_set_multi_level():
|
def test_set_multi_level():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(conf, 'a.b', 'c')
|
new_conf = config.set_item_in_config(conf, "a.b", "c")
|
||||||
new_conf = config.set_item_in_config(new_conf, 'a.d', 'e')
|
new_conf = config.set_item_in_config(new_conf, "a.d", "e")
|
||||||
new_conf = config.set_item_in_config(new_conf, 'f', 'g')
|
new_conf = config.set_item_in_config(new_conf, "f", "g")
|
||||||
assert new_conf == {'a': {'b': 'c', 'd': 'e'}, 'f': 'g'}
|
assert new_conf == {"a": {"b": "c", "d": "e"}, "f": "g"}
|
||||||
|
|
||||||
|
|
||||||
def test_set_overwrite():
|
def test_set_overwrite():
|
||||||
@@ -41,124 +41,124 @@ def test_set_overwrite():
|
|||||||
|
|
||||||
This might be surprising destructive behavior to some :D
|
This might be surprising destructive behavior to some :D
|
||||||
"""
|
"""
|
||||||
conf = {'a': 'b'}
|
conf = {"a": "b"}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(conf, 'a', 'c')
|
new_conf = config.set_item_in_config(conf, "a", "c")
|
||||||
assert new_conf == {'a': 'c'}
|
assert new_conf == {"a": "c"}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(new_conf, 'a.b', 'd')
|
new_conf = config.set_item_in_config(new_conf, "a.b", "d")
|
||||||
assert new_conf == {'a': {'b': 'd'}}
|
assert new_conf == {"a": {"b": "d"}}
|
||||||
|
|
||||||
new_conf = config.set_item_in_config(new_conf, 'a', 'hi')
|
new_conf = config.set_item_in_config(new_conf, "a", "hi")
|
||||||
assert new_conf == {'a': 'hi'}
|
assert new_conf == {"a": "hi"}
|
||||||
|
|
||||||
|
|
||||||
def test_unset_no_mutate():
|
def test_unset_no_mutate():
|
||||||
conf = {'a': 'b'}
|
conf = {"a": "b"}
|
||||||
|
|
||||||
new_conf = config.unset_item_from_config(conf, 'a')
|
new_conf = config.unset_item_from_config(conf, "a")
|
||||||
assert conf == {'a': 'b'}
|
assert conf == {"a": "b"}
|
||||||
|
|
||||||
|
|
||||||
def test_unset_one_level():
|
def test_unset_one_level():
|
||||||
conf = {'a': 'b'}
|
conf = {"a": "b"}
|
||||||
|
|
||||||
new_conf = config.unset_item_from_config(conf, 'a')
|
new_conf = config.unset_item_from_config(conf, "a")
|
||||||
assert new_conf == {}
|
assert new_conf == {}
|
||||||
|
|
||||||
|
|
||||||
def test_unset_multi_level():
|
def test_unset_multi_level():
|
||||||
conf = {'a': {'b': 'c', 'd': 'e'}, 'f': 'g'}
|
conf = {"a": {"b": "c", "d": "e"}, "f": "g"}
|
||||||
|
|
||||||
new_conf = config.unset_item_from_config(conf, 'a.b')
|
new_conf = config.unset_item_from_config(conf, "a.b")
|
||||||
assert new_conf == {'a': {'d': 'e'}, 'f': 'g'}
|
assert new_conf == {"a": {"d": "e"}, "f": "g"}
|
||||||
new_conf = config.unset_item_from_config(new_conf, 'a.d')
|
new_conf = config.unset_item_from_config(new_conf, "a.d")
|
||||||
assert new_conf == {'f': 'g'}
|
assert new_conf == {"f": "g"}
|
||||||
new_conf = config.unset_item_from_config(new_conf, 'f')
|
new_conf = config.unset_item_from_config(new_conf, "f")
|
||||||
assert new_conf == {}
|
assert new_conf == {}
|
||||||
|
|
||||||
|
|
||||||
def test_unset_and_clean_empty_configs():
|
def test_unset_and_clean_empty_configs():
|
||||||
conf = {'a': {'b': {'c': {'d': {'e': 'f'}}}}}
|
conf = {"a": {"b": {"c": {"d": {"e": "f"}}}}}
|
||||||
|
|
||||||
new_conf = config.unset_item_from_config(conf, 'a.b.c.d.e')
|
new_conf = config.unset_item_from_config(conf, "a.b.c.d.e")
|
||||||
assert new_conf == {}
|
assert new_conf == {}
|
||||||
|
|
||||||
|
|
||||||
def test_unset_config_error():
|
def test_unset_config_error():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.unset_item_from_config({}, 'a')
|
config.unset_item_from_config({}, "a")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.unset_item_from_config({'a': 'b'}, 'b')
|
config.unset_item_from_config({"a": "b"}, "b")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.unset_item_from_config({'a': {'b': 'c'}}, 'a.z')
|
config.unset_item_from_config({"a": {"b": "c"}}, "a.z")
|
||||||
|
|
||||||
|
|
||||||
def test_add_to_config_one_level():
|
def test_add_to_config_one_level():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.add_item_to_config(conf, 'a.b', 'c')
|
new_conf = config.add_item_to_config(conf, "a.b", "c")
|
||||||
assert new_conf == {'a': {'b': ['c']}}
|
assert new_conf == {"a": {"b": ["c"]}}
|
||||||
|
|
||||||
|
|
||||||
def test_add_to_config_zero_level():
|
def test_add_to_config_zero_level():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.add_item_to_config(conf, 'a', 'b')
|
new_conf = config.add_item_to_config(conf, "a", "b")
|
||||||
assert new_conf == {'a': ['b']}
|
assert new_conf == {"a": ["b"]}
|
||||||
|
|
||||||
|
|
||||||
def test_add_to_config_multiple():
|
def test_add_to_config_multiple():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.add_item_to_config(conf, 'a.b.c', 'd')
|
new_conf = config.add_item_to_config(conf, "a.b.c", "d")
|
||||||
assert new_conf == {'a': {'b': {'c': ['d']}}}
|
assert new_conf == {"a": {"b": {"c": ["d"]}}}
|
||||||
|
|
||||||
new_conf = config.add_item_to_config(new_conf, 'a.b.c', 'e')
|
new_conf = config.add_item_to_config(new_conf, "a.b.c", "e")
|
||||||
assert new_conf == {'a': {'b': {'c': ['d', 'e']}}}
|
assert new_conf == {"a": {"b": {"c": ["d", "e"]}}}
|
||||||
|
|
||||||
|
|
||||||
def test_remove_from_config():
|
def test_remove_from_config():
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
new_conf = config.add_item_to_config(conf, 'a.b.c', 'd')
|
new_conf = config.add_item_to_config(conf, "a.b.c", "d")
|
||||||
new_conf = config.add_item_to_config(new_conf, 'a.b.c', 'e')
|
new_conf = config.add_item_to_config(new_conf, "a.b.c", "e")
|
||||||
assert new_conf == {'a': {'b': {'c': ['d', 'e']}}}
|
assert new_conf == {"a": {"b": {"c": ["d", "e"]}}}
|
||||||
|
|
||||||
new_conf = config.remove_item_from_config(new_conf, 'a.b.c', 'e')
|
new_conf = config.remove_item_from_config(new_conf, "a.b.c", "e")
|
||||||
assert new_conf == {'a': {'b': {'c': ['d']}}}
|
assert new_conf == {"a": {"b": {"c": ["d"]}}}
|
||||||
|
|
||||||
|
|
||||||
def test_remove_from_config_error():
|
def test_remove_from_config_error():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.remove_item_from_config({}, 'a.b.c', 'e')
|
config.remove_item_from_config({}, "a.b.c", "e")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.remove_item_from_config({'a': 'b'}, 'a.b', 'e')
|
config.remove_item_from_config({"a": "b"}, "a.b", "e")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
config.remove_item_from_config({'a': ['b']}, 'a', 'e')
|
config.remove_item_from_config({"a": ["b"]}, "a", "e")
|
||||||
|
|
||||||
|
|
||||||
def test_reload_hub():
|
def test_reload_hub():
|
||||||
with mock.patch('tljh.systemd.restart_service') as restart_service, mock.patch(
|
with mock.patch("tljh.systemd.restart_service") as restart_service, mock.patch(
|
||||||
'tljh.systemd.check_service_active'
|
"tljh.systemd.check_service_active"
|
||||||
) as check_active, mock.patch('tljh.config.check_hub_ready') as check_ready:
|
) as check_active, mock.patch("tljh.config.check_hub_ready") as check_ready:
|
||||||
config.reload_component('hub')
|
config.reload_component("hub")
|
||||||
assert restart_service.called_with('jupyterhub')
|
assert restart_service.called_with("jupyterhub")
|
||||||
assert check_active.called_with('jupyterhub')
|
assert check_active.called_with("jupyterhub")
|
||||||
|
|
||||||
|
|
||||||
def test_reload_proxy(tljh_dir):
|
def test_reload_proxy(tljh_dir):
|
||||||
with mock.patch("tljh.systemd.restart_service") as restart_service, mock.patch(
|
with mock.patch("tljh.systemd.restart_service") as restart_service, mock.patch(
|
||||||
"tljh.systemd.check_service_active"
|
"tljh.systemd.check_service_active"
|
||||||
) as check_active:
|
) as check_active:
|
||||||
config.reload_component('proxy')
|
config.reload_component("proxy")
|
||||||
assert restart_service.called_with('traefik')
|
assert restart_service.called_with("traefik")
|
||||||
assert check_active.called_with('traefik')
|
assert check_active.called_with("traefik")
|
||||||
assert os.path.exists(os.path.join(config.STATE_DIR, 'traefik.toml'))
|
assert os.path.exists(os.path.join(config.STATE_DIR, "traefik.toml"))
|
||||||
|
|
||||||
|
|
||||||
def test_cli_no_command(capsys):
|
def test_cli_no_command(capsys):
|
||||||
@@ -172,41 +172,41 @@ def test_cli_no_command(capsys):
|
|||||||
def test_cli_set_bool(tljh_dir, arg, value):
|
def test_cli_set_bool(tljh_dir, arg, value):
|
||||||
config.main(["set", "https.enabled", arg])
|
config.main(["set", "https.enabled", arg])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['https']['enabled'] == value
|
assert cfg["https"]["enabled"] == value
|
||||||
|
|
||||||
|
|
||||||
def test_cli_set_int(tljh_dir):
|
def test_cli_set_int(tljh_dir):
|
||||||
config.main(["set", "https.port", "123"])
|
config.main(["set", "https.port", "123"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['https']['port'] == 123
|
assert cfg["https"]["port"] == 123
|
||||||
|
|
||||||
|
|
||||||
def test_cli_unset(tljh_dir):
|
def test_cli_unset(tljh_dir):
|
||||||
config.main(["set", "foo.bar", "1"])
|
config.main(["set", "foo.bar", "1"])
|
||||||
config.main(["set", "foo.bar2", "2"])
|
config.main(["set", "foo.bar2", "2"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['foo'] == {'bar': 1, 'bar2': 2}
|
assert cfg["foo"] == {"bar": 1, "bar2": 2}
|
||||||
|
|
||||||
config.main(["unset", "foo.bar"])
|
config.main(["unset", "foo.bar"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
|
|
||||||
assert cfg['foo'] == {'bar2': 2}
|
assert cfg["foo"] == {"bar2": 2}
|
||||||
|
|
||||||
|
|
||||||
def test_cli_add_float(tljh_dir):
|
def test_cli_add_float(tljh_dir):
|
||||||
config.main(["add-item", "foo.bar", "1.25"])
|
config.main(["add-item", "foo.bar", "1.25"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['foo']['bar'] == [1.25]
|
assert cfg["foo"]["bar"] == [1.25]
|
||||||
|
|
||||||
|
|
||||||
def test_cli_remove_int(tljh_dir):
|
def test_cli_remove_int(tljh_dir):
|
||||||
config.main(["add-item", "foo.bar", "1"])
|
config.main(["add-item", "foo.bar", "1"])
|
||||||
config.main(["add-item", "foo.bar", "2"])
|
config.main(["add-item", "foo.bar", "2"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['foo']['bar'] == [1, 2]
|
assert cfg["foo"]["bar"] == [1, 2]
|
||||||
config.main(["remove-item", "foo.bar", "1"])
|
config.main(["remove-item", "foo.bar", "1"])
|
||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
assert cfg['foo']['bar'] == [2]
|
assert cfg["foo"]["bar"] == [2]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ def test_default_base_url():
|
|||||||
Test default JupyterHub base_url
|
Test default JupyterHub base_url
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config({})
|
c = apply_mock_config({})
|
||||||
assert c.JupyterHub.base_url == '/'
|
assert c.JupyterHub.base_url == "/"
|
||||||
|
|
||||||
|
|
||||||
def test_set_base_url():
|
def test_set_base_url():
|
||||||
"""
|
"""
|
||||||
Test set JupyterHub base_url
|
Test set JupyterHub base_url
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config({'base_url': '/custom-base'})
|
c = apply_mock_config({"base_url": "/custom-base"})
|
||||||
assert c.JupyterHub.base_url == '/custom-base'
|
assert c.JupyterHub.base_url == "/custom-base"
|
||||||
|
|
||||||
|
|
||||||
def test_default_memory_limit():
|
def test_default_memory_limit():
|
||||||
@@ -48,8 +48,8 @@ def test_set_memory_limit():
|
|||||||
"""
|
"""
|
||||||
Test setting per user memory limit
|
Test setting per user memory limit
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config({'limits': {'memory': '42G'}})
|
c = apply_mock_config({"limits": {"memory": "42G"}})
|
||||||
assert c.Spawner.mem_limit == '42G'
|
assert c.Spawner.mem_limit == "42G"
|
||||||
|
|
||||||
|
|
||||||
def test_app_default():
|
def test_app_default():
|
||||||
@@ -58,23 +58,23 @@ def test_app_default():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config({})
|
c = apply_mock_config({})
|
||||||
# default_url is not set, so JupyterHub will pick default.
|
# default_url is not set, so JupyterHub will pick default.
|
||||||
assert 'default_url' not in c.Spawner
|
assert "default_url" not in c.Spawner
|
||||||
|
|
||||||
|
|
||||||
def test_app_jupyterlab():
|
def test_app_jupyterlab():
|
||||||
"""
|
"""
|
||||||
Test setting JupyterLab as default application
|
Test setting JupyterLab as default application
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config({'user_environment': {'default_app': 'jupyterlab'}})
|
c = apply_mock_config({"user_environment": {"default_app": "jupyterlab"}})
|
||||||
assert c.Spawner.default_url == '/lab'
|
assert c.Spawner.default_url == "/lab"
|
||||||
|
|
||||||
|
|
||||||
def test_app_nteract():
|
def test_app_nteract():
|
||||||
"""
|
"""
|
||||||
Test setting nteract as default application
|
Test setting nteract as default application
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config({'user_environment': {'default_app': 'nteract'}})
|
c = apply_mock_config({"user_environment": {"default_app": "nteract"}})
|
||||||
assert c.Spawner.default_url == '/nteract'
|
assert c.Spawner.default_url == "/nteract"
|
||||||
|
|
||||||
|
|
||||||
def test_auth_default():
|
def test_auth_default():
|
||||||
@@ -85,7 +85,7 @@ def test_auth_default():
|
|||||||
|
|
||||||
assert (
|
assert (
|
||||||
c.JupyterHub.authenticator_class
|
c.JupyterHub.authenticator_class
|
||||||
== 'firstuseauthenticator.FirstUseAuthenticator'
|
== "firstuseauthenticator.FirstUseAuthenticator"
|
||||||
)
|
)
|
||||||
# Do not auto create users who haven't been manually added by default
|
# Do not auto create users who haven't been manually added by default
|
||||||
assert not c.FirstUseAuthenticator.create_users
|
assert not c.FirstUseAuthenticator.create_users
|
||||||
@@ -96,10 +96,10 @@ def test_auth_dummy():
|
|||||||
Test setting Dummy Authenticator & password
|
Test setting Dummy Authenticator & password
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{'auth': {'type': 'dummy', 'DummyAuthenticator': {'password': 'test'}}}
|
{"auth": {"type": "dummy", "DummyAuthenticator": {"password": "test"}}}
|
||||||
)
|
)
|
||||||
assert c.JupyterHub.authenticator_class == 'dummy'
|
assert c.JupyterHub.authenticator_class == "dummy"
|
||||||
assert c.DummyAuthenticator.password == 'test'
|
assert c.DummyAuthenticator.password == "test"
|
||||||
|
|
||||||
|
|
||||||
def test_user_groups():
|
def test_user_groups():
|
||||||
@@ -108,8 +108,8 @@ def test_user_groups():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{
|
{
|
||||||
'users': {
|
"users": {
|
||||||
'extra_user_groups': {"g1": ["u1", "u2"], "g2": ["u3", "u4"]},
|
"extra_user_groups": {"g1": ["u1", "u2"], "g2": ["u3", "u4"]},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -122,15 +122,15 @@ def test_auth_firstuse():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{
|
{
|
||||||
'auth': {
|
"auth": {
|
||||||
'type': 'firstuseauthenticator.FirstUseAuthenticator',
|
"type": "firstuseauthenticator.FirstUseAuthenticator",
|
||||||
'FirstUseAuthenticator': {'create_users': True},
|
"FirstUseAuthenticator": {"create_users": True},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
c.JupyterHub.authenticator_class
|
c.JupyterHub.authenticator_class
|
||||||
== 'firstuseauthenticator.FirstUseAuthenticator'
|
== "firstuseauthenticator.FirstUseAuthenticator"
|
||||||
)
|
)
|
||||||
assert c.FirstUseAuthenticator.create_users
|
assert c.FirstUseAuthenticator.create_users
|
||||||
|
|
||||||
@@ -141,20 +141,20 @@ def test_auth_github():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{
|
{
|
||||||
'auth': {
|
"auth": {
|
||||||
'type': 'oauthenticator.github.GitHubOAuthenticator',
|
"type": "oauthenticator.github.GitHubOAuthenticator",
|
||||||
'GitHubOAuthenticator': {
|
"GitHubOAuthenticator": {
|
||||||
'client_id': 'something',
|
"client_id": "something",
|
||||||
'client_secret': 'something-else',
|
"client_secret": "something-else",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator'
|
c.JupyterHub.authenticator_class == "oauthenticator.github.GitHubOAuthenticator"
|
||||||
)
|
)
|
||||||
assert c.GitHubOAuthenticator.client_id == 'something'
|
assert c.GitHubOAuthenticator.client_id == "something"
|
||||||
assert c.GitHubOAuthenticator.client_secret == 'something-else'
|
assert c.GitHubOAuthenticator.client_secret == "something-else"
|
||||||
|
|
||||||
|
|
||||||
def test_traefik_api_default():
|
def test_traefik_api_default():
|
||||||
@@ -163,7 +163,7 @@ def test_traefik_api_default():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config({})
|
c = apply_mock_config({})
|
||||||
|
|
||||||
assert c.TraefikTomlProxy.traefik_api_username == 'api_admin'
|
assert c.TraefikTomlProxy.traefik_api_username == "api_admin"
|
||||||
assert len(c.TraefikTomlProxy.traefik_api_password) == 0
|
assert len(c.TraefikTomlProxy.traefik_api_password) == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -172,10 +172,10 @@ def test_set_traefik_api():
|
|||||||
Test setting per traefik api credentials
|
Test setting per traefik api credentials
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{'traefik_api': {'username': 'some_user', 'password': '1234'}}
|
{"traefik_api": {"username": "some_user", "password": "1234"}}
|
||||||
)
|
)
|
||||||
assert c.TraefikTomlProxy.traefik_api_username == 'some_user'
|
assert c.TraefikTomlProxy.traefik_api_username == "some_user"
|
||||||
assert c.TraefikTomlProxy.traefik_api_password == '1234'
|
assert c.TraefikTomlProxy.traefik_api_password == "1234"
|
||||||
|
|
||||||
|
|
||||||
def test_cull_service_default():
|
def test_cull_service_default():
|
||||||
@@ -186,18 +186,18 @@ def test_cull_service_default():
|
|||||||
|
|
||||||
cull_cmd = [
|
cull_cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
'-m',
|
"-m",
|
||||||
'jupyterhub_idle_culler',
|
"jupyterhub_idle_culler",
|
||||||
'--timeout=600',
|
"--timeout=600",
|
||||||
'--cull-every=60',
|
"--cull-every=60",
|
||||||
'--concurrency=5',
|
"--concurrency=5",
|
||||||
'--max-age=0',
|
"--max-age=0",
|
||||||
]
|
]
|
||||||
assert c.JupyterHub.services == [
|
assert c.JupyterHub.services == [
|
||||||
{
|
{
|
||||||
'name': 'cull-idle',
|
"name": "cull-idle",
|
||||||
'admin': True,
|
"admin": True,
|
||||||
'command': cull_cmd,
|
"command": cull_cmd,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -207,23 +207,23 @@ def test_set_cull_service():
|
|||||||
Test setting cull service options
|
Test setting cull service options
|
||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{'services': {'cull': {'every': 10, 'users': True, 'max_age': 60}}}
|
{"services": {"cull": {"every": 10, "users": True, "max_age": 60}}}
|
||||||
)
|
)
|
||||||
cull_cmd = [
|
cull_cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
'-m',
|
"-m",
|
||||||
'jupyterhub_idle_culler',
|
"jupyterhub_idle_culler",
|
||||||
'--timeout=600',
|
"--timeout=600",
|
||||||
'--cull-every=10',
|
"--cull-every=10",
|
||||||
'--concurrency=5',
|
"--concurrency=5",
|
||||||
'--max-age=60',
|
"--max-age=60",
|
||||||
'--cull-users',
|
"--cull-users",
|
||||||
]
|
]
|
||||||
assert c.JupyterHub.services == [
|
assert c.JupyterHub.services == [
|
||||||
{
|
{
|
||||||
'name': 'cull-idle',
|
"name": "cull-idle",
|
||||||
'admin': True,
|
"admin": True,
|
||||||
'command': cull_cmd,
|
"command": cull_cmd,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -232,11 +232,11 @@ def test_load_secrets(tljh_dir):
|
|||||||
"""
|
"""
|
||||||
Test loading secret files
|
Test loading secret files
|
||||||
"""
|
"""
|
||||||
with open(os.path.join(tljh_dir, 'state', 'traefik-api.secret'), 'w') as f:
|
with open(os.path.join(tljh_dir, "state", "traefik-api.secret"), "w") as f:
|
||||||
f.write("traefik-password")
|
f.write("traefik-password")
|
||||||
|
|
||||||
tljh_config = configurer.load_config()
|
tljh_config = configurer.load_config()
|
||||||
assert tljh_config['traefik_api']['password'] == "traefik-password"
|
assert tljh_config["traefik_api"]["password"] == "traefik-password"
|
||||||
c = apply_mock_config(tljh_config)
|
c = apply_mock_config(tljh_config)
|
||||||
assert c.TraefikTomlProxy.traefik_api_password == "traefik-password"
|
assert c.TraefikTomlProxy.traefik_api_password == "traefik-password"
|
||||||
|
|
||||||
@@ -247,13 +247,13 @@ def test_auth_native():
|
|||||||
"""
|
"""
|
||||||
c = apply_mock_config(
|
c = apply_mock_config(
|
||||||
{
|
{
|
||||||
'auth': {
|
"auth": {
|
||||||
'type': 'nativeauthenticator.NativeAuthenticator',
|
"type": "nativeauthenticator.NativeAuthenticator",
|
||||||
'NativeAuthenticator': {
|
"NativeAuthenticator": {
|
||||||
'open_signup': True,
|
"open_signup": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert c.JupyterHub.authenticator_class == 'nativeauthenticator.NativeAuthenticator'
|
assert c.JupyterHub.authenticator_class == "nativeauthenticator.NativeAuthenticator"
|
||||||
assert c.NativeAuthenticator.open_signup == True
|
assert c.NativeAuthenticator.open_signup == True
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ def test_ensure_config_yaml(tljh_dir):
|
|||||||
installer.ensure_config_yaml(pm)
|
installer.ensure_config_yaml(pm)
|
||||||
assert os.path.exists(installer.CONFIG_FILE)
|
assert os.path.exists(installer.CONFIG_FILE)
|
||||||
assert os.path.isdir(installer.CONFIG_DIR)
|
assert os.path.isdir(installer.CONFIG_DIR)
|
||||||
assert os.path.isdir(os.path.join(installer.CONFIG_DIR, 'jupyterhub_config.d'))
|
assert os.path.isdir(os.path.join(installer.CONFIG_DIR, "jupyterhub_config.d"))
|
||||||
# verify that old config doesn't exist
|
# verify that old config doesn't exist
|
||||||
assert not os.path.exists(os.path.join(tljh_dir, 'config.yaml'))
|
assert not os.path.exists(os.path.join(tljh_dir, "config.yaml"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"admins, expected_config",
|
"admins, expected_config",
|
||||||
[
|
[
|
||||||
([['a1'], ['a2'], ['a3']], ['a1', 'a2', 'a3']),
|
([["a1"], ["a2"], ["a3"]], ["a1", "a2", "a3"]),
|
||||||
([['a1:p1'], ['a2']], ['a1', 'a2']),
|
([["a1:p1"], ["a2"]], ["a1", "a2"]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_ensure_admins(tljh_dir, admins, expected_config):
|
def test_ensure_admins(tljh_dir, admins, expected_config):
|
||||||
@@ -35,4 +35,4 @@ def test_ensure_admins(tljh_dir, admins, expected_config):
|
|||||||
config = yaml.load(f)
|
config = yaml.load(f)
|
||||||
|
|
||||||
# verify the list was flattened
|
# verify the list was flattened
|
||||||
assert config['users']['admin'] == expected_config
|
assert config["users"]["admin"] == expected_config
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ def test_generate_username():
|
|||||||
"""
|
"""
|
||||||
usernames = {
|
usernames = {
|
||||||
# Very short
|
# Very short
|
||||||
'jupyter-test': 'jupyter-test',
|
"jupyter-test": "jupyter-test",
|
||||||
# Very long
|
# Very long
|
||||||
'jupyter-aelie9sohjeequ9iemeipuimuoshahz4aitugiuteeg4ohioh5yuiha6aei7te5z': 'jupyter-aelie9sohjeequ9iem-4b726',
|
"jupyter-aelie9sohjeequ9iemeipuimuoshahz4aitugiuteeg4ohioh5yuiha6aei7te5z": "jupyter-aelie9sohjeequ9iem-4b726",
|
||||||
# 26 characters, just below our cutoff for hashing
|
# 26 characters, just below our cutoff for hashing
|
||||||
'jupyter-abcdefghijklmnopq': 'jupyter-abcdefghijklmnopq',
|
"jupyter-abcdefghijklmnopq": "jupyter-abcdefghijklmnopq",
|
||||||
# 27 characters, just above our cutoff for hashing
|
# 27 characters, just above our cutoff for hashing
|
||||||
'jupyter-abcdefghijklmnopqr': 'jupyter-abcdefghijklmnopqr-e375e',
|
"jupyter-abcdefghijklmnopqr": "jupyter-abcdefghijklmnopqr-e375e",
|
||||||
}
|
}
|
||||||
for hub_user, system_user in usernames.items():
|
for hub_user, system_user in usernames.items():
|
||||||
assert generate_system_username(hub_user) == system_user
|
assert generate_system_username(hub_user) == system_user
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def test_ensure_user():
|
|||||||
Test user creation & removal
|
Test user creation & removal
|
||||||
"""
|
"""
|
||||||
# Use a prefix to make sure we never start with a number
|
# Use a prefix to make sure we never start with a number
|
||||||
username = 'u' + str(uuid.uuid4())[:8]
|
username = "u" + str(uuid.uuid4())[:8]
|
||||||
# Validate that no user exists
|
# Validate that no user exists
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
pwd.getpwnam(username)
|
pwd.getpwnam(username)
|
||||||
@@ -57,7 +57,7 @@ def test_ensure_group():
|
|||||||
Test group creation & removal
|
Test group creation & removal
|
||||||
"""
|
"""
|
||||||
# Use a prefix to make sure we never start with a number
|
# Use a prefix to make sure we never start with a number
|
||||||
groupname = 'g' + str(uuid.uuid4())[:8]
|
groupname = "g" + str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
# Validate that no group exists
|
# Validate that no group exists
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
@@ -83,8 +83,8 @@ def test_group_membership():
|
|||||||
"""
|
"""
|
||||||
Test group memberships can be added / removed
|
Test group memberships can be added / removed
|
||||||
"""
|
"""
|
||||||
username = 'u' + str(uuid.uuid4())[:8]
|
username = "u" + str(uuid.uuid4())[:8]
|
||||||
groupname = 'g' + str(uuid.uuid4())[:8]
|
groupname = "g" + str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
# Validate that no group exists
|
# Validate that no group exists
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
def test_run_subprocess_exception(mocker):
|
def test_run_subprocess_exception(mocker):
|
||||||
logger = logging.getLogger('tljh')
|
logger = logging.getLogger("tljh")
|
||||||
mocker.patch.object(logger, 'error')
|
mocker.patch.object(logger, "error")
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
utils.run_subprocess(['/bin/bash', '-c', 'echo error; exit 1'])
|
utils.run_subprocess(["/bin/bash", "-c", "echo error; exit 1"])
|
||||||
logger.error.assert_called_with('error\n')
|
logger.error.assert_called_with("error\n")
|
||||||
|
|
||||||
|
|
||||||
def test_run_subprocess(mocker):
|
def test_run_subprocess(mocker):
|
||||||
logger = logging.getLogger('tljh')
|
logger = logging.getLogger("tljh")
|
||||||
mocker.patch.object(logger, 'debug')
|
mocker.patch.object(logger, "debug")
|
||||||
utils.run_subprocess(['/bin/bash', '-c', 'echo success'])
|
utils.run_subprocess(["/bin/bash", "-c", "echo success"])
|
||||||
logger.debug.assert_called_with('success\n')
|
logger.debug.assert_called_with("success\n")
|
||||||
|
|||||||
22
tljh/apt.py
22
tljh/apt.py
@@ -13,9 +13,9 @@ def trust_gpg_key(key):
|
|||||||
key is a GPG public key (bytes) that can be passed to apt-key add via stdin.
|
key is a GPG public key (bytes) that can be passed to apt-key add via stdin.
|
||||||
"""
|
"""
|
||||||
# If gpg2 doesn't exist, install it.
|
# If gpg2 doesn't exist, install it.
|
||||||
if not os.path.exists('/usr/bin/gpg2'):
|
if not os.path.exists("/usr/bin/gpg2"):
|
||||||
install_packages(['gnupg2'])
|
install_packages(["gnupg2"])
|
||||||
utils.run_subprocess(['apt-key', 'add', '-'], input=key)
|
utils.run_subprocess(["apt-key", "add", "-"], input=key)
|
||||||
|
|
||||||
|
|
||||||
def add_source(name, source_url, section):
|
def add_source(name, source_url, section):
|
||||||
@@ -27,20 +27,20 @@ def add_source(name, source_url, section):
|
|||||||
# lsb_release is not installed in most docker images by default
|
# lsb_release is not installed in most docker images by default
|
||||||
distro = (
|
distro = (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
['/bin/bash', '-c', 'source /etc/os-release && echo ${VERSION_CODENAME}'],
|
["/bin/bash", "-c", "source /etc/os-release && echo ${VERSION_CODENAME}"],
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
.decode()
|
.decode()
|
||||||
.strip()
|
.strip()
|
||||||
)
|
)
|
||||||
line = f'deb {source_url} {distro} {section}\n'
|
line = f"deb {source_url} {distro} {section}\n"
|
||||||
with open(os.path.join('/etc/apt/sources.list.d/', name + '.list'), 'a+') as f:
|
with open(os.path.join("/etc/apt/sources.list.d/", name + ".list"), "a+") as f:
|
||||||
# Write out deb line only if it already doesn't exist
|
# Write out deb line only if it already doesn't exist
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
if line not in f.read():
|
if line not in f.read():
|
||||||
f.write(line)
|
f.write(line)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
utils.run_subprocess(['apt-get', 'update', '--yes'])
|
utils.run_subprocess(["apt-get", "update", "--yes"])
|
||||||
|
|
||||||
|
|
||||||
def install_packages(packages):
|
def install_packages(packages):
|
||||||
@@ -48,9 +48,9 @@ def install_packages(packages):
|
|||||||
Install debian packages
|
Install debian packages
|
||||||
"""
|
"""
|
||||||
# Check if an apt-get update is required
|
# Check if an apt-get update is required
|
||||||
if len(os.listdir('/var/lib/apt/lists')) == 0:
|
if len(os.listdir("/var/lib/apt/lists")) == 0:
|
||||||
utils.run_subprocess(['apt-get', 'update', '--yes'])
|
utils.run_subprocess(["apt-get", "update", "--yes"])
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
# Stop apt from asking questions!
|
# Stop apt from asking questions!
|
||||||
env['DEBIAN_FRONTEND'] = 'noninteractive'
|
env["DEBIAN_FRONTEND"] = "noninteractive"
|
||||||
utils.run_subprocess(['apt-get', 'install', '--yes'] + packages, env=env)
|
utils.run_subprocess(["apt-get", "install", "--yes"] + packages, env=env)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def check_miniconda_version(prefix, version):
|
|||||||
try:
|
try:
|
||||||
installed_version = (
|
installed_version = (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[os.path.join(prefix, 'bin', 'conda'), '-V'], stderr=subprocess.STDOUT
|
[os.path.join(prefix, "bin", "conda"), "-V"], stderr=subprocess.STDOUT
|
||||||
)
|
)
|
||||||
.decode()
|
.decode()
|
||||||
.strip()
|
.strip()
|
||||||
@@ -53,7 +53,7 @@ def download_miniconda_installer(installer_url, sha256sum):
|
|||||||
of given version, verifies the sha256sum & provides path to it to the `with`
|
of given version, verifies the sha256sum & provides path to it to the `with`
|
||||||
block to run.
|
block to run.
|
||||||
"""
|
"""
|
||||||
with tempfile.NamedTemporaryFile('wb') as f:
|
with tempfile.NamedTemporaryFile("wb") as f:
|
||||||
f.write(requests.get(installer_url).content)
|
f.write(requests.get(installer_url).content)
|
||||||
# Remain in the NamedTemporaryFile context, but flush changes, see:
|
# Remain in the NamedTemporaryFile context, but flush changes, see:
|
||||||
# https://docs.python.org/3/library/os.html#os.fsync
|
# https://docs.python.org/3/library/os.html#os.fsync
|
||||||
@@ -61,7 +61,7 @@ def download_miniconda_installer(installer_url, sha256sum):
|
|||||||
os.fsync(f.fileno())
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
if sha256_file(f.name) != sha256sum:
|
if sha256_file(f.name) != sha256sum:
|
||||||
raise Exception('sha256sum hash mismatch! Downloaded file corrupted')
|
raise Exception("sha256sum hash mismatch! Downloaded file corrupted")
|
||||||
|
|
||||||
yield f.name
|
yield f.name
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ def install_miniconda(installer_path, prefix):
|
|||||||
"""
|
"""
|
||||||
Install miniconda with installer at installer_path under prefix
|
Install miniconda with installer at installer_path under prefix
|
||||||
"""
|
"""
|
||||||
utils.run_subprocess(['/bin/bash', installer_path, '-u', '-b', '-p', prefix])
|
utils.run_subprocess(["/bin/bash", installer_path, "-u", "-b", "-p", prefix])
|
||||||
# fix permissions on initial install
|
# fix permissions on initial install
|
||||||
# a few files have the wrong ownership and permissions initially
|
# a few files have the wrong ownership and permissions initially
|
||||||
# when the installer is run as root
|
# when the installer is run as root
|
||||||
@@ -97,7 +97,7 @@ def ensure_conda_packages(prefix, packages):
|
|||||||
Note that conda seem to update dependencies by default, so there is probably
|
Note that conda seem to update dependencies by default, so there is probably
|
||||||
no need to have a update parameter exposed for this function.
|
no need to have a update parameter exposed for this function.
|
||||||
"""
|
"""
|
||||||
conda_executable = [os.path.join(prefix, 'bin', 'mamba')]
|
conda_executable = [os.path.join(prefix, "bin", "mamba")]
|
||||||
abspath = os.path.abspath(prefix)
|
abspath = os.path.abspath(prefix)
|
||||||
# Let subprocess errors propagate
|
# Let subprocess errors propagate
|
||||||
# Explicitly do *not* capture stderr, since that's not always JSON!
|
# Explicitly do *not* capture stderr, since that's not always JSON!
|
||||||
@@ -106,11 +106,11 @@ def ensure_conda_packages(prefix, packages):
|
|||||||
raw_output = subprocess.check_output(
|
raw_output = subprocess.check_output(
|
||||||
conda_executable
|
conda_executable
|
||||||
+ [
|
+ [
|
||||||
'install',
|
"install",
|
||||||
'-c',
|
"-c",
|
||||||
'conda-forge', # Make customizable if we ever need to
|
"conda-forge", # Make customizable if we ever need to
|
||||||
'--json',
|
"--json",
|
||||||
'--prefix',
|
"--prefix",
|
||||||
abspath,
|
abspath,
|
||||||
]
|
]
|
||||||
+ packages
|
+ packages
|
||||||
@@ -118,17 +118,17 @@ def ensure_conda_packages(prefix, packages):
|
|||||||
# `conda install` outputs JSON lines for fetch updates,
|
# `conda install` outputs JSON lines for fetch updates,
|
||||||
# and a undelimited output at the end. There is no reasonable way to
|
# and a undelimited output at the end. There is no reasonable way to
|
||||||
# parse this outside of this kludge.
|
# parse this outside of this kludge.
|
||||||
filtered_output = '\n'.join(
|
filtered_output = "\n".join(
|
||||||
[
|
[
|
||||||
l
|
l
|
||||||
for l in raw_output.split('\n')
|
for l in raw_output.split("\n")
|
||||||
# Sometimes the JSON messages start with a \x00. The lstrip removes these.
|
# Sometimes the JSON messages start with a \x00. The lstrip removes these.
|
||||||
# conda messages seem to randomly throw \x00 in places for no reason
|
# conda messages seem to randomly throw \x00 in places for no reason
|
||||||
if not l.lstrip('\x00').startswith('{"fetch"')
|
if not l.lstrip("\x00").startswith('{"fetch"')
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
output = json.loads(filtered_output.lstrip('\x00'))
|
output = json.loads(filtered_output.lstrip("\x00"))
|
||||||
if 'success' in output and output['success'] == True:
|
if "success" in output and output["success"] == True:
|
||||||
return
|
return
|
||||||
fix_permissions(prefix)
|
fix_permissions(prefix)
|
||||||
|
|
||||||
@@ -138,10 +138,10 @@ def ensure_pip_packages(prefix, packages, upgrade=False):
|
|||||||
Ensure pip packages are installed in the given conda prefix.
|
Ensure pip packages are installed in the given conda prefix.
|
||||||
"""
|
"""
|
||||||
abspath = os.path.abspath(prefix)
|
abspath = os.path.abspath(prefix)
|
||||||
pip_executable = [os.path.join(abspath, 'bin', 'python'), '-m', 'pip']
|
pip_executable = [os.path.join(abspath, "bin", "python"), "-m", "pip"]
|
||||||
pip_cmd = pip_executable + ['install']
|
pip_cmd = pip_executable + ["install"]
|
||||||
if upgrade:
|
if upgrade:
|
||||||
pip_cmd.append('--upgrade')
|
pip_cmd.append("--upgrade")
|
||||||
utils.run_subprocess(pip_cmd + packages)
|
utils.run_subprocess(pip_cmd + packages)
|
||||||
fix_permissions(prefix)
|
fix_permissions(prefix)
|
||||||
|
|
||||||
@@ -153,9 +153,9 @@ def ensure_pip_requirements(prefix, requirements_path, upgrade=False):
|
|||||||
requirements_path can be a file or a URL.
|
requirements_path can be a file or a URL.
|
||||||
"""
|
"""
|
||||||
abspath = os.path.abspath(prefix)
|
abspath = os.path.abspath(prefix)
|
||||||
pip_executable = [os.path.join(abspath, 'bin', 'python'), '-m', 'pip']
|
pip_executable = [os.path.join(abspath, "bin", "python"), "-m", "pip"]
|
||||||
pip_cmd = pip_executable + ['install']
|
pip_cmd = pip_executable + ["install"]
|
||||||
if upgrade:
|
if upgrade:
|
||||||
pip_cmd.append('--upgrade')
|
pip_cmd.append("--upgrade")
|
||||||
utils.run_subprocess(pip_cmd + ['--requirement', requirements_path])
|
utils.run_subprocess(pip_cmd + ["--requirement", requirements_path])
|
||||||
fix_permissions(prefix)
|
fix_permissions(prefix)
|
||||||
|
|||||||
122
tljh/config.py
122
tljh/config.py
@@ -25,12 +25,12 @@ import requests
|
|||||||
from .yaml import yaml
|
from .yaml import yaml
|
||||||
|
|
||||||
|
|
||||||
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')
|
STATE_DIR = os.path.join(INSTALL_PREFIX, "state")
|
||||||
CONFIG_DIR = os.path.join(INSTALL_PREFIX, 'config')
|
CONFIG_DIR = os.path.join(INSTALL_PREFIX, "config")
|
||||||
CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yaml')
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yaml")
|
||||||
|
|
||||||
|
|
||||||
def set_item_in_config(config, property_path, value):
|
def set_item_in_config(config, property_path, value):
|
||||||
@@ -42,7 +42,7 @@ def set_item_in_config(config, property_path, value):
|
|||||||
property_path is a series of dot separated values. Any part of the path
|
property_path is a series of dot separated values. Any part of the path
|
||||||
that does not exist is created.
|
that does not exist is created.
|
||||||
"""
|
"""
|
||||||
path_components = property_path.split('.')
|
path_components = property_path.split(".")
|
||||||
|
|
||||||
# Mutate a copy of the config, not config itself
|
# Mutate a copy of the config, not config itself
|
||||||
cur_part = config_copy = deepcopy(config)
|
cur_part = config_copy = deepcopy(config)
|
||||||
@@ -69,7 +69,7 @@ def unset_item_from_config(config, property_path):
|
|||||||
|
|
||||||
property_path is a series of dot separated values.
|
property_path is a series of dot separated values.
|
||||||
"""
|
"""
|
||||||
path_components = property_path.split('.')
|
path_components = property_path.split(".")
|
||||||
|
|
||||||
# Mutate a copy of the config, not config itself
|
# Mutate a copy of the config, not config itself
|
||||||
cur_part = config_copy = deepcopy(config)
|
cur_part = config_copy = deepcopy(config)
|
||||||
@@ -94,13 +94,13 @@ def unset_item_from_config(config, property_path):
|
|||||||
for i, cur_path in enumerate(path_components):
|
for i, cur_path in enumerate(path_components):
|
||||||
if i == len(path_components) - 1:
|
if i == len(path_components) - 1:
|
||||||
if cur_path not in cur_part:
|
if cur_path not in cur_part:
|
||||||
raise ValueError(f'{property_path} does not exist in config!')
|
raise ValueError(f"{property_path} does not exist in config!")
|
||||||
del cur_part[cur_path]
|
del cur_part[cur_path]
|
||||||
remove_empty_configs(config_copy, path_components[:-1])
|
remove_empty_configs(config_copy, path_components[:-1])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if cur_path not in cur_part:
|
if cur_path not in cur_part:
|
||||||
raise ValueError(f'{property_path} does not exist in config!')
|
raise ValueError(f"{property_path} does not exist in config!")
|
||||||
cur_part = cur_part[cur_path]
|
cur_part = cur_part[cur_path]
|
||||||
|
|
||||||
return config_copy
|
return config_copy
|
||||||
@@ -110,7 +110,7 @@ def add_item_to_config(config, property_path, value):
|
|||||||
"""
|
"""
|
||||||
Add an item to a list in config.
|
Add an item to a list in config.
|
||||||
"""
|
"""
|
||||||
path_components = property_path.split('.')
|
path_components = property_path.split(".")
|
||||||
|
|
||||||
# Mutate a copy of the config, not config itself
|
# Mutate a copy of the config, not config itself
|
||||||
cur_part = config_copy = deepcopy(config)
|
cur_part = config_copy = deepcopy(config)
|
||||||
@@ -136,7 +136,7 @@ def remove_item_from_config(config, property_path, value):
|
|||||||
"""
|
"""
|
||||||
Remove an item from a list in config.
|
Remove an item from a list in config.
|
||||||
"""
|
"""
|
||||||
path_components = property_path.split('.')
|
path_components = property_path.split(".")
|
||||||
|
|
||||||
# Mutate a copy of the config, not config itself
|
# Mutate a copy of the config, not config itself
|
||||||
cur_part = config_copy = deepcopy(config)
|
cur_part = config_copy = deepcopy(config)
|
||||||
@@ -144,12 +144,12 @@ def remove_item_from_config(config, property_path, value):
|
|||||||
if i == len(path_components) - 1:
|
if i == len(path_components) - 1:
|
||||||
# Final component, it must be a list and we delete from it
|
# Final component, it must be a list and we delete from it
|
||||||
if cur_path not in cur_part or not _is_list(cur_part[cur_path]):
|
if cur_path not in cur_part or not _is_list(cur_part[cur_path]):
|
||||||
raise ValueError(f'{property_path} is not a list')
|
raise ValueError(f"{property_path} is not a list")
|
||||||
cur_part = cur_part[cur_path]
|
cur_part = cur_part[cur_path]
|
||||||
cur_part.remove(value)
|
cur_part.remove(value)
|
||||||
else:
|
else:
|
||||||
if cur_path not in cur_part or not _is_dict(cur_part[cur_path]):
|
if cur_path not in cur_part or not _is_dict(cur_part[cur_path]):
|
||||||
raise ValueError(f'{property_path} does not exist in config!')
|
raise ValueError(f"{property_path} does not exist in config!")
|
||||||
cur_part = cur_part[cur_path]
|
cur_part = cur_part[cur_path]
|
||||||
|
|
||||||
return config_copy
|
return config_copy
|
||||||
@@ -182,7 +182,7 @@ def set_config_value(config_path, key_path, value):
|
|||||||
|
|
||||||
config = set_item_in_config(config, key_path, value)
|
config = set_item_in_config(config, key_path, value)
|
||||||
|
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ def unset_config_value(config_path, key_path):
|
|||||||
|
|
||||||
config = unset_item_from_config(config, key_path)
|
config = unset_item_from_config(config, key_path)
|
||||||
|
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ def add_config_value(config_path, key_path, value):
|
|||||||
|
|
||||||
config = add_item_to_config(config, key_path, value)
|
config = add_item_to_config(config, key_path, value)
|
||||||
|
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
@@ -236,19 +236,19 @@ def remove_config_value(config_path, key_path, value):
|
|||||||
|
|
||||||
config = remove_item_from_config(config, key_path, value)
|
config = remove_item_from_config(config, key_path, value)
|
||||||
|
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def check_hub_ready():
|
def check_hub_ready():
|
||||||
from .configurer import load_config
|
from .configurer import load_config
|
||||||
|
|
||||||
base_url = load_config()['base_url']
|
base_url = load_config()["base_url"]
|
||||||
base_url = base_url[:-1] if base_url[-1] == '/' else base_url
|
base_url = base_url[:-1] if base_url[-1] == "/" else base_url
|
||||||
http_port = load_config()['http']['port']
|
http_port = load_config()["http"]["port"]
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
'http://127.0.0.1:%d%s/hub/api' % (http_port, base_url), verify=False
|
"http://127.0.0.1:%d%s/hub/api" % (http_port, base_url), verify=False
|
||||||
)
|
)
|
||||||
return r.status_code == 200
|
return r.status_code == 200
|
||||||
except:
|
except:
|
||||||
@@ -264,33 +264,33 @@ def reload_component(component):
|
|||||||
# import here to avoid circular imports
|
# import here to avoid circular imports
|
||||||
from tljh import systemd, traefik
|
from tljh import systemd, traefik
|
||||||
|
|
||||||
if component == 'hub':
|
if component == "hub":
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service("jupyterhub")
|
||||||
# Ensure hub is back up
|
# Ensure hub is back up
|
||||||
while not systemd.check_service_active('jupyterhub'):
|
while not systemd.check_service_active("jupyterhub"):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
while not check_hub_ready():
|
while not check_hub_ready():
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print('Hub reload with new configuration complete')
|
print("Hub reload with new configuration complete")
|
||||||
elif component == 'proxy':
|
elif component == "proxy":
|
||||||
traefik.ensure_traefik_config(STATE_DIR)
|
traefik.ensure_traefik_config(STATE_DIR)
|
||||||
systemd.restart_service('traefik')
|
systemd.restart_service("traefik")
|
||||||
while not systemd.check_service_active('traefik'):
|
while not systemd.check_service_active("traefik"):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print('Proxy reload with new configuration complete')
|
print("Proxy reload with new configuration complete")
|
||||||
|
|
||||||
|
|
||||||
def parse_value(value_str):
|
def parse_value(value_str):
|
||||||
"""Parse a value string"""
|
"""Parse a value string"""
|
||||||
if value_str is None:
|
if value_str is None:
|
||||||
return value_str
|
return value_str
|
||||||
if re.match(r'^\d+$', value_str):
|
if re.match(r"^\d+$", value_str):
|
||||||
return int(value_str)
|
return int(value_str)
|
||||||
elif re.match(r'^\d+\.\d*$', value_str):
|
elif re.match(r"^\d+\.\d*$", value_str):
|
||||||
return float(value_str)
|
return float(value_str)
|
||||||
elif value_str.lower() == 'true':
|
elif value_str.lower() == "true":
|
||||||
return True
|
return True
|
||||||
elif value_str.lower() == 'false':
|
elif value_str.lower() == "false":
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# it's a string
|
# it's a string
|
||||||
@@ -327,67 +327,67 @@ def main(argv=None):
|
|||||||
|
|
||||||
argparser = argparse.ArgumentParser()
|
argparser = argparse.ArgumentParser()
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'--config-path', default=CONFIG_FILE, help='Path to TLJH config.yaml file'
|
"--config-path", default=CONFIG_FILE, help="Path to TLJH config.yaml file"
|
||||||
)
|
)
|
||||||
subparsers = argparser.add_subparsers(dest='action')
|
subparsers = argparser.add_subparsers(dest="action")
|
||||||
|
|
||||||
show_parser = subparsers.add_parser('show', help='Show current configuration')
|
show_parser = subparsers.add_parser("show", help="Show current configuration")
|
||||||
|
|
||||||
unset_parser = subparsers.add_parser('unset', help='Unset a configuration property')
|
unset_parser = subparsers.add_parser("unset", help="Unset a configuration property")
|
||||||
unset_parser.add_argument(
|
unset_parser.add_argument(
|
||||||
'key_path', help='Dot separated path to configuration key to unset'
|
"key_path", help="Dot separated path to configuration key to unset"
|
||||||
)
|
)
|
||||||
|
|
||||||
set_parser = subparsers.add_parser('set', help='Set a configuration property')
|
set_parser = subparsers.add_parser("set", help="Set a configuration property")
|
||||||
set_parser.add_argument(
|
set_parser.add_argument(
|
||||||
'key_path', help='Dot separated path to configuration key to set'
|
"key_path", help="Dot separated path to configuration key to set"
|
||||||
)
|
)
|
||||||
set_parser.add_argument('value', help='Value to set the configuration key to')
|
set_parser.add_argument("value", help="Value to set the configuration key to")
|
||||||
|
|
||||||
add_item_parser = subparsers.add_parser(
|
add_item_parser = subparsers.add_parser(
|
||||||
'add-item', help='Add a value to a list for a configuration property'
|
"add-item", help="Add a value to a list for a configuration property"
|
||||||
)
|
)
|
||||||
add_item_parser.add_argument(
|
add_item_parser.add_argument(
|
||||||
'key_path', help='Dot separated path to configuration key to add value to'
|
"key_path", help="Dot separated path to configuration key to add value to"
|
||||||
)
|
)
|
||||||
add_item_parser.add_argument('value', help='Value to add to the configuration key')
|
add_item_parser.add_argument("value", help="Value to add to the configuration key")
|
||||||
|
|
||||||
remove_item_parser = subparsers.add_parser(
|
remove_item_parser = subparsers.add_parser(
|
||||||
'remove-item', help='Remove a value from a list for a configuration property'
|
"remove-item", help="Remove a value from a list for a configuration property"
|
||||||
)
|
)
|
||||||
remove_item_parser.add_argument(
|
remove_item_parser.add_argument(
|
||||||
'key_path', help='Dot separated path to configuration key to remove value from'
|
"key_path", help="Dot separated path to configuration key to remove value from"
|
||||||
)
|
)
|
||||||
remove_item_parser.add_argument('value', help='Value to remove from key_path')
|
remove_item_parser.add_argument("value", help="Value to remove from key_path")
|
||||||
|
|
||||||
reload_parser = subparsers.add_parser(
|
reload_parser = subparsers.add_parser(
|
||||||
'reload', help='Reload a component to apply configuration change'
|
"reload", help="Reload a component to apply configuration change"
|
||||||
)
|
)
|
||||||
reload_parser.add_argument(
|
reload_parser.add_argument(
|
||||||
'component',
|
"component",
|
||||||
choices=('hub', 'proxy'),
|
choices=("hub", "proxy"),
|
||||||
help='Which component to reload',
|
help="Which component to reload",
|
||||||
default='hub',
|
default="hub",
|
||||||
nargs='?',
|
nargs="?",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = argparser.parse_args(argv)
|
args = argparser.parse_args(argv)
|
||||||
|
|
||||||
if args.action == 'show':
|
if args.action == "show":
|
||||||
show_config(args.config_path)
|
show_config(args.config_path)
|
||||||
elif args.action == 'set':
|
elif args.action == "set":
|
||||||
set_config_value(args.config_path, args.key_path, parse_value(args.value))
|
set_config_value(args.config_path, args.key_path, parse_value(args.value))
|
||||||
elif args.action == 'unset':
|
elif args.action == "unset":
|
||||||
unset_config_value(args.config_path, args.key_path)
|
unset_config_value(args.config_path, args.key_path)
|
||||||
elif args.action == 'add-item':
|
elif args.action == "add-item":
|
||||||
add_config_value(args.config_path, args.key_path, parse_value(args.value))
|
add_config_value(args.config_path, args.key_path, parse_value(args.value))
|
||||||
elif args.action == 'remove-item':
|
elif args.action == "remove-item":
|
||||||
remove_config_value(args.config_path, args.key_path, parse_value(args.value))
|
remove_config_value(args.config_path, args.key_path, parse_value(args.value))
|
||||||
elif args.action == 'reload':
|
elif args.action == "reload":
|
||||||
reload_component(args.component)
|
reload_component(args.component)
|
||||||
else:
|
else:
|
||||||
argparser.print_help()
|
argparser.print_help()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -17,50 +17,50 @@ from .yaml import yaml
|
|||||||
# Default configuration for tljh
|
# Default configuration for tljh
|
||||||
# User provided config is merged into this
|
# User provided config is merged into this
|
||||||
default = {
|
default = {
|
||||||
'base_url': '/',
|
"base_url": "/",
|
||||||
'auth': {
|
"auth": {
|
||||||
'type': 'firstuseauthenticator.FirstUseAuthenticator',
|
"type": "firstuseauthenticator.FirstUseAuthenticator",
|
||||||
'FirstUseAuthenticator': {'create_users': False},
|
"FirstUseAuthenticator": {"create_users": False},
|
||||||
},
|
},
|
||||||
'users': {'allowed': [], 'banned': [], 'admin': [], 'extra_user_groups': {}},
|
"users": {"allowed": [], "banned": [], "admin": [], "extra_user_groups": {}},
|
||||||
'limits': {
|
"limits": {
|
||||||
'memory': None,
|
"memory": None,
|
||||||
'cpu': None,
|
"cpu": None,
|
||||||
},
|
},
|
||||||
'http': {
|
"http": {
|
||||||
'port': 80,
|
"port": 80,
|
||||||
},
|
},
|
||||||
'https': {
|
"https": {
|
||||||
'enabled': False,
|
"enabled": False,
|
||||||
'port': 443,
|
"port": 443,
|
||||||
'tls': {
|
"tls": {
|
||||||
'cert': '',
|
"cert": "",
|
||||||
'key': '',
|
"key": "",
|
||||||
},
|
},
|
||||||
'letsencrypt': {
|
"letsencrypt": {
|
||||||
'email': '',
|
"email": "",
|
||||||
'domains': [],
|
"domains": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'traefik_api': {
|
"traefik_api": {
|
||||||
'ip': "127.0.0.1",
|
"ip": "127.0.0.1",
|
||||||
'port': 8099,
|
"port": 8099,
|
||||||
'username': 'api_admin',
|
"username": "api_admin",
|
||||||
'password': '',
|
"password": "",
|
||||||
},
|
},
|
||||||
'user_environment': {
|
"user_environment": {
|
||||||
'default_app': 'classic',
|
"default_app": "classic",
|
||||||
},
|
},
|
||||||
'services': {
|
"services": {
|
||||||
'cull': {
|
"cull": {
|
||||||
'enabled': True,
|
"enabled": True,
|
||||||
'timeout': 600,
|
"timeout": 600,
|
||||||
'every': 60,
|
"every": 60,
|
||||||
'concurrency': 5,
|
"concurrency": 5,
|
||||||
'users': False,
|
"users": False,
|
||||||
'max_age': 0,
|
"max_age": 0,
|
||||||
},
|
},
|
||||||
'configurator': {'enabled': False},
|
"configurator": {"enabled": False},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,14 +109,14 @@ def set_if_not_none(parent, key, value):
|
|||||||
|
|
||||||
def load_traefik_api_credentials():
|
def load_traefik_api_credentials():
|
||||||
"""Load traefik api secret from a file"""
|
"""Load traefik api secret from a file"""
|
||||||
proxy_secret_path = os.path.join(STATE_DIR, 'traefik-api.secret')
|
proxy_secret_path = os.path.join(STATE_DIR, "traefik-api.secret")
|
||||||
if not os.path.exists(proxy_secret_path):
|
if not os.path.exists(proxy_secret_path):
|
||||||
return {}
|
return {}
|
||||||
with open(proxy_secret_path) as f:
|
with open(proxy_secret_path) as f:
|
||||||
password = f.read()
|
password = f.read()
|
||||||
return {
|
return {
|
||||||
'traefik_api': {
|
"traefik_api": {
|
||||||
'password': password,
|
"password": password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ def update_base_url(c, config):
|
|||||||
"""
|
"""
|
||||||
Update base_url of JupyterHub through tljh config
|
Update base_url of JupyterHub through tljh config
|
||||||
"""
|
"""
|
||||||
c.JupyterHub.base_url = config['base_url']
|
c.JupyterHub.base_url = config["base_url"]
|
||||||
|
|
||||||
|
|
||||||
def update_auth(c, config):
|
def update_auth(c, config):
|
||||||
@@ -172,13 +172,13 @@ def update_auth(c, config):
|
|||||||
c.JupyterHub.authenticator_class and any configured value being None won't
|
c.JupyterHub.authenticator_class and any configured value being None won't
|
||||||
be set.
|
be set.
|
||||||
"""
|
"""
|
||||||
tljh_auth_config = config['auth']
|
tljh_auth_config = config["auth"]
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = tljh_auth_config['type']
|
c.JupyterHub.authenticator_class = tljh_auth_config["type"]
|
||||||
|
|
||||||
for auth_key, auth_value in tljh_auth_config.items():
|
for auth_key, auth_value in tljh_auth_config.items():
|
||||||
if not (auth_key[0] == auth_key[0].upper() and isinstance(auth_value, dict)):
|
if not (auth_key[0] == auth_key[0].upper() and isinstance(auth_value, dict)):
|
||||||
if auth_key == 'type':
|
if auth_key == "type":
|
||||||
continue
|
continue
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Error: auth.{auth_key} was ignored, it didn't look like a valid configuration"
|
f"Error: auth.{auth_key} was ignored, it didn't look like a valid configuration"
|
||||||
@@ -194,75 +194,75 @@ def update_userlists(c, config):
|
|||||||
"""
|
"""
|
||||||
Set user whitelists & admin lists
|
Set user whitelists & admin lists
|
||||||
"""
|
"""
|
||||||
users = config['users']
|
users = config["users"]
|
||||||
|
|
||||||
c.Authenticator.allowed_users = set(users['allowed'])
|
c.Authenticator.allowed_users = set(users["allowed"])
|
||||||
c.Authenticator.blocked_users = set(users['banned'])
|
c.Authenticator.blocked_users = set(users["banned"])
|
||||||
c.Authenticator.admin_users = set(users['admin'])
|
c.Authenticator.admin_users = set(users["admin"])
|
||||||
|
|
||||||
|
|
||||||
def update_usergroups(c, config):
|
def update_usergroups(c, config):
|
||||||
"""
|
"""
|
||||||
Set user groups
|
Set user groups
|
||||||
"""
|
"""
|
||||||
users = config['users']
|
users = config["users"]
|
||||||
c.UserCreatingSpawner.user_groups = users['extra_user_groups']
|
c.UserCreatingSpawner.user_groups = users["extra_user_groups"]
|
||||||
|
|
||||||
|
|
||||||
def update_limits(c, config):
|
def update_limits(c, config):
|
||||||
"""
|
"""
|
||||||
Set user server limits
|
Set user server limits
|
||||||
"""
|
"""
|
||||||
limits = config['limits']
|
limits = config["limits"]
|
||||||
|
|
||||||
c.Spawner.mem_limit = limits['memory']
|
c.Spawner.mem_limit = limits["memory"]
|
||||||
c.Spawner.cpu_limit = limits['cpu']
|
c.Spawner.cpu_limit = limits["cpu"]
|
||||||
|
|
||||||
|
|
||||||
def update_user_environment(c, config):
|
def update_user_environment(c, config):
|
||||||
"""
|
"""
|
||||||
Set user environment configuration
|
Set user environment configuration
|
||||||
"""
|
"""
|
||||||
user_env = config['user_environment']
|
user_env = config["user_environment"]
|
||||||
|
|
||||||
# Set default application users are launched into
|
# Set default application users are launched into
|
||||||
if user_env['default_app'] == 'jupyterlab':
|
if user_env["default_app"] == "jupyterlab":
|
||||||
c.Spawner.default_url = '/lab'
|
c.Spawner.default_url = "/lab"
|
||||||
elif user_env['default_app'] == 'nteract':
|
elif user_env["default_app"] == "nteract":
|
||||||
c.Spawner.default_url = '/nteract'
|
c.Spawner.default_url = "/nteract"
|
||||||
|
|
||||||
|
|
||||||
def update_user_account_config(c, config):
|
def update_user_account_config(c, config):
|
||||||
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
c.SystemdSpawner.username_template = "jupyter-{USERNAME}"
|
||||||
|
|
||||||
|
|
||||||
def update_traefik_api(c, config):
|
def update_traefik_api(c, config):
|
||||||
"""
|
"""
|
||||||
Set traefik api endpoint credentials
|
Set traefik api endpoint credentials
|
||||||
"""
|
"""
|
||||||
c.TraefikTomlProxy.traefik_api_username = config['traefik_api']['username']
|
c.TraefikTomlProxy.traefik_api_username = config["traefik_api"]["username"]
|
||||||
c.TraefikTomlProxy.traefik_api_password = config['traefik_api']['password']
|
c.TraefikTomlProxy.traefik_api_password = config["traefik_api"]["password"]
|
||||||
|
|
||||||
|
|
||||||
def set_cull_idle_service(config):
|
def set_cull_idle_service(config):
|
||||||
"""
|
"""
|
||||||
Set Idle Culler service
|
Set Idle Culler service
|
||||||
"""
|
"""
|
||||||
cull_cmd = [sys.executable, '-m', 'jupyterhub_idle_culler']
|
cull_cmd = [sys.executable, "-m", "jupyterhub_idle_culler"]
|
||||||
cull_config = config['services']['cull']
|
cull_config = config["services"]["cull"]
|
||||||
print()
|
print()
|
||||||
|
|
||||||
cull_cmd += ['--timeout=%d' % cull_config['timeout']]
|
cull_cmd += ["--timeout=%d" % cull_config["timeout"]]
|
||||||
cull_cmd += ['--cull-every=%d' % cull_config['every']]
|
cull_cmd += ["--cull-every=%d" % cull_config["every"]]
|
||||||
cull_cmd += ['--concurrency=%d' % cull_config['concurrency']]
|
cull_cmd += ["--concurrency=%d" % cull_config["concurrency"]]
|
||||||
cull_cmd += ['--max-age=%d' % cull_config['max_age']]
|
cull_cmd += ["--max-age=%d" % cull_config["max_age"]]
|
||||||
if cull_config['users']:
|
if cull_config["users"]:
|
||||||
cull_cmd += ['--cull-users']
|
cull_cmd += ["--cull-users"]
|
||||||
|
|
||||||
cull_service = {
|
cull_service = {
|
||||||
'name': 'cull-idle',
|
"name": "cull-idle",
|
||||||
'admin': True,
|
"admin": True,
|
||||||
'command': cull_cmd,
|
"command": cull_cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
return cull_service
|
return cull_service
|
||||||
@@ -280,9 +280,9 @@ def set_configurator(config):
|
|||||||
f"--Configurator.config_file={HERE}/jupyterhub_configurator_config.py",
|
f"--Configurator.config_file={HERE}/jupyterhub_configurator_config.py",
|
||||||
]
|
]
|
||||||
configurator_service = {
|
configurator_service = {
|
||||||
'name': 'configurator',
|
"name": "configurator",
|
||||||
'url': 'http://127.0.0.1:10101',
|
"url": "http://127.0.0.1:10101",
|
||||||
'command': configurator_cmd,
|
"command": configurator_cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
return configurator_service
|
return configurator_service
|
||||||
@@ -291,9 +291,9 @@ def set_configurator(config):
|
|||||||
def update_services(c, config):
|
def update_services(c, config):
|
||||||
c.JupyterHub.services = []
|
c.JupyterHub.services = []
|
||||||
|
|
||||||
if config['services']['cull']['enabled']:
|
if config["services"]["cull"]["enabled"]:
|
||||||
c.JupyterHub.services.append(set_cull_idle_service(config))
|
c.JupyterHub.services.append(set_cull_idle_service(config))
|
||||||
if config['services']['configurator']['enabled']:
|
if config["services"]["configurator"]["enabled"]:
|
||||||
c.JupyterHub.services.append(set_configurator(config))
|
c.JupyterHub.services.append(set_configurator(config))
|
||||||
|
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ def _merge_dictionaries(a, b, path=None, update=True):
|
|||||||
elif update:
|
elif update:
|
||||||
a[key] = b[key]
|
a[key] = b[key]
|
||||||
else:
|
else:
|
||||||
raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
|
raise Exception("Conflict at %s" % ".".join(path + [str(key)]))
|
||||||
else:
|
else:
|
||||||
a[key] = b[key]
|
a[key] = b[key]
|
||||||
return a
|
return a
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ Hook specifications that pluggy plugins can override
|
|||||||
"""
|
"""
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
hookspec = pluggy.HookspecMarker('tljh')
|
hookspec = pluggy.HookspecMarker("tljh")
|
||||||
hookimpl = pluggy.HookimplMarker('tljh')
|
hookimpl = pluggy.HookimplMarker("tljh")
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
|
|||||||
@@ -46,18 +46,18 @@ def remove_chp():
|
|||||||
Ensure CHP is not running
|
Ensure CHP is not running
|
||||||
"""
|
"""
|
||||||
if os.path.exists("/etc/systemd/system/configurable-http-proxy.service"):
|
if os.path.exists("/etc/systemd/system/configurable-http-proxy.service"):
|
||||||
if systemd.check_service_active('configurable-http-proxy.service'):
|
if systemd.check_service_active("configurable-http-proxy.service"):
|
||||||
try:
|
try:
|
||||||
systemd.stop_service('configurable-http-proxy.service')
|
systemd.stop_service("configurable-http-proxy.service")
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
logger.info("Cannot stop configurable-http-proxy...")
|
logger.info("Cannot stop configurable-http-proxy...")
|
||||||
if systemd.check_service_enabled('configurable-http-proxy.service'):
|
if systemd.check_service_enabled("configurable-http-proxy.service"):
|
||||||
try:
|
try:
|
||||||
systemd.disable_service('configurable-http-proxy.service')
|
systemd.disable_service("configurable-http-proxy.service")
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
logger.info("Cannot disable configurable-http-proxy...")
|
logger.info("Cannot disable configurable-http-proxy...")
|
||||||
try:
|
try:
|
||||||
systemd.uninstall_unit('configurable-http-proxy.service')
|
systemd.uninstall_unit("configurable-http-proxy.service")
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
logger.info("Cannot uninstall configurable-http-proxy...")
|
logger.info("Cannot uninstall configurable-http-proxy...")
|
||||||
|
|
||||||
@@ -70,36 +70,36 @@ def ensure_jupyterhub_service(prefix):
|
|||||||
remove_chp()
|
remove_chp()
|
||||||
systemd.reload_daemon()
|
systemd.reload_daemon()
|
||||||
|
|
||||||
with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f:
|
with open(os.path.join(HERE, "systemd-units", "jupyterhub.service")) as f:
|
||||||
hub_unit_template = f.read()
|
hub_unit_template = f.read()
|
||||||
|
|
||||||
with open(os.path.join(HERE, 'systemd-units', 'traefik.service')) as f:
|
with open(os.path.join(HERE, "systemd-units", "traefik.service")) as f:
|
||||||
traefik_unit_template = f.read()
|
traefik_unit_template = f.read()
|
||||||
|
|
||||||
# Set up proxy / hub secret token if it is not already setup
|
# Set up proxy / hub secret token if it is not already setup
|
||||||
proxy_secret_path = os.path.join(STATE_DIR, 'traefik-api.secret')
|
proxy_secret_path = os.path.join(STATE_DIR, "traefik-api.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(secrets.token_hex(32))
|
f.write(secrets.token_hex(32))
|
||||||
|
|
||||||
traefik.ensure_traefik_config(STATE_DIR)
|
traefik.ensure_traefik_config(STATE_DIR)
|
||||||
|
|
||||||
unit_params = dict(
|
unit_params = dict(
|
||||||
python_interpreter_path=sys.executable,
|
python_interpreter_path=sys.executable,
|
||||||
jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'),
|
jupyterhub_config_path=os.path.join(HERE, "jupyterhub_config.py"),
|
||||||
install_prefix=INSTALL_PREFIX,
|
install_prefix=INSTALL_PREFIX,
|
||||||
)
|
)
|
||||||
systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params))
|
systemd.install_unit("jupyterhub.service", hub_unit_template.format(**unit_params))
|
||||||
systemd.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
systemd.install_unit("traefik.service", traefik_unit_template.format(**unit_params))
|
||||||
systemd.reload_daemon()
|
systemd.reload_daemon()
|
||||||
|
|
||||||
# If JupyterHub is running, we want to restart it.
|
# If JupyterHub is running, we want to restart it.
|
||||||
systemd.restart_service('jupyterhub')
|
systemd.restart_service("jupyterhub")
|
||||||
systemd.restart_service('traefik')
|
systemd.restart_service("traefik")
|
||||||
|
|
||||||
# Mark JupyterHub & traefik to start at boot time
|
# Mark JupyterHub & traefik to start at boot time
|
||||||
systemd.enable_service('jupyterhub')
|
systemd.enable_service("jupyterhub")
|
||||||
systemd.enable_service('traefik')
|
systemd.enable_service("traefik")
|
||||||
|
|
||||||
|
|
||||||
def ensure_jupyterhub_package(prefix):
|
def ensure_jupyterhub_package(prefix):
|
||||||
@@ -115,8 +115,8 @@ def ensure_jupyterhub_package(prefix):
|
|||||||
# Install pycurl. JupyterHub prefers pycurl over SimpleHTTPClient automatically
|
# Install pycurl. JupyterHub prefers pycurl over SimpleHTTPClient automatically
|
||||||
# pycurl is generally more bugfree - see https://github.com/jupyterhub/the-littlest-jupyterhub/issues/289
|
# pycurl is generally more bugfree - see https://github.com/jupyterhub/the-littlest-jupyterhub/issues/289
|
||||||
# build-essential is also generally useful to everyone involved, and required for pycurl
|
# build-essential is also generally useful to everyone involved, and required for pycurl
|
||||||
apt.install_packages(['libssl-dev', 'libcurl4-openssl-dev', 'build-essential'])
|
apt.install_packages(["libssl-dev", "libcurl4-openssl-dev", "build-essential"])
|
||||||
conda.ensure_pip_packages(prefix, ['pycurl==7.*'], upgrade=True)
|
conda.ensure_pip_packages(prefix, ["pycurl==7.*"], upgrade=True)
|
||||||
|
|
||||||
conda.ensure_pip_packages(
|
conda.ensure_pip_packages(
|
||||||
prefix,
|
prefix,
|
||||||
@@ -140,17 +140,17 @@ def ensure_usergroups():
|
|||||||
"""
|
"""
|
||||||
Sets up user groups & sudo rules
|
Sets up user groups & sudo rules
|
||||||
"""
|
"""
|
||||||
user.ensure_group('jupyterhub-admins')
|
user.ensure_group("jupyterhub-admins")
|
||||||
user.ensure_group('jupyterhub-users')
|
user.ensure_group("jupyterhub-users")
|
||||||
|
|
||||||
logger.info("Granting passwordless sudo to JupyterHub admins...")
|
logger.info("Granting passwordless sudo to JupyterHub admins...")
|
||||||
with open('/etc/sudoers.d/jupyterhub-admins', 'w') as f:
|
with open("/etc/sudoers.d/jupyterhub-admins", "w") as f:
|
||||||
# JupyterHub admins should have full passwordless sudo access
|
# JupyterHub admins should have full passwordless sudo access
|
||||||
f.write('%jupyterhub-admins ALL = (ALL) NOPASSWD: ALL\n')
|
f.write("%jupyterhub-admins ALL = (ALL) NOPASSWD: ALL\n")
|
||||||
# `sudo -E` should preserve the $PATH we set. This allows
|
# `sudo -E` should preserve the $PATH we set. This allows
|
||||||
# admins in jupyter terminals to do `sudo -E pip install <package>`,
|
# admins in jupyter terminals to do `sudo -E pip install <package>`,
|
||||||
# `pip` is in the $PATH we set in jupyterhub_config.py to include the user conda env.
|
# `pip` is in the $PATH we set in jupyterhub_config.py to include the user conda env.
|
||||||
f.write('Defaults exempt_group = jupyterhub-admins\n')
|
f.write("Defaults exempt_group = jupyterhub-admins\n")
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_environment(user_requirements_txt_file):
|
def ensure_user_environment(user_requirements_txt_file):
|
||||||
@@ -159,50 +159,58 @@ def ensure_user_environment(user_requirements_txt_file):
|
|||||||
"""
|
"""
|
||||||
logger.info("Setting up user environment...")
|
logger.info("Setting up user environment...")
|
||||||
|
|
||||||
miniconda_old_version = '4.5.4'
|
miniconda_old_version = "4.5.4"
|
||||||
miniconda_new_version = '4.7.10'
|
miniconda_new_version = "4.7.10"
|
||||||
# Install mambaforge using an installer from
|
# Install mambaforge using an installer from
|
||||||
# https://github.com/conda-forge/miniforge/releases
|
# https://github.com/conda-forge/miniforge/releases
|
||||||
mambaforge_new_version = '4.10.3-7'
|
mambaforge_new_version = "4.10.3-7"
|
||||||
# Check system architecture, set appropriate installer checksum
|
# Check system architecture, set appropriate installer checksum
|
||||||
if os.uname().machine == 'aarch64':
|
if os.uname().machine == "aarch64":
|
||||||
installer_sha256 = "ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
installer_sha256 = (
|
||||||
elif os.uname().machine == 'x86_64':
|
"ac95f137b287b3408e4f67f07a284357b1119ee157373b788b34e770ef2392b2"
|
||||||
installer_sha256 = "fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
)
|
||||||
|
elif os.uname().machine == "x86_64":
|
||||||
|
installer_sha256 = (
|
||||||
|
"fc872522ec427fcab10167a93e802efaf251024b58cc27b084b915a9a73c4474"
|
||||||
|
)
|
||||||
# Check OS, set appropriate string for conda installer path
|
# Check OS, set appropriate string for conda installer path
|
||||||
if os.uname().sysname != 'Linux':
|
if os.uname().sysname != "Linux":
|
||||||
raise OSError("TLJH is only supported on Linux platforms.")
|
raise OSError("TLJH is only supported on Linux platforms.")
|
||||||
# Then run `mamba --version` to get the conda and mamba versions
|
# Then run `mamba --version` to get the conda and mamba versions
|
||||||
# Keep these in sync with tests/test_conda.py::prefix
|
# Keep these in sync with tests/test_conda.py::prefix
|
||||||
mambaforge_conda_new_version = '4.10.3'
|
mambaforge_conda_new_version = "4.10.3"
|
||||||
mambaforge_mamba_version = '0.16.0'
|
mambaforge_mamba_version = "0.16.0"
|
||||||
|
|
||||||
if conda.check_miniconda_version(USER_ENV_PREFIX, mambaforge_conda_new_version):
|
if conda.check_miniconda_version(USER_ENV_PREFIX, mambaforge_conda_new_version):
|
||||||
conda_version = '4.10.3'
|
conda_version = "4.10.3"
|
||||||
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_new_version):
|
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_new_version):
|
||||||
conda_version = '4.8.1'
|
conda_version = "4.8.1"
|
||||||
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_old_version):
|
elif conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_old_version):
|
||||||
conda_version = '4.5.8'
|
conda_version = "4.5.8"
|
||||||
# If no prior miniconda installation is found, we can install a newer version
|
# If no prior miniconda installation is found, we can install a newer version
|
||||||
else:
|
else:
|
||||||
logger.info('Downloading & setting up user environment...')
|
logger.info("Downloading & setting up user environment...")
|
||||||
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(v=mambaforge_new_version, arch=os.uname().machine)
|
installer_url = "https://github.com/conda-forge/miniforge/releases/download/{v}/Mambaforge-{v}-Linux-{arch}.sh".format(
|
||||||
with conda.download_miniconda_installer(installer_url, installer_sha256) as installer_path:
|
v=mambaforge_new_version, arch=os.uname().machine
|
||||||
|
)
|
||||||
|
with conda.download_miniconda_installer(
|
||||||
|
installer_url, installer_sha256
|
||||||
|
) as installer_path:
|
||||||
conda.install_miniconda(installer_path, USER_ENV_PREFIX)
|
conda.install_miniconda(installer_path, USER_ENV_PREFIX)
|
||||||
conda_version = '4.10.3'
|
conda_version = "4.10.3"
|
||||||
|
|
||||||
conda.ensure_conda_packages(
|
conda.ensure_conda_packages(
|
||||||
USER_ENV_PREFIX,
|
USER_ENV_PREFIX,
|
||||||
[
|
[
|
||||||
# Conda's latest version is on conda much more so than on PyPI.
|
# Conda's latest version is on conda much more so than on PyPI.
|
||||||
'conda==' + conda_version,
|
"conda==" + conda_version,
|
||||||
'mamba==' + mambaforge_mamba_version,
|
"mamba==" + mambaforge_mamba_version,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
conda.ensure_pip_requirements(
|
conda.ensure_pip_requirements(
|
||||||
USER_ENV_PREFIX,
|
USER_ENV_PREFIX,
|
||||||
os.path.join(HERE, 'requirements-base.txt'),
|
os.path.join(HERE, "requirements-base.txt"),
|
||||||
upgrade=True,
|
upgrade=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,25 +239,25 @@ def ensure_admins(admin_password_list):
|
|||||||
else:
|
else:
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
config['users'] = config.get('users', {})
|
config["users"] = config.get("users", {})
|
||||||
|
|
||||||
db_passw = os.path.join(STATE_DIR, 'passwords.dbm')
|
db_passw = os.path.join(STATE_DIR, "passwords.dbm")
|
||||||
|
|
||||||
admins = []
|
admins = []
|
||||||
for admin_password_entry in admin_password_list:
|
for admin_password_entry in admin_password_list:
|
||||||
for admin_password_pair in admin_password_entry:
|
for admin_password_pair in admin_password_entry:
|
||||||
if ":" in admin_password_pair:
|
if ":" in admin_password_pair:
|
||||||
admin, password = admin_password_pair.split(':')
|
admin, password = admin_password_pair.split(":")
|
||||||
admins.append(admin)
|
admins.append(admin)
|
||||||
# Add admin:password to the db
|
# Add admin:password to the db
|
||||||
password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
||||||
with dbm.open(db_passw, 'c', 0o600) as db:
|
with dbm.open(db_passw, "c", 0o600) as db:
|
||||||
db[admin] = password
|
db[admin] = password
|
||||||
else:
|
else:
|
||||||
admins.append(admin_password_pair)
|
admins.append(admin_password_pair)
|
||||||
config['users']['admin'] = admins
|
config["users"]["admin"] = admins
|
||||||
|
|
||||||
with open(config_path, 'w+') as f:
|
with open(config_path, "w+") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
@@ -262,12 +270,12 @@ def ensure_jupyterhub_running(times=20):
|
|||||||
|
|
||||||
for i in range(times):
|
for i in range(times):
|
||||||
try:
|
try:
|
||||||
logger.info(f'Waiting for JupyterHub to come up ({i + 1}/{times} tries)')
|
logger.info(f"Waiting for JupyterHub to come up ({i + 1}/{times} tries)")
|
||||||
# Because we don't care at this level that SSL is valid, we can suppress
|
# Because we don't care at this level that SSL is valid, we can suppress
|
||||||
# InsecureRequestWarning for this request.
|
# InsecureRequestWarning for this request.
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
|
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
|
||||||
requests.get('http://127.0.0.1', verify=False)
|
requests.get("http://127.0.0.1", verify=False)
|
||||||
return
|
return
|
||||||
except requests.HTTPError as h:
|
except requests.HTTPError as h:
|
||||||
if h.response.status_code in [404, 502, 503]:
|
if h.response.status_code in [404, 502, 503]:
|
||||||
@@ -299,15 +307,15 @@ def ensure_symlinks(prefix):
|
|||||||
around this with sudo -E and extra entries in the sudoers file, but this
|
around this with sudo -E and extra entries in the sudoers file, but this
|
||||||
is far more secure at the cost of upsetting some FHS purists.
|
is far more secure at the cost of upsetting some FHS purists.
|
||||||
"""
|
"""
|
||||||
tljh_config_src = os.path.join(prefix, 'bin', 'tljh-config')
|
tljh_config_src = os.path.join(prefix, "bin", "tljh-config")
|
||||||
tljh_config_dest = '/usr/bin/tljh-config'
|
tljh_config_dest = "/usr/bin/tljh-config"
|
||||||
if os.path.exists(tljh_config_dest):
|
if os.path.exists(tljh_config_dest):
|
||||||
if os.path.realpath(tljh_config_dest) != tljh_config_src:
|
if os.path.realpath(tljh_config_dest) != tljh_config_src:
|
||||||
# tljh-config exists that isn't ours. We should *not* delete this file,
|
# tljh-config exists that isn't ours. We should *not* delete this file,
|
||||||
# instead we throw an error and abort. Deleting files owned by other people
|
# instead we throw an error and abort. Deleting files owned by other people
|
||||||
# while running as root is dangerous, especially with symlinks involved.
|
# while running as root is dangerous, especially with symlinks involved.
|
||||||
raise FileExistsError(
|
raise FileExistsError(
|
||||||
f'/usr/bin/tljh-config exists but is not a symlink to {tljh_config_src}'
|
f"/usr/bin/tljh-config exists but is not a symlink to {tljh_config_src}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# We have a working symlink, so do nothing
|
# We have a working symlink, so do nothing
|
||||||
@@ -324,9 +332,9 @@ def setup_plugins(plugins=None):
|
|||||||
conda.ensure_pip_packages(HUB_ENV_PREFIX, plugins, upgrade=True)
|
conda.ensure_pip_packages(HUB_ENV_PREFIX, plugins, upgrade=True)
|
||||||
|
|
||||||
# Set up plugin infrastructure
|
# Set up plugin infrastructure
|
||||||
pm = pluggy.PluginManager('tljh')
|
pm = pluggy.PluginManager("tljh")
|
||||||
pm.add_hookspecs(hooks)
|
pm.add_hookspecs(hooks)
|
||||||
pm.load_setuptools_entrypoints('tljh')
|
pm.load_setuptools_entrypoints("tljh")
|
||||||
|
|
||||||
return pm
|
return pm
|
||||||
|
|
||||||
@@ -340,8 +348,8 @@ def run_plugin_actions(plugin_manager):
|
|||||||
apt_packages = list(set(itertools.chain(*hook.tljh_extra_apt_packages())))
|
apt_packages = list(set(itertools.chain(*hook.tljh_extra_apt_packages())))
|
||||||
if apt_packages:
|
if apt_packages:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Installing {} apt packages collected from plugins: {}'.format(
|
"Installing {} apt packages collected from plugins: {}".format(
|
||||||
len(apt_packages), ' '.join(apt_packages)
|
len(apt_packages), " ".join(apt_packages)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
apt.install_packages(apt_packages)
|
apt.install_packages(apt_packages)
|
||||||
@@ -350,8 +358,8 @@ def run_plugin_actions(plugin_manager):
|
|||||||
hub_pip_packages = list(set(itertools.chain(*hook.tljh_extra_hub_pip_packages())))
|
hub_pip_packages = list(set(itertools.chain(*hook.tljh_extra_hub_pip_packages())))
|
||||||
if hub_pip_packages:
|
if hub_pip_packages:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Installing {} hub pip packages collected from plugins: {}'.format(
|
"Installing {} hub pip packages collected from plugins: {}".format(
|
||||||
len(hub_pip_packages), ' '.join(hub_pip_packages)
|
len(hub_pip_packages), " ".join(hub_pip_packages)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
conda.ensure_pip_packages(
|
conda.ensure_pip_packages(
|
||||||
@@ -364,8 +372,8 @@ def run_plugin_actions(plugin_manager):
|
|||||||
conda_packages = list(set(itertools.chain(*hook.tljh_extra_user_conda_packages())))
|
conda_packages = list(set(itertools.chain(*hook.tljh_extra_user_conda_packages())))
|
||||||
if conda_packages:
|
if conda_packages:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Installing {} user conda packages collected from plugins: {}'.format(
|
"Installing {} user conda packages collected from plugins: {}".format(
|
||||||
len(conda_packages), ' '.join(conda_packages)
|
len(conda_packages), " ".join(conda_packages)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
conda.ensure_conda_packages(USER_ENV_PREFIX, conda_packages)
|
conda.ensure_conda_packages(USER_ENV_PREFIX, conda_packages)
|
||||||
@@ -374,8 +382,8 @@ def run_plugin_actions(plugin_manager):
|
|||||||
user_pip_packages = list(set(itertools.chain(*hook.tljh_extra_user_pip_packages())))
|
user_pip_packages = list(set(itertools.chain(*hook.tljh_extra_user_pip_packages())))
|
||||||
if user_pip_packages:
|
if user_pip_packages:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Installing {} user pip packages collected from plugins: {}'.format(
|
"Installing {} user pip packages collected from plugins: {}".format(
|
||||||
len(user_pip_packages), ' '.join(user_pip_packages)
|
len(user_pip_packages), " ".join(user_pip_packages)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
conda.ensure_pip_packages(
|
conda.ensure_pip_packages(
|
||||||
@@ -393,7 +401,7 @@ def ensure_config_yaml(plugin_manager):
|
|||||||
Ensure we have a config.yaml present
|
Ensure we have a config.yaml present
|
||||||
"""
|
"""
|
||||||
# ensure config dir exists and is private
|
# ensure config dir exists and is private
|
||||||
for path in [CONFIG_DIR, os.path.join(CONFIG_DIR, 'jupyterhub_config.d')]:
|
for path in [CONFIG_DIR, os.path.join(CONFIG_DIR, "jupyterhub_config.d")]:
|
||||||
os.makedirs(path, mode=0o700, exist_ok=True)
|
os.makedirs(path, mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
migrator.migrate_config_files()
|
migrator.migrate_config_files()
|
||||||
@@ -407,7 +415,7 @@ def ensure_config_yaml(plugin_manager):
|
|||||||
hook = plugin_manager.hook
|
hook = plugin_manager.hook
|
||||||
hook.tljh_config_post_install(config=config)
|
hook.tljh_config_post_install(config=config)
|
||||||
|
|
||||||
with open(CONFIG_FILE, 'w+') as f:
|
with open(CONFIG_FILE, "w+") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
@@ -418,17 +426,17 @@ def main():
|
|||||||
|
|
||||||
argparser = argparse.ArgumentParser()
|
argparser = argparse.ArgumentParser()
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'--admin', nargs='*', action='append', help='List of usernames set to be admin'
|
"--admin", nargs="*", action="append", help="List of usernames set to be admin"
|
||||||
)
|
)
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'--user-requirements-txt-url',
|
"--user-requirements-txt-url",
|
||||||
help='URL to a requirements.txt file that should be installed in the user environment',
|
help="URL to a requirements.txt file that should be installed in the user environment",
|
||||||
)
|
)
|
||||||
argparser.add_argument('--plugin', nargs='*', help='Plugin pip-specs to install')
|
argparser.add_argument("--plugin", nargs="*", help="Plugin pip-specs to install")
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'--progress-page-server-pid',
|
"--progress-page-server-pid",
|
||||||
type=int,
|
type=int,
|
||||||
help='The pid of the progress page server',
|
help="The pid of the progress page server",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
@@ -463,5 +471,5 @@ def main():
|
|||||||
logger.info("Done!")
|
logger.info("Done!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ c.JupyterHub.hub_port = 15001
|
|||||||
|
|
||||||
c.TraefikTomlProxy.should_start = False
|
c.TraefikTomlProxy.should_start = False
|
||||||
|
|
||||||
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, 'state', 'rules', 'rules.toml')
|
dynamic_conf_file_path = os.path.join(INSTALL_PREFIX, "state", "rules", "rules.toml")
|
||||||
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
c.TraefikTomlProxy.toml_dynamic_config_file = dynamic_conf_file_path
|
||||||
c.JupyterHub.proxy_class = TraefikTomlProxy
|
c.JupyterHub.proxy_class = TraefikTomlProxy
|
||||||
|
|
||||||
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')]
|
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, "bin")]
|
||||||
c.SystemdSpawner.default_shell = '/bin/bash'
|
c.SystemdSpawner.default_shell = "/bin/bash"
|
||||||
# Drop the '-singleuser' suffix present in the default template
|
# Drop the '-singleuser' suffix present in the default template
|
||||||
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}'
|
c.SystemdSpawner.unit_name_template = "jupyter-{USERNAME}"
|
||||||
|
|
||||||
tljh_config = configurer.load_config()
|
tljh_config = configurer.load_config()
|
||||||
configurer.apply_config(tljh_config, c)
|
configurer.apply_config(tljh_config, c)
|
||||||
@@ -41,6 +41,6 @@ pm.hook.tljh_custom_jupyterhub_config(c=c)
|
|||||||
|
|
||||||
# Load arbitrary .py config files if they exist.
|
# Load arbitrary .py config files if they exist.
|
||||||
# This is our escape hatch
|
# This is our escape hatch
|
||||||
extra_configs = sorted(glob(os.path.join(CONFIG_DIR, 'jupyterhub_config.d', '*.py')))
|
extra_configs = sorted(glob(os.path.join(CONFIG_DIR, "jupyterhub_config.d", "*.py")))
|
||||||
for ec in extra_configs:
|
for ec in extra_configs:
|
||||||
load_subconfig(ec)
|
load_subconfig(ec)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def generate_system_username(username):
|
|||||||
if len(username) < 26:
|
if len(username) < 26:
|
||||||
return username
|
return username
|
||||||
|
|
||||||
userhash = hashlib.sha256(username.encode('utf-8')).hexdigest()
|
userhash = hashlib.sha256(username.encode("utf-8")).hexdigest()
|
||||||
return '{username_trunc}-{hash}'.format(
|
return "{username_trunc}-{hash}".format(
|
||||||
username_trunc=username[:26], hash=userhash[:5]
|
username_trunc=username[:26], hash=userhash[:5]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,43 +13,43 @@ def reload_daemon():
|
|||||||
|
|
||||||
Makes systemd discover new units.
|
Makes systemd discover new units.
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'daemon-reload'], check=True)
|
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
||||||
|
|
||||||
|
|
||||||
def install_unit(name, unit, path='/etc/systemd/system'):
|
def install_unit(name, unit, path="/etc/systemd/system"):
|
||||||
"""
|
"""
|
||||||
Install unit with given name
|
Install unit with given name
|
||||||
"""
|
"""
|
||||||
with open(os.path.join(path, name), 'w') as f:
|
with open(os.path.join(path, name), "w") as f:
|
||||||
f.write(unit)
|
f.write(unit)
|
||||||
|
|
||||||
|
|
||||||
def uninstall_unit(name, path='/etc/systemd/system'):
|
def uninstall_unit(name, path="/etc/systemd/system"):
|
||||||
"""
|
"""
|
||||||
Uninstall unit with given name
|
Uninstall unit with given name
|
||||||
"""
|
"""
|
||||||
subprocess.run(['rm', os.path.join(path, name)], check=True)
|
subprocess.run(["rm", os.path.join(path, name)], check=True)
|
||||||
|
|
||||||
|
|
||||||
def start_service(name):
|
def start_service(name):
|
||||||
"""
|
"""
|
||||||
Start service with given name.
|
Start service with given name.
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'start', name], check=True)
|
subprocess.run(["systemctl", "start", name], check=True)
|
||||||
|
|
||||||
|
|
||||||
def stop_service(name):
|
def stop_service(name):
|
||||||
"""
|
"""
|
||||||
Start service with given name.
|
Start service with given name.
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'stop', name], check=True)
|
subprocess.run(["systemctl", "stop", name], check=True)
|
||||||
|
|
||||||
|
|
||||||
def restart_service(name):
|
def restart_service(name):
|
||||||
"""
|
"""
|
||||||
Restart service with given name.
|
Restart service with given name.
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'restart', name], check=True)
|
subprocess.run(["systemctl", "restart", name], check=True)
|
||||||
|
|
||||||
|
|
||||||
def enable_service(name):
|
def enable_service(name):
|
||||||
@@ -58,7 +58,7 @@ def enable_service(name):
|
|||||||
|
|
||||||
This most likely makes the service start on bootup
|
This most likely makes the service start on bootup
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'enable', name], check=True)
|
subprocess.run(["systemctl", "enable", name], check=True)
|
||||||
|
|
||||||
|
|
||||||
def disable_service(name):
|
def disable_service(name):
|
||||||
@@ -67,7 +67,7 @@ def disable_service(name):
|
|||||||
|
|
||||||
This most likely makes the service start on bootup
|
This most likely makes the service start on bootup
|
||||||
"""
|
"""
|
||||||
subprocess.run(['systemctl', 'disable', name], check=True)
|
subprocess.run(["systemctl", "disable", name], check=True)
|
||||||
|
|
||||||
|
|
||||||
def check_service_active(name):
|
def check_service_active(name):
|
||||||
@@ -75,7 +75,7 @@ def check_service_active(name):
|
|||||||
Check if a service is currently active (running)
|
Check if a service is currently active (running)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subprocess.run(['systemctl', 'is-active', name], check=True)
|
subprocess.run(["systemctl", "is-active", name], check=True)
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return False
|
return False
|
||||||
@@ -86,7 +86,7 @@ def check_service_enabled(name):
|
|||||||
Check if a service is enabled
|
Check if a service is enabled
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subprocess.run(['systemctl', 'is-enabled', name], check=True)
|
subprocess.run(["systemctl", "is-enabled", name], check=True)
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from tljh.configurer import load_config, _merge_dictionaries
|
|||||||
# traefik 2.7.x is not supported yet, use v1.7.x for now
|
# traefik 2.7.x is not supported yet, use v1.7.x for now
|
||||||
# see: https://github.com/jupyterhub/traefik-proxy/issues/97
|
# see: https://github.com/jupyterhub/traefik-proxy/issues/97
|
||||||
machine = os.uname().machine
|
machine = os.uname().machine
|
||||||
if machine == 'aarch64':
|
if machine == "aarch64":
|
||||||
plat = "linux-arm64"
|
plat = "linux-arm64"
|
||||||
elif machine == 'x86_64':
|
elif machine == "x86_64":
|
||||||
plat = "linux-amd64"
|
plat = "linux-amd64"
|
||||||
else:
|
else:
|
||||||
raise OSError(f"Error. Platform: {os.uname().sysname} / {machine} Not supported.")
|
raise OSError(f"Error. Platform: {os.uname().sysname} / {machine} Not supported.")
|
||||||
@@ -26,7 +26,7 @@ traefik_version = "1.7.33"
|
|||||||
# record sha256 hashes for supported platforms here
|
# record sha256 hashes for supported platforms here
|
||||||
checksums = {
|
checksums = {
|
||||||
"linux-amd64": "314ffeaa4cd8ed6ab7b779e9b6773987819f79b23c28d7ab60ace4d3683c5935",
|
"linux-amd64": "314ffeaa4cd8ed6ab7b779e9b6773987819f79b23c28d7ab60ace4d3683c5935",
|
||||||
"linux-arm64": "0640fa665125efa6b598fc08c100178e24de66c5c6035ce5d75668d3dc3706e1"
|
"linux-arm64": "0640fa665125efa6b598fc08c100178e24de66c5c6035ce5d75668d3dc3706e1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ def ensure_traefik_binary(prefix):
|
|||||||
response = requests.get(traefik_url)
|
response = requests.get(traefik_url)
|
||||||
if response.status_code == 206:
|
if response.status_code == 206:
|
||||||
raise Exception("ContentTooShort")
|
raise Exception("ContentTooShort")
|
||||||
with open(traefik_bin, 'wb') as f:
|
with open(traefik_bin, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
os.chmod(traefik_bin, 0o755)
|
os.chmod(traefik_bin, 0o755)
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ def compute_basic_auth(username, password):
|
|||||||
|
|
||||||
|
|
||||||
def load_extra_config(extra_config_dir):
|
def load_extra_config(extra_config_dir):
|
||||||
extra_configs = sorted(glob(os.path.join(extra_config_dir, '*.toml')))
|
extra_configs = sorted(glob(os.path.join(extra_config_dir, "*.toml")))
|
||||||
# Load the toml list of files into dicts and merge them
|
# Load the toml list of files into dicts and merge them
|
||||||
config = toml.load(extra_configs)
|
config = toml.load(extra_configs)
|
||||||
return config
|
return config
|
||||||
@@ -102,9 +102,9 @@ def ensure_traefik_config(state_dir):
|
|||||||
traefik_dynamic_config_dir = os.path.join(state_dir, "rules")
|
traefik_dynamic_config_dir = os.path.join(state_dir, "rules")
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
config['traefik_api']['basic_auth'] = compute_basic_auth(
|
config["traefik_api"]["basic_auth"] = compute_basic_auth(
|
||||||
config['traefik_api']['username'],
|
config["traefik_api"]["username"],
|
||||||
config['traefik_api']['password'],
|
config["traefik_api"]["password"],
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
with open(os.path.join(os.path.dirname(__file__), "traefik.toml.tpl")) as f:
|
||||||
|
|||||||
14
tljh/user.py
14
tljh/user.py
@@ -25,9 +25,9 @@ def ensure_user(username):
|
|||||||
# User doesn't exist, time to create!
|
# User doesn't exist, time to create!
|
||||||
pass
|
pass
|
||||||
|
|
||||||
subprocess.check_call(['useradd', '--create-home', username])
|
subprocess.check_call(["useradd", "--create-home", username])
|
||||||
|
|
||||||
subprocess.check_call(['chmod', 'o-rwx', expanduser(f'~{username}')])
|
subprocess.check_call(["chmod", "o-rwx", expanduser(f"~{username}")])
|
||||||
|
|
||||||
pm = get_plugin_manager()
|
pm = get_plugin_manager()
|
||||||
pm.hook.tljh_new_user_create(username=username)
|
pm.hook.tljh_new_user_create(username=username)
|
||||||
@@ -43,14 +43,14 @@ def remove_user(username):
|
|||||||
# User doesn't exist, nothing to do
|
# User doesn't exist, nothing to do
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.check_call(['deluser', '--quiet', username])
|
subprocess.check_call(["deluser", "--quiet", username])
|
||||||
|
|
||||||
|
|
||||||
def ensure_group(groupname):
|
def ensure_group(groupname):
|
||||||
"""
|
"""
|
||||||
Ensure given group exists
|
Ensure given group exists
|
||||||
"""
|
"""
|
||||||
subprocess.check_call(['groupadd', '--force', groupname])
|
subprocess.check_call(["groupadd", "--force", groupname])
|
||||||
|
|
||||||
|
|
||||||
def remove_group(groupname):
|
def remove_group(groupname):
|
||||||
@@ -63,7 +63,7 @@ def remove_group(groupname):
|
|||||||
# Group doesn't exist, nothing to do
|
# Group doesn't exist, nothing to do
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.check_call(['delgroup', '--quiet', groupname])
|
subprocess.check_call(["delgroup", "--quiet", groupname])
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_group(username, groupname):
|
def ensure_user_group(username, groupname):
|
||||||
@@ -76,7 +76,7 @@ def ensure_user_group(username, groupname):
|
|||||||
if username in group.gr_mem:
|
if username in group.gr_mem:
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.check_call(['gpasswd', '--add', username, groupname])
|
subprocess.check_call(["gpasswd", "--add", username, groupname])
|
||||||
|
|
||||||
|
|
||||||
def remove_user_group(username, groupname):
|
def remove_user_group(username, groupname):
|
||||||
@@ -87,4 +87,4 @@ def remove_user_group(username, groupname):
|
|||||||
if username not in group.gr_mem:
|
if username not in group.gr_mem:
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.check_call(['gpasswd', '--delete', username, groupname])
|
subprocess.check_call(["gpasswd", "--delete", username, groupname])
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ class CustomSpawner(SystemdSpawner):
|
|||||||
Perform system user activities before starting server
|
Perform system user activities before starting server
|
||||||
"""
|
"""
|
||||||
# FIXME: Move this elsewhere? Into the Authenticator?
|
# FIXME: Move this elsewhere? Into the Authenticator?
|
||||||
system_username = generate_system_username('jupyter-' + self.user.name)
|
system_username = generate_system_username("jupyter-" + self.user.name)
|
||||||
|
|
||||||
# FIXME: This is a hack. Allow setting username directly instead
|
# FIXME: This is a hack. Allow setting username directly instead
|
||||||
self.username_template = system_username
|
self.username_template = system_username
|
||||||
user.ensure_user(system_username)
|
user.ensure_user(system_username)
|
||||||
user.ensure_user_group(system_username, 'jupyterhub-users')
|
user.ensure_user_group(system_username, "jupyterhub-users")
|
||||||
if self.user.admin:
|
if self.user.admin:
|
||||||
user.ensure_user_group(system_username, 'jupyterhub-admins')
|
user.ensure_user_group(system_username, "jupyterhub-admins")
|
||||||
else:
|
else:
|
||||||
user.remove_user_group(system_username, 'jupyterhub-admins')
|
user.remove_user_group(system_username, "jupyterhub-admins")
|
||||||
if self.user_groups:
|
if self.user_groups:
|
||||||
for group, users in self.user_groups.items():
|
for group, users in self.user_groups.items():
|
||||||
if self.user.name in users:
|
if self.user.name in users:
|
||||||
@@ -40,11 +40,11 @@ class CustomSpawner(SystemdSpawner):
|
|||||||
cfg = configurer.load_config()
|
cfg = configurer.load_config()
|
||||||
# Use the jupyterhub-configurator mixin only if configurator is enabled
|
# Use the jupyterhub-configurator mixin only if configurator is enabled
|
||||||
# otherwise, any bugs in the configurator backend will stop new user spawns!
|
# otherwise, any bugs in the configurator backend will stop new user spawns!
|
||||||
if cfg['services']['configurator']['enabled']:
|
if cfg["services"]["configurator"]["enabled"]:
|
||||||
# Dynamically create the Spawner class using `type`(https://docs.python.org/3/library/functions.html?#type),
|
# Dynamically create the Spawner class using `type`(https://docs.python.org/3/library/functions.html?#type),
|
||||||
# based on whether or not it should inherit from ConfiguratorSpawnerMixin
|
# based on whether or not it should inherit from ConfiguratorSpawnerMixin
|
||||||
UserCreatingSpawner = type(
|
UserCreatingSpawner = type(
|
||||||
'UserCreatingSpawner', (ConfiguratorSpawnerMixin, CustomSpawner), {}
|
"UserCreatingSpawner", (ConfiguratorSpawnerMixin, CustomSpawner), {}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
UserCreatingSpawner = type('UserCreatingSpawner', (CustomSpawner,), {})
|
UserCreatingSpawner = type("UserCreatingSpawner", (CustomSpawner,), {})
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ def run_subprocess(cmd, *args, **kwargs):
|
|||||||
In TLJH, this sends successful output to the installer log,
|
In TLJH, this sends successful output to the installer log,
|
||||||
and failed output directly to the user's screen
|
and failed output directly to the user's screen
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('tljh')
|
logger = logging.getLogger("tljh")
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *args, **kwargs
|
||||||
)
|
)
|
||||||
printable_command = ' '.join(cmd)
|
printable_command = " ".join(cmd)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
# Our process failed! Show output to the user
|
# Our process failed! Show output to the user
|
||||||
logger.error(
|
logger.error(
|
||||||
'Ran {command} with exit code {code}'.format(
|
"Ran {command} with exit code {code}".format(
|
||||||
command=printable_command, code=proc.returncode
|
command=printable_command, code=proc.returncode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@ def run_subprocess(cmd, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
# This goes into installer.log
|
# This goes into installer.log
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Ran {command} with exit code {code}'.format(
|
"Ran {command} with exit code {code}".format(
|
||||||
command=printable_command, code=proc.returncode
|
command=printable_command, code=proc.returncode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -54,8 +54,8 @@ def get_plugin_manager():
|
|||||||
Return plugin manager instance
|
Return plugin manager instance
|
||||||
"""
|
"""
|
||||||
# Set up plugin infrastructure
|
# Set up plugin infrastructure
|
||||||
pm = pluggy.PluginManager('tljh')
|
pm = pluggy.PluginManager("tljh")
|
||||||
pm.add_hookspecs(hooks)
|
pm.add_hookspecs(hooks)
|
||||||
pm.load_setuptools_entrypoints('tljh')
|
pm.load_setuptools_entrypoints("tljh")
|
||||||
|
|
||||||
return pm
|
return pm
|
||||||
|
|||||||
Reference in New Issue
Block a user