diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index bf5e827c77..93990bd373 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -181,6 +181,15 @@ stages: micVersion: ${{ parameters.micVersion }} dependsOnStage: ${{ parameters.baseImageArtifactStage }} + # Build Trident test image (regular) for AZL4 + - template: stages/build_image/build-image.yml + parameters: + imageName: trident-azl4-testimage + micBuildType: ${{ parameters.micBuildType }} + micVersion: ${{ parameters.micVersion }} + dependsOnStage: ${{ parameters.baseImageArtifactStage }} + azureLinuxVersion: "4.0-preview" + # Build Trident test image (container) - template: stages/build_image/build-image.yml parameters: @@ -409,6 +418,15 @@ stages: buildPurpose: "pullrequest" runtimeEnv: "container" + # VM Testing (host, pullrequest) for AZL4 + - template: stages/testing_vm/netlaunch-testing.yml + parameters: + buildPurpose: "pullrequest" + runtimeEnv: "host" + distro: azl4 + stageSuffix: "_azl4" + testSecureBoot: false + # Run direct streaming tests for amd64 with streaming images - template: direct-streaming-test.yml parameters: diff --git a/.pipelines/templates/stages/common_tasks/push-to-acr.yml b/.pipelines/templates/stages/common_tasks/push-to-acr.yml index b6d16d075d..cc63b2b5cf 100644 --- a/.pipelines/templates/stages/common_tasks/push-to-acr.yml +++ b/.pipelines/templates/stages/common_tasks/push-to-acr.yml @@ -17,6 +17,7 @@ parameters: type: string values: - trident-testimage + - trident-azl4-testimage - trident-container-testimage - name: "config" diff --git a/.pipelines/templates/stages/common_tasks/remove-from-acr.yml b/.pipelines/templates/stages/common_tasks/remove-from-acr.yml index 0c4dcb792c..a061ddb12f 100644 --- a/.pipelines/templates/stages/common_tasks/remove-from-acr.yml +++ b/.pipelines/templates/stages/common_tasks/remove-from-acr.yml @@ -17,6 +17,7 @@ parameters: type: string values: - trident-testimage + - trident-azl4-testimage - trident-container-testimage - name: "config" diff --git a/.pipelines/templates/stages/testing_common/download-test-images.yml b/.pipelines/templates/stages/testing_common/download-test-images.yml index 141c3cb4ed..3c4a431574 100644 --- a/.pipelines/templates/stages/testing_common/download-test-images.yml +++ b/.pipelines/templates/stages/testing_common/download-test-images.yml @@ -19,6 +19,7 @@ parameters: values: - trident-container-testimage - trident-testimage + - trident-azl4-testimage - azurelinux-direct-streaming-testimage-amd64 - azurelinux-direct-streaming-testimage-arm64 - ubuntu-direct-streaming-testimage-2204-amd64 diff --git a/.pipelines/templates/stages/testing_common/get-tests.yml b/.pipelines/templates/stages/testing_common/get-tests.yml index 646711bfac..c70e7bd960 100644 --- a/.pipelines/templates/stages/testing_common/get-tests.yml +++ b/.pipelines/templates/stages/testing_common/get-tests.yml @@ -31,6 +31,13 @@ parameters: type: boolean default: true + - name: distro + type: string + default: "azl3" + values: + - azl3 + - azl4 + jobs: - job: DefineTests @@ -51,8 +58,12 @@ jobs: if [ "${{ parameters.testSecureBoot }}" == "False" ]; then SKIP_ENCRYPTION_TESTS_ARG="--skipEncryptionTests" fi + CONFIGURATIONS_FILE=./tests/e2e_tests/target-configurations.yaml + if [ "${{ parameters.distro }}" == "azl4" ]; then + CONFIGURATIONS_FILE=./tests/e2e_tests/target-configurations-azl4.yaml + fi python3 ./tests/e2e_tests/helpers/read_target_configurations.py \ - --configurations ./tests/e2e_tests/target-configurations.yaml \ + --configurations $CONFIGURATIONS_FILE \ --env ${{ parameters.deploymentEnvironment }} \ --runtimeEnv ${{ parameters.runtimeEnv }} \ --purpose ${{ parameters.buildPurpose }} \ diff --git a/.pipelines/templates/stages/testing_common/trident-prep.yml b/.pipelines/templates/stages/testing_common/trident-prep.yml index d3276dc5c9..4d39668ae6 100644 --- a/.pipelines/templates/stages/testing_common/trident-prep.yml +++ b/.pipelines/templates/stages/testing_common/trident-prep.yml @@ -19,6 +19,7 @@ parameters: default: trident-testimage values: - trident-testimage + - trident-azl4-testimage - trident-container-testimage - name: config diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index cd7b4187a7..e99e9e809c 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -39,9 +39,20 @@ parameters: - maritimus-dev-acr-write-umi - trident-dev-acr-write-umi-ECF + - name: distro + type: string + default: azl3 + values: + - azl3 + - azl4 + + - name: stageSuffix + type: string + default: "" + stages: - - stage: DefineTests_VM_${{ parameters.runtimeEnv }} - displayName: Test List for VM:${{ parameters.runtimeEnv }} + - stage: DefineTests_VM_${{ parameters.runtimeEnv }}${{ parameters.stageSuffix }} + displayName: Test List for VM:${{ parameters.runtimeEnv }} (${{ parameters.distro }}) jobs: - template: ../testing_common/get-tests.yml parameters: @@ -49,27 +60,45 @@ stages: deploymentEnvironment: virtualMachine runtimeEnv: ${{ parameters.runtimeEnv }} testSecureBoot: ${{ parameters.testSecureBoot }} + distro: ${{ parameters.distro }} - - stage: DeploymentTesting_${{ parameters.runtimeEnv }} - displayName: Deployment VM ${{ parameters.runtimeEnv }} Testing + - stage: DeploymentTesting_${{ parameters.runtimeEnv }}${{ parameters.stageSuffix }} + displayName: Deployment VM ${{ parameters.runtimeEnv }} Testing (${{ parameters.distro }}) dependsOn: - - DefineTests_VM_${{ parameters.runtimeEnv }} + - DefineTests_VM_${{ parameters.runtimeEnv }}${{ parameters.stageSuffix }} - ${{ if eq(parameters.testingRun, true) }}: - DownloadTestingElements - ${{ else }}: - BuildingTools - - ${{ if eq(parameters.runtimeEnv, 'container') }}: - - BuildTridentContainerImage - - TridentTestImg_trident_container_installer - - TridentTestImg_trident_container_testimage - - TridentTestImg_trident_container_verity_testimage - - TridentTestImg_trident_container_usrverity_testimage - - ${{ else }}: - - TridentTestImg_trident_split_installer - - TridentTestImg_trident_installer - - TridentTestImg_trident_testimage - - TridentTestImg_trident_verity_testimage - - TridentTestImg_trident_usrverity_testimage + - ${{ if eq(parameters.distro, 'azl3') }}: + - ${{ if eq(parameters.runtimeEnv, 'container') }}: + - BuildTridentContainerImage + - TridentTestImg_trident_container_installer + - TridentTestImg_trident_container_testimage + - TridentTestImg_trident_container_verity_testimage + - TridentTestImg_trident_container_usrverity_testimage + - ${{ else }}: + - TridentTestImg_trident_split_installer + - TridentTestImg_trident_installer + - TridentTestImg_trident_testimage + - TridentTestImg_trident_verity_testimage + - TridentTestImg_trident_usrverity_testimage + - ${{ if eq(parameters.distro, 'azl4') }}: + - ${{ if eq(parameters.runtimeEnv, 'container') }}: + # TODO: the following are placeholders until container supported for AZL4 + - BuildTridentContainerImage + - TridentTestImg_trident_container_installer + - TridentTestImg_trident_container_testimage + - TridentTestImg_trident_container_verity_testimage + - TridentTestImg_trident_container_usrverity_testimage + - ${{ else }}: + - TridentTestImg_trident_installer + - TridentTestImg_trident_azl4_testimage + # TODO: the following are placeholders until the verity + # and usrverity test images are built for AZL4 + - TridentTestImg_trident_split_installer + - TridentTestImg_trident_verity_testimage + - TridentTestImg_trident_usrverity_testimage jobs: - job: Testing @@ -80,7 +109,7 @@ stages: hostArchitecture: amd64 strategy: - matrix: $[ stageDependencies.DefineTests_VM_${{ parameters.runtimeEnv }}.DefineTests.outputs['setConfigurations.matrixConfigurations'] ] + matrix: $[ stageDependencies.DefineTests_VM_${{ parameters.runtimeEnv }}${{ parameters.stageSuffix }}.DefineTests.outputs['setConfigurations.matrixConfigurations'] ] variables: # Sourced from the matrix @@ -90,28 +119,55 @@ stages: - name: tridentConfigPath value: tests/e2e_tests/trident_configurations/$(tridentConfigurationName) - - ${{ if eq(parameters.runtimeEnv, 'container') }}: - - name: installerISOName - value: trident-container-installer - - name: testImageName - value: trident-container-testimage - - name: verityTestImageName - value: trident-container-verity-testimage - - name: usrVerityTestImageName - value: trident-container-usrverity-testimage - - name: downloadTridentContainer - value: true - - ${{ else }}: - - name: installerISOName - value: trident-installer - - name: testImageName - value: trident-testimage - - name: verityTestImageName - value: trident-verity-testimage - - name: usrVerityTestImageName - value: trident-usrverity-testimage - - name: downloadTridentContainer - value: false + - ${{ if eq(parameters.distro, 'azl3') }}: + - ${{ if eq(parameters.runtimeEnv, 'container') }}: + - name: installerISOName + value: trident-container-installer + - name: testImageName + value: trident-container-testimage + - name: verityTestImageName + value: trident-container-verity-testimage + - name: usrVerityTestImageName + value: trident-container-usrverity-testimage + - name: downloadTridentContainer + value: true + - ${{ else }}: + - name: installerISOName + value: trident-installer + - name: testImageName + value: trident-testimage + - name: verityTestImageName + value: trident-verity-testimage + - name: usrVerityTestImageName + value: trident-usrverity-testimage + - name: downloadTridentContainer + value: false + - ${{ if eq(parameters.distro, 'azl4') }}: + - ${{ if eq(parameters.runtimeEnv, 'container') }}: + # TODO: the following variables are placeholders until container supported for AZL4 + - name: installerISOName + value: trident-container-installer + - name: testImageName + value: trident-container-testimage + - name: verityTestImageName + value: trident-container-verity-testimage + - name: usrVerityTestImageName + value: trident-container-usrverity-testimage + - name: downloadTridentContainer + value: true + - ${{ else }}: + - name: installerISOName + value: trident-installer + - name: testImageName + value: trident-azl4-testimage + # TODO: the following variables are placeholders until the verity + # and usrverity test images are built for AZL4 + - name: verityTestImageName + value: trident-verity-testimage + - name: usrVerityTestImageName + value: trident-usrverity-testimage + - name: downloadTridentContainer + value: false - name: ob_outputDirectory value: /tmp/deployment_logs diff --git a/tests/e2e_tests/azl4_test.py b/tests/e2e_tests/azl4_test.py new file mode 100644 index 0000000000..ef7ed36389 --- /dev/null +++ b/tests/e2e_tests/azl4_test.py @@ -0,0 +1,7 @@ +import pytest + +pytestmark = [pytest.mark.azl4] + + +def test_azl4() -> None: + print("AZL4") diff --git a/tests/e2e_tests/base_test.py b/tests/e2e_tests/base_test.py index 3e60948054..95554175d3 100644 --- a/tests/e2e_tests/base_test.py +++ b/tests/e2e_tests/base_test.py @@ -421,7 +421,11 @@ def test_users(connection, hostConfiguration): expected_users = list() expected_groups = dict() - for user_info in hostConfiguration["os"]["users"]: + users = hostConfiguration.get("os", {}).get("users") + if not users: + pytest.skip("No os.users in trident config (user baked into image by MIC)") + + for user_info in users: expected_users.append(user_info["name"]) if "groups" in user_info: for group in user_info["groups"]: diff --git a/tests/e2e_tests/helpers/edit_host_config.py b/tests/e2e_tests/helpers/edit_host_config.py index 0f425dc1cc..67e0766329 100644 --- a/tests/e2e_tests/helpers/edit_host_config.py +++ b/tests/e2e_tests/helpers/edit_host_config.py @@ -28,9 +28,24 @@ def add_key(host_config_path, public_key): with open(host_config_path, "r") as f: host_config = yaml.safe_load(f) - for index_user in range(len(host_config["os"]["users"])): - if host_config["os"]["users"][index_user]["name"] == "testing-user": - host_config["os"]["users"][index_user]["sshPublicKeys"].append(public_key) + users = host_config.get("os", {}).get("users") + if not users: + raise ValueError( + f"{host_config_path}: expected os.users to be present so the test " + "SSH key can be added, but it is missing or empty" + ) + + added = False + for user in users: + if user.get("name") == "testing-user": + user.setdefault("sshPublicKeys", []).append(public_key) + added = True + + if not added: + raise ValueError( + f"{host_config_path}: no os.users entry named 'testing-user' was " + "found to add the test SSH key to" + ) with open(host_config_path, "w") as f: yaml.safe_dump(host_config, f) diff --git a/tests/e2e_tests/pytest.ini b/tests/e2e_tests/pytest.ini index 1301865bfa..0435c13fc8 100644 --- a/tests/e2e_tests/pytest.ini +++ b/tests/e2e_tests/pytest.ini @@ -7,6 +7,7 @@ markers = encryption: Tests designed to verify the encryption feature ops in Trident. ab_update_staged: Tests designed to verify that A/B update was staged correctly. extensions: Tests designed to verify that the servicing of sysexts and confexts succeeded. + azl4: Tests designed to verify Azure Linux 4.0 (AZL4) base image support. # Special markers, do not use for tests, specify by Trident configuration: compatible: Tests that are compatible with a Trident configuration. diff --git a/tests/e2e_tests/target-configurations-azl4.yaml b/tests/e2e_tests/target-configurations-azl4.yaml new file mode 100644 index 0000000000..b1793e8df4 --- /dev/null +++ b/tests/e2e_tests/target-configurations-azl4.yaml @@ -0,0 +1,38 @@ +# bareMetal: +# host: +# daily: +# - base-azl4 +# validation: +# - base-azl4 +# weekly: +# - base-azl4 +# container: +# daily: +# - base-azl4 +# validation: +# - base-azl4 +# weekly: +# - base-azl4 +virtualMachine: + host: + daily: + - base-azl4 + post_merge: + - base-azl4 + pullrequest: + - base-azl4 + validation: + - base-azl4 + weekly: + - base-azl4 + # container: + # daily: + # - base-azl4 + # post_merge: + # - base-azl4 + # pullrequest: + # - base-azl4 + # validation: + # - base-azl4 + # weekly: + # - base-azl4 diff --git a/tests/e2e_tests/trident_configurations/base-azl4/test-selection.yaml b/tests/e2e_tests/trident_configurations/base-azl4/test-selection.yaml new file mode 100644 index 0000000000..b724409802 --- /dev/null +++ b/tests/e2e_tests/trident_configurations/base-azl4/test-selection.yaml @@ -0,0 +1,5 @@ +compatible: + # identify configuration as azl4 + - azl4 + # Use the shared pytest assertions where appropriate. + - base diff --git a/tests/e2e_tests/trident_configurations/base-azl4/trident-config.yaml b/tests/e2e_tests/trident_configurations/base-azl4/trident-config.yaml new file mode 100644 index 0000000000..c918e15151 --- /dev/null +++ b/tests/e2e_tests/trident_configurations/base-azl4/trident-config.yaml @@ -0,0 +1,74 @@ +image: + url: http://NETLAUNCH_HOST_ADDRESS/files/regular.cosi + sha384: ignored +# AZL4 does not ship grub2-efi-binary-noprefix. Trident handles this +# automatically — no disableGrubNoprefixCheck override needed. +storage: + disks: + - id: os + device: /dev/disk/by-path/pci-0000:00:1f.2-ata-2 + partitionTableType: gpt + partitions: + - id: root-a + type: root + size: 8G + - id: root-b + type: root + size: 8G + - id: esp + type: esp + size: 1G + - id: swap + type: swap + size: 2G + - id: trident + type: linux-generic + size: 1G + - id: disk2 + device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 + partitionTableType: gpt + partitions: [] + abUpdate: + volumePairs: + - id: root + volumeAId: root-a + volumeBId: root-b + filesystems: + - deviceId: trident + source: new + mountPoint: /var/lib/trident + - deviceId: esp + mountPoint: + path: /boot/efi + options: umask=0077 + - deviceId: root + mountPoint: / + swap: + - swap +# /home partition omitted: the COSI bakes a user home directory onto +# root via MIC os.users. Trident's newroot mount rejects non-empty +# mount points, so a separate /home partition conflicts with the +# pre-existing /home/. AZL3 avoids this by only testing /home +# in container mode. Container mode support for AZL4 is tracked as +# a follow-up. +scripts: + postConfigure: + - name: testing-privilege + runOn: + - clean-install + - ab-update + content: echo 'testing-user ALL=(ALL:ALL) NOPASSWD:ALL' > /etc/sudoers.d/testing-user +os: + selinux: + mode: disabled + netplan: + version: 2 + ethernets: + vmeths: + match: + name: eth* + dhcp4: true + users: + - name: testing-user + sshPublicKeys: [] + sshMode: key-only \ No newline at end of file diff --git a/tests/images/testimages.py b/tests/images/testimages.py index 9a0683f76f..89c67403f9 100755 --- a/tests/images/testimages.py +++ b/tests/images/testimages.py @@ -196,6 +196,15 @@ ssh_key="files/id_rsa.pub", architecture=SystemArchitecture.ARM64, ), + # Test images (azl4) + ImageConfig( + "trident-azl4-testimage", + config="trident-testimage", + base_image=BaseImage.AZL4_QEMU_GUEST, + output_and_config={ + OutputFormat.COSI: "base/baseimg-azl4.yaml", + }, + ), # VM test images (azl4) ImageConfig( "trident-vm-grub-azl4-testimage", diff --git a/tests/images/trident-testimage/base/baseimg-azl4.yaml b/tests/images/trident-testimage/base/baseimg-azl4.yaml new file mode 100644 index 0000000000..66236f3ea7 --- /dev/null +++ b/tests/images/trident-testimage/base/baseimg-azl4.yaml @@ -0,0 +1,144 @@ +storage: + bootType: efi + + disks: + - partitionTableType: gpt + maxSize: 5G + partitions: + - id: esp + type: esp + size: 16M + + - id: rootfs + size: grow + + filesystems: + - deviceId: esp + type: fat32 + mountPoint: + path: /boot/efi + options: umask=0077 + + - deviceId: rootfs + type: ext4 + mountPoint: + path: / + +os: + bootloader: + resetType: hard-reset + hostname: trident-azl4-testimg + + selinux: + mode: disabled + + kernelCommandLine: + extraCommandLine: + - console=tty0 + - console=ttyS0 + - rd.info + - log_buf_len=1M + - net.ifnames=0 + - loglevel=6 + - systemd.journald.forward_to_console=1 + + packages: + install: + - curl + # AZL4: dnf5 (no `dnf` package on AZL4) + - dnf5 + # ^^^ + - efibootmgr + # AZL4: grub2-efi-x64 + grub2-efi-x64-modules (no `-noprefix` package) + - grub2-efi-x64 + - grub2-efi-x64-modules + - grub2-tools + - grub2-tools-efi + # ^^^ + - iproute + # AZL4: iptables-nft (no plain `iptables` meta on AZL4 base) + - iptables-nft + # ^^^ + - lsof + - mdadm + # AZL4 netplan and AZL4 systemd are incompatible at boot ... use + # cached netplan RPMs built from main + - netplan + - netplan-default-backend-networkd + - openssh-server + - tpm2-tools + # AZL4: shim added (AZL4 ships a real signed shim chain) + - shim + # WHY? maybe baseimage is missing them? + - sudo + - systemd-networkd + - systemd-resolved + # AZL4 systemd (258.4) moves systemd-dissect and mount.ddi from the + # main systemd package into the systemd-container subpackage. mount.ddi + # is needed for sysext/sysconf + - systemd-sysext + - systemd-confext + - systemd-dissect + # ^^^ + - trident-service + - vim + - audit + # Packages required for functional tests, which currently use this image: + - device-mapper + - dosfstools + - lvm2 + - veritysetup + # Optional dependencies for NTFS + - ntfs-3g + - ntfsprogs + + services: + enable: + - sshd + - systemd-networkd + - systemd-resolved + - trident + - tridentd.socket + # netplan-main (generate/configure split) defers virtual-device + # creation to this service; Fedora ships it preset-disabled. + - netplan-configure.service + + additionalFiles: + # AZL4 lacks a /usr/bin/hostname binary; the pytest framework smoke- + # tests SSH with `hostname`, so we ship a tiny shim. + - source: files/hostname-shim.sh + destination: /usr/local/bin/hostname + permissions: "755" + # For tests, override the public trident.service behavior and use + # grpc-client commit. + - source: files/use-grpc-client-commit.conf + destination: /etc/systemd/system/trident.service.d/override.conf + # ssh-move-host-keys-azl4.sh points sshd HostKey at /var/srv/etc/ssh. + # The srv partition is created fresh on every clean install, so the + # host keys must be generated there on first boot (see + # regen-sshd-keys.service), otherwise sshd exits with no hostkeys + # available and nothing listens on TCP :22. + - source: files/regen-sshd-keys.service + destination: /etc/systemd/system/regen-sshd-keys.service + +scripts: + postCustomization: + - path: post-install.sh + - path: scripts/ssh-move-host-keys-azl4.sh + # Enable regen-sshd-keys.service via a wants symlink so sshd has + # host keys in /var/srv on first boot. + - path: scripts/enable-regen-sshd-keys.sh + # Rebuild initramfs with --no-hostonly + extra SATA drivers so the + # qcow2 boots regardless of which bus the consumer's libvirt config + # picks (storm-trident uses bus=sata; the original boot test on + # karhu-ubuntu used bus=virtio). MUST run BEFORE strip-selinux-xattrs + # because dracut writes new files with the build-time SELinux + # context, and we want those stripped too. + - path: scripts/rebuild-initrd-azl4.sh + # Strip SELinux labels from all files. See the script header for the + # full rationale, but in short: the AZL4 base VHDX bakes in AZL4 + # SELinux contexts that Trident-from-AZL3-MOS can't preserve during + # install (MOS's loaded policy rejects unknown contexts via + # setxattr). Stripping at cosi build time sidesteps the cascade. + # Must run LAST so other postCustomization scripts don't re-label. + - path: scripts/strip-selinux-xattrs.sh \ No newline at end of file diff --git a/tests/images/trident-testimage/base/files/hostname-shim.sh b/tests/images/trident-testimage/base/files/hostname-shim.sh new file mode 100644 index 0000000000..b12b3807c9 --- /dev/null +++ b/tests/images/trident-testimage/base/files/hostname-shim.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# AZL4 doesn't ship a `hostname` binary in `coreutils` (Fedora moved it to +# its own package which AZL4 hasn't picked up yet). The pytest E2E +# framework uses `hostname` as a smoke test of the SSH session in +# tests/e2e_tests/conftest.py, so without this shim every test errors out +# at fixture setup. +# +# Tiny POSIX-only replacement that reads /etc/hostname, plus a passthrough +# for `hostname -s` and `hostname -f` for completeness. +case "$1" in + -s|--short) + cat /etc/hostname | cut -d. -f1 + ;; + -f|--fqdn|"") + cat /etc/hostname + ;; + *) + cat /etc/hostname + ;; +esac diff --git a/tests/images/trident-testimage/base/files/regen-sshd-keys.service b/tests/images/trident-testimage/base/files/regen-sshd-keys.service new file mode 100644 index 0000000000..e33478caae --- /dev/null +++ b/tests/images/trident-testimage/base/files/regen-sshd-keys.service @@ -0,0 +1,16 @@ +[Unit] +Description=Generate sshd host keys in /var/srv on first boot +ConditionPathExists=|!/var/srv/etc/ssh/ssh_host_rsa_key +ConditionPathExists=|!/var/srv/etc/ssh/ssh_host_ecdsa_key +ConditionPathExists=|!/var/srv/etc/ssh/ssh_host_ed25519_key +Before=sshd.service +After=local-fs.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStartPre=/usr/bin/mkdir -p /var/srv/etc/ssh +ExecStart=/usr/bin/ssh-keygen -A -f /var/srv -q + +[Install] +WantedBy=multi-user.target diff --git a/tests/images/trident-testimage/base/scripts/enable-regen-sshd-keys.sh b/tests/images/trident-testimage/base/scripts/enable-regen-sshd-keys.sh new file mode 100755 index 0000000000..feb05323f3 --- /dev/null +++ b/tests/images/trident-testimage/base/scripts/enable-regen-sshd-keys.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# regen-sshd-keys is a one-shot service that generates SSH host keys in +# /var/srv on first boot. Enable it via wants symlink because the generic +# `services.enable` in MIC config is reserved for systemd unit names that +# come from packages, and our unit is delivered via additionalFiles. +set -euo pipefail +mkdir -p /etc/systemd/system/multi-user.target.wants +ln -sf /etc/systemd/system/regen-sshd-keys.service \ + /etc/systemd/system/multi-user.target.wants/regen-sshd-keys.service diff --git a/tests/images/trident-testimage/base/scripts/rebuild-initrd-azl4.sh b/tests/images/trident-testimage/base/scripts/rebuild-initrd-azl4.sh new file mode 100644 index 0000000000..38212b4c0e --- /dev/null +++ b/tests/images/trident-testimage/base/scripts/rebuild-initrd-azl4.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Regenerate initrd with --no-hostonly so all storage drivers are +# included, not just the ones MIC's build environment happens to need. +# +# Why: storm-trident's rollback test (tools/storm/utils/vm/qemu/qemu.go) +# attaches the qcow2 to a virt-install VM with `bus=sata`. MIC builds +# the qcow2 in a virtio-backed environment, so dracut's default +# hostonly mode produces an initramfs with only virtio drivers. On a +# SATA-backed boot, the initramfs can't find the root partition by +# UUID and systemd hangs forever waiting for /dev/disk/by-uuid/. +# +# Rebuilding with --no-hostonly bakes in ahci, ata_piix, sata_sil, etc. +# along with virtio so the same qcow2 boots regardless of the bus type +# the consumer chooses. +# +# Runs inside the MIC chroot where /sys and /proc are bind-mounted but +# the host's SELinux is not loaded (MIC strips that), so dracut's +# cp -a doesn't hit the security.selinux setxattr issue that bites in +# AZL3 MOS during install (see strip-selinux-xattrs.sh for the parallel +# write-up). + +set -euo pipefail + +# Find the kernel version installed in this image. We require exactly +# one — `ls | head -1` would silently pick the wrong one if any future +# AZL4 variant ships multiple (kernel + kernel-hyperv, extramodules-*, +# etc.). Fail loudly rather than generate an initramfs for the wrong +# kernel: the failure mode of that misstep is "boot hangs waiting for +# /dev/disk/by-uuid/", which is the exact bug this script is +# meant to prevent. +# nullglob so a missing/empty /usr/lib/modules yields an empty array +# (otherwise the glob stays literal and the 0) branch never matches). +shopt -s nullglob +KVERS=( /usr/lib/modules/* ) +case ${#KVERS[@]} in + 0) + echo "ERROR: no kernel modules dir under /usr/lib/modules" >&2 + exit 1 + ;; + 1) + KVER=$(basename "${KVERS[0]}") + ;; + *) + echo "ERROR: expected exactly one kernel under /usr/lib/modules, found:" >&2 + printf ' %s\n' "${KVERS[@]}" >&2 + exit 1 + ;; +esac +echo "Regenerating initramfs for kernel $KVER with --no-hostonly" + +# `--no-hostonly` includes all storage modules; `--no-hostonly-cmdline` +# prevents dracut from baking the build-host's /proc/cmdline parameters +# into the initramfs (which would fight the qcow2's grub cmdline at +# runtime); `--reproducible` keeps the output bit-stable across builds +# so we can detect spurious regenerations. +dracut \ + --no-hostonly \ + --no-hostonly-cmdline \ + --reproducible \ + --add-drivers "ahci ata_piix sata_sil sata_nv sata_via sd_mod" \ + --force \ + --kver "$KVER" + +echo "Regenerated initramfs:" +ls -lh /boot/initramfs-* diff --git a/tests/images/trident-testimage/base/scripts/ssh-move-host-keys-azl4.sh b/tests/images/trident-testimage/base/scripts/ssh-move-host-keys-azl4.sh new file mode 100755 index 0000000000..ede3fdbaa2 --- /dev/null +++ b/tests/images/trident-testimage/base/scripts/ssh-move-host-keys-azl4.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# AZL4-compatible variant of ssh-move-host-keys.sh. +# +# AZL3 sshd reads the main /etc/ssh/sshd_config and we appended HostKey +# lines to it. AZL4 sshd 10.0+ supports drop-ins under /etc/ssh/sshd_config.d/ +# which is the cleaner approach. +SSH_VAR_DIR="/var/srv/etc/ssh" +mkdir -p /etc/ssh/sshd_config.d +cat > /etc/ssh/sshd_config.d/50-trident-host-keys.conf <&1 >/dev/null); then + count=$((count + 1)) + else + case "$err" in + *"No such attribute"*|*"Operation not supported"*) + : # nothing to strip, expected for files without the xattr + ;; + *) + fail_count=$((fail_count + 1)) + echo "setfattr failed on '$f': $err" >&2 + ;; + esac + fi +done < <(find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune \ + -o \( -type f -o -type d -o -type l \) -print0) + +echo "Stripped security.selinux from ${count} files/dirs" + +if [ "$fail_count" -gt 0 ]; then + echo "ERROR: setfattr failed (non-ENODATA) on ${fail_count} entries" >&2 + exit 1 +fi + +# Verify the strip actually took effect by scanning a representative set +# of paths (rootfs, /boot if present, /usr/lib/systemd, /etc). Any +# residual security.selinux means we missed something — fail loudly +# rather than warning, since the whole point of the script is to leave +# the image bare. +sentinel_dirs=( "/etc" "/usr/lib/systemd" "/usr/bin" ) +if [ -d /boot ]; then + sentinel_dirs+=( "/boot" ) +fi +for d in "${sentinel_dirs[@]}"; do + if getfattr -R -m security.selinux "$d" 2>/dev/null | grep -q security.selinux; then + echo "ERROR: security.selinux xattr still present under '$d'" >&2 + getfattr -R -m security.selinux "$d" 2>/dev/null | head -10 >&2 + exit 1 + fi +done diff --git a/tools/storm/utils/ssh/sftp/sftp.go b/tools/storm/utils/ssh/sftp/sftp.go index c59d291a8d..98514c21d7 100644 --- a/tools/storm/utils/ssh/sftp/sftp.go +++ b/tools/storm/utils/ssh/sftp/sftp.go @@ -9,8 +9,16 @@ import ( ) const ( - AZL3_SFTP_SERVER_PATH = "/usr/libexec/sftp-server" - AZL3_SFTP_SERVER_CMD = "sudo -n " + AZL3_SFTP_SERVER_PATH + // sftp-server is installed under different libexec paths depending on the + // distro / openssh packaging: + // - AZL3 / upstream openssh: /usr/libexec/sftp-server + // - AZL4 (Fedora-based) / RHEL: /usr/libexec/openssh/sftp-server + // - Debian / Ubuntu: /usr/lib/openssh/sftp-server + // Exec the first path that exists so the SFTP protocol speaks over + // stdin/stdout regardless of where the binary lives. Hard-coding the AZL3 + // path made SudoSFTP fail on AZL4 (binary not found -> channel closes -> + // "error receiving version packet from server: unexpected EOF"). + SFTP_SERVER_CMD = `/bin/sh -c 'for p in /usr/libexec/openssh/sftp-server /usr/libexec/sftp-server /usr/lib/openssh/sftp-server; do [ -x "$p" ] && exec sudo -n -- "$p"; done; echo "sftp-server not found" >&2; exit 127'` ) type SftpSudoClient struct { @@ -46,9 +54,9 @@ func NewSftpSudoClient(client *ssh.Client, opts ...sftp.ClientOption) (*SftpSudo return nil, fmt.Errorf("failed to create SSH session: %w", err) } - ok, err := session.SendRequest("exec", true, ssh.Marshal(struct{ Command string }{AZL3_SFTP_SERVER_CMD})) + ok, err := session.SendRequest("exec", true, ssh.Marshal(struct{ Command string }{SFTP_SERVER_CMD})) if err == nil && !ok { - err = fmt.Errorf("sftp: command %v failed", AZL3_SFTP_SERVER_CMD) + err = fmt.Errorf("sftp: command %v failed", SFTP_SERVER_CMD) } if err != nil { return nil, err