diff --git a/CHANGELOG.md b/CHANGELOG.md index 8832384..2e40413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,16 @@ ## v1 (active) -The v1 release supports Cisco IOS-XR release version 7.7.1. -It is planned to support 7.8.1 when this version is released. +The v1 release supports Cisco IOS-XR release versions 7.7.1 and 7.8.1. +## v1.1.0 (2022-12-02) + +Changes corresponding to the release of XR version 7.8.1. + +- Add `--boot-log-level` arg in `launch-xrd` (supported in XR 7.8.1 onwards) +- Stop passing host `/sys/fs/cgroup` mount through to the container +- Update cgroup check in `host-check` and remove corresponding "Systemd mounts" check (no longer required for XR 7.8.1 onwards) +- Remove hard requirement for cgroups v1 in `host-check` (cgroups v2 supported for lab use) ### v1.0.4 (2022-11-30) diff --git a/docs/version-compatibility.md b/docs/version-compatibility.md index ee2ee4c..5ade446 100644 --- a/docs/version-compatibility.md +++ b/docs/version-compatibility.md @@ -4,6 +4,28 @@ This file documents compatibility between versions of this project with XR relea In some cases extra arguments will be required to continue using older XR releases, and those cases will be documented here. +## XR v1.1 + +Supports XR 7.7.1 and 7.8.1 (the first and most recent released versions of XRd). + +### XR 7.7.1 + +- Does not support the `--boot-log-level` arg in `launch-xrd`. +- Requires the host cgroup mount to be passed into the container manually: + - Use '`--args '-v /sys/fs/cgroup:/sys/fs/cgroup:ro'`' to `launch-xrd`. + - Add the following under the 'service' section in an `xr-compose` topology: + ```yaml + volumes: + - source: /sys/fs/cgroup + target: /sys/fs/cgroup + type: bind + read_only: True + ``` +- Does not support running with `--cgroupns=private`. +- Requires `/sys/fs/cgroup/systemd/` to be mounted read-write on the host. +- Does not support cgroups v2. + + ## v1.0 -Supports XR 7.7.1 only (the first and most recent released version of XRd). +Supports XR 7.7.1 (the first released version of XRd). diff --git a/scripts/host-check b/scripts/host-check index 3ae0319..18317e3 100755 --- a/scripts/host-check +++ b/scripts/host-check @@ -256,6 +256,41 @@ def _is_module_loaded(module: str) -> bool: return cmd_is_ok(f"lsmod | grep -q '^{module} '") +def _mount_exists(path: str, *, type_: Optional[str] = None) -> bool: + """Check whether the specified mount exists.""" + cmd = ["findmnt", path] + if type_: + cmd.extend(["-t", type_]) + proc = subprocess.run( + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5 + ) + return proc.returncode == 0 + + +def _get_cgroup_version() -> int: + """ + Find the cgroup version in use by inspecting /sys/fs/cgroup/. + + :return: + The cgroup version in use, either 1 or 2. + """ + if ( # pylint: disable=consider-using-with,unspecified-encoding + _mount_exists("/sys/fs/cgroup", type_="cgroup2") + and len( + open(os.path.join("/sys/fs/cgroup", "cgroup.controllers")).read() + ) + > 0 + ): + return 2 + elif any( + _mount_exists(subpath, type_="cgroup") + for subpath in glob.glob(os.path.join("/sys/fs/cgroup", "*")) + ): + return 1 + else: + raise Exception("Unable to detect cgroup version in use") + + # ----------------------------------------------------------------------------- # Checks # ----------------------------------------------------------------------------- @@ -396,78 +431,24 @@ def check_base_kernel_modules() -> CheckFuncReturn: ) -def check_cgroups_version() -> CheckFuncReturn: - """Check the cgroups version is supported.""" - try: - cmd = "stat -fc %T /sys/fs/cgroup/" - output, _ = run_cmd(shlex.split(cmd)) - cgroupfs = str(output.strip()) - except subprocess.SubprocessError: - return ( - CheckState.WARNING, - f"Error running {cmd!r} to determine the cgroups\n" - "version - /sys/fs/cgroup is expected to be a cgroup v1 mount.", - ) - - if cgroupfs == "cgroup2fs": - version = 2 - return ( - CheckState.FAILED, - f"Cgroups version {version} is in use, but this is not supported by XRd.\n" - "Please use cgroups version 1.", - ) - - elif cgroupfs != "tmpfs": - return ( - CheckState.FAILED, - "Unrecognised /sys/fs/cgroup mount, cgroups must be version 1.", - ) - - else: - version = 1 - return CheckState.SUCCESS, f"v{version}" - - -def check_systemd_mounts() -> CheckFuncReturn: - """Check the required systemd mounts are present.""" - expected_mounts_msg = ( - "/sys/fs/cgroup must be mounted (read-only or read-write) and\n" - "/sys/fs/cgroup/systemd must be mounted read-write." - ) +def check_cgroups() -> CheckFuncReturn: + """Check cgroups are correctly set up, also checking cgroups version.""" try: - output, _ = run_cmd(["mount"]) - except subprocess.SubprocessError: - return ( - CheckState.WARNING, - "Error running 'mount' to check the required systemd mounts exist.\n" - + expected_mounts_msg, - ) - - mounts = [] - for line in output.splitlines(): - match = re.match( - r"\S+ on (?P/\S*) type \S+ \((?Prw|ro),", line - ) - if match: - mounts.append(match.groups()) - - if "/sys/fs/cgroup" not in [m[0] for m in mounts]: + version = _get_cgroup_version() + except Exception: return ( CheckState.FAILED, - "/sys/fs/cgroup mount not found.\n" + expected_mounts_msg, + "Error trying to determine the cgroups version - /sys/fs/cgroup is expected to\n" + "contain cgroup v1 mounts.", ) - if ("/sys/fs/cgroup/systemd", "rw") not in mounts: + if version == 2: return ( - CheckState.FAILED, - "/sys/fs/cgroup/systemd not read-write mounted on host.\n" - + expected_mounts_msg, + CheckState.NEUTRAL, + "Cgroups v2 is in use - this is not supported for production environments.", ) - - return ( - CheckState.SUCCESS, - "/sys/fs/cgroup and /sys/fs/cgroup/systemd mounted correctly.", - ) + assert version == 1 + return CheckState.SUCCESS, f"v{version}" def check_inotify_limits(setting: str) -> CheckFuncReturn: @@ -1202,8 +1183,7 @@ BASE_CHECKS = [ Check( "Base kernel modules", check_base_kernel_modules, ["Kernel version"] ), - Check("Cgroups version", check_cgroups_version, []), - Check("systemd mounts", check_systemd_mounts, ["Cgroups version"]), + Check("Cgroups", check_cgroups, []), Check( "Inotify max user instances", functools.partial(check_inotify_limits, "max_user_instances"), diff --git a/scripts/launch-xrd b/scripts/launch-xrd index 17b26f1..829237c 100755 --- a/scripts/launch-xrd +++ b/scripts/launch-xrd @@ -69,6 +69,9 @@ Optional arguments: up after boot, by default ZTP is disabled (cannot be used with IP snooping) --ztp-config FILE Enable ZTP with custom ZTP ini configuration + --boot-log-level LEVEL Control the level at which boot logging starts + being printed to the console, one of: ERROR, + WARNING (default), INFO, DEBUG --args ' ...' Extra arguments to pass to ' run' XRd Control Plane arguments: @@ -168,7 +171,7 @@ OPTS=$(getopt -o hnkf:e:v:p:c: \ --long help,dry-run,keep,privileged,name:,plat:,platform:,\ first-boot-config:,every-boot-config:,first-boot-script:,every-boot-script:,\ xrd-volume:,ctr-client:,interfaces:,mgmt-interfaces:,disk-limit:,\ -ztp-enable,ztp-config:,args: \ +boot-log-level:,ztp-enable,ztp-config:,args: \ -n 'parse-options' -- "$@") || bad_usage eval set -- "$OPTS" @@ -238,6 +241,10 @@ while [[ $# -gt 0 ]]; do ZTP_CONFIG=$2 shift 2 ;; + --boot-log-level ) + BOOT_LOG_LEVEL=$2 + shift 2 + ;; --privileged ) IS_PRIVILEGED=1 shift @@ -392,8 +399,6 @@ else # AppArmor and SELinux are not supported with the default profiles. "--security-opt" "apparmor=unconfined" "--security-opt" "label=disable" - # Pass through the host's cgroup mount, needed by systemd. - "-v" "/sys/fs/cgroup:/sys/fs/cgroup:ro" ) # Add XRd vRouter specific unprivileged arguments: if [[ $PLATFORM == "xrd-vrouter" ]]; then @@ -444,6 +449,9 @@ fi if [[ $IS_ZTP_ENABLED == 1 ]]; then env_args+=("--env" "XR_ZTP_ENABLE=1") fi +if [[ $BOOT_LOG_LEVEL ]]; then + env_args+=("--env" "XR_BOOT_LOG_LEVEL=$BOOT_LOG_LEVEL") +fi if [[ $CONTAINER_NAME ]]; then name_args+=("--name" "$CONTAINER_NAME") diff --git a/scripts/xr-compose b/scripts/xr-compose index 5860602..585edf8 100755 --- a/scripts/xr-compose +++ b/scripts/xr-compose @@ -1632,14 +1632,6 @@ class XRCompose: service_yaml.setdefault("devices", []).extend( ["/dev/fuse", "/dev/net/tun"] ) - service_yaml.setdefault("volumes", []).append( - { - "type": "bind", - "source": "/sys/fs/cgroup", - "target": "/sys/fs/cgroup", - "read_only": True, - } - ) def add_service_networks(self, service: Service) -> None: """ diff --git a/tests/test_host_check.py b/tests/test_host_check.py index fcbee2e..0c8c599 100644 --- a/tests/test_host_check.py +++ b/tests/test_host_check.py @@ -970,6 +970,20 @@ def test_unexpected_error(self, capsys): ) assert not success + def test_timeout_error(self, capsys): + """Test timeout error being raised.""" + success, output = self.perform_check( + capsys, + cmd_effects=subprocess.TimeoutExpired(cmd=self.cmds, timeout=5), + ) + assert output == textwrap.dedent( + f"""\ + WARN -- Base kernel modules + Unexpected error: Timed out while executing command: {" ".join(self.cmds)} + """ + ) + assert not success + def test_failed_dependency(self, capsys): """Test a dependency failure.""" success, output = self.perform_check(capsys, failed_deps=self.deps) @@ -982,82 +996,60 @@ def test_failed_dependency(self, capsys): assert not success -class TestCgroupsVersion(_CheckTestBase): - """Tests for the cgroups version check.""" +class TestCgroups(_CheckTestBase): + """Tests for the cgroups check.""" check_group = "base" - check_name = "Cgroups version" - cmds = ["stat -fc %T /sys/fs/cgroup/"] + check_name = "Cgroups" + cmds = [ + "findmnt /sys/fs/cgroup -t cgroup2", + "findmnt /sys/fs/cgroup/memory -t cgroup", + ] - def test_success(self, capsys): - """Test the success case.""" - success, output = self.perform_check(capsys, cmd_effects="tmpfs") + @staticmethod + @pytest.fixture(scope="class", autouse=True) + def mock_cgroup_dirs(): + with mock.patch("glob.glob", return_value=["/sys/fs/cgroup/memory"]): + yield + + def test_v1_success(self, capsys): + """Test the cgroups v1 success case.""" + success, output = self.perform_check( + capsys, + cmd_effects=[mock.Mock(returncode=1), mock.Mock(returncode=0)], + ) assert output == textwrap.dedent( """\ - PASS -- Cgroups version (v1) + PASS -- Cgroups (v1) """ ) assert success def test_unknown_version(self, capsys): """Test the case where the cgroups version is unrecognised.""" - success, output = self.perform_check(capsys, cmd_effects="unknown") - assert output == textwrap.dedent( - """\ - FAIL -- Cgroups version - Unrecognised /sys/fs/cgroup mount, cgroups must be version 1. - """ - ) - assert not success - - def test_v2(self, capsys): - """Test the case where v2 cgroups are in use.""" - success, output = self.perform_check(capsys, cmd_effects="cgroup2fs") - assert output == textwrap.dedent( - """\ - FAIL -- Cgroups version - Cgroups version 2 is in use, but this is not supported by XRd. - Please use cgroups version 1. - """ - ) - assert not success - - def test_subproc_error(self, capsys): - """Test a subprocess error being raised.""" success, output = self.perform_check( - capsys, cmd_effects=subprocess.SubprocessError + capsys, + cmd_effects=[mock.Mock(returncode=1), mock.Mock(returncode=1)], ) assert output == textwrap.dedent( """\ - WARN -- Cgroups version - Error running 'stat -fc %T /sys/fs/cgroup/' to determine the cgroups - version - /sys/fs/cgroup is expected to be a cgroup v1 mount. + FAIL -- Cgroups + Error trying to determine the cgroups version - /sys/fs/cgroup is expected to + contain cgroup v1 mounts. """ ) assert not success - -class TestSystemdMounts(_CheckTestBase): - """Tests for the systemd mounts check.""" - - check_group = "base" - check_name = "systemd mounts" - deps = ["Cgroups version"] - cmds = ["mount"] - - def test_success(self, capsys): - """Test the success case.""" - cmd_output = textwrap.dedent( - """ - tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755) - cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr) - """ - ) - success, output = self.perform_check(capsys, cmd_effects=cmd_output) + def test_v2_info(self, capsys): + """Test the case where v2 cgroups are in use.""" + with mock.patch("builtins.open", mock.mock_open(read_data="memory")): + success, output = self.perform_check( + capsys, cmd_effects=[mock.Mock(returncode=0), None] + ) assert output == textwrap.dedent( """\ - PASS -- systemd mounts - /sys/fs/cgroup and /sys/fs/cgroup/systemd mounted correctly. + INFO -- Cgroups + Cgroups v2 is in use - this is not supported for production environments. """ ) assert success @@ -1069,92 +1061,9 @@ def test_subproc_error(self, capsys): ) assert output == textwrap.dedent( """\ - WARN -- systemd mounts - Error running 'mount' to check the required systemd mounts exist. - /sys/fs/cgroup must be mounted (read-only or read-write) and - /sys/fs/cgroup/systemd must be mounted read-write. - """ - ) - assert not success - - def test_no_cgroup_mount(self, capsys): - """Test no cgroup mount.""" - cmd_output = textwrap.dedent( - """ - cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr) - """ - ) - success, output = self.perform_check(capsys, cmd_effects=cmd_output) - assert output == textwrap.dedent( - """\ - FAIL -- systemd mounts - /sys/fs/cgroup mount not found. - /sys/fs/cgroup must be mounted (read-only or read-write) and - /sys/fs/cgroup/systemd must be mounted read-write. - """ - ) - assert not success - - def test_no_systemd_cgroup_mount(self, capsys): - """Test no cgroup/systemd mount.""" - cmd_output = textwrap.dedent( - """ - tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755) - """ - ) - success, output = self.perform_check(capsys, cmd_effects=cmd_output) - assert output == textwrap.dedent( - """\ - FAIL -- systemd mounts - /sys/fs/cgroup/systemd not read-write mounted on host. - /sys/fs/cgroup must be mounted (read-only or read-write) and - /sys/fs/cgroup/systemd must be mounted read-write. - """ - ) - assert not success - - def test_systemd_cgroup_mount_readonly(self, capsys): - """Test cgroup/systemd mount being read-only.""" - cmd_output = textwrap.dedent( - """ - tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,mode=755) - cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr) - """ - ) - success, output = self.perform_check(capsys, cmd_effects=cmd_output) - assert output == textwrap.dedent( - """\ - FAIL -- systemd mounts - /sys/fs/cgroup/systemd not read-write mounted on host. - /sys/fs/cgroup must be mounted (read-only or read-write) and - /sys/fs/cgroup/systemd must be mounted read-write. - """ - ) - assert not success - - def test_unexpected_error(self, capsys): - """Test unexpected error being raised.""" - success, output = self.perform_check( - capsys, cmd_effects=Exception("test exception") - ) - assert output == textwrap.dedent( - """\ - FAIL -- systemd mounts - Unexpected error: test exception - """ - ) - assert not success - - def test_timeout_error(self, capsys): - """Test timeout error being raised.""" - success, output = self.perform_check( - capsys, - cmd_effects=subprocess.TimeoutExpired(cmd=self.cmds, timeout=5), - ) - assert output == textwrap.dedent( - f"""\ - WARN -- systemd mounts - Unexpected error: Timed out while executing command: {" ".join(self.cmds)} + FAIL -- Cgroups + Error trying to determine the cgroups version - /sys/fs/cgroup is expected to + contain cgroup v1 mounts. """ ) assert not success