From eee749084efc39bed233a5972a2cda91553aa4e2 Mon Sep 17 00:00:00 2001 From: "Jason C. Nucciarone" Date: Wed, 10 Jul 2024 14:12:14 -0400 Subject: [PATCH 1/4] feat: add `is_container` utility library Signed-off-by: Jason C. Nucciarone --- lib/charms/hpc_libs/v0/is_container.py | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 lib/charms/hpc_libs/v0/is_container.py diff --git a/lib/charms/hpc_libs/v0/is_container.py b/lib/charms/hpc_libs/v0/is_container.py new file mode 100644 index 0000000..52e29a1 --- /dev/null +++ b/lib/charms/hpc_libs/v0/is_container.py @@ -0,0 +1,85 @@ +# Copyright 2024 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Detect if machine is a container instance. + +Even though Juju supports using LXD containers as the backing cloud for +deploying charmed operators, not all HPC applications work within system containers, +and some need additional configuration. This simple charm library provides the `is_container` +function, a simple, deterministic way to identify if the charm is deployed into +a system container using `systemd-detect-virt`. + +Exit code 0 means that the charm is running with a container. A non-zero exit code +means that the charm is not running within a container. + +### Example Usage: + +```python3 +from charms.hpc_libs.v0.is_container import is_container + +class ApplicationCharm(CharmBase): + + def __init__(self, *args): + super().__init__(*args) + + self.framework.observe(self.on.install, self._on_install) + + def _on_install(self, _: InstallEvent) -> None: + if is_container(): + self.unit.status = BlockedStatus("app does not support container runtime") + + # Proceed with installation. + ... +``` +""" + +import shutil +import subprocess + +# The unique Charmhub library identifier, never change it +LIBID = "eb95ad73da1941c0af186ee670f96507" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + + +class DetectVirtNotFoundError(Exception): + """Raise error if `systemd-detect-virt` executable is not found on machine.""" + + @property + def message(self) -> str: + """Return message passed as argument to exception.""" + return self.args[0] + + +def is_container() -> bool: + """Detect if the machine is a container instance. + + Raises: + DetectVirtNotFoundError: Raised if `systemd-detect-virt` is not found on machine. + """ + if shutil.which("systemd-detect-virt") is None: + raise DetectVirtNotFoundError( + ( + "executable `systemd-detect-virt` not found. " + + "cannot determine if machine is a container instance" + ) + ) + + result = subprocess.run(["systemd-detect-virt", "--container"]) + return result.returncode == 0 From 11f45087b53b31bf7ed6069e9867434fdaa994a4 Mon Sep 17 00:00:00 2001 From: "Jason C. Nucciarone" Date: Wed, 10 Jul 2024 14:12:40 -0400 Subject: [PATCH 2/4] tests: add unit & integration tests for `is_container` Signed-off-by: Jason C. Nucciarone --- .../is_container/test_is_container.py | 10 +++++ tests/integration/test_hpc_libs.yaml | 11 ++++- tests/unit/test_is_container.py | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/integration/is_container/test_is_container.py create mode 100644 tests/unit/test_is_container.py diff --git a/tests/integration/is_container/test_is_container.py b/tests/integration/is_container/test_is_container.py new file mode 100644 index 0000000..41c311f --- /dev/null +++ b/tests/integration/is_container/test_is_container.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +from lib.charms.hpc_libs.v0.is_container import is_container + + +def test_is_container() -> None: + """Test that `is_container` properly detects the system container.""" + assert is_container() is True diff --git a/tests/integration/test_hpc_libs.yaml b/tests/integration/test_hpc_libs.yaml index b9b22c0..331c85e 100644 --- a/tests/integration/test_hpc_libs.yaml +++ b/tests/integration/test_hpc_libs.yaml @@ -26,6 +26,8 @@ acts: path: dev-requirements.txt - host-path: tests/integration/slurm_ops path: slurm_ops + - host-path: tests/integration/is_container + path: is_container scenes: - name: "Install dependencies in a virtual environment" run: | @@ -34,10 +36,17 @@ acts: apt install -y python3-venv python3-yaml python3 -m venv venv --system-site-packages venv/bin/python3 -m pip install -r dev-requirements.txt - - name: "Run integration tests with pytest" + - name: "Run `slurm_ops` integration tests" run: | venv/bin/python3 -m pytest -v \ -s \ --tb native \ --log-cli-level=INFO \ slurm_ops + - name: "Run `is_container` integration tests" + run: | + venv/bin/python3 -m pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + is_container diff --git a/tests/unit/test_is_container.py b/tests/unit/test_is_container.py new file mode 100644 index 0000000..48496b4 --- /dev/null +++ b/tests/unit/test_is_container.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Test `is_container` library.""" + +from unittest import TestCase +from unittest.mock import patch + +from charms.hpc_libs.v0.is_container import DetectVirtNotFoundError, is_container + + +@patch("charms.hpc_libs.v0.is_container.shutil.which", return_value="/usr/bin/systemd-detect-virt") +@patch("charms.hpc_libs.v0.is_container.subprocess.run") +class TestIsContainer(TestCase): + + def test_inside_container(self, run, _) -> None: + """Test that `is_container` returns True when inside a container.""" + run.return_value.returncode = 0 + self.assertTrue(is_container()) + + def test_inside_virtual_machine(self, run, _) -> None: + """Test that `is_container` returns False when inside a virtual machine.""" + run.return_value.returncode = 1 + self.assertFalse(is_container()) + + def test_detect_virt_not_found(self, _, which) -> None: + """Test that correct error is thrown if `systemd-detect-virt` is not found.""" + which.return_value = None + + try: + is_container() + except DetectVirtNotFoundError as e: + self.assertEqual( + e.message, + ( + "executable `systemd-detect-virt` not found. " + + "cannot determine if machine is a container instance" + ), + ) From a2791deac8901d95ff4879ef20f938012870f7c0 Mon Sep 17 00:00:00 2001 From: "Jason C. Nucciarone" Date: Wed, 10 Jul 2024 14:38:36 -0400 Subject: [PATCH 3/4] fix(docs): remove info about library internals Signed-off-by: Jason C. Nucciarone --- lib/charms/hpc_libs/v0/is_container.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/charms/hpc_libs/v0/is_container.py b/lib/charms/hpc_libs/v0/is_container.py index 52e29a1..0d7b0b5 100644 --- a/lib/charms/hpc_libs/v0/is_container.py +++ b/lib/charms/hpc_libs/v0/is_container.py @@ -16,12 +16,8 @@ Even though Juju supports using LXD containers as the backing cloud for deploying charmed operators, not all HPC applications work within system containers, -and some need additional configuration. This simple charm library provides the `is_container` -function, a simple, deterministic way to identify if the charm is deployed into -a system container using `systemd-detect-virt`. - -Exit code 0 means that the charm is running with a container. A non-zero exit code -means that the charm is not running within a container. +and some need additional configuration. This simple charm library provides utilities +for identifying the virtualization runtime for a charmed operator. ### Example Usage: From 392b1bed410825ec98b189c7267f0fff9e743247 Mon Sep 17 00:00:00 2001 From: "Jason C. Nucciarone" Date: Wed, 10 Jul 2024 14:41:51 -0400 Subject: [PATCH 4/4] refactor: `DetectVirtNotFoundError` -> `UnknownVirtStateError` Signed-off-by: Jason C. Nucciarone --- lib/charms/hpc_libs/v0/is_container.py | 6 +++--- tests/unit/test_is_container.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/charms/hpc_libs/v0/is_container.py b/lib/charms/hpc_libs/v0/is_container.py index 0d7b0b5..f5aa999 100644 --- a/lib/charms/hpc_libs/v0/is_container.py +++ b/lib/charms/hpc_libs/v0/is_container.py @@ -54,8 +54,8 @@ def _on_install(self, _: InstallEvent) -> None: LIBPATCH = 1 -class DetectVirtNotFoundError(Exception): - """Raise error if `systemd-detect-virt` executable is not found on machine.""" +class UnknownVirtStateError(Exception): + """Raise error if unknown virtualization state is returned.""" @property def message(self) -> str: @@ -70,7 +70,7 @@ def is_container() -> bool: DetectVirtNotFoundError: Raised if `systemd-detect-virt` is not found on machine. """ if shutil.which("systemd-detect-virt") is None: - raise DetectVirtNotFoundError( + raise UnknownVirtStateError( ( "executable `systemd-detect-virt` not found. " + "cannot determine if machine is a container instance" diff --git a/tests/unit/test_is_container.py b/tests/unit/test_is_container.py index 48496b4..1cdb533 100644 --- a/tests/unit/test_is_container.py +++ b/tests/unit/test_is_container.py @@ -7,7 +7,7 @@ from unittest import TestCase from unittest.mock import patch -from charms.hpc_libs.v0.is_container import DetectVirtNotFoundError, is_container +from charms.hpc_libs.v0.is_container import UnknownVirtStateError, is_container @patch("charms.hpc_libs.v0.is_container.shutil.which", return_value="/usr/bin/systemd-detect-virt") @@ -30,7 +30,7 @@ def test_detect_virt_not_found(self, _, which) -> None: try: is_container() - except DetectVirtNotFoundError as e: + except UnknownVirtStateError as e: self.assertEqual( e.message, (