diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f784caf..e42c711 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,8 +24,6 @@ on: jobs: test: - runs-on: ubuntu-22.04 - strategy: fail-fast: false matrix: @@ -33,17 +31,23 @@ jobs: # oldest supported python and jupyterhub version - python-version: "3.8" pip-install-spec: "jupyterhub==2.3.0 tornado==5.1.0 sqlalchemy==1.*" + runs-on: ubuntu-20.04 - python-version: "3.9" pip-install-spec: "jupyterhub==2.* sqlalchemy==1.*" + runs-on: ubuntu-22.04 - python-version: "3.10" pip-install-spec: "jupyterhub==3.*" + runs-on: ubuntu-22.04 - python-version: "3.11" pip-install-spec: "jupyterhub==4.*" + runs-on: ubuntu-22.04 # latest version of python and jupyterhub (including pre-releases) - python-version: "3.x" pip-install-spec: "--pre jupyterhub" + runs-on: ubuntu-latest + runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/README.md b/README.md index b32cbcd..d8e03d2 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,18 @@ The following features are currently available: ## Requirements -### Systemd +### Systemd and Linux distributions -Systemd Spawner requires you to use a Linux Distro that ships with at least -systemd v211. The security related features require systemd v228 or v227. We recommend running -with at least systemd v228. You can check which version of systemd is running with: +SystemdSpawner 1 is recommended to be used with systemd version 245 or higher, +but _may_ work with systemd version 243-244 as well. Below are examples of Linux +distributions that use systemd and has a recommended version. -```bash -$ systemctl --version | head -1 -systemd 231 -``` +- Ubuntu 20.04+ +- Debian 11+ +- Rocky 9+ / CentOS 9+ + +The command `systemctl --version` can be used to verify that systemd is used, +and what version is used. ### Kernel Configuration @@ -115,26 +117,6 @@ are required. Systemd will automatically create dynamic users as required. See [this blog post](http://0pointer.net/blog/dynamic-users-with-systemd.html) for details. -### Linux Distro compatibility - -#### Ubuntu 16.04 LTS - -We recommend running this with systemd spawner. The default kernel has all the features -we need, and a recent enough version of systemd to give us all the features. - -#### Debian Jessie - -The systemd version that ships by default with Jessie doesn't provide all the features -we need, and the default kernel doesn't ship with the features we need. However, if -you [enable jessie-backports](https://backports.debian.org/Instructions/) you can -install a new enough version of systemd and linux kernel to get it to work fine. - -#### Centos 7 - -The kernel has all the features we need, but the version of systemd (219) is too old -for the security related features of systemdspawner. However, basic spawning, -memory & cpu limiting will work. - ## Installation You can install it from PyPI with: @@ -332,9 +314,6 @@ c.SystemdSpawner.isolate_tmp = True Defaults to false. -This requires systemd version > 227. If you enable this in earlier versions, spawning will -fail. - ### `isolate_devices` Setting this to true provides a separate, private `/dev` for each user. This prevents the @@ -348,9 +327,6 @@ c.SystemdSpawner.isolate_devices = True Defaults to false. -This requires systemd version > 227. If you enable this in earlier versions, spawning will -fail. - ### `disable_user_sudo` Setting this to true prevents users from being able to use `sudo` (or any other means) to @@ -364,9 +340,6 @@ c.SystemdSpawner.disable_user_sudo = True Defaults to false. -This requires systemd version > 228. If you enable this in earlier versions, spawning will -fail. - ### `readonly_paths` List of filesystem paths that should be mounted readonly for the users' notebook server. This @@ -384,9 +357,6 @@ appropriate values for the user being spawned. Defaults to `None` which disables this feature. -This requires systemd version > 228. If you enable this in earlier versions, spawning will -fail. It can also contain only directories (not files) until systemd version 231. - ### `readwrite_paths` List of filesystem paths that should be mounted readwrite for the users' notebook server. This @@ -403,9 +373,6 @@ appropriate values for the user being spawned. Defaults to `None` which disables this feature. -This requires systemd version > 228. If you enable this in earlier versions, spawning will -fail. It can also contain only directories (not files) until systemd version 231. - ### `dynamic_users` Allocate system users dynamically for each user. @@ -418,8 +385,6 @@ is deallocated whenever the user's server is not running. See http://0pointer.net/blog/dynamic-users-with-systemd.html for more information. -Requires systemd 235. - ### `slice` Run the spawned notebook in a given systemd slice. This allows aggregate configuration that diff --git a/systemdspawner/systemd.py b/systemdspawner/systemd.py index 4c7ed88..d114668 100644 --- a/systemdspawner/systemd.py +++ b/systemdspawner/systemd.py @@ -9,6 +9,7 @@ import os import re import shlex +import subprocess import warnings # light validation of environment variable keys @@ -198,3 +199,20 @@ async def reset_service(unit_name): """ proc = await asyncio.create_subprocess_exec("systemctl", "reset-failed", unit_name) await proc.wait() + + +def get_systemd_version(): + """ + Returns systemd's major version, or None if failing to do so. + """ + try: + version_response = subprocess.check_output(["systemctl", "--version"]) + # Example response from Ubuntu 22.04: + # + # systemd 249 (249.11-0ubuntu3.9) + # +PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY -P11KIT -QRENCODE +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified + # + version = int(float(version_response.split()[1])) + return version + except: + return None diff --git a/systemdspawner/systemdspawner.py b/systemdspawner/systemdspawner.py index 4c799f7..cc2850d 100644 --- a/systemdspawner/systemdspawner.py +++ b/systemdspawner/systemdspawner.py @@ -1,6 +1,7 @@ import asyncio import os import pwd +import sys from jupyterhub.spawner import Spawner from jupyterhub.utils import random_port @@ -8,6 +9,9 @@ from systemdspawner import systemd +SYSTEMD_REQUIRED_VERSION = 243 +SYSTEMD_LOWEST_RECOMMENDED_VERSION = 245 + class SystemdSpawner(Spawner): user_workingdir = Unicode( @@ -60,8 +64,6 @@ class SystemdSpawner(Spawner): """, ).tag(config=True) - # FIXME: Do not allow enabling this for systemd versions < 227, - # since that is when it was introduced. isolate_tmp = Bool( False, help=""" @@ -131,8 +133,6 @@ class SystemdSpawner(Spawner): See http://0pointer.net/blog/dynamic-users-with-systemd.html for more information. - - Requires systemd 235. """, ).tag(config=True) @@ -155,6 +155,21 @@ def __init__(self, *args, **kwargs): "user:%s Initialized spawner with unit %s", self.user.name, self.unit_name ) + systemd_version = systemd.get_systemd_version() + if systemd_version is None: + self.log.warning( + "Failed to parse systemd version from 'systemctl --version'" + ) + elif systemd_version < SYSTEMD_REQUIRED_VERSION: + self.log.critical( + f"systemd version {SYSTEMD_REQUIRED_VERSION} or higher is required, version {systemd_version} is used" + ) + sys.exit(1) + elif systemd_version < SYSTEMD_LOWEST_RECOMMENDED_VERSION: + self.log.warning( + f"systemd version {SYSTEMD_LOWEST_RECOMMENDED_VERSION} or higher is recommended, version {systemd_version} is used" + ) + def _expand_user_vars(self, string): """ Expand user related variables in a given string diff --git a/tests/test_systemd.py b/tests/test_systemd.py index e9a8a60..c06c296 100644 --- a/tests/test_systemd.py +++ b/tests/test_systemd.py @@ -11,6 +11,17 @@ from systemdspawner import systemd +def test_get_systemd_version(): + """ + Test getting systemd version as an integer, where the assumption for the + tests are that systemd is actually running at all. + """ + systemd_version = systemd.get_systemd_version() + assert isinstance( + systemd_version, int + ), "Either systemd wasn't running, or we failed to parse the version into an integer!" + + async def test_simple_start(): unit_name = "systemdspawner-unittest-" + str(time.time()) await systemd.start_transient_service(