diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e2d66a..0708b94 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,13 @@ jobs: run: tox -e unit integration-test: - name: Integration tests (LXD) + strategy: + fail-fast: true + matrix: + bases: + - ubuntu@20.04 + - ubuntu@22.04 + name: Integration tests (LXD) | ${{ matrix.bases }} runs-on: ubuntu-latest needs: - inclusive-naming-check @@ -68,4 +74,4 @@ jobs: provider: lxd juju-channel: 3.1/stable - name: Run tests - run: tox -e integration + run: tox run -e integration -- --charm-base=${{ matrix.bases }} diff --git a/pyproject.toml b/pyproject.toml index ee2a736..ca3e80b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,9 @@ extend-ignore = [ "D409", "D413", ] -ignore = ["E501"] +ignore = ["E501", "D107"] extend-exclude = ["__pycache__", "*.egg_info"] +per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} [tool.ruff.mccabe] max-complexity = 10 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d4a4496..005796b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -17,12 +17,25 @@ import pathlib +import pytest +from _pytest.config.argparsing import Parser from helpers import ETCD, NHC, VERSION -from pytest import fixture from pytest_operator.plugin import OpsTest -@fixture(scope="module") +def pytest_addoption(parser: Parser) -> None: + parser.addoption( + "--charm-base", action="store", default="ubuntu@22.04", help="Charm base to test." + ) + + +@pytest.fixture(scope="module") +def charm_base(request) -> str: + """Get slurmdbd charm base to use.""" + return request.config.getoption("--charm-base") + + +@pytest.fixture(scope="module") async def slurmd_charm(ops_test: OpsTest): """Build slurmd charm to use for integration tests.""" charm = await ops_test.build_charm(".") diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 9b21ee6..b59eceb 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -21,6 +21,8 @@ from typing import Dict from urllib import request +from pylxd import Client + logger = logging.getLogger(__name__) ETCD = "etcd-v3.5.0-linux-amd64.tar.gz" @@ -33,13 +35,28 @@ ).stdout.strip("\n") +def modify_default_profile() -> None: + """Modify the default LXD profile. + + Notes: + The default profile needs to be modified so that slurmd can + use proctrack/cgroup for process tracking inside an LXD container. + """ + client = Client() + config = {"security.nesting": "true"} + logger.info(f"Updating default LXD profile configuration to {config}") + default = client.profiles.get("default") + default.config.update(config) + default.save() + + def get_slurmctld_res() -> Dict[str, pathlib.Path]: """Get slurmctld resources needed for charm deployment.""" if not (version := pathlib.Path(VERSION)).exists(): - logger.info(f"Setting resource {VERSION} to value {VERSION_NUM}...") + logger.info(f"Setting resource {VERSION} to value {VERSION_NUM}") version.write_text(VERSION_NUM) if not (etcd := pathlib.Path(ETCD)).exists(): - logger.info(f"Getting resource {ETCD} from {ETCD_URL}...") + logger.info(f"Getting resource {ETCD} from {ETCD_URL}") request.urlretrieve(ETCD_URL, etcd) return {"etcd": etcd} @@ -48,7 +65,7 @@ def get_slurmctld_res() -> Dict[str, pathlib.Path]: def get_slurmd_res() -> Dict[str, pathlib.Path]: """Get slurmd resources needed for charm deployment.""" if not (nhc := pathlib.Path(NHC)).exists(): - logger.info(f"Getting resource {NHC} from {NHC_URL}...") + logger.info(f"Getting resource {NHC} from {NHC_URL}") request.urlretrieve(NHC_URL, nhc) return {"nhc": nhc} diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index dfb82e3..61c3761 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -16,15 +16,16 @@ """Test slurmd charm against other SLURM charms in the latest/edge channel.""" import asyncio +import logging +import pathlib +from typing import Any, Coroutine import pytest -from helpers import ( - get_slurmctld_res, - get_slurmd_res, -) +from helpers import get_slurmctld_res, get_slurmd_res, modify_default_profile from pytest_operator.plugin import OpsTest -SERIES = ["focal"] +logger = logging.getLogger(__name__) + SLURMD = "slurmd" SLURMDBD = "slurmdbd" SLURMCTLD = "slurmctld" @@ -33,14 +34,16 @@ @pytest.mark.abort_on_fail -@pytest.mark.parametrize("series", SERIES) @pytest.mark.skip_if_deployed -async def test_build_and_deploy(ops_test: OpsTest, series: str, slurmd_charm): - """Test that the slurmd charm can stabilize against slurmctld, slurmdbd and percona.""" +@pytest.mark.order(1) +async def test_build_and_deploy( + ops_test: OpsTest, slurmd_charm: Coroutine[Any, Any, pathlib.Path], charm_base: str +) -> None: + """Test that the slurmd charm can stabilize against slurmctld, slurmdbd and MySQL.""" + logger.info(f"Deploying {SLURMD} against {SLURMCTLD}, {SLURMDBD}, and {DATABASE}") + modify_default_profile() res_slurmd = get_slurmd_res() res_slurmctld = get_slurmctld_res() - - # Fetch edge from charmhub for slurmctld, slurmdbd and percona and deploy await asyncio.gather( ops_test.model.deploy( SLURMCTLD, @@ -48,36 +51,36 @@ async def test_build_and_deploy(ops_test: OpsTest, series: str, slurmd_charm): channel="edge", num_units=1, resources=res_slurmctld, - series=series, + base=charm_base, ), ops_test.model.deploy( SLURMDBD, application_name=SLURMDBD, channel="edge", num_units=1, - series=series, + base=charm_base, ), ops_test.model.deploy( ROUTER, application_name=f"{SLURMDBD}-{ROUTER}", channel="dpe/edge", - num_units=1, - series=series, + num_units=0, + base=charm_base, ), ops_test.model.deploy( DATABASE, application_name=DATABASE, - channel="edge", + channel="8.0/edge", num_units=1, - series="jammy", + base="ubuntu@22.04", ), ) # Attach resources to charms. await ops_test.juju("attach-resource", SLURMCTLD, f"etcd={res_slurmctld['etcd']}") # Set relations for charmed applications. - await ops_test.model.relate(f"{SLURMDBD}:{SLURMDBD}", f"{SLURMCTLD}:{SLURMDBD}") - await ops_test.model.relate(f"{SLURMDBD}-{ROUTER}:backend-database", f"{DATABASE}:database") - await ops_test.model.relate(f"{SLURMDBD}:database", f"{SLURMDBD}-{ROUTER}:database") + await ops_test.model.integrate(f"{SLURMDBD}:{SLURMDBD}", f"{SLURMCTLD}:{SLURMDBD}") + await ops_test.model.integrate(f"{SLURMDBD}-{ROUTER}:backend-database", f"{DATABASE}:database") + await ops_test.model.integrate(f"{SLURMDBD}:database", f"{SLURMDBD}-{ROUTER}:database") # IMPORTANT: It's possible for slurmd to be stuck waiting for slurmctld despite slurmctld and slurmdbd # available. Relation between slurmd and slurmctld has to be added after slurmctld is ready # otherwise risk running into race-condition type behavior. @@ -88,12 +91,12 @@ async def test_build_and_deploy(ops_test: OpsTest, series: str, slurmd_charm): application_name=SLURMD, num_units=1, resources=res_slurmd, - series=series, + base=charm_base, ) # Attach resources to slurmd application. await ops_test.juju("attach-resource", SLURMD, f"nhc={res_slurmd['nhc']}") # Set relations for slurmd application. - await ops_test.model.relate(f"{SLURMD}:{SLURMD}", f"{SLURMCTLD}:{SLURMD}") + await ops_test.model.integrate(f"{SLURMD}:{SLURMD}", f"{SLURMCTLD}:{SLURMD}") # Reduce the update status frequency to accelerate the triggering of deferred events. async with ops_test.fast_forward(): await ops_test.model.wait_for_idle(apps=[SLURMD], status="active", timeout=1000) @@ -101,8 +104,10 @@ async def test_build_and_deploy(ops_test: OpsTest, series: str, slurmd_charm): @pytest.mark.abort_on_fail +@pytest.mark.order(2) async def test_munge_is_active(ops_test: OpsTest): """Test that munge is active.""" + logger.info("Checking that munge is active inside Juju unit") unit = ops_test.model.applications[SLURMD].units[0] cmd_res = (await unit.ssh(command="systemctl is-active munge")).strip("\n") assert cmd_res == "active" @@ -112,8 +117,10 @@ async def test_munge_is_active(ops_test: OpsTest): # systemd service failing. @pytest.mark.xfail @pytest.mark.abort_on_fail +@pytest.mark.order(3) async def test_slurmd_is_active(ops_test: OpsTest): """Test that slurmd is active.""" + logger.info("Checking that slurmd is active inside Juju unit") unit = ops_test.model.applications[SLURMD].units[0] cmd_res = (await unit.ssh(command="systemctl is-active slurmd")).strip("\n") assert cmd_res == "active" diff --git a/tox.ini b/tox.ini index 3d35bae..7dfa42a 100644 --- a/tox.ini +++ b/tox.ini @@ -57,14 +57,16 @@ commands = description = Run integration tests deps = juju==3.1.0.1 + pylxd==2.3.1 pytest==7.2.0 - pytest-operator==0.22.0 + pytest-operator==0.26.0 + pytest-order==1.1.0 + tenacity==8.2.2 + -r{toxinidir}/requirements.txt commands = pytest -v \ - -s \ - --tb native \ - --ignore={[vars]tst_path}unit \ - --log-cli-level=INFO \ - --model controller \ - --keep-models \ - {posargs} + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tst_path}integration \ + {posargs}