diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f37369160..7af62e7d4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,7 +28,8 @@ RUN if [ "$USERNAME" != "vscode" ]; then \ && chmod 0440 /etc/sudoers.d/${USERNAME}; \ fi -RUN apt-get update && apt-get install -y qemu-system-x86 iputils-ping tcpdump cloud-image-utils +RUN apt-get update && apt-get install -y qemu-system-x86 iputils-ping tcpdump cloud-image-utils libguestfs-tools linux-image-$(uname -r) \ + && chmod o+r /boot/vmlinuz-* # Set the default user for the container USER ${USERNAME} diff --git a/.github/workflows/build_and_test_host.yml b/.github/workflows/build_and_test_host.yml index 72ed2d463..fdb3b1f15 100644 --- a/.github/workflows/build_and_test_host.yml +++ b/.github/workflows/build_and_test_host.yml @@ -67,7 +67,7 @@ jobs: - name: Install tools needed for ITF tests run: | sudo apt-get update - sudo apt-get install -y qemu-system-x86 iputils-ping tcpdump cloud-image-utils + sudo apt-get install -y qemu-system-x86 qemu-system-arm iputils-ping tcpdump cloud-image-utils libguestfs-tools - name: Allow linux-sandbox uses: eclipse-score/cicd-actions/unblock-user-namespace-for-linux-sandbox@212bbf86267e9381da9d2daf962d12f6feafbc90 # v0.0.2 - run: df -h @@ -76,8 +76,16 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Allow unprivileged user namespaces + run: | + sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 + - name: Allow supermin to read kernel + run: | + sudo chmod a+r /boot/vmlinuz-$(uname -r) - name: Run Linux QEMU integration tests run: | - bazel test --config=qemu-integration //quality/... //tests/integration_test/... + bazel test \ + --config=qemu-integration \ + //quality/... //tests/integration_test/... - run: df -h if: always() diff --git a/MODULE.bazel b/MODULE.bazel index 1983fca52..546714e2e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -263,16 +263,22 @@ http_archive( # ============================================================================ # Integration Testing # ============================================================================ +bazel_dep(name = "os_images", version = "1.0", dev_dependency = True) bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) bazel_dep(name = "score_rules_imagefs", version = "0.0.4", dev_dependency = True) -http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +git_override( + module_name = "score_itf", + commit = "15891e79da565fa6b038fbf22074b86fd9bc55a1", + patch_strip = 1, + patches = ["//:third_party/score_itf-qemu-ping-timeout.patch"], + remote = "https://github.com/elektrobit-contrib/eclipse-score_itf.git", +) -http_file( - name = "ubuntu_24_04_cloudimg", - downloaded_file_path = "noble-minimal-cloudimg-amd64.img", - integrity = "sha256-vZAj5xt7tb3tFOT7DJmODZPXFJ6QVsPmi0b/kJfdF9U=", - url = "https://cloud-images.ubuntu.com/minimal/releases/noble/release-20260626/ubuntu-24.04-minimal-cloudimg-amd64.img", +git_override( + module_name = "os_images", + commit = "a024787e2ddb4ceed6ac6fe63b3847ab937c0b5d", + remote = "https://github.com/eclipse-score/os_images.git", ) imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 6e3dbb4f0..e474a1e26 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -53,6 +53,9 @@ "https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/MODULE.bazel": "74bf20a7a6bd5f2be09607fdb4196cfd6f203422ea271752ec2b1afe95426101", "https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/source.json": "411ec9d79d6f5fe8a083359588c21d01a5b48d88a2cbd334a4c90365015b7836", "https://bcr.bazel.build/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", + "https://bcr.bazel.build/modules/aspect_rules_lint/1.0.3/MODULE.bazel": "ed0fe929647ba21d2041e14ea3d757133ca306b72d4998e8a3d0d2f515196765", + "https://bcr.bazel.build/modules/aspect_rules_lint/1.3.1/MODULE.bazel": "06ce330900a7d6403bc8d88e5dfad6aeeb8ae40179f66bb89e69c8bf6f6b1a0b", + "https://bcr.bazel.build/modules/aspect_rules_lint/1.4.2/MODULE.bazel": "78d025facf6fa675fd6f0b62fd6a9a2bec7ef5ae1e288e5b53f4383b98017105", "https://bcr.bazel.build/modules/aspect_rules_lint/2.0.0/MODULE.bazel": "1f4c81edd9af0c54e65dedbe36653fc63dc9a91605313d54d5ab371f853d693c", "https://bcr.bazel.build/modules/aspect_rules_lint/2.3.0/MODULE.bazel": "4d36bce3c1347bb4fed5906877dce0e683cb207f4032711aff32334003bcf835", "https://bcr.bazel.build/modules/aspect_rules_lint/2.5.0/MODULE.bazel": "24f49acb3b8375fa138ba6ab53bcf517ae92529770a04ab04d47c958ff47b63e", @@ -76,6 +79,7 @@ "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://bcr.bazel.build/modules/bazel_features/1.2.0/MODULE.bazel": "122b2b606622afbaa498913d54f52d9bcd2d19a5edd1bd6d6c5aa17441c4d5f9", "https://bcr.bazel.build/modules/bazel_features/1.20.0/MODULE.bazel": "8b85300b9c8594752e0721a37210e34879d23adc219ed9dc8f4104a4a1750920", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", @@ -215,6 +219,7 @@ "https://bcr.bazel.build/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", "https://bcr.bazel.build/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", "https://bcr.bazel.build/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", + "https://bcr.bazel.build/modules/buildifier_prebuilt/6.1.2/MODULE.bazel": "2ef4962c8b0b6d8d21928a89190755619254459bc67f870dc0ccb9ba9952d444", "https://bcr.bazel.build/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "37389c6b5a40c59410b4226d3bb54b08637f393d66e2fa57925c6fcf68e64bf4", "https://bcr.bazel.build/modules/buildifier_prebuilt/7.3.1/MODULE.bazel": "537faf0ad9f5892910074b8e43b4c91c96f1d5d86b6ed04bdbe40cf68aa48b68", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.0.2/MODULE.bazel": "a9b689711d5b69f9db741649b218c119b9fdf82924ba390415037e09798edd03", @@ -482,8 +487,10 @@ "https://bcr.bazel.build/modules/rules_multirun/0.9.0/MODULE.bazel": "32d628ef586b5b23f67e55886b7bc38913ea4160420d66ae90521dda2ff37df0", "https://bcr.bazel.build/modules/rules_multirun/0.9.0/source.json": "e882ba77962fa6c5fe68619e5c7d0374ec9a219fb8d03c42eadaf6d0243771bd", "https://bcr.bazel.build/modules/rules_multitool/0.11.0/MODULE.bazel": "8d9dda78d2398e136300d3ef4fbcc89ede7c32c158d8c016fa7d032df41c4aaf", + "https://bcr.bazel.build/modules/rules_multitool/0.4.0/MODULE.bazel": "15517987d5c00c9e7faab41fbe22ee67a350b6eabcc1e08baded5c6d9025897f", "https://bcr.bazel.build/modules/rules_multitool/1.11.1/MODULE.bazel": "f826d2d394e8d964e44ebb4a75ebcfe4e9cd4eb150e2ddcd60398ffeb939696a", "https://bcr.bazel.build/modules/rules_multitool/1.11.1/source.json": "201f43de1d35bd17f25a4fed3ba5a2ec500ef5e08b7d4b341bb9fd39cef0cbc6", + "https://bcr.bazel.build/modules/rules_multitool/1.2.0/MODULE.bazel": "8d818d6104f4030930291bbbbc5684702c237dbcdee7229097543e6a6035adaa", "https://bcr.bazel.build/modules/rules_multitool/1.9.0/MODULE.bazel": "8a042b0dbf35e4aaa94c28ad69efa75c9e673e9ea4bd5c0fb70bab75ef9c636b", "https://bcr.bazel.build/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", "https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e", @@ -502,6 +509,7 @@ "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", + "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc2/MODULE.bazel": "e17f94f8a347e2c808517b65d74988839d2d62daceb50073e44060193b785eb1", "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", @@ -577,6 +585,7 @@ "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/MODULE.bazel": "9b8be503a4fcfd3b8b952525bff0869177a5234d5c35dc3e566b9f5ca2f755a1", "https://bcr.bazel.build/modules/toolchains_llvm/1.7.0/MODULE.bazel": "55aca6a8c5b372651f663c5e22faf3664d81165e40074c98fb8a30ee5af83272", "https://bcr.bazel.build/modules/toolchains_llvm/1.7.0/source.json": "cda5fa1abeab5561e811860222952f6cb9373f34b88c5b3f4417459dca057a6d", + "https://bcr.bazel.build/modules/toolchains_protoc/0.2.1/MODULE.bazel": "2f08433ff5e659069b3a1abfee2377d68f510f2de1da50678ed992c455b4ff91", "https://bcr.bazel.build/modules/trlc/3.0.0/MODULE.bazel": "45444f9112300e7a716bf842beb17ed8f84d7d1f84c3d1ed126f57bec9bdf2a9", "https://bcr.bazel.build/modules/trlc/3.0.1/MODULE.bazel": "9ece74d6ae5a718850a6f39d94ac52a84c002e0a5c6dadf676433e330c2e2f6c", "https://bcr.bazel.build/modules/trlc/3.0.1/source.json": "3bd9ec62c5a44d759377acee89b7ed966e2ce227292b2b175424bf4343ed0b66", @@ -644,6 +653,9 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_js/2.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_js/2.3.8/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/1.0.3/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/1.3.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/1.4.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/2.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/2.3.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/2.5.0/MODULE.bazel": "not found", @@ -663,6 +675,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.17.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.18.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.19.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.2.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.20.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.21.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_features/1.23.0/MODULE.bazel": "not found", @@ -752,6 +765,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boringssl/0.20240913.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boringssl/0.20241024.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/buildifier_prebuilt/6.1.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/buildifier_prebuilt/7.3.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/buildifier_prebuilt/8.2.0.2/MODULE.bazel": "not found", @@ -966,7 +980,9 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_license/1.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multirun/0.9.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multitool/0.11.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multitool/0.4.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multitool/1.11.1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multitool/1.2.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_multitool/1.9.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/5.8.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.2.0/MODULE.bazel": "not found", @@ -982,6 +998,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/4.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/6.0.0-rc2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/6.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/6.0.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_proto/7.0.2/MODULE.bazel": "not found", @@ -1055,12 +1072,14 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.6.1/MODULE.bazel": "0dae734ea8a99970a7417829b3115af02717b29f0fc40a8ebd3c1afcc71e1e21", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/MODULE.bazel": "ab2af2d8fab73e4512d2e2bd399a64d10c5c5463388322f7025637b13ec7585c", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/source.json": "c3992257800c4e3408be5e4656c0903fc19d3933011cc58e85d9a29c7214f374", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/MODULE.bazel": "d3ae40173462419b4e14e3fea96a49eab8822be7292f94890beb8cc73173eaa1", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/source.json": "3c0fcd761e221bc33e6ebbeb0f92ce71b5d6f1f946b60ccf4fe7cf3a4c00d211", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_format_checker/0.1.1/MODULE.bazel": "1acc254faa90e9f97b79ac69af25b6c21c561f8d6079914f6352b9b20d26bd37", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_format_checker/0.1.1/source.json": "e3d10be878d1a007967b75a5c6d9c3ec3d47c320685904b7f31c689b3cbcdb3a", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.6.0/MODULE.bazel": "2496bc24311f69f49449ee85d8bb38e3b970cbfcf10d0a7f19b2d5262ce80e8d", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/2.0.1/MODULE.bazel": "88bff0ed46da79d87f8c441a6bf6b760ee7c194b282e7e54a8b7db6ea2354db9", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/2.0.2/MODULE.bazel": "6d2b227bb6880e9f6871cc8cf94c83399e35c188424f7e9b826262de8149ed43", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/2.0.2/source.json": "5ae55f0dabcddeb5bfae16ce480e2c065f907d2b0ed6a8e4ed7409b259b004ee", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.4/MODULE.bazel": "53bd16dfbb1fb8ecf6822fb26f9f4e8333bac7b14d12bb02bf84078063820a31", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.4/source.json": "28ed0207a16f8498a84fae4983fa73060805003a4fbd68c6d749e2f1cc14601f", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_qnx_unit_tests/0.2.0/MODULE.bazel": "aaffff67916dd058737aac8e04368f12b214e7646ff58adbab48e6429d1a5410", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_qnx_unit_tests/0.2.0/source.json": "c40a614fd88beab7cb65b8640debd44eff3fbc32a7839734ef1112f265d2eeef", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rules_imagefs/0.0.3/MODULE.bazel": "d87fcd3485438284fa96d6a5a81923d6b6672468947423517cc43c4d7692ca6a", @@ -1088,6 +1107,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/tar.bzl/0.7.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchain_utils/1.0.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchains_llvm/1.7.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/toolchains_protoc/0.2.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/trlc/3.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/trlc/3.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "not found", diff --git a/REUSE.toml b/REUSE.toml index 887521a53..080e09d18 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -51,8 +51,7 @@ path = [ "tests/integration_test/vsomeip.json", "tests/benchmarks/config/benchmark_mw_com_config.json", "tests/tc8_conformance/**/*.json", - "quality/integration_testing/environments/qnx8_qemu/qemu_config.json", - "quality/integration_testing/environments/ubuntu24_04_qemu/*" + "quality/integration_testing/environments/qnx8_qemu/qemu_config.json" ] SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" SPDX-License-Identifier = "Apache-2.0" diff --git a/quality/README.md b/quality/README.md index ce44b5090..dacaaa4c4 100644 --- a/quality/README.md +++ b/quality/README.md @@ -52,13 +52,13 @@ Integration tests are enabled only for QEMU-backed runs: ### filesystem tar -The tests and their data are packaged into a filesystem tar and either build into (Docker) the image or uploaded after image startup. +The tests and their data are packaged into a filesystem tar and overlaid onto the QEMU rootfs image before test execution. ### What Runs in QEMU #### Linux QEMU -If QEMU is selected, the custom plugin [`linux_qemu`](integration_testing/plugins/linux_qemu/README.md) is used. +If QEMU is selected, the upstream ITF QEMU plugin is used. #### QNX QEMU diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD b/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD deleted file mode 100644 index de53ab78d..000000000 --- a/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD +++ /dev/null @@ -1,104 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -exports_files( - [ - "qemu_config.json", - "user-data", - "meta-data", - "prepare_image.sh", - ], - visibility = ["//:__subpackages__"], -) - -filegroup( - name = "qemu_config", - srcs = ["qemu_config.json"], - visibility = ["//:__subpackages__"], -) - -# Network config for cloud-init -# Tagged "manual" so it is only built when explicitly needed (linux_qemu tests). -genrule( - name = "network_config", - srcs = ["qemu_config.json"], - outs = ["network-config.yaml"], - cmd = """ -set -euo pipefail - -python3 - <<'PY' "$(location qemu_config.json)" "$@" -import json -import sys - -config_path, output_path = sys.argv[1:3] - -with open(config_path, "r", encoding="utf-8") as config_file: - config = json.load(config_file) - -networks = config.get("networks", []) -if not networks: - raise ValueError("qemu_config.json must define at least one network") - -network = networks[0] - -with open(output_path, "w", encoding="utf-8") as network_config: - network_config.write("version: 2\\n") - network_config.write("ethernets:\\n") - network_config.write(" testnic0:\\n") - network_config.write(" match:\\n") - network_config.write(' name: "en*"\\n') - network_config.write(" dhcp4: false\\n") - network_config.write(" dhcp6: false\\n") - network_config.write(" optional: true\\n") - network_config.write(" link-local: []\\n") - network_config.write(" addresses:\\n") - network_config.write(f" - {network['ip_address']}/16\\n") - network_config.write(" routes:\\n") - network_config.write(" - to: default\\n") - network_config.write(" on-link: true\\n") - network_config.write(f" via: {network['gateway']}\\n") -PY -""", - tags = ["manual"], - visibility = ["//:__subpackages__"], -) - -# Cloud-init seed image generation to configure the Ubuntu 24.04 QEMU image on boot. -# Tagged "manual" so it is only built when explicitly needed (linux_qemu tests). -genrule( - name = "seed_iso", - srcs = [ - ":network_config", - "user-data", - "meta-data", - ], - outs = ["seed.img"], - cmd = "cloud-localds -N $(location :network_config) $@ $(location user-data) $(location meta-data)", - tags = ["manual"], - visibility = ["//:__subpackages__"], -) - -# Build a persistent Ubuntu base image with tools needed by integration tests. -# It boots once with internet access and installs iputils-ping into the base image. -genrule( - name = "prepared_image", - srcs = ["@ubuntu_24_04_cloudimg//file"], - outs = ["ubuntu24_04_prepared.qcow2"], - cmd = "bash $(location prepare_image.sh) $(location @ubuntu_24_04_cloudimg//file) $@", - tags = [ - "manual", - "requires-network", - ], - tools = ["prepare_image.sh"], - visibility = ["//:__subpackages__"], -) diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data b/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data deleted file mode 100644 index 15f31fe9d..000000000 --- a/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data +++ /dev/null @@ -1,2 +0,0 @@ -instance-id: score-integration-test -local-hostname: ubuntu-score diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/prepare_image.sh b/quality/integration_testing/environments/ubuntu24_04_qemu/prepare_image.sh deleted file mode 100755 index cd7bdd464..000000000 --- a/quality/integration_testing/environments/ubuntu24_04_qemu/prepare_image.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -set -euo pipefail - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -base_image="$1" -output_image="$2" - -tmp_dir="$(mktemp -d)" -cleanup() { - rm -rf "${tmp_dir}" -} -trap cleanup EXIT - -cp --reflink=auto "${base_image}" "${output_image}" -chmod u+w "${output_image}" - -cat >"${tmp_dir}/user-data" <<'EOF' -#cloud-config -# Prepare base image: minimize boot overhead by disabling non-essential packages and services. - -package_update: true -packages: - - iputils-ping - - libatomic1 - -# Remove unnecessary packages to reduce image size and boot time -bootcmd: - # Disable non-essential services - be conservative to avoid breaking system - # Disable update services (not needed for test environment) - - systemctl mask apt-daily.timer apt-daily-upgrade.timer unattended-upgrades - # Disable hardware-specific services not needed for QEMU - - systemctl mask accounts-daemon multipathd e2scrub.timer e2scrub_all.timer - # Disable bluetooth, printing, and mDNS services - - systemctl mask bluetooth cups cups-browsed avahi-daemon - # Disable unnecessary timer jobs - - systemctl mask motd-news.timer - -power_state: - mode: poweroff - timeout: 120 -EOF - -cat >"${tmp_dir}/meta-data" <<'EOF' -instance-id: ubuntu24-prepare-image -local-hostname: ubuntu24-prepare-image -EOF - -cloud-localds "${tmp_dir}/seed.img" "${tmp_dir}/user-data" "${tmp_dir}/meta-data" - -# Boot once with user-mode networking (NAT) to provide internet access for apt. -# Cloud-init installs iputils-ping and powers the VM off when complete. -timeout 25m qemu-system-x86_64 \ - -machine accel=kvm:tcg \ - -cpu max \ - -smp 2 \ - -m 2048 \ - -drive if=virtio,format=qcow2,file="${output_image}" \ - -drive if=virtio,format=raw,file="${tmp_dir}/seed.img" \ - -nic user,model=virtio-net-pci \ - -nographic \ - -display none \ - -serial none \ - -monitor none \ - -no-reboot - -qemu-img info "${output_image}" >/dev/null diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json b/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json deleted file mode 100644 index b992bbb1b..000000000 --- a/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "networks": [ - { - "name": "tap0", - "ip_address": "169.254.158.190", - "gateway": "169.254.21.88" - } - ], - "ssh_port": 22, - "qemu_num_cores": 2, - "qemu_ram_size": "2G", - "boot_mode": "disk" -} diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/user-data b/quality/integration_testing/environments/ubuntu24_04_qemu/user-data deleted file mode 100644 index 6915b5faa..000000000 --- a/quality/integration_testing/environments/ubuntu24_04_qemu/user-data +++ /dev/null @@ -1,14 +0,0 @@ -#cloud-config -# Cloud-init configuration for QEMU-based integration tests. -# Enables root SSH access with empty password for the ITF test harness. -# All config is in bootcmd to ensure it runs before sshd starts. -# Services are already masked from image prep; this ensures minimal runtime overhead. - -ssh_pwauth: true -disable_root: false - -bootcmd: - - sed -i 's/^root:[^:]*:/root::/' /etc/shadow - - printf 'PermitRootLogin yes\nPermitEmptyPasswords yes\nPasswordAuthentication yes\n' > /etc/ssh/sshd_config.d/99-integration-test.conf - - printf 'auth sufficient pam_permit.so\naccount sufficient pam_permit.so\nsession sufficient pam_permit.so\n' > /etc/pam.d/sshd - - iface=$(ip -4 route show default | awk '{print $5; exit}'); [ -n "$iface" ] && ip route replace 224.0.0.0/4 dev "$iface" diff --git a/quality/integration_testing/integration_testing.bzl b/quality/integration_testing/integration_testing.bzl index 415c6e084..947dd70af 100644 --- a/quality/integration_testing/integration_testing.bzl +++ b/quality/integration_testing/integration_testing.bzl @@ -14,7 +14,7 @@ """Integration test macro with QEMU-only backends.""" load("@rules_pkg//pkg:tar.bzl", "pkg_tar") -load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_itf//:defs.bzl", "copy_files_onto_image", "py_itf_test") load("@score_rules_imagefs//rules/qnx:ifs.bzl", "qnx_ifs") def _extend_list_in_kwargs(kwargs, key, values): @@ -54,11 +54,19 @@ def integration_test(name, srcs, filesystem, **kwargs): pkg_tar( name = filesystem_tar, srcs = [filesystem], + tags = ["manual"], ) - linux_qemu_config = Label("//quality/integration_testing/environments/ubuntu24_04_qemu:qemu_config") - linux_qemu_image = Label("//quality/integration_testing/environments/ubuntu24_04_qemu:prepared_image") - linux_qemu_seed_iso = Label("//quality/integration_testing/environments/ubuntu24_04_qemu:seed_iso") + linux_qemu_config = Label("@os_images//ubuntu_x86_64:qemu_config") + linux_qemu_image = Label("@os_images//ubuntu_x86_64:image") + + filesystem_rootfs_overlay = "_qemu_rootfs_overlay_{}".format(name) + copy_files_onto_image( + name = filesystem_rootfs_overlay, + image = linux_qemu_image, + srcs = [filesystem_tar], + tags = ["manual"], + ) # --- QNX QEMU artifacts --- QNX_TARGET_COMPATIBLE_WITH = [ @@ -81,10 +89,9 @@ def integration_test(name, srcs, filesystem, **kwargs): _extend_list_in_kwargs(kwargs, "data", select({ "@platforms//os:qnx": [qemu_image, qnx_qemu_config], "//quality/integration_testing/flags:linux_qemu": [ - filesystem_tar, + filesystem_rootfs_overlay, linux_qemu_config, - linux_qemu_image, - linux_qemu_seed_iso, + linux_qemu_image, # backing file must be accessible at runtime ], "//conditions:default": [], })) @@ -95,14 +102,12 @@ def integration_test(name, srcs, filesystem, **kwargs): "@platforms//os:qnx": [ "--log-cli-level=DEBUG", "--qemu-config=$(location {})".format(qnx_qemu_config), - "--qemu-image=$(location {})".format(qemu_image), + "--qemu-kernel=$(location {})".format(qemu_image), ], "//quality/integration_testing/flags:linux_qemu": [ "--log-cli-level=DEBUG", "--qemu-config=$(location {})".format(linux_qemu_config), - "--qemu-image=$(location {})".format(linux_qemu_image), - "--qemu-seed-iso=$(location {})".format(linux_qemu_seed_iso), - "--qemu-filesystem-tar=$(location {})".format(filesystem_tar), + "--qemu-rootfs=$(location {})".format(filesystem_rootfs_overlay), ], "//conditions:default": [], }), @@ -147,7 +152,7 @@ def integration_test(name, srcs, filesystem, **kwargs): "@score_itf//score/itf/plugins:qemu_plugin", ], "//quality/integration_testing/flags:linux_qemu": [ - "//quality/integration_testing/plugins/linux_qemu:linux_qemu_plugin", + "@score_itf//score/itf/plugins:qemu_plugin", ], "//conditions:default": [], }), diff --git a/quality/integration_testing/plugins/linux_qemu/BUILD b/quality/integration_testing/plugins/linux_qemu/BUILD deleted file mode 100644 index 1a089b7bd..000000000 --- a/quality/integration_testing/plugins/linux_qemu/BUILD +++ /dev/null @@ -1,55 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -load("@rules_python//python:defs.bzl", "py_library", "py_test") -load("@score_itf//bazel:py_itf_plugin.bzl", "py_itf_plugin") - -py_library( - name = "linux_qemu_lib", - srcs = [ - "__init__.py", - "config.py", - "qemu.py", - "qemu_process.py", - ], - imports = ["../.."], - target_compatible_with = ["@platforms//os:linux"], - visibility = ["//visibility:public"], - deps = [ - "@score_itf//score/itf/core/process", - "@score_itf//score/itf/core/utils", - "@score_itf//score/itf/plugins:core", - "@score_itf//score/itf/plugins/qemu", - "@score_someip_gateway_pip//pytest", - ], -) - -py_itf_plugin( - name = "linux_qemu_plugin", - enabled_plugins = [ - "quality.integration_testing.plugins.linux_qemu", - ], - py_library = ":linux_qemu_lib", - visibility = ["//visibility:public"], -) - -py_test( - name = "linux_qemu_unit_tests", - srcs = ["qemu_test.py"], - imports = ["../.."], - main = "qemu_test.py", - target_compatible_with = ["@platforms//os:linux"], - deps = [ - ":linux_qemu_lib", - ], -) diff --git a/quality/integration_testing/plugins/linux_qemu/README.md b/quality/integration_testing/plugins/linux_qemu/README.md deleted file mode 100644 index ef881d83a..000000000 --- a/quality/integration_testing/plugins/linux_qemu/README.md +++ /dev/null @@ -1,38 +0,0 @@ - - -# Linux QEMU - -For Linux QEMU, the macro provide the following arguments: - -- `--qemu-config` QEMU config following the format of ITF QEMU config -- `--qemu-image` Linux base image with [cloud-init](https://docs.cloud-init.io/en/latest/index.html) support -- `--qemu-seed-iso` Image containing [cloud-init](https://docs.cloud-init.io/en/latest/index.html) configuration -- `--qemu-filesystem-tar` built from the `filesystem` target - -The custom plugin behavior is: - -1. Create a **temporary qcow2 overlay** backed by `--qemu-image`. -2. Boot QEMU from that overlay (optionally with seed ISO). -3. Upload and extract `--qemu-filesystem-tar` into `/` inside the guest. -4. Remove the temporary overlay after the test session. - -This is intentional: the Linux base image is treated as a pristine input and is **not modified** by test execution. -That ensures that tests cannot leave artifacts influencing subsequent tests. - -## Why this plugin - -The ITF QEMU plugin does not support specifying more than one disk via `--qemu-seed-iso`, -which is needed for customization without modification of the base image. diff --git a/quality/integration_testing/plugins/linux_qemu/__init__.py b/quality/integration_testing/plugins/linux_qemu/__init__.py deleted file mode 100644 index 128b280ca..000000000 --- a/quality/integration_testing/plugins/linux_qemu/__init__.py +++ /dev/null @@ -1,209 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""Custom QEMU plugin for Linux disk-boot integration tests. - -This plugin extends the S-CORE ITF QEMU plugin to support booting from a disk -image (qcow2) with an optional cloud-init seed ISO, without requiring patches -to the upstream score_itf package. -""" - -import logging -import os -import socket -import subprocess -import tempfile -import time - -import pytest - -from score.itf.core.utils.bunch import Bunch -from score.itf.plugins.qemu.qemu_target import QemuTarget - -from quality.integration_testing.plugins.linux_qemu.qemu_process import LinuxQemuProcess -from quality.integration_testing.plugins.linux_qemu.config import load_configuration - -logger = logging.getLogger(__name__) - - -# Cloud-init first-boot needs significantly more time than a pre-configured VM. -_SSH_BOOT_TIMEOUT = 20 -_SSH_BOOT_RETRIES = 18 -_TARGET_READY_ATTEMPTS = 8 -_TARGET_READY_BACKOFF_SECONDS = 20 - - -def _wait_for_target_ready(target): - """Wait for SSH and SFTP to become available on the target. - - Uses longer timeouts than the upstream pre_tests_phase to accommodate - cloud-init first-boot provisioning on a fresh qcow2 overlay. - """ - last_error = None - for attempt in range(1, _TARGET_READY_ATTEMPTS + 1): - try: - with target.ssh( - timeout=_SSH_BOOT_TIMEOUT, - n_retries=_SSH_BOOT_RETRIES, - retry_interval=5, - ) as ssh: - result = ssh.execute_command("echo ready") - if result != 0: - raise RuntimeError("SSH command on target failed after boot") - logger.info("Target SSH is ready") - - with target.sftp() as sftp: - result = sftp.list_dirs_and_files("/") - if not result: - raise RuntimeError("SFTP command on target failed") - logger.info("Target SFTP is ready") - return - except Exception as error: # noqa: BLE001 - external transport exceptions vary by backend. - last_error = error - if attempt >= _TARGET_READY_ATTEMPTS: - break - - backoff_seconds = attempt * _TARGET_READY_BACKOFF_SECONDS - logger.warning( - "Target is not ready yet (attempt %d/%d): %s. Retrying in %ds...", - attempt, - _TARGET_READY_ATTEMPTS, - error, - backoff_seconds, - ) - time.sleep(backoff_seconds) - - raise RuntimeError( - f"Target readiness check failed after {_TARGET_READY_ATTEMPTS} attempts: {last_error}" - ) from last_error - - -def pytest_addoption(parser): - parser.addoption( - "--qemu-config", - action="store", - required=True, - help="Path to JSON file with QEMU target configuration.", - ) - parser.addoption( - "--qemu-image", - action="store", - required=True, - help="Path to a QEMU disk image (qcow2).", - ) - parser.addoption( - "--qemu-seed-iso", - action="store", - default=None, - help="Path to a cloud-init NoCloud seed ISO.", - ) - parser.addoption( - "--qemu-filesystem-tar", - action="store", - default=None, - help="Path to a tar archive containing the test filesystem to deploy onto the QEMU target.", - ) - - -@pytest.fixture(scope="session") -def dlt(): - """Overrideable fixture for enabling DLT collection.""" - pass - - -@pytest.fixture(scope="session") -def config(request): - qemu_image = os.path.abspath(request.config.getoption("qemu_image")) - qemu_seed_iso = request.config.getoption("qemu_seed_iso") - if qemu_seed_iso: - qemu_seed_iso = os.path.abspath(qemu_seed_iso) - - return Bunch( - qemu_config=load_configuration( - os.path.abspath(request.config.getoption("qemu_config")) - ), - qemu_image=qemu_image, - qemu_seed_iso=qemu_seed_iso, - ) - - -@pytest.fixture(scope="session") -def target_init(config, request, dlt): - logger.info(f"Starting tests on host: {socket.gethostname()}") - - # Create a qcow2 overlay backed by the pristine base image so that each - # test session starts from an unmodified disk. All writes go to the - # ephemeral overlay which is discarded after the session. - base_image = config.qemu_image - overlay_fd, overlay_path = tempfile.mkstemp(suffix=".qcow2", prefix="qemu_overlay_") - os.close(overlay_fd) - try: - subprocess.run( - [ - "qemu-img", - "create", - "-f", - "qcow2", - "-b", - base_image, - "-F", - "qcow2", - overlay_path, - ], - check=True, - capture_output=True, - ) - logger.info(f"Created qcow2 overlay: {overlay_path} (backing: {base_image})") - - process = LinuxQemuProcess( - path_to_qemu_image=overlay_path, - available_ram=config.qemu_config.qemu_ram_size, - available_cores=config.qemu_config.qemu_num_cores, - network_adapters=[adapter.name for adapter in config.qemu_config.networks], - port_forwarding=config.qemu_config.port_forwarding, - seed_iso=config.qemu_seed_iso, - ) - - with process: - target = QemuTarget(process, config.qemu_config) - _wait_for_target_ready(target) - yield target - finally: - if os.path.exists(overlay_path): - os.remove(overlay_path) - logger.info(f"Removed qcow2 overlay: {overlay_path}") - - -@pytest.fixture(scope="session", autouse=True) -def deploy_filesystem(request, target_init): - """Upload and extract the test filesystem tar onto the QEMU target.""" - tar_path = request.config.getoption("qemu_filesystem_tar") - if not tar_path: - return - - tar_path = os.path.abspath(tar_path) - if not os.path.isfile(tar_path): - pytest.skip(f"Filesystem tar not found: {tar_path}") - return - - remote_tar = "/tmp/_test_filesystem.tar" - - logger.info(f"Uploading test filesystem from {tar_path} to {remote_tar}") - target_init.upload(tar_path, remote_tar) - - logger.info("Extracting test filesystem on target") - exit_code, output = target_init.execute(f"tar xf {remote_tar} -C /") - if exit_code != 0: - pytest.fail(f"Failed to extract filesystem tar on target: {output}") - - exit_code, _ = target_init.execute(f"rm -f {remote_tar}") - logger.info("Test filesystem deployed successfully") diff --git a/quality/integration_testing/plugins/linux_qemu/config.py b/quality/integration_testing/plugins/linux_qemu/config.py deleted file mode 100644 index c66088e00..000000000 --- a/quality/integration_testing/plugins/linux_qemu/config.py +++ /dev/null @@ -1,126 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""Configuration loading for the Linux QEMU plugin. - -Provides a lightweight configuration loader that does not depend on pydantic, -avoiding the need to reference the score_itf-internal pip hub. -""" - -import ipaddress -import json -import logging -import re - -logger = logging.getLogger(__name__) - -_RAM_SIZE_PATTERN = re.compile(r"^[0-9]+[KMGTP]$") - - -class PortForwarding: - __slots__ = ("host_port", "guest_port") - - def __init__(self, host_port: int, guest_port: int): - if not (1 <= host_port <= 65535): - raise ValueError(f"host_port must be 1..65535, got {host_port}") - if not (1 <= guest_port <= 65535): - raise ValueError(f"guest_port must be 1..65535, got {guest_port}") - self.host_port = host_port - self.guest_port = guest_port - - -class Network: - __slots__ = ("name", "ip_address", "gateway") - - def __init__(self, name: str, ip_address: str, gateway: str): - if not name: - raise ValueError("network name must not be empty") - ipaddress.IPv4Address(ip_address) - ipaddress.IPv4Address(gateway) - self.name = name - self.ip_address = ip_address - self.gateway = gateway - - -class QemuConfig: - """Validated QEMU configuration.""" - - __slots__ = ( - "networks", - "ssh_port", - "qemu_num_cores", - "qemu_ram_size", - "port_forwarding", - ) - - def __init__( - self, networks, ssh_port, qemu_num_cores, qemu_ram_size, port_forwarding - ): - self.networks = networks - self.ssh_port = ssh_port - self.qemu_num_cores = qemu_num_cores - self.qemu_ram_size = qemu_ram_size - self.port_forwarding = port_forwarding - - -def load_configuration(config_file: str) -> QemuConfig: - """Load and validate a QEMU configuration file. - - Args: - config_file: Path to a JSON configuration file. - - Returns: - A validated QemuConfig instance. - - Raises: - ValueError: If validation fails. - """ - logger.info(f"Loading configuration from {config_file}") - - with open(config_file, "r") as f: - data = json.load(f) - - # Networks - raw_networks = data.get("networks", []) - if not raw_networks: - raise ValueError( - f"Invalid config '{config_file}': at least one network required" - ) - networks = [Network(**n) for n in raw_networks] - - # SSH port - ssh_port = data.get("ssh_port") - if not isinstance(ssh_port, int) or not (1 <= ssh_port <= 65535): - raise ValueError(f"Invalid config '{config_file}': ssh_port must be 1..65535") - - # Cores - qemu_num_cores = data.get("qemu_num_cores") - if not isinstance(qemu_num_cores, int) or qemu_num_cores < 1: - raise ValueError(f"Invalid config '{config_file}': qemu_num_cores must be >= 1") - - # RAM - qemu_ram_size = data.get("qemu_ram_size", "") - if not _RAM_SIZE_PATTERN.match(qemu_ram_size): - raise ValueError( - f"Invalid config '{config_file}': qemu_ram_size must match [0-9]+[KMGTP]" - ) - - # Port forwarding - port_forwarding = [PortForwarding(**pf) for pf in data.get("port_forwarding", [])] - - return QemuConfig( - networks=networks, - ssh_port=ssh_port, - qemu_num_cores=qemu_num_cores, - qemu_ram_size=qemu_ram_size, - port_forwarding=port_forwarding, - ) diff --git a/quality/integration_testing/plugins/linux_qemu/qemu.py b/quality/integration_testing/plugins/linux_qemu/qemu.py deleted file mode 100644 index cb4b9e4eb..000000000 --- a/quality/integration_testing/plugins/linux_qemu/qemu.py +++ /dev/null @@ -1,190 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""QEMU launcher with disk boot support. - -Provides a standalone QEMU class that supports booting from a qcow2 disk image -with an optional cloud-init seed ISO. This avoids patching the upstream -score_itf Qemu class whose private methods use Python name mangling. -""" - -import logging -import os -import subprocess -import sys -from subprocess import TimeoutExpired - -logger = logging.getLogger(__name__) - - -class DiskBootQemu: - """QEMU instance that boots from a qcow2 disk image. - - Supports an optional cloud-init seed ISO for automated guest provisioning. - Port forwarding is used for host-to-guest networking (user-mode networking). - """ - - def __init__( - self, - path_to_image, - ram="1G", - cores="2", - seed_iso=None, - cpu="Cascadelake-Server-v5", - network_adapters=None, - port_forwarding=None, - ): - self._qemu_path = "/usr/bin/qemu-system-x86_64" - self._path_to_image = path_to_image - self._ram = ram - self._cores = cores - self._seed_iso = seed_iso - self._cpu = cpu - self._network_adapters = network_adapters or [] - self._port_forwarding = port_forwarding or [] - - self._check_qemu_is_installed() - self._find_available_kvm_support() - self._check_kvm_readable_when_necessary() - - self._subprocess = None - - def __enter__(self): - return self.start() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop() - - def start(self, subprocess_params=None): - cmd = self._build_command() - logger.debug(cmd) - subprocess_args = {"args": cmd} - if subprocess_params: - subprocess_args.update(subprocess_params) - self._subprocess = subprocess.Popen(**subprocess_args) - return self._subprocess - - def stop(self): - if self._subprocess is None: - return - - if self._subprocess.poll() is None: - self._subprocess.terminate() - try: - self._subprocess.wait(2) - except TimeoutExpired: - logger.warning("QEMU did not terminate in time. Killing process.") - if self._subprocess.poll() is None: - self._subprocess.kill() - try: - self._subprocess.wait(2) - except TimeoutExpired: - logger.error("QEMU did not exit after kill.") - ret = self._subprocess.returncode - if ret != 0: - raise Exception(f"QEMU process returned: {ret}") - - def _check_qemu_is_installed(self): - if not os.path.isfile(self._qemu_path): - logger.fatal(f"QEMU is not installed under {self._qemu_path}") - sys.exit(-1) - - def _find_available_kvm_support(self): - self._accelerator = "kvm" - with open("/proc/cpuinfo") as cpuinfo: - cpu_options = str(cpuinfo.read()) - if "vmx" not in cpu_options and "svm" not in cpu_options: - logger.error("No virtualization capability. Using TCG accel.") - self._accelerator = "tcg" - if not os.path.exists("/dev/kvm"): - logger.error("No KVM available. Using TCG accel.") - self._accelerator = "tcg" - - def _check_kvm_readable_when_necessary(self): - if self._accelerator == "kvm" and not os.access("/dev/kvm", os.R_OK): - logger.fatal( - "No access to /dev/kvm. Consider adding yourself to kvm group." - ) - sys.exit(-1) - - def _build_command(self): - image_path = os.path.abspath(self._path_to_image) - cmd = [ - self._qemu_path, - "-smp", - f"{self._cores},maxcpus={self._cores},cores={self._cores}", - "-cpu", - self._cpu, - "-m", - self._ram, - "-drive", - f"file={image_path},format=qcow2,if=virtio", - ] - - cmd += ["-enable-kvm"] if self._accelerator == "kvm" else ["-accel", "tcg"] - - if self._seed_iso: - seed_path = os.path.abspath(self._seed_iso) - cmd.extend( - [ - # Attach NoCloud seed as a second disk. With cloud-localds this is a - # vfat image labeled 'cidata', which cloud-init detects reliably. - "-drive", - # Bazel runfiles are read-only; mounting the seed image as read-only - # prevents permission errors when QEMU opens the backing file. - f"file={seed_path},format=raw,if=virtio,readonly=on", - ] - ) - - cmd.extend( - [ - "-nographic", - "-serial", - "mon:stdio", - "-object", - "rng-random,filename=/dev/urandom,id=rng0", - "-device", - "virtio-rng-pci,rng=rng0", - ] - ) - - cmd.extend(self._network_devices_args()) - cmd.extend(self._port_forwarding_args()) - - return cmd - - def _network_devices_args(self): - result = [] - for idx, adapter in enumerate(self._network_adapters, start=1): - if not adapter.startswith("lo"): - result.extend( - [ - "-netdev", - f"tap,id=t{idx},ifname={adapter},script=no,downscript=no", - "-device", - f"virtio-net-pci,netdev=t{idx},id=nic{idx},guest_csum=off", - ] - ) - return result - - def _port_forwarding_args(self): - result = [] - for idx, forwarding in enumerate(self._port_forwarding, start=1): - result.extend( - [ - "-netdev", - f"user,id=net{idx},hostfwd=tcp::{forwarding.host_port}-:{forwarding.guest_port}", - "-device", - f"virtio-net-pci,netdev=net{idx}", - ] - ) - return result diff --git a/quality/integration_testing/plugins/linux_qemu/qemu_process.py b/quality/integration_testing/plugins/linux_qemu/qemu_process.py deleted file mode 100644 index 20fcc0bf7..000000000 --- a/quality/integration_testing/plugins/linux_qemu/qemu_process.py +++ /dev/null @@ -1,79 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""QEMU process management with disk boot support.""" - -import logging -import subprocess - -from score.itf.core.process.console import PipeConsole -from quality.integration_testing.plugins.linux_qemu.qemu import DiskBootQemu - -logger = logging.getLogger(__name__) - - -class LinuxQemuProcess: - """Manages a QEMU process lifecycle with disk boot support.""" - - def __init__( - self, - path_to_qemu_image, - available_ram, - available_cores, - network_adapters=None, - port_forwarding=None, - seed_iso=None, - ): - self._path_to_qemu_image = path_to_qemu_image - self._available_ram = available_ram - self._available_cores = available_cores - self._network_adapters = network_adapters or [] - self._port_forwarding = port_forwarding or [] - self._qemu = DiskBootQemu( - self._path_to_qemu_image, - self._available_ram, - self._available_cores, - seed_iso=seed_iso, - network_adapters=self._network_adapters, - port_forwarding=self._port_forwarding, - ) - self._console = None - - def __enter__(self): - return self.start() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop() - - def start(self): - logger.info("Starting QEMU (disk boot)...") - logger.info(f"Using QEMU image: {self._path_to_qemu_image}") - subprocess_params = { - "stdin": subprocess.PIPE, - "stdout": subprocess.PIPE, - "stderr": subprocess.STDOUT, - } - qemu_subprocess = self._qemu.start(subprocess_params) - self._console = PipeConsole("QEMU", qemu_subprocess) - return self - - def stop(self): - logger.info("Stopping QEMU...") - self._qemu.stop() - - def restart(self): - self.stop() - self.start() - - @property - def console(self): - return self._console diff --git a/quality/integration_testing/plugins/linux_qemu/qemu_test.py b/quality/integration_testing/plugins/linux_qemu/qemu_test.py deleted file mode 100644 index 90bafe64b..000000000 --- a/quality/integration_testing/plugins/linux_qemu/qemu_test.py +++ /dev/null @@ -1,114 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -import unittest -import importlib.util -import os -from pathlib import Path -from subprocess import TimeoutExpired -from unittest.mock import patch - - -def _load_qemu_module(): - module_path = Path(__file__).with_name("qemu.py") - spec = importlib.util.spec_from_file_location("linux_qemu_qemu", module_path) - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(module) - return module - - -DiskBootQemu = _load_qemu_module().DiskBootQemu - - -class DiskBootQemuTest(unittest.TestCase): - def _new_qemu(self, seed_iso=None): - with ( - patch.object(DiskBootQemu, "_check_qemu_is_installed"), - patch.object(DiskBootQemu, "_find_available_kvm_support"), - patch.object(DiskBootQemu, "_check_kvm_readable_when_necessary"), - ): - qemu = DiskBootQemu( - path_to_image="/tmp/image.qcow2", - seed_iso=seed_iso, - ) - qemu._accelerator = "tcg" - return qemu - - def test_build_command_adds_seed_iso_as_drive(self): - qemu = self._new_qemu(seed_iso="/tmp/seed.iso") - - cmd = qemu._build_command() - - self.assertIn("-drive", cmd) - self.assertIn("file=/tmp/seed.iso,format=raw,if=virtio,readonly=on", cmd) - - def test_build_command_without_seed_iso_omits_seed_drive(self): - qemu = self._new_qemu(seed_iso=None) - - cmd = qemu._build_command() - - self.assertNotIn("file=/tmp/seed.iso,format=raw,if=virtio,readonly=on", cmd) - - def test_build_command_normalizes_relative_seed_iso_to_absolute(self): - qemu = self._new_qemu( - seed_iso="quality/integration_testing/environments/ubuntu24_04_qemu/seed.img" - ) - - cmd = qemu._build_command() - - expected = os.path.abspath( - "quality/integration_testing/environments/ubuntu24_04_qemu/seed.img" - ) - self.assertIn(f"file={expected},format=raw,if=virtio,readonly=on", cmd) - - def test_build_command_uses_valid_tcg_acceleration_args(self): - qemu = self._new_qemu(seed_iso=None) - qemu._accelerator = "tcg" - - cmd = qemu._build_command() - - self.assertIn("-accel", cmd) - accel_pos = cmd.index("-accel") - self.assertEqual(cmd[accel_pos + 1], "tcg") - - def test_build_command_uses_enable_kvm_flag(self): - qemu = self._new_qemu(seed_iso=None) - qemu._accelerator = "kvm" - - cmd = qemu._build_command() - - self.assertIn("-enable-kvm", cmd) - - def test_stop_handles_unstarted_process(self): - qemu = self._new_qemu(seed_iso=None) - - qemu.stop() - - def test_stop_falls_back_to_kill_after_terminate_timeout(self): - qemu = self._new_qemu(seed_iso=None) - - process = unittest.mock.MagicMock() - process.poll.side_effect = [None, None, 0] - process.wait.side_effect = [TimeoutExpired(cmd="qemu", timeout=2), 0] - process.returncode = 0 - qemu._subprocess = process - - qemu.stop() - - process.terminate.assert_called_once() - process.kill.assert_called_once() - - -if __name__ == "__main__": - unittest.main() diff --git a/quality/integration_testing/test/BUILD b/quality/integration_testing/test/BUILD index c0537ac75..4e7ed11e5 100644 --- a/quality/integration_testing/test/BUILD +++ b/quality/integration_testing/test/BUILD @@ -12,6 +12,10 @@ # ******************************************************************************* load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_itf//bazel:copy_files_onto_image.bzl", "copy_files_onto_image") +load("@score_rules_imagefs//rules/qnx:ifs.bzl", "qnx_ifs") load("//quality/integration_testing:integration_testing.bzl", "integration_test") cc_binary( @@ -36,6 +40,22 @@ pkg_files( prefix = "/", ) +pkg_files( + name = "example-app-ebclfsa-pkg", + srcs = [ + "example-app-aarch64.sh", + "example-app2-aarch64.sh", + ], + attributes = pkg_attributes( + mode = "0777", + ), + prefix = "/", + renames = { + "example-app-aarch64.sh": "example-app", + "example-app2-aarch64.sh": "example-app2", + }, +) + integration_test( name = "test_hello_world", timeout = "moderate", @@ -44,3 +64,112 @@ integration_test( ], filesystem = ":example-app-pkg", ) + +pkg_tar( + name = "_qemu_filesystem_test_hello_world_ebclfsa_aarch64_integration_test", + srcs = [":example-app-ebclfsa-pkg"], + tags = ["manual"], +) + +copy_files_onto_image( + name = "_qemu_rootfs_overlay_test_hello_world_ebclfsa_aarch64_integration_test", + srcs = [":_qemu_filesystem_test_hello_world_ebclfsa_aarch64_integration_test"], + image = "@os_images//ebclfsa_aarch64:image", + tags = ["manual"], +) + +qnx_ifs( + name = "_qemu_image_test_hello_world_ebclfsa_x86_64_qnx_integration_test", + srcs = [ + ":example-app-ebclfsa-pkg", + "//quality/integration_testing/environments/qnx8_qemu:qnx_config", + ], + out = "init_ifs_test_hello_world_ebclfsa_x86_64_qnx_integration_test.img", + build_file = "//quality/integration_testing/environments/qnx8_qemu:init_build", + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:qnx", + ], +) + +py_itf_test( + name = "test_hello_world_ebclfsa_aarch64_integration_test", + size = "enormous", + timeout = "moderate", + srcs = ["test_hello_world.py"], + args = select({ + "//quality/integration_testing/flags:linux_qemu": [ + "--log-cli-level=DEBUG", + "--qemu-config=$(location @os_images//ebclfsa_aarch64:qemu_config)", + "--qemu-kernel=$(location @os_images//ebclfsa_aarch64:kernel)", + "--qemu-rootfs=$(location _qemu_rootfs_overlay_test_hello_world_ebclfsa_aarch64_integration_test)", + ], + "//conditions:default": [], + }), + data = select({ + "//quality/integration_testing/flags:linux_qemu": [ + "_qemu_rootfs_overlay_test_hello_world_ebclfsa_aarch64_integration_test", + "@os_images//ebclfsa_aarch64:image", # backing file must be accessible at runtime + "@os_images//ebclfsa_aarch64:kernel", + "@os_images//ebclfsa_aarch64:qemu_config", + ], + "//conditions:default": [], + }), + env = {"DOCKER_HOST": ""}, + plugins = select({ + "//quality/integration_testing/flags:linux_qemu": [ + "@score_itf//score/itf/plugins:qemu_plugin", + ], + "//conditions:default": [], + }), + tags = [ + "block-network", + "requires-fakeroot", + ], + target_compatible_with = select({ + "//quality/integration_testing/flags:linux_qemu": [ + "@platforms//os:linux", + ], + "//conditions:default": ["@platforms//:incompatible"], + }), +) + +py_itf_test( + name = "test_hello_world_ebclfsa_x86_64_qnx_integration_test", + size = "enormous", + timeout = "moderate", + srcs = ["test_hello_world.py"], + args = select({ + "@platforms//os:qnx": [ + "--log-cli-level=DEBUG", + "--qemu-config=$(location //quality/integration_testing/environments/qnx8_qemu:qemu_config)", + "--qemu-kernel=$(location _qemu_image_test_hello_world_ebclfsa_x86_64_qnx_integration_test)", + ], + "//conditions:default": [], + }), + data = select({ + "@platforms//os:qnx": [ + "_qemu_image_test_hello_world_ebclfsa_x86_64_qnx_integration_test", + "//quality/integration_testing/environments/qnx8_qemu:qemu_config", + ], + "//conditions:default": [], + }), + env = {"DOCKER_HOST": ""}, + plugins = select({ + "@platforms//os:qnx": [ + "@score_itf//score/itf/plugins:qemu_plugin", + ], + "//conditions:default": [], + }), + tags = [ + "block-network", + "requires-fakeroot", + ], + target_compatible_with = select({ + "@platforms//os:qnx": [ + "@platforms//cpu:x86_64", + "@platforms//os:qnx", + ], + "//conditions:default": ["@platforms//:incompatible"], + }), +) diff --git a/quality/integration_testing/test/example-app-aarch64.sh b/quality/integration_testing/test/example-app-aarch64.sh new file mode 100755 index 000000000..97eb19b6d --- /dev/null +++ b/quality/integration_testing/test/example-app-aarch64.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +printf 'Hello!\n' +: > /tmp/Hello.txt diff --git a/quality/integration_testing/test/example-app2-aarch64.sh b/quality/integration_testing/test/example-app2-aarch64.sh new file mode 100755 index 000000000..0efeac408 --- /dev/null +++ b/quality/integration_testing/test/example-app2-aarch64.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +while [ ! -f /tmp/Hello.txt ]; do + sleep 0.05 +done diff --git a/third_party/score_itf-qemu-ping-timeout.patch b/third_party/score_itf-qemu-ping-timeout.patch new file mode 100644 index 000000000..540406ab5 --- /dev/null +++ b/third_party/score_itf-qemu-ping-timeout.patch @@ -0,0 +1,13 @@ +diff --git a/score/itf/plugins/qemu/checks.py b/score/itf/plugins/qemu/checks.py +index e220cd4..ce98971 100644 +--- a/score/itf/plugins/qemu/checks.py ++++ b/score/itf/plugins/qemu/checks.py +@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) + + + def pre_tests_phase(target): +- _check_ping(target, check_timeout=10) ++ _check_ping(target, check_timeout=30) + _check_ssh_is_up(target, check_timeout=5, check_n_retries=5) + _check_sftp_is_up(target) + # TODO Add more checks in pre_tests_phase