From 019fd2ebbcbc6cd15ee0c341815d4fc5d0fa2bfe Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Mon, 6 Jul 2026 14:29:29 +0200 Subject: [PATCH 1/8] feat: change x86_64 default instance type to c8i.4xlarge Replace c5a.4xlarge (AMD EPYC) with c8i.4xlarge (Intel Xeon 6) as the default x86_64 instance type in DEFAULT_PLATFORM_MAP. The c8i family provides a newer CPU generation with equivalent compute resources (16 vCPUs, 32 GB RAM) and is a prerequisite for enabling nested virtualization support. This change affects KernelCI pull-lab jobs: when the poller translates a job_definition for an x86_64 workload, it will now launch c8i.4xlarge instances instead of c5a.4xlarge. Signed-off-by: Norbert Manthey --- src/kernel_ci_cloud_labs/pull_labs_translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel_ci_cloud_labs/pull_labs_translate.py b/src/kernel_ci_cloud_labs/pull_labs_translate.py index 2695d21..fe78a70 100644 --- a/src/kernel_ci_cloud_labs/pull_labs_translate.py +++ b/src/kernel_ci_cloud_labs/pull_labs_translate.py @@ -24,7 +24,7 @@ # argument of translate_job() to customise per-deployment. DEFAULT_PLATFORM_MAP: Dict[str, Dict[str, str]] = { "x86_64": { - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", }, "arm64": { From 2e52e4f5a04c95ded3332825127ae00baaa4bf16 Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Mon, 6 Jul 2026 14:34:10 +0200 Subject: [PATCH 2/8] feat: enable nested virtualization for supported instance families Add automatic detection and enablement of nested virtualization when launching EC2 instances from supported Intel-based families. When a supported instance type is detected, the CpuOptions parameter with NestedVirtualization=enabled is passed to the RunInstances API call. This exposes Intel VT-x to the guest, allowing KVM and Hyper-V workloads to run inside the EC2 instance. Supported families per AWS documentation: C8i, M8i, R8i, X8i, C7i, M7i, R7i, I7i and their variants (flex, d). Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/amazon-ec2-nested-virtualization.html Signed-off-by: Norbert Manthey --- src/kernel_ci_cloud_labs/launch_vm.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/kernel_ci_cloud_labs/launch_vm.py b/src/kernel_ci_cloud_labs/launch_vm.py index ac4add2..8cad9aa 100644 --- a/src/kernel_ci_cloud_labs/launch_vm.py +++ b/src/kernel_ci_cloud_labs/launch_vm.py @@ -175,6 +175,25 @@ def _resolve_ssm_parameter(self, parameter_name): log_error(f"Failed to resolve SSM parameter {parameter_name}: {e}") raise + def _supports_nested_virtualization(self): + """Check if the instance type supports nested virtualization. + + AWS supports nested virtualization on C8i, M8i, R8i, C7i, M7i, R7i + and related instance families. + Returns True if the current instance_type belongs to a supported family. + + See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/amazon-ec2-nested-virtualization.html + """ + nested_families = ( + "c8i", "m8i", "r8i", "c8id", "m8id", "r8id", + "c8i-flex", "m8i-flex", "r8i-flex", "x8i", + "c7i", "m7i", "r7i", "c7id", "m7id", "r7id", + "c7i-flex", "m7i-flex", "i7i", + ) + # Extract family: e.g. "c8i.4xlarge" -> "c8i" + instance_family = self.instance_type.rsplit(".", 1)[0].lower() + return instance_family in nested_families + def prepare_test_artifacts(self): """Verify test payload zip exists in S3.""" log_info(f"\n=== Verifying test artifacts for {self.test} ===") @@ -254,6 +273,12 @@ def spawn_vm(self): params["IamInstanceProfile"] = {"Name": self.role_name} + # Enable nested virtualization for instance families that support it. + # Required for KVM/Hyper-V workloads inside the EC2 instance. + if self._supports_nested_virtualization(): + params["CpuOptions"] = {"NestedVirtualization": "enabled"} + log_info(f"Nested virtualization enabled for {self.instance_type}") + log_not("Calling run_instances...") response = self.ec2.run_instances(**params) From 215c11d30fa695d91e66ad6d221d2a38cc126f84 Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Mon, 6 Jul 2026 14:34:35 +0200 Subject: [PATCH 3/8] chore: update example configs to use c8i.4xlarge Update instance_type from c5a.4xlarge to c8i.4xlarge in both example configuration files to match the new default platform map. This ensures local runs and integration tests use the same instance type as KernelCI-triggered pull-lab jobs. Signed-off-by: Norbert Manthey --- examples/aws/alltests-config.json | 16 ++++++++-------- examples/aws/config.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/aws/alltests-config.json b/examples/aws/alltests-config.json index 374f9f0..055e70c 100644 --- a/examples/aws/alltests-config.json +++ b/examples/aws/alltests-config.json @@ -205,7 +205,7 @@ "vms": [ { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -215,7 +215,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -225,7 +225,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -235,7 +235,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -245,7 +245,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -255,7 +255,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -265,7 +265,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -275,7 +275,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ diff --git a/examples/aws/config.json b/examples/aws/config.json index 7348f26..3ea2344 100644 --- a/examples/aws/config.json +++ b/examples/aws/config.json @@ -219,7 +219,7 @@ "vms": [ { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ @@ -229,7 +229,7 @@ }, { "ami_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64", - "instance_type": "c5a.4xlarge", + "instance_type": "c8i.4xlarge", "root_volume_size": 40, "max_runtime": 3600, "test": [ From 80af59331738158cde0d64196fd125c849ef8c3d Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Tue, 7 Jul 2026 14:13:03 +0200 Subject: [PATCH 4/8] fix(ci): update workflows to use 'main' branch instead of 'mainline' The repository default branch is 'main' but all workflows referenced 'mainline', causing them to never trigger on pull requests or pushes. Changes: - basic-ci.yml: trigger on main, drop Python 3.9/3.10 from matrix (package requires >=3.11) - coverage-check.yml: checkout origin/main for base coverage - codeql.yml: trigger on main Signed-off-by: Norbert Manthey --- .github/workflows/basic-ci.yml | 6 +++--- .github/workflows/codeql.yml | 4 ++-- .github/workflows/coverage-check.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/basic-ci.yml b/.github/workflows/basic-ci.yml index 0032d3b..11eea61 100644 --- a/.github/workflows/basic-ci.yml +++ b/.github/workflows/basic-ci.yml @@ -2,9 +2,9 @@ name: Basic CI on: push: - branches: [ mainline ] + branches: [ main ] pull_request: - branches: [ mainline ] + branches: [ main ] permissions: contents: read @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d1fb096..e35a710 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL" on: push: - branches: [ mainline ] + branches: [ main ] pull_request: - branches: [ mainline ] + branches: [ main ] jobs: analyze: diff --git a/.github/workflows/coverage-check.yml b/.github/workflows/coverage-check.yml index a84ea13..87847c5 100644 --- a/.github/workflows/coverage-check.yml +++ b/.github/workflows/coverage-check.yml @@ -29,7 +29,7 @@ jobs: - name: Get base branch coverage run: | - git checkout origin/mainline + git checkout origin/main pip install -e ".[dev]" python -m pytest tests/ -m "not integration" --cov=src --cov-report=json:coverage-base.json --tb=no -q || true BASE_COVERAGE=$(python -c "import json; print(json.load(open('coverage-base.json'))['totals']['percent_covered'])") From e961b783f465b852611e14d5daeaf32ea648d1b0 Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Tue, 7 Jul 2026 14:24:32 +0200 Subject: [PATCH 5/8] fix: resolve flake8 lint errors - cli.py: remove empty f-string, wrap long line - artifacts.py: remove trailing blank line - pull_labs_poller.py: remove unused submit_tests import - test_kcidb_submit.py: add blank lines before nested definitions - test_pull_labs_poller.py: remove unused tempfile import, fix over-indented continuation lines, rewrite test to not patch the removed submit_tests symbol - test_pull_labs_translate.py: remove unused DEFAULT_TEST_TYPE_MAP import Signed-off-by: Norbert Manthey --- .gitignore | 1 + src/kernel_ci_cloud_labs/cli.py | 7 ++- src/kernel_ci_cloud_labs/core/artifacts.py | 2 - src/kernel_ci_cloud_labs/pull_labs_poller.py | 1 - tests/test_kcidb_submit.py | 2 + tests/test_pull_labs_poller.py | 56 ++++++++------------ tests/test_pull_labs_translate.py | 1 - 7 files changed, 29 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 7ea422a..6c3487e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ analysis/data/ # Distribution archives share/ +test-nested.json diff --git a/src/kernel_ci_cloud_labs/cli.py b/src/kernel_ci_cloud_labs/cli.py index 2187bce..76df4ac 100644 --- a/src/kernel_ci_cloud_labs/cli.py +++ b/src/kernel_ci_cloud_labs/cli.py @@ -292,8 +292,11 @@ def main(): val_parser.add_argument("--bucket", help="S3 bucket to verify (and create with --fix)") val_parser.add_argument("--role", help="IAM role name used by VM instance profiles") val_parser.add_argument("--region", default="us-west-2", help="AWS region (default: us-west-2)") - val_parser.add_argument("--api-url", help=f"KernelCI API base URI (overrides $KERNELCI_API_BASE_URI)") - val_parser.add_argument("--fix", action="store_true", help="Create missing resources (S3 bucket) instead of just reporting them") + val_parser.add_argument("--api-url", help="KernelCI API base URI (overrides $KERNELCI_API_BASE_URI)") + val_parser.add_argument( + "--fix", action="store_true", + help="Create missing resources (S3 bucket) instead of just reporting them", + ) val_parser.set_defaults(func=cmd_setup_validate) args = parser.parse_args() diff --git a/src/kernel_ci_cloud_labs/core/artifacts.py b/src/kernel_ci_cloud_labs/core/artifacts.py index ebe330d..4b4de9b 100644 --- a/src/kernel_ci_cloud_labs/core/artifacts.py +++ b/src/kernel_ci_cloud_labs/core/artifacts.py @@ -262,5 +262,3 @@ def collect_run_artifacts( manifest_path, ) return manifest - - diff --git a/src/kernel_ci_cloud_labs/pull_labs_poller.py b/src/kernel_ci_cloud_labs/pull_labs_poller.py index 124e596..3984d5f 100644 --- a/src/kernel_ci_cloud_labs/pull_labs_poller.py +++ b/src/kernel_ci_cloud_labs/pull_labs_poller.py @@ -45,7 +45,6 @@ from kernel_ci_cloud_labs.kcidb_submit import ( build_test_row, - submit_tests, to_kcidb_status, ) from kernel_ci_cloud_labs.pull_labs_translate import translate_job diff --git a/tests/test_kcidb_submit.py b/tests/test_kcidb_submit.py index 5c2a8d2..5ca0eed 100644 --- a/tests/test_kcidb_submit.py +++ b/tests/test_kcidb_submit.py @@ -187,8 +187,10 @@ def test_submit_revision_posts_with_bearer_auth(self): class FakeResp: def __enter__(self): return self + def __exit__(self, *a): return False + def read(self): return b'{"status":"ok","id":"sub-1"}' diff --git a/tests/test_pull_labs_poller.py b/tests/test_pull_labs_poller.py index 787c341..b5ee0d0 100644 --- a/tests/test_pull_labs_poller.py +++ b/tests/test_pull_labs_poller.py @@ -8,7 +8,6 @@ import json import logging import os -import tempfile import urllib.error from unittest.mock import patch @@ -626,14 +625,16 @@ class TestProcessEventNodeResult: def _run(self, poller, event, translate=None): translate = translate or {"return_value": {}} captured = {} - with patch.object(poller, "_claim_node", return_value=True), \ - patch.object( - poller, "_finish_node", - side_effect=lambda nid, outcome: captured.update(outcome=outcome), - ), \ - patch(_GET, return_value={"artifacts": {}}), \ - patch("kernel_ci_cloud_labs.pull_labs_poller.translate_job", **translate), \ - patch("kernel_ci_cloud_labs.pull_labs_poller.submit_tests", return_value={}): + with ( + patch.object(poller, "_claim_node", return_value=True), + patch.object( + poller, "_finish_node", + side_effect=lambda nid, outcome: captured.update(outcome=outcome), + ), + patch(_GET, return_value={"artifacts": {}}), + patch("kernel_ci_cloud_labs.pull_labs_poller.translate_job", + **translate), + ): poller.process_event(event) return captured["outcome"] @@ -682,9 +683,10 @@ def test_translate_failure_finishes_invalid_job_params(self): assert "missing artifacts.kernel" in outcome.error_msg def test_per_instance_rows_carry_log_url_and_stable_test_id(self): - """When executor returns per-instance rows with log_url, the submitted - KCIDB rows must each carry that URL and a test_id derived from the - instance_id (not the positional index).""" + """When executor returns per-instance rows with log_url, the node + outcome must carry those URLs in artifacts.test_log (first URL) and + test_log_N (subsequent URLs), and the node result must reflect the + aggregated per-instance statuses.""" per_test = [ {"name": "boot", "status": "PASS", "instance_id": "i-aaaa1111", "log_url": "https://b.s3.eu-west-1.amazonaws.com/a.log"}, @@ -695,31 +697,15 @@ def test_per_instance_rows_carry_log_url_and_stable_test_id(self): _minimal_kc(), job_executor=lambda cfg: (per_test, None), ) - seen = {} - with patch.object(p, "_claim_node", return_value=True), \ - patch.object(p, "_finish_node"), \ - patch(_GET, return_value={"artifacts": {}}), \ - patch("kernel_ci_cloud_labs.pull_labs_poller.translate_job", - return_value={}), \ - patch( - "kernel_ci_cloud_labs.pull_labs_poller.submit_tests", - side_effect=lambda url, jwt, origin, build_id, rows: seen.update(rows=rows), - ): - p.process_event(_job_event(node_id="ndX")) - - rows = seen["rows"] - assert len(rows) == 2 - by_id = {r["id"]: r for r in rows} - # test_id derived from instance_id => stable across retries. - assert set(by_id) == {"pullab_cloud_aws:ndX.i-aaaa1111", "pullab_cloud_aws:ndX.i-bbbb2222"} - # Per-row log_url survives the build_test_row pass-through. - assert by_id["pullab_cloud_aws:ndX.i-aaaa1111"]["log_url"] == \ + outcome = self._run(p, _job_event(node_id="ndX")) + + # One FAIL among results -> overall node result is "fail". + assert outcome.result == "fail" + # Log URLs are attached to the outcome artifacts for send_kcidb. + assert outcome.artifacts["test_log"] == \ "https://b.s3.eu-west-1.amazonaws.com/a.log" - assert by_id["pullab_cloud_aws:ndX.i-bbbb2222"]["log_url"] == \ + assert outcome.artifacts["test_log_1"] == \ "https://b.s3.eu-west-1.amazonaws.com/b.log" - # instance_id surfaces in misc for traceability. - assert by_id["pullab_cloud_aws:ndX.i-aaaa1111"]["misc"]["instance_id"] == "i-aaaa1111" - # Aggregated node outcome from per-instance statuses. # (one fail among two -> fail; verified indirectly via existing tests). diff --git a/tests/test_pull_labs_translate.py b/tests/test_pull_labs_translate.py index 68a33a2..b1a6298 100644 --- a/tests/test_pull_labs_translate.py +++ b/tests/test_pull_labs_translate.py @@ -9,7 +9,6 @@ from kernel_ci_cloud_labs.pull_labs_translate import ( DEFAULT_PLATFORM_MAP, - DEFAULT_TEST_TYPE_MAP, translate_job, ) From a30d0d224b94a8a3293973739387287d6d38a915 Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Tue, 7 Jul 2026 14:33:40 +0200 Subject: [PATCH 6/8] fix: resolve pylint warnings - setup.py: add analysis deps (pandas, matplotlib, seaborn) to [dev] extra so pylint can resolve all imports during linting - pull_labs_poller.py: suppress unused-import on intentional boto3 availability check - setup_validate.py: remove redundant json.JSONDecodeError from except clauses (already covered by parent ValueError) - test_pull_labs_poller.py: disable protected-access check (tests intentionally exercise private methods) - test_role_manager.py: disable protected-access check - test_kcidb_submit.py: suppress unused-argument on mock callbacks that must match urlopen signature Signed-off-by: Norbert Manthey --- setup.py | 3 +++ src/kernel_ci_cloud_labs/pull_labs_poller.py | 2 +- src/kernel_ci_cloud_labs/setup_validate.py | 4 ++-- tests/test_kcidb_submit.py | 4 ++-- tests/test_pull_labs_poller.py | 1 + tests/test_role_manager.py | 1 + 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 8e39de9..8c11b9e 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,9 @@ "isort>=5.0", "pre-commit>=2.0", "pytest-cov>=2.0", + "pandas>=1.3.0", + "matplotlib>=3.4.0", + "seaborn>=0.11.0", ], "analysis": [ "pandas>=1.3.0", diff --git a/src/kernel_ci_cloud_labs/pull_labs_poller.py b/src/kernel_ci_cloud_labs/pull_labs_poller.py index 3984d5f..5057fe6 100644 --- a/src/kernel_ci_cloud_labs/pull_labs_poller.py +++ b/src/kernel_ci_cloud_labs/pull_labs_poller.py @@ -313,7 +313,7 @@ def _validate_default_executor_deps() -> None: problems: List[str] = [] try: - import boto3 # noqa: F401,PLC0415 + import boto3 # noqa: F401,PLC0415 # pylint: disable=unused-import except ImportError as e: problems.append( f"boto3 import failed ({e}) — run: python3.11 -m pip install -e ." diff --git a/src/kernel_ci_cloud_labs/setup_validate.py b/src/kernel_ci_cloud_labs/setup_validate.py index bb51f5b..da0419b 100644 --- a/src/kernel_ci_cloud_labs/setup_validate.py +++ b/src/kernel_ci_cloud_labs/setup_validate.py @@ -200,7 +200,7 @@ def _check_bucket_policy_statement(s3, bucket_name: str, fix: bool) -> bool: print(f"✗ Could not read bucket policy ({code}): {e}") return False existing = None - except (ValueError, json.JSONDecodeError) as e: + except ValueError as e: print(f"✗ Bucket policy is not valid JSON: {e}") return False @@ -365,7 +365,7 @@ def check_kcidb_jwt() -> bool: # JWT payload is base64url; pad to a multiple of 4 before decoding. payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4) payload = json.loads(base64.urlsafe_b64decode(payload_b64)) - except (ValueError, json.JSONDecodeError) as e: + except ValueError as e: print(f"✗ JWT payload not decodable: {e}") return False diff --git a/tests/test_kcidb_submit.py b/tests/test_kcidb_submit.py index 5ca0eed..fd1dfeb 100644 --- a/tests/test_kcidb_submit.py +++ b/tests/test_kcidb_submit.py @@ -239,7 +239,7 @@ def __enter__(self): return self def __exit__(self, *a): return False def read(self): return b'{}' - def fake_urlopen(req, timeout=None): + def fake_urlopen(req, timeout=None): # pylint: disable=unused-argument captured["body"] = json.loads(req.data.decode("utf-8")) return FakeResp() @@ -266,7 +266,7 @@ def __enter__(self): return self def __exit__(self, *a): return False def read(self): return b'{}' - def fake_urlopen(req, timeout=None): + def fake_urlopen(req, timeout=None): # pylint: disable=unused-argument captured["body"] = json.loads(req.data.decode("utf-8")) return FakeResp() diff --git a/tests/test_pull_labs_poller.py b/tests/test_pull_labs_poller.py index b5ee0d0..8b5e980 100644 --- a/tests/test_pull_labs_poller.py +++ b/tests/test_pull_labs_poller.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # # Copyright (C) 2026 Collabora Limited +# pylint: disable=protected-access # Author: Denys Fedoryshchenko """Unit tests for pull_labs_poller (no network, no AWS).""" diff --git a/tests/test_role_manager.py b/tests/test_role_manager.py index c3afbfb..dede843 100644 --- a/tests/test_role_manager.py +++ b/tests/test_role_manager.py @@ -1,4 +1,5 @@ """Unit tests for AWS Role Manager""" +# pylint: disable=protected-access __authors__ = ["Max Hubmann ", "Norbert Manthey "] __copyright__ = "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved." From bf3e19d3ebfcb17b5e6acf2754a19c4c95a904c5 Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Tue, 7 Jul 2026 14:56:40 +0200 Subject: [PATCH 7/8] test: add unit tests for nested virtualization detection Add test_nested_virtualization.py covering _supports_nested_virtualization() in VMLauncher. Tests verify that C8i, M8i, R8i, C7i, M7i, R7i, X8i, I7i families (and flex/d variants) are correctly detected as supporting nested virtualization, while older families (c5a, c6g, t3, m5) are not. Signed-off-by: Norbert Manthey --- tests/test_nested_virtualization.py | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_nested_virtualization.py diff --git a/tests/test_nested_virtualization.py b/tests/test_nested_virtualization.py new file mode 100644 index 0000000..c664b27 --- /dev/null +++ b/tests/test_nested_virtualization.py @@ -0,0 +1,83 @@ +"""Unit tests for nested virtualization support in launch_vm.""" +# pylint: disable=protected-access + +__authors__ = ["Norbert Manthey "] +__copyright__ = "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved." +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import patch, MagicMock + +import pytest + +from kernel_ci_cloud_labs.launch_vm import VMLauncher + + +@pytest.fixture +def _mock_boto3(): + """Patch boto3 so VMLauncher.__init__ doesn't need real AWS credentials.""" + with patch("kernel_ci_cloud_labs.launch_vm.boto3") as mock_boto: + mock_boto.client.return_value = MagicMock() + yield mock_boto + + +class TestNestedVirtualization: + """Tests for _supports_nested_virtualization and CpuOptions.""" + + def _make_launcher(self, instance_type, _mock_boto3): + """Create a VMLauncher with a given instance_type, mocking AWS.""" + vm_config = { + "instance_type": instance_type, + "ami_id": "ami-test123", + "role_name": "test-role", + "s3_bucket": "test-bucket", + "run_prefix": "run_test", + } + return VMLauncher(vm_config) + + def test_c8i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("c8i.4xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_c8i_flex_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("c8i-flex.2xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_m8i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("m8i.large", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_r8i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("r8i.xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_c7i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("c7i.2xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_m7i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("m7i.xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_x8i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("x8i.large", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_i7i_supports_nested(self, _mock_boto3): + launcher = self._make_launcher("i7i.xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is True + + def test_c5a_does_not_support_nested(self, _mock_boto3): + launcher = self._make_launcher("c5a.4xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is False + + def test_c6g_does_not_support_nested(self, _mock_boto3): + launcher = self._make_launcher("c6g.4xlarge", _mock_boto3) + assert launcher._supports_nested_virtualization() is False + + def test_t3_does_not_support_nested(self, _mock_boto3): + launcher = self._make_launcher("t3.micro", _mock_boto3) + assert launcher._supports_nested_virtualization() is False + + def test_m5_does_not_support_nested(self, _mock_boto3): + launcher = self._make_launcher("m5.large", _mock_boto3) + assert launcher._supports_nested_virtualization() is False From 2d3abf3daa944a6cebef588544d695ec321f71cd Mon Sep 17 00:00:00 2001 From: Norbert Manthey Date: Tue, 7 Jul 2026 15:11:36 +0200 Subject: [PATCH 8/8] fix: add long_description_content_type to setup.py Twine check failed because the content type was missing, defaulting to RST which cannot parse Markdown backticks. Set content type explicitly to text/markdown. Signed-off-by: Norbert Manthey --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8c11b9e..520742b 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ version="1.0.0", description="Kernel-ci-cloud-labs", long_description=long_description, + long_description_content_type="text/markdown", package_dir={"": "src"}, packages=find_packages(where="src"), python_requires=">=3.11",