From 1f711ddeb9262d1c3bd71db57035c3ed424042cd Mon Sep 17 00:00:00 2001 From: Jiri Appl Date: Tue, 6 May 2025 17:43:01 +0000 Subject: [PATCH 01/99] Merged PR 23042: Remove last log line " logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description VM servicing is printing last serial log line on failures to help with diagnosing issues. Alas, for Azure VM serial logs, these sometimes ended up with output of " only. This PR fixes the filtering logic to remove these unhelpful logs. # 🤔 Rationale Improve diagnosability of pipeline failures. Related work items: #12095 --- scripts/loop-update/fetch-logs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/loop-update/fetch-logs.sh b/scripts/loop-update/fetch-logs.sh index 0a249a834..196a79802 100755 --- a/scripts/loop-update/fetch-logs.sh +++ b/scripts/loop-update/fetch-logs.sh @@ -54,7 +54,7 @@ downloadAzureSerialLog() { # - remove lines with only dashes # - remove empty lines az vm boot-diagnostics get-boot-log --name "$VM_NAME" --resource-group "$TEST_RESOURCE_GROUP" > "$DEST.raw" - cat "$DEST.raw" | sed -r 's/\\r\\n/\n/g;s/\\u[a-z0-9]{4}//g;/^"$/d;/^-+$/d;/^$/d' > "$DEST" + cat "$DEST.raw" | sed -r 's/\\r\\n/\n/g;s/\\u[a-z0-9]{4}//g;/^-+$/d;/^$/d' | sed -r '/^"$/d' > "$DEST" } if [ "$TEST_PLATFORM" == "azure" ]; then From adc44cce3f04634f3d6c908f93cc5f3360fd5443 Mon Sep 17 00:00:00 2001 From: Jiri Appl Date: Wed, 7 May 2025 20:05:00 +0000 Subject: [PATCH 02/99] Merged PR 23057: Stop capturing the raw log file, as we are no longer getting logs with single... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description No longer need to publish the original log, as the issue has been resolved. Related work items: #12095 --- scripts/loop-update/fetch-logs.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/loop-update/fetch-logs.sh b/scripts/loop-update/fetch-logs.sh index 196a79802..6bdc339b2 100755 --- a/scripts/loop-update/fetch-logs.sh +++ b/scripts/loop-update/fetch-logs.sh @@ -50,11 +50,11 @@ downloadAzureSerialLog() { # clean it up a bit: # - convert \r\n to newlines # - remove unicode characters - # - remove lines with only quotes # - remove lines with only dashes # - remove empty lines - az vm boot-diagnostics get-boot-log --name "$VM_NAME" --resource-group "$TEST_RESOURCE_GROUP" > "$DEST.raw" - cat "$DEST.raw" | sed -r 's/\\r\\n/\n/g;s/\\u[a-z0-9]{4}//g;/^-+$/d;/^$/d' | sed -r '/^"$/d' > "$DEST" + # - remove lines with only quotes + az vm boot-diagnostics get-boot-log --name "$VM_NAME" --resource-group "$TEST_RESOURCE_GROUP" | + sed -r 's/\\r\\n/\n/g;s/\\u[a-z0-9]{4}//g;/^-+$/d;/^$/d' | sed -r '/^"$/d' > "$DEST" } if [ "$TEST_PLATFORM" == "azure" ]; then From cae4faf6dd0d0e6bd45f17f4918920cb8b901904 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 7 May 2025 22:14:31 +0000 Subject: [PATCH 03/99] Merged PR 23026: engineering: build clones in e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Build actual image clones instead of doing the hacky FS UUID replacement Depends on !23032 ---- #### AI description (iteration 1) #### PR Classification Enhancement of end-to-end (e2e) testing infrastructure. #### PR Summary This pull request introduces a new build template for runtime images in e2e tests, improving the testing process by building clones. It also involves restructuring existing templates for better organization and clarity. - Added a new file `/.pipelines/templates/stages/build_image/build-runtime.yml` to define parameters and stages for building runtime images. - Updated `/.pipelines/templates/e2e-template.yml` to use the new `build-runtime.yml` template for building various Trident test images. - Moved `/.pipelines/templates/stages/build_image/trident-testimg.yml` to `/.pipelines/templates/stages/build_image/build-installer.yml` without code changes. Related work items: #12138 --- .pipelines/templates/e2e-template.yml | 30 ++-- ...rident-testimg.yml => build-installer.yml} | 17 +- .../stages/build_image/build-runtime.yml | 86 +++++++++ .../testing_common/download-test-images.yml | 29 +-- .../scripts/transfer-update-os-image.sh | 35 ++-- storm/suites/trident/helpers/init.go | 1 + .../suites/trident/helpers/prepare_images.go | 165 ++++++++++++++++++ 7 files changed, 297 insertions(+), 66 deletions(-) rename .pipelines/templates/stages/build_image/{trident-testimg.yml => build-installer.yml} (92%) create mode 100644 .pipelines/templates/stages/build_image/build-runtime.yml create mode 100644 storm/suites/trident/helpers/prepare_images.go diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index 21120c67d..1f7653f7e 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -88,7 +88,7 @@ stages: baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} # Build Trident installer ISO (host) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-installer.yml parameters: imageName: build/trident-installer-testimage.iso outputArtifactName: trident-installer-testimage @@ -98,7 +98,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident split (stage and finalize separated) installer ISO (host) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-installer.yml parameters: imageName: build/trident-split-installer-testimage.iso outputArtifactName: trident-split-installer-testimage @@ -108,7 +108,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident installer ISO (container) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-installer.yml parameters: imageName: build/trident-container-installer-testimage.iso outputArtifactName: trident-container-installer-testimage @@ -119,48 +119,40 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image (regular) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-testimage.cosi - outputArtifactName: trident-testimage + imageName: trident-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} - mkcosiTemplate: "regular" micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} # Build Trident test image (container) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-container-testimage.cosi - outputArtifactName: trident-container-testimage + imageName: trident-container-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} runtimeEnv: "container" - mkcosiTemplate: "regular" micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} # Build Trident test image for verity (host) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-verity-testimage.cosi - outputArtifactName: trident-verity-testimage + imageName: trident-verity-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} - mkcosiTemplate: "verity" micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} # Build Trident test image for verity (container) - - template: stages/build_image/trident-testimg.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-container-verity-testimage.cosi - outputArtifactName: trident-container-verity-testimage + imageName: trident-container-verity-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} runtimeEnv: "container" - mkcosiTemplate: "verity" micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} diff --git a/.pipelines/templates/stages/build_image/trident-testimg.yml b/.pipelines/templates/stages/build_image/build-installer.yml similarity index 92% rename from .pipelines/templates/stages/build_image/trident-testimg.yml rename to .pipelines/templates/stages/build_image/build-installer.yml index 833cd05e5..6460160ef 100644 --- a/.pipelines/templates/stages/build_image/trident-testimg.yml +++ b/.pipelines/templates/stages/build_image/build-installer.yml @@ -27,21 +27,13 @@ parameters: - host - container - - name: mkcosiTemplate - type: string - default: "none" - values: - - none - - regular - - verity - - name: micBuildType displayName: MIC Build Type type: string values: - - dev - - preview - - release + - dev + - preview + - release default: release - name: micVersion @@ -49,7 +41,6 @@ parameters: type: string default: "*.*.*" - stages: - stage: TridentTestImg_${{ replace(parameters.outputArtifactName, '-', '_') }} displayName: Build ${{ parameters.outputArtifactName }} @@ -74,7 +65,7 @@ stages: - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + artifactFeeds: "mariner/Mariner-Pypi-Feed" - task: DownloadPipelineArtifact@2 inputs: diff --git a/.pipelines/templates/stages/build_image/build-runtime.yml b/.pipelines/templates/stages/build_image/build-runtime.yml new file mode 100644 index 000000000..85e408862 --- /dev/null +++ b/.pipelines/templates/stages/build_image/build-runtime.yml @@ -0,0 +1,86 @@ +parameters: + - name: imageName + type: string + + - name: baseimgBuildType + displayName: Base Image build type + type: string + values: + - dev + - preview + - release + default: "release" + + - name: baseImagePipelineBuildId + displayName: "Build Id of the pipeline run, default will select latest successful run from pipeline 2116 ([AMD64-6-OneBranch]-Prod-BuildImages) with tag 3.0-preview" + type: string + default: "latestFromBranch" + + - name: runtimeEnv + displayName: "Runtime environment (host vs container)" + type: string + default: "host" + values: + - host + - container + + - name: micBuildType + displayName: MIC Build Type + type: string + values: + - dev + - preview + - release + default: release + + - name: micVersion + displayName: MIC Version + type: string + default: "*.*.*" + +stages: + - stage: TridentTestImg_${{ replace(parameters.imageName, '-', '_') }} + displayName: Build ${{ parameters.imageName }} + dependsOn: + - ${{ if eq(parameters.runtimeEnv, 'host') }}: + - GetTridentBinaries_rpms + - ${{ else }}: [] + + jobs: + - job: BuildTridentTestImg + displayName: Build + timeoutInMinutes: 20 + pool: + type: linux + + variables: + ob_outputDirectory: $(Pipeline.Workspace)/s/output + ob_artifactBaseName: ${{ parameters.imageName }} + + steps: + - task: PipAuthenticate@1 + displayName: Provision - Authenticate Pip + inputs: + artifactFeeds: "mariner/Mariner-Pypi-Feed" + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: current + artifactName: trident-binaries + targetPath: "$(Build.ArtifactStagingDirectory)/trident" + displayName: Download Trident RPMs + condition: eq('${{ parameters.runtimeEnv }}', 'host') + + - template: ../common_tasks/find-base-image-version.yml + parameters: + baseimgBuildType: ${{ parameters.baseimgBuildType }} + baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} + + - template: .pipelines/templates/build-image.yml@test-images + parameters: + imageName: ${{ parameters.imageName }} + clones: 2 + baseimgBuildType: ${{ parameters.baseimgBuildType }} + baseimgVersion: ${{ variables.BASEIMG_AZURE_LINUX_VERSION }} + micBuildType: ${{ parameters.micBuildType }} + micVersion: ${{ parameters.micVersion }} diff --git a/.pipelines/templates/stages/testing_common/download-test-images.yml b/.pipelines/templates/stages/testing_common/download-test-images.yml index 08f0a8edb..7fd76ac02 100644 --- a/.pipelines/templates/stages/testing_common/download-test-images.yml +++ b/.pipelines/templates/stages/testing_common/download-test-images.yml @@ -113,6 +113,14 @@ steps: targetPath: "${{ parameters.toolsDirectory }}" - bash: | + # Debug log + echo "REGULAR IMAGES:" + ls -alh "${{ parameters.testImageDir }}" + + echo "" + echo "VERITY IMAGES:" + ls -alh "${{ parameters.testImageDirVerity }}" + set -eux # Set tools to be executable @@ -120,18 +128,15 @@ steps: chmod +x ${{ parameters.toolsDirectory }}/mkcosi chmod +x ${{ parameters.toolsDirectory }}/storm-trident - # Create the target directory - mkdir -p "${{ parameters.targetDirectory }}" - - # If testImageDir is not an empty string, rename the Trident test images - if [ -n "${{ parameters.testImageDir }}" ]; then - mv "${{ parameters.testImageDir }}/${{ parameters.tridentTestImage }}.cosi" "${{ parameters.targetDirectory }}/regular.cosi" - fi - - # If testImageDirVerity is not an empty string, rename the Trident verity test images - if [ -n "${{ parameters.testImageDirVerity }}" ]; then - mv "${{ parameters.testImageDirVerity }}/${{ parameters.tridentTestImageVerity }}.cosi" "${{ parameters.targetDirectory }}/verity.cosi" - fi + # Call helper to copy and rename images! + ${{ parameters.toolsDirectory }}/storm-trident \ + helper prepare-images -a -- \ + "${{ parameters.testImageDir }}" \ + "${{ parameters.testImageDirVerity }}" \ + "${{ parameters.tridentTestImage }}" \ + "${{ parameters.tridentTestImageVerity }}" \ + "${{ parameters.targetDirectory }}" \ + -v 4 # List tools in the target directory ls -lh "${{ parameters.toolsDirectory }}" diff --git a/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh b/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh index 6f8df0d13..64e8aa493 100644 --- a/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh +++ b/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh @@ -12,33 +12,24 @@ DESTINATION_DIR=$5 VERSION=$6 VERITY_REQUIRED=$7 -# Define path to the COSI file -COSI_FILE="$ARTIFACTS_DIR/regular.cosi" -# Define path to the update COSI file, i.e. with randomized FS UUID required for A/B update testing -UPDATE_COSI_FILE="$ARTIFACTS_DIR/regular-update.cosi" -# Define path to the COSI file on the host -HOST_COSI_FILE="$DESTINATION_DIR/regular_v$VERSION.cosi" - -# If needed, change the name/path of the COSI file based on verityRequired parameter +FILE_NAME_BASE="regular" + if [ "$VERITY_REQUIRED" = "true" ]; then - echo "Transferring verity COSI file onto the host" - COSI_FILE="$ARTIFACTS_DIR/verity.cosi" - UPDATE_COSI_FILE="$ARTIFACTS_DIR/verity-update.cosi" - HOST_COSI_FILE="$DESTINATION_DIR/verity_v$VERSION.cosi" - - # Before transferring the COSI file, randomize the FS UUID - ./bin/mkcosi randomize-fs-uuid "$COSI_FILE" "$UPDATE_COSI_FILE" /boot -else - echo "Transferring regular COSI file onto the host" - # Create destination directory on the host - ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$USER_NAME"@"$HOST_IP" "sudo mkdir -p '$DESTINATION_DIR'" - - ./bin/mkcosi randomize-fs-uuid "$COSI_FILE" "$UPDATE_COSI_FILE" / + FILE_NAME_BASE="verity" fi +COSI_FILE_NAME="${FILE_NAME_BASE}_v${VERSION}.cosi" +LOCAL_FILE="$ARTIFACTS_DIR/$COSI_FILE_NAME" +REMOTE_FILE="$DESTINATION_DIR/$COSI_FILE_NAME" + +echo "Transferring $LOCAL_FILE to the host into $REMOTE_FILE" + +# Create destination directory on the host it it doesn't exist +ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$USER_NAME"@"$HOST_IP" "sudo mkdir -p '$DESTINATION_DIR'" + # Prepare destination directory on the host ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$USER_NAME"@"$HOST_IP" "sudo chown '$USER_NAME:$USER_NAME' '$DESTINATION_DIR' && sudo chmod 755 '$DESTINATION_DIR'" # SCP the file onto the host -scp -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$UPDATE_COSI_FILE" "$USER_NAME"@"$HOST_IP":"$HOST_COSI_FILE" +scp -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$LOCAL_FILE" "$USER_NAME"@"$HOST_IP":"$REMOTE_FILE" echo "Transferred COSI file to the host" diff --git a/storm/suites/trident/helpers/init.go b/storm/suites/trident/helpers/init.go index b265bf28d..5a41fa966 100644 --- a/storm/suites/trident/helpers/init.go +++ b/storm/suites/trident/helpers/init.go @@ -5,4 +5,5 @@ import "storm/pkg/storm" var TRIDENT_HELPERS = []storm.Helper{ &CheckSshHelper{}, &AbUpdateHelper{}, + &PrepareImages{}, } diff --git a/storm/suites/trident/helpers/prepare_images.go b/storm/suites/trident/helpers/prepare_images.go new file mode 100644 index 000000000..a3317e6c1 --- /dev/null +++ b/storm/suites/trident/helpers/prepare_images.go @@ -0,0 +1,165 @@ +package helpers + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "storm/pkg/storm" + + "github.com/sirupsen/logrus" +) + +const ( + COSI_EXTENSION = "cosi" + REGULAR_IMAGE_NAME = "regular" + VERITY_IMAGE_NAME = "verity" +) + +type PrepareImages struct { + args struct { + RegularTestImageDir string `arg:"" help:"Directory containing the regular test images" type:"path"` + VerityTestImageDir string `arg:"" help:"Directory containing the verity test images" type:"path"` + RegularImageName string `arg:"" help:"Name of the regular test image"` + VerityImageName string `arg:"" help:"Name of the verity test image"` + OutputDir string `arg:"" help:"Directory in which to place the prepared images" type:"path"` + Versions uint `short:"v" help:"Number of versions to create of each image type" default:"1"` + } +} + +func (h PrepareImages) Name() string { + return "prepare-images" +} + +func (h *PrepareImages) Args() any { + return &h.args +} + +func (h *PrepareImages) RegisterTestCases(r storm.TestRegistrar) error { + r.RegisterTestCase("copy-regular", h.copyRegularImages) + r.RegisterTestCase("copy-verity", h.copyVerityImages) + return nil +} + +func (h *PrepareImages) copyRegularImages(tc storm.TestCase) error { + // Skip test if the path doesn't exist + if _, err := os.Stat(h.args.RegularTestImageDir); os.IsNotExist(err) { + tc.Skip(fmt.Sprintf("Directory %s does not exist", h.args.RegularTestImageDir)) + } + + return copyImages( + tc.Logger(), + h.args.RegularTestImageDir, + h.args.OutputDir, + h.args.RegularImageName, + COSI_EXTENSION, + REGULAR_IMAGE_NAME, + h.args.Versions, + ) +} + +func (h *PrepareImages) copyVerityImages(tc storm.TestCase) error { + // Skip test if the path doesn't exist + if _, err := os.Stat(h.args.VerityTestImageDir); os.IsNotExist(err) { + tc.Skip(fmt.Sprintf("Directory %s does not exist", h.args.VerityTestImageDir)) + } + + return copyImages( + tc.Logger(), + h.args.VerityTestImageDir, + h.args.OutputDir, + h.args.VerityImageName, + COSI_EXTENSION, + VERITY_IMAGE_NAME, + h.args.Versions, + ) +} + +func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ext string, outputFilename string, versions uint) error { + srcDir, err := filepath.Abs(srcDir) + if err != nil { + return fmt.Errorf("failed to get absolute path of source directory %s: %v", srcDir, err) + } + destDir, err = filepath.Abs(destDir) + if err != nil { + return fmt.Errorf("failed to get absolute path of destination directory %s: %v", destDir, err) + } + + glob := fmt.Sprintf("%s/%s*.%s", srcDir, imageName, ext) + files, err := filepath.Glob(glob) + if err != nil { + return fmt.Errorf("failed to list files in directory %s: %v", srcDir, err) + } + + if len(files) == 0 { + return fmt.Errorf("no '%s' files found in directory %s", glob, srcDir) + } + + log.Infof("Found %d files in %s matching glob %s", len(files), srcDir, glob) + + singleFilePattern := fmt.Sprintf("%s.%s", imageName, ext) + multipleFilePattern := fmt.Sprintf(`%s_(\d+).%s`, regexp.QuoteMeta(imageName), regexp.QuoteMeta(ext)) + + if len(files) == 1 && filepath.Base(files[0]) != singleFilePattern { + // Single file, must be names exactly as the image name + extension + return fmt.Errorf("file '%s' does not match the expected pattern '%s'", filepath.Base(files[0]), singleFilePattern) + } else if len(files) > 1 { + compiled, err := regexp.Compile(multipleFilePattern) + if err != nil { + return fmt.Errorf("failed to compile regex %s: %v", multipleFilePattern, err) + } + + // Multiple files, must match the pattern imageName_0.ext, imageName_1.ext, etc. + for _, file := range files { + if !compiled.MatchString(filepath.Base(file)) { + return fmt.Errorf("file %s does not match the expected pattern %s", file, multipleFilePattern) + } + } + } + + // Create output directory if it doesn't exist + if _, err := os.Stat(destDir); os.IsNotExist(err) { + log.Debugf("Creating directory %s", destDir) + err := os.MkdirAll(destDir, 0755) + if err != nil { + return fmt.Errorf("failed to create directory %s: %v", destDir, err) + } + } + + outputFiles := make([]string, 0) + + for i, file := range files { + var newFileName string + if i == 0 { + newFileName = fmt.Sprintf("%s.%s", outputFilename, ext) + } else { + // Add 1 because we expect the first update to consume v2 + newFileName = fmt.Sprintf("%s_v%d.%s", outputFilename, i+1, ext) + } + + log.Infof("Moving file '%s' to '%s'", file, newFileName) + + newFilePath := filepath.Join(destDir, newFileName) + err := os.Rename(file, newFilePath) + if err != nil { + return fmt.Errorf("failed to rename file %s to %s: %v", file, newFilePath, err) + } + + outputFiles = append(outputFiles, newFilePath) + } + + for v := len(outputFiles); v < int(versions); v++ { + // Add 1 because we expect the first update to consume v2 + newFileName := fmt.Sprintf("%s_v%d.%s", outputFilename, v+1, ext) + baseFile := outputFiles[v%len(outputFiles)] + // Create a hard link to the base file + newFilePath := filepath.Join(destDir, newFileName) + log.Infof("Linking file '%s' to '%s'", baseFile, newFilePath) + err := os.Link(baseFile, newFilePath) + if err != nil { + return fmt.Errorf("failed to link file %s to %s: %v", baseFile, newFilePath, err) + } + } + + return nil +} From 868ff1a486adde1e9f395dcf44dcb85008f3071e Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 8 May 2025 18:22:08 +0000 Subject: [PATCH 04/99] Merged PR 23070: bug: Fix parsing of Prism history #### AI description (iteration 1) #### PR Classification Bug fix: Resolve the parsing error for Prism history when partition size is null. #### PR Summary This pull request fixes the parsing issue by handling null partition sizes during offline initialization. The changes include: - In `src/offline_init/mod.rs`, updating the partition size logic to use a match on an optional value and defaulting to `PartitionSize::Grow` when size is missing. - In the test suite within `src/offline_init/mod.rs`, adding a test that validates the successful deserialization of Prism history JSON. - In the `PrismPartition` structure, changing the type of `size` from `String` to `Option` for more robust parsing. Related work items: #12145 --- src/offline_init/aksee_prism_history.json | 408 ++++++++++++++++++++++ src/offline_init/mod.rs | 17 +- 2 files changed, 418 insertions(+), 7 deletions(-) create mode 100644 src/offline_init/aksee_prism_history.json diff --git a/src/offline_init/aksee_prism_history.json b/src/offline_init/aksee_prism_history.json new file mode 100644 index 000000000..adb962f4e --- /dev/null +++ b/src/offline_init/aksee_prism_history.json @@ -0,0 +1,408 @@ +[ + { + "timestamp": "2025-05-08T01:00:36Z", + "toolVersion": "0.13.0", + "imageUuid": "7671f584-b262-15ce-9fb0-14519e97dceb", + "config": { + "input": { + "image": {} + }, + "storage": { + "bootType": "efi", + "disks": [ + { + "partitionTableType": "gpt", + "maxSize": "17791M", + "partitions": [ + { + "id": "esp", + "label": "esp", + "start": "1M", + "size": "8M", + "type": "esp" + }, + { + "id": "boot-a", + "label": "boot-a", + "start": "9M", + "size": "192M" + }, + { + "id": "root-a", + "label": "root-a", + "start": "201M", + "size": "4G" + }, + { + "id": "boot-b", + "label": "boot-b", + "start": "4297M", + "size": "192M" + }, + { + "id": "root-b", + "label": "root-b", + "start": "4489M", + "size": "4G" + }, + { + "id": "root-hash-a", + "label": "root-hash-a", + "start": "8585M", + "size": "256M" + }, + { + "id": "root-hash-b", + "label": "root-hash-b", + "start": "8841M", + "size": "256M" + }, + { + "id": "trident", + "label": "trident", + "start": "9097M", + "size": "256M" + }, + { + "id": "trident-overlay-a", + "label": "trident-overlay-a", + "start": "9353M", + "size": "256M" + }, + { + "id": "trident-overlay-b", + "label": "trident-overlay-b", + "start": "9609M", + "size": "256M" + }, + { + "id": "log", + "label": "log", + "start": "9865M", + "size": "1G" + }, + { + "id": "data", + "label": "data", + "start": "16009M", + "end": "17790M", + "size": null + } + ] + } + ], + "filesystems": [ + { + "deviceId": "esp", + "type": "fat32", + "mountPoint": { + "idType": "uuid", + "options": "umask=0077", + "path": "/boot/efi" + } + }, + { + "deviceId": "boot-a", + "type": "ext4", + "mountPoint": { + "idType": "uuid", + "path": "/boot" + } + }, + { + "deviceId": "rootverity", + "type": "ext4", + "mountPoint": { + "options": "defaults,ro", + "path": "/" + } + }, + { + "deviceId": "boot-b", + "type": "ext4" + }, + { + "deviceId": "root-b", + "type": "ext4" + }, + { + "deviceId": "trident", + "type": "ext4", + "mountPoint": { + "idType": "part-label", + "path": "/var/lib/trident" + } + }, + { + "deviceId": "trident-overlay-a", + "type": "ext4", + "mountPoint": { + "idType": "part-label", + "path": "/var/lib/trident-overlay" + } + }, + { + "deviceId": "trident-overlay-b", + "type": "ext4" + }, + { + "deviceId": "log", + "type": "ext4", + "mountPoint": { + "idType": "part-label", + "path": "/var/log" + } + }, + { + "deviceId": "data", + "type": "ext4", + "mountPoint": { + "idType": "part-label", + "path": "/var" + } + } + ], + "verity": [ + { + "id": "rootverity", + "name": "root", + "dataDeviceId": "root-a", + "dataDeviceMountIdType": "part-label", + "hashDeviceId": "root-hash-a", + "hashDeviceMountIdType": "part-label", + "corruptionOption": "ignore" + } + ] + }, + "os": { + "hostname": "iotedge-vm", + "packages": { + "install": [ + "aziot-identity-service", + "aziot-edge", + "babeltrace2", + "bash", + "bc", + "bzip2", + "ca-certificates", + "ca-certificates-base", + "chrony", + "cloud-init", + "cloud-utils-growpart", + "conntrack-tools", + "containerd", + "cpio", + "cracklib-dicts", + "cryptsetup", + "curl", + "device-mapper", + "defender-iot-micro-agent-edge-os-config", + "dbus", + "dosfstools", + "dracut", + "dracut-overlayfs", + "dmidecode", + "dnf", + "e2fsprogs", + "efibootmgr", + "elfutils-libelf", + "expat", + "eflow-proxy", + "file", + "filesystem", + "findutils", + "fio", + "gdbm", + "grep", + "gzip", + "git", + "hyperv-daemons", + "iana-etc", + "icu", + "iproute", + "ipset", + "iptables", + "iputils", + "irqbalance", + "iscsi-initiator-utils", + "jq", + "keyutils", + "libevent", + "libnfsidmap", + "libnvidia-container1", + "libnvidia-container-tools", + "libtool", + "lvm2", + "lsof", + "nano", + "ncurses-libs", + "net-tools", + "netplan", + "nfs-utils", + "nspr", + "nss-libs", + "nvidia-container-runtime", + "nvidia-container-toolkit-base", + "nvidia-container-toolkit", + "opengcs", + "openssh", + "openssh-clients", + "openssl", + "procps-ng", + "pkgconf", + "rpcbind", + "readline", + "rpm", + "sed", + "selinux-policy", + "socat", + "sqlite-libs", + "sudo", + "systemd", + "systemd-udev", + "tar", + "tdnf", + "tdnf-plugin-repogpgcheck", + "tpm2-abrmd", + "tpm2-tss", + "tzdata", + "util-linux", + "vim", + "veritysetup", + "wget", + "which", + "xz", + "zlib", + "trident" + ] + }, + "selinux": { + "mode": "disabled" + }, + "kernelCommandLine": { + "extraCommandLine": [ + "rd.shell=0", + "log_buf_len=1M", + "ipv6.disable=1" + ] + }, + "additionalFiles": [ + { + "destination": "etc/yum.repos.d/cloud-native.repo", + "source": "../repos/cloud-native.repo", + "sha256hash": "75237565dc573bd00afbee902f2187cedab7e644a9753d18bc9d9c9d5d5e15f8" + }, + { + "destination": "/usr/lib/systemd/system/getty@.service", + "source": "configs/getty@.service", + "sha256hash": "e5432ef8100b84f2a396f7744501c5de5a2811cba9e29cd70c510704cab0eca5" + }, + { + "destination": "/usr/lib/systemd/system/serial-getty@.service", + "source": "configs/serial-getty@.service", + "sha256hash": "92ca782e9aec3fa95b05b513ad102f3319b3aa3bae0de11e0fb3cf3f97bcb896" + }, + { + "destination": "/etc/cloud/cloud.cfg", + "source": "configs/cloud-init.cfg", + "sha256hash": "5238cf3b0ccda3def748a58f3d6daadf457eb959637131ff146743a391a6728e" + }, + { + "destination": "/etc/ssh/sshd_config", + "source": "configs/sshd_config", + "permissions": 436, + "sha256hash": "3b5fa89136b71aed6010851cde85b071e0e02fbf97ddfe79b81322e2615194b7" + }, + { + "destination": "/lib/systemd/system/sshd_vsock.socket", + "source": "configs/sshd_vsock.socket", + "permissions": 436, + "sha256hash": "5ca9f0768a981aabde6c8bfeed5687f553e5040753829c4e4b79b3dfc36039df" + }, + { + "destination": "/lib/systemd/system/sshd_vsock@.service", + "source": "configs/sshd_vsock@.service", + "permissions": 436, + "sha256hash": "ade6b152e68138ab200734991b3d9ecfde2196465e9711fe71dcc783c9e0909a" + }, + { + "destination": "/etc/docker/daemon.json", + "source": "configs/daemon.json", + "sha256hash": "84439b0836c8df0bb7d44192d92275879f0fbde2d460437c14999b781b1e10c5" + }, + { + "destination": "/etc/aziot/tpmd/config.d/50-eflow.toml", + "source": "configs/50-eflow.toml", + "sha256hash": "00d53f167de6929408340da36dfdb79b6d3676f19afc67bdaf2dfdb62c8a15ef" + }, + { + "destination": "/etc/trident/install/host_status.yaml", + "source": "configs/trident/install/host_status.yaml", + "sha256hash": "d7c7aa8f0227480bff84583a626022a4fa6d7e5dee1d8488e83fc253734077df" + }, + { + "destination": "/etc/trident/config.yaml", + "source": "configs/trident/config.yaml", + "sha256hash": "daffb31ac8ccab85a7c33aeb6af6fb68e4b30ea461660b7edb6a107147248799" + }, + { + "destination": "/etc/trident/trident-init.sh", + "source": "configs/trident/trident-init.sh", + "permissions": 493, + "sha256hash": "110d43a9b48ab5f3c541a227d2fa0de353e1a6edf05b292c41529c09b7b24ddc" + }, + { + "destination": "/etc/systemd/system/var-etc-mount.service", + "source": "configs/services/var-etc-mount.service", + "sha256hash": "f28e55de5e15dc960363a1a2da404ce120516a75a9023a50ca8eb063c1518db1" + }, + { + "destination": "/usr/local/bin/var-etc-mount.sh", + "source": "configs/services/var-etc-mount.sh", + "permissions": 511, + "sha256hash": "69373ce99e86f37ceb7c38eb1ce85d751b9767f6b83d1df0569849e15609c2f3" + } + ], + "services": { + "enable": [ + "gcs", + "sshd_vsock.socket", + "var-etc-mount" + ] + }, + "modules": [ + { + "name": "hv_sock", + "loadMode": "always" + }, + { + "name": "ip_tables", + "loadMode": "always" + } + ], + "bootloader": { + "resetType": "hard-reset" + } + }, + "scripts": { + "postCustomization": [ + { + "path": "configure-image-gen.sh", + "sha256hash": "954d21e75d87ab238b1aa9c10c5e522eb5432da8d096dc095f99fa1c7a3cbc29" + }, + { + "path": "configure-image-post.sh", + "sha256hash": "5826585a2c8c45fba3f36853be21beaddb18e0a0ecec8d943a8509d1987e3f52" + }, + { + "path": "create-rw-overlay-dirs.sh", + "sha256hash": "84deef7817380aaa9cfca6c22f7ae307695aa3ed68efac7ccc96ee162567eb70" + } + ] + }, + "output": { + "image": {} + } + } + } +] \ No newline at end of file diff --git a/src/offline_init/mod.rs b/src/offline_init/mod.rs index 2d1806592..cc61fc5c1 100644 --- a/src/offline_init/mod.rs +++ b/src/offline_init/mod.rs @@ -35,7 +35,7 @@ struct PrismPartition { #[allow(unused)] start: String, #[allow(unused)] - size: String, + size: Option, #[serde(rename = "type")] ty: Option, @@ -134,12 +134,12 @@ fn generate_host_status( } else { PartitionType::LinuxGeneric }, - size: PartitionSize::from_str(&partition.size) - .structured(InvalidInputError::ParsePrismHistory) - .message(format!( - "Failed to parse partition size '{}'", - partition.size - ))?, + size: match &partition.size { + Some(s) => PartitionSize::from_str(s) + .structured(InvalidInputError::ParsePrismHistory) + .message(format!("Failed to parse partition size '{s}'"))?, + None => PartitionSize::Grow, + }, }); } @@ -408,6 +408,9 @@ mod tests { assert_eq!(disk.partitions.len(), 14); assert_eq!(disk.partitions[0].id, "esp"); assert_eq!(disk.partitions[1].id, "boot-a"); + + let _history2: Vec = + serde_json::from_str(include_str!("aksee_prism_history.json")).unwrap(); } #[test] From c247e95c161a0c6d24c6b3773ee70895ec8bff1f Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 8 May 2025 19:27:42 +0000 Subject: [PATCH 05/99] Merged PR 23065: bug: Handle unknown partition table types from lsblk #### AI description (iteration 1) #### PR Classification Bug fix addressing lsblk output parsing failures. #### PR Summary This PR fixes lsblk parsing errors by adding support for unknown partition table types. - `/osutils/src/lsblk.rs`: Added an `Unknown` enum variant annotated with `#[serde(other)]` to gracefully handle unsupported partition table types. Related work items: #12139 --- osutils/src/lsblk.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/osutils/src/lsblk.rs b/osutils/src/lsblk.rs index fef90248b..012d780d0 100644 --- a/osutils/src/lsblk.rs +++ b/osutils/src/lsblk.rs @@ -140,6 +140,9 @@ pub enum PartitionTableType { /// Master Boot Record #[serde(rename = "mbr", alias = "dos")] Mbr, + + #[serde(other)] + Unknown, } /// Returns a list of all block devices on the system. @@ -1180,6 +1183,92 @@ mod tests { parse_lsblk_output(output).unwrap(); } + + #[test] + fn unknown_partition_table_type() { + let output = r#"{ +"blockdevices": [ + { + "alignment": 0, + "id-link": null, + "id": null, + "disc-aln": 0, + "dax": false, + "disc-gran": 4096, + "disk-seq": 4309, + "disc-max": 4294966784, + "disc-zero": false, + "fsavail": null, + "fsroots": [ + null + ], + "fssize": null, + "fstype": null, + "fsused": null, + "fsuse%": null, + "fsver": null, + "group": "daemon", + "hctl": null, + "hotplug": false, + "kname": "loop32", + "label": null, + "log-sec": 512, + "maj:min": "7:32", + "maj": "7", + "min": "32", + "min-io": 512, + "mode": "brw-rw----", + "model": null, + "mq": " 1", + "name": "loop32", + "opt-io": 0, + "owner": "root", + "partflags": null, + "partlabel": null, + "partn": null, + "parttype": null, + "parttypename": null, + "partuuid": null, + "path": "/dev/loop32", + "phy-sec": 512, + "pkname": null, + "pttype": "PMBR", + "ptuuid": null, + "ra": 128, + "rand": false, + "rev": null, + "rm": false, + "ro": false, + "rota": false, + "rq-size": 128, + "sched": "none", + "serial": null, + "size": 13893632000, + "start": null, + "state": null, + "subsystems": "block", + "mountpoint": null, + "mountpoints": [ + null + ], + "tran": null, + "type": "loop", + "uuid": null, + "vendor": null, + "wsame": 0, + "wwn": null, + "zoned": "none", + "zone-sz": 0, + "zone-wgran": 0, + "zone-app": 0, + "zone-nr": 0, + "zone-omax": 0, + "zone-amax": 0, + "children": [] + } +]}"#; + let _ = parse_lsblk_output(output).unwrap(); + } } #[cfg(feature = "functional-test")] From c42b5ac99784eb82b2d7d7bdca2012091b82a8fe Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Thu, 8 May 2025 20:41:08 +0000 Subject: [PATCH 06/99] Merged PR 22966: bug: Fix error message when filesystem requires more space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Clarifies the error message when a filesystem requires more space than the underlying block device, by including file system mount point and block device size. ---- #### AI description (iteration 1) #### PR Classification Bug fix to correct the error message when the filesystem requires more space than available on the block device. #### PR Summary This pull request updates the error message to provide more detailed information about the filesystem and block device size mismatch. - `trident_api/src/error.rs`: Modified the error message to include the mount point and provide a clearer comparison between filesystem size and block device size. - `src/subsystems/storage/osimage.rs`: Updated the error handling to include the mount point and convert sizes to human-readable format for better clarity. Related work items: #12067 --- src/subsystems/storage/osimage.rs | 4 +++- trident_api/src/error.rs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/subsystems/storage/osimage.rs b/src/subsystems/storage/osimage.rs index 17a6280b9..fb21a29b4 100644 --- a/src/subsystems/storage/osimage.rs +++ b/src/subsystems/storage/osimage.rs @@ -424,8 +424,10 @@ fn validate_filesystem_blkdev_fit( if fs_size > blkdev_size { return Err(TridentError::new( InvalidInputError::FilesystemSizeExceedsBlockDevice { + mount_point: fs.mount_point.display().to_string(), device_id: device_id.to_string(), - min_size: fs_size, + fs_size: ByteCount::from(fs_size), + device_size: ByteCount::from(blkdev_size), }, )); }; diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index dac40a53d..ac414b4e4 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -11,6 +11,7 @@ use url::Url; use crate::{ config::{HostConfigurationDynamicValidationError, HostConfigurationStaticValidationError}, + primitives::bytes::ByteCount, status::ServicingState, storage_graph::error::StorageGraphBuildError, }; @@ -141,10 +142,16 @@ pub enum InvalidInputError { CleanInstallOnProvisionedHost, #[error( - "Filesystem size exceeds underlying block device's size for device '{device_id}'. \ - The filesystem requires at least {min_size} bytes on the block device." + "Filesystem mounted at '{mount_point}' requires at least {} [{fs_size} bytes] of storage. \ + However, the underlying block device '{device_id}' has storage size {} [{device_size} bytes].", + fs_size.to_human_readable_approx(), device_size.to_human_readable_approx() )] - FilesystemSizeExceedsBlockDevice { device_id: String, min_size: u64 }, + FilesystemSizeExceedsBlockDevice { + device_id: String, + mount_point: String, + device_size: ByteCount, + fs_size: ByteCount, + }, #[error("Image is corrupt: multiple partitions have be assigned the same FS UUID: {uuid}")] DuplicateFsUuid { uuid: String }, From efae01b8681e9bc31ce969fed084368cbe825348 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 8 May 2025 22:30:09 +0000 Subject: [PATCH 07/99] Merged PR 22984: doc: rfc: COSI 1.1 Revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description COSI 1.1 revision to add bootloader metadata and resolve some things that annoy me. ---- #### AI description (iteration 1) #### PR Classification Documentation update for the revision of the Composable OS Image (COSI) specification to version 1.1. #### PR Summary This pull request revises the COSI specification document to include new fields and objects related to bootloader configuration and filesystem metadata. It introduces version 1.1 of the specification with detailed descriptions of new components. - `Composable-OS-Image.md`: Added `Bootloader` object and related enums and objects for bootloader configuration, including `BootloaderType`, `SystemDBoot`, and `SystemDBootEntry`. - `Composable-OS-Image.md`: Updated metadata schema to include `bootloader` and `filesystems` fields, with `images` now deprecated as an alias for `filesystems`. - `Composable-OS-Image.md`: Revised `VerityConfig` and `ImageFile` objects to include versioning information. - `Composable-OS-Image.md`: Updated `OsPackage` object to include versioning information for fields. Related work items: #12113 --- dev-docs/specs/Composable-OS-Image.md | 279 +++++++++++++++++--------- 1 file changed, 182 insertions(+), 97 deletions(-) diff --git a/dev-docs/specs/Composable-OS-Image.md b/dev-docs/specs/Composable-OS-Image.md index 19f4a759d..dbd5202ed 100644 --- a/dev-docs/specs/Composable-OS-Image.md +++ b/dev-docs/specs/Composable-OS-Image.md @@ -2,9 +2,10 @@ ## Revision Summary -| Revision | Date | Comment | -| -------- | ---------- | ---------------- | -| 1.0 | 2024-10-09 | Initial version. | +| Revision | Spec Date | +| ------------------- | ---------- | +| [1.1](#revision-11) | TBD | +| [1.0](#revision-10) | 2024-10-09 | ## Table of Contents @@ -21,15 +22,22 @@ - [Metadata JSON File](#metadata-json-file) - [Schema](#schema) - [Root Object](#root-object) - - [`Image` Object](#image-object) + - [`Filesystem` Object](#filesystem-object) - [`VerityConfig` Object](#verityconfig-object) - [`ImageFile` Object](#imagefile-object) - [`OsArchitecture` Enum](#osarchitecture-enum) - [`OsPackage` Object](#ospackage-object) + - [`Bootloader` Object](#bootloader-object) + - [`BootloaderType` Enum](#bootloadertype-enum) + - [`SystemDBoot` Object](#systemdboot-object) + - [`SystemDBootEntry` Object](#systemdbootentry-object) + - [`SystemDBootEntryType` Enum](#systemdbootentrytype-enum) - [Samples](#samples) - [Simple Image](#simple-image) - - [Verity Image](#verity-image) - - [Packages](#packages) + - [Verity Image with UKI](#verity-image-with-uki) + - [Changelog](#changelog) + - [Revision 1.1](#revision-11) + - [Revision 1.0](#revision-10) - [FAQ and Notes](#faq-and-notes) ## Background @@ -134,71 +142,68 @@ tarball. The metadata file MUST be a valid JSON file. The metadata file MUST contain a JSON object with the following fields: -| Field | Type | Required | Description | -| ------------ | -------------------------------------- | -------- | ------------------------------------------------------ | -| `version` | string `MAJOR.MINOR` | Yes | The version of the metadata schema. | -| `osArch` | [OsArchitecture](#osarchitecture-enum) | Yes | The architecture of the OS. | -| `osRelease` | string | Yes | The contents of `/etc/os-release` verbatim. | -| `images` | [Image](#image-object)[] | Yes | Metadata of partition images that contain filesystems. | -| `osPackages` | [OsPackage](#ospackage-object)[] | No | The list of packages installed in the OS. | -| `id` | UUID (string, case insensitive) | No | A unique identifier for the COSI file. | - -To allow for future extensions, the object MAY contain other fields, but Trident -MUST ignore them. The object SHOULD NOT contain any extra fields that will not -be used by Trident. - -##### `Image` Object - -| Field | Type | Required | Description | -| ------------ | ------------------------------------ | -------- | ----------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the image file in the tarball. | -| `mountPoint` | string | Yes | The mount point of the partition. | -| `fsType` | string | Yes | The filesystem type of the partition. [1] | -| `fsUuid` | string | Yes | The UUID of the filesystem. [2] | -| `partType` | UUID (string, case insensitive) | Yes | The GPT partition type. [3] [4] [5] | -| `verity` | [VerityConfig](#verityconfig-object) | No | The verity metadata of the partition. | +| Field | Type | Added in | Required | Description | +| ------------ | -------------------------------------- | -------- | --------------- | ------------------------------------------------ | +| `version` | string `MAJOR.MINOR` | 1.0 | Yes (since 1.0) | The version of the metadata schema. | +| `osArch` | [OsArchitecture](#osarchitecture-enum) | 1.0 | Yes (since 1.0) | The architecture of the OS. | +| `osRelease` | string | 1.0 | Yes (since 1.0) | The contents of `/etc/os-release` verbatim. | +| `images` | [Filesystem](#filesystem-object)[] | 1.0 | Yes (since 1.0) | Filesystem metadata. | +| `osPackages` | [OsPackage](#ospackage-object)[] | 1.0 | Yes (since 1.1) | The list of packages installed in the OS. | +| `bootloader` | [Bootloader](#bootloader-object) | 1.1 | Yes (since 1.1) | Information about the bootloader used by the OS. | +| `id` | UUID (string, case insensitive) | 1.0 | No | A unique identifier for the COSI file. | + +If the object contains other fields, readers MUST ignore them. A writer SHOULD +NOT add any other files to the object. + +##### `Filesystem` Object + +This object carries information about a filesystem and the partition it comes +from in a virtual disk. + +| Field | Type | Added in | Required | Description | +| ------------ | ------------------------------------ | -------- | ---------------- | ----------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the image file in the tarball. | +| `mountPoint` | string | 1.0 | Yes (since 1.0) | The mount point of the filesystem. | +| `fsType` | string | 1.0 | Yes (since 1.0) | The filesystem's type. [1] | +| `fsUuid` | string | 1.0 | Yes (since 1.0) | The UUID of the filesystem. [2] | +| `partType` | UUID (string, case insensitive) | 1.0 | Yes (since 1.0) | The GPT partition type. [3] [4] [5] | +| `verity` | [VerityConfig](#verityconfig-object) | 1.0 | Conditionally[6] | The verity metadata of the filesystem. | _Notes:_ -- **[1]** It MUST use the name recognized by the kernel. For example, `ext4` for ext4 filesystems, - `vfat` for FAT32 filesystems, etc. - -- **[2]** It MUST be unique across all filesystems in the COSI tarball. Additionally, volumes in an - A/B volume pair MUST have unique filesystem UUIDs. +- **[1]** It MUST use the name recognized by the kernel. For example, `ext4` for + ext4 filesystems, `vfat` for FAT32 filesystems, etc. +- **[2]** It MUST be unique across all filesystems in the COSI tarball. + Additionally, volumes in an A/B volume pair MUST have unique filesystem UUIDs. - **[3]** It MUST be a UUID defined by the [Discoverable Partition Specification - (DPS)](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) when - the applicable type exists in the DPS. Other partition types MAY be used for types not defined - in DPS (e.g. Windows partitions). - -- **[4]** The EFI Sytem Partition (ESP) MUST be identified with the UUID established by the DPS: - `c12a7328-f81f-11d2-ba4b-00a0c93ec93b`. - -- **[5]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic Linux Data) if the - partition type cannot be determined. + (DPS)](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) + when the applicable type exists in the DPS. Other partition types MAY be + used for types not defined in DPS (e.g. Windows partitions). +- **[4]** The EFI Sytem Partition (ESP) MUST be identified with the UUID + established by the DPS: `c12a7328-f81f-11d2-ba4b-00a0c93ec93b`. +- **[5]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic + Linux Data) if the partition type cannot be determined. +- **[6]** The `verity` field MUST be specified if the OS is configured to open this + filesystem with `dm-verity`. Otherwise, it MUST be omitted OR set to `null`. ##### `VerityConfig` Object The `VerityConfig` object contains information required to set up a verity -device on top of a data partition. +device on top of a data device. -| Field | Type | Required | Description | -| ---------- | ------------------------------ | -------- | -------------------------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the hash partition image file in the tarball. | -| `roothash` | string | Yes | Verity root hash. | +| Field | Type | Added in | Required | Description | +| ---------- | ------------------------------ | -------- | --------------- | -------------------------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tarball. | +| `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | ##### `ImageFile` Object -| Field | Type | Required | Description | -| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------------- | -| `path` | string | Yes | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | -| `compressedSize` | number | Yes | Size of the compressed image in bytes. | -| `uncompressedSize` | number | Yes | Size of the raw uncompressed image in bytes. | -| `sha384` | string | No[5] | SHA-384 hash of the compressed hash image. | - -_Notes:_ - -- **[5]** The `sha384` field is optional, but it is RECOMMENDED to include it - for integrity verification. +| Field | Type | Added in | Required | Description | +| ------------------ | ------ | -------- | --------------- | ----------------------------------------------------------------------------------------- | +| `path` | string | 1.0 | Yes (since 1.0) | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | +| `compressedSize` | number | 1.0 | Yes (since 1.0) | Size of the compressed image in bytes. | +| `uncompressedSize` | number | 1.0 | Yes (since 1.0) | Size of the raw uncompressed image in bytes. | +| `sha384` | string | 1.0 | Yes (since 1.1) | SHA-384 hash of the compressed hash image. | ##### `OsArchitecture` Enum @@ -216,19 +221,15 @@ The `osArch` field is case-insensitive. ##### `OsPackage` Object -When present, the `osPackages` field in the root object MUST contain an array of -`OsPackage` objects. Each object represents a package installed in the OS. - -The field is strictly optional, but recommended. Trident MAY use this field to -figure out if the new OS is compatible with the Host Configuration by, for -example, identifying missing dependencies. +The `osPackages` field in the root object MUST contain an array of `OsPackage` +objects. Each object represents a package installed in the OS. -| Field | Type | Required | Description | -| --------- | ------ | -------- | ------------------------------------- | -| `name` | string | Yes | The name of the package. | -| `version` | string | Yes | The version of the package installed. | -| `release` | string | No | The release of the package. | -| `arch` | string | No | The architecture of the package. | +| Field | Type | Added in | Required | Description | +| --------- | ------ | -------- | --------------- | ------------------------------------- | +| `name` | string | 1.0 | Yes (since 1.0) | The name of the package. | +| `version` | string | 1.0 | Yes (since 1.0) | The version of the package installed. | +| `release` | string | 1.0 | Yes (since 1.1) | The release of the package. | +| `arch` | string | 1.0 | Yes (since 1.1) | The architecture of the package. | A suggested way to obtain this information is by running: @@ -236,13 +237,65 @@ A suggested way to obtain this information is by running: rpm -qa --queryformat "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n" ``` +##### `Bootloader` Object + +| Field | Type | Added in | Required | Description | +| ------------- | ---------------------------------------- | -------- | -------------------------------- | --------------------------- | +| `type` | [`BootloaderType`](#bootloadertype-enum) | 1.1 | Yes (since 1.1) | The type of the bootloader. | +| `systemdBoot` | [`SystemDBoot`](#systemdboot-object) | 1.1 | When `type` == `systemd-boot`[7] | systemd-boot configuration. | + +_Notes:_ + +- **[7]** The `systemd-boot` field is required if the `type` field is set to + `systemd-boot`. It MUST be omitted OR set to `null` if the `type` + field is set to any other value. + +##### `BootloaderType` Enum + +A string that represents the primary bootloader used in the contained OS. These +are the valid values for the `type` field in the `bootloader` object: + +| Value | Description | +| -------------- | --------------------------------------------------- | +| `systemd-boot` | The system is using systemd-boot as the bootloader. | +| `grub` | The system is using GRUB as the bootloader. | + +##### `SystemDBoot` Object + +This object contains metadata about how systemd-boot is configured in the OS. + +| Field | Type | Added in | Required | Description | +| --------- | ------------------------------------------------ | -------- | --------------- | ------------------------------------------------------------------------------------ | +| `entries` | [`SystemDBootEntry`](#systemdbootentry-object)[] | 1.1 | Yes (since 1.1) | The contents of the `loader/entries/*.conf` files in the systemd-boot EFI partition. | + +##### `SystemDBootEntry` Object + +This object contains metadata about a specific systemd-boot entry. + +| Field | Type | Added in | Required | Description | +| --------- | ---------------------------------------------------- | -------- | --------------- | ------------------------------------------------------ | +| `type` | [`SystemDBootEntryType`](#systemdbootentrytype-enum) | 1.1 | Yes (since 1.1) | The type of the entry. | +| `path` | string | 1.1 | Yes (since 1.1) | Absolute path (from the root FS) to the UKI or config. | +| `cmdline` | string | 1.1 | Yes (since 1.1) | The kernel command line. | +| `kernel` | string | 1.1 | Yes (since 1.1) | Kernel release as a string. | + +##### `SystemDBootEntryType` Enum + +A string that represents the type of the systemd-boot entry. + +| Value | Description | +| ---------------- | ------------------------------------------------------------------ | +| `uki-standalone` | The entry is a bare UKI file in the ESP. | +| `uki-config` | The entry is a config file with a UKI. | +| `config` | The entry is a config file with a kernel, initrd and command line. | + #### Samples ##### Simple Image ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -271,15 +324,39 @@ rpm -qa --queryformat "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n" "verity": null } ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "grub" + }, + "osPackages": [ + { + "name": "bash", + "version": "5.1.8", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "coreutils", + "version": "8.32", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + }, + // More packages... + ] } ``` -##### Verity Image +##### Verity Image with UKI ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -304,37 +381,45 @@ rpm -qa --queryformat "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n" }, // More images... ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" -} -``` - -##### Packages - -```json -{ - "version": "1.0", - "images": [ - // Images... - ], - "osRelease": "", + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "systemd-boot", + "systemdBoot": { + "entries": [ + { + "type": "uki-standalone", + "path": "/boot/efi/EFI/Linux/azurelinux-uki.efi", + "cmdline": "root=/dev/disk/by-partuuid/88d2fa9b-7a32-450a-a9f8-aa9c3de79298 ro", + "kernel": "6.6.78.1-3.azl3" + } + ] + } + }, "osPackages": [ - { - "name": "bash", - "version": "5.1.8" - }, - { - "name": "coreutils", - "version": "8.32" - }, { "name": "systemd", - "version": "255" + "version": "255", + "release": "20.azl3", + "arch": "x86_64" }, // More packages... ] } ``` +## Changelog + +### Revision 1.1 + +- Added `bootloader` field to the root object. +- Root field `osPackages` is now required. +- Field `sha384` in `ImageFile` object is now required. +- Fields `release` and `arch` in `OsPackage` object are now required. + +### Revision 1.0 + +- Initial revision + ## FAQ and Notes **Why tar?** From 6e2106aded49811998f0e90d7afe9b3b07ef0224 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Fri, 9 May 2025 17:57:29 +0000 Subject: [PATCH 08/99] Merged PR 22963: feature: Unwind to main before starting reboot Return an `ExitKind` enum all the way to the main function before running reboot. This way all the spans will be closed and all state will be dropped before the system goes down ---- #### AI description (iteration 1) #### PR Classification New feature: Implemented a mechanism to ensure the system unwinds to the main state before initiating a reboot. #### PR Summary This pull request introduces a new feature that ensures the system unwinds to the main state before starting a reboot, enhancing system stability and error handling. - Modified `src/lib.rs` to introduce `ExitKind` enum for handling operations that require a reboot and updated error handling logic. - Updated `src/main.rs` to handle the new `ExitKind` return type, ensuring proper reboot execution. - Adjusted `src/engine/clean_install.rs` and `src/engine/update.rs` to return `ExitKind` for operations, indicating when a reboot is necessary. - Enhanced the `Trident` implementation to manage host configuration updates and reboots effectively. Related work items: #12151 --- src/engine/clean_install.rs | 15 +++--- src/engine/update.rs | 18 +++---- src/lib.rs | 94 ++++++++++++++++++++++--------------- src/main.rs | 50 +++++++++++++------- 4 files changed, 104 insertions(+), 73 deletions(-) diff --git a/src/engine/clean_install.rs b/src/engine/clean_install.rs index 891fbb04a..a334f5a68 100644 --- a/src/engine/clean_install.rs +++ b/src/engine/clean_install.rs @@ -27,7 +27,7 @@ use crate::{ datastore::DataStore, engine::{self, boot::esp, bootentries, osimage, storage, EngineContext, SUBSYSTEMS}, subsystems::hooks::HooksSubsystem, - SAFETY_OVERRIDE_CHECK_PATH, + ExitKind, SAFETY_OVERRIDE_CHECK_PATH, }; #[cfg(feature = "grpc-dangerous")] use crate::{grpc, GrpcSender}; @@ -41,7 +41,7 @@ pub(crate) fn clean_install( allowed_operations: &Operations, multiboot: bool, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, -) -> Result<(), TridentError> { +) -> Result { info!("Starting clean install"); tracing::info!(metric_name = "clean_install_start", value = true); let clean_install_start_time = Instant::now(); @@ -90,6 +90,7 @@ pub(crate) fn clean_install( debug!("Unmounting '{}'", root_mount.path().display()); root_mount.unmount_all()?; + Ok(ExitKind::Done) } else { finalize_clean_install( state, @@ -97,10 +98,8 @@ pub(crate) fn clean_install( Some(clean_install_start_time), #[cfg(feature = "grpc-dangerous")] sender, - )?; + ) } - - Ok(()) } /// Performs a safety check to ensure that the clean install can proceed. @@ -267,7 +266,7 @@ pub(crate) fn finalize_clean_install( new_root: Option, clean_install_start_time: Option, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, -) -> Result<(), TridentError> { +) -> Result { info!("Finalizing clean install"); let ctx = EngineContext { @@ -343,12 +342,12 @@ pub(crate) fn finalize_clean_install( .internal_params .get_flag(NO_TRANSITION) { - engine::reboot() + Ok(ExitKind::NeedsReboot) } else { warn!( "Skipping reboot as requested by internal parameter '{}'", NO_TRANSITION ); - Ok(()) + Ok(ExitKind::Done) } } diff --git a/src/engine/update.rs b/src/engine/update.rs index a4c3c950a..cfdc0fd42 100644 --- a/src/engine/update.rs +++ b/src/engine/update.rs @@ -23,6 +23,7 @@ use crate::{ EngineContext, NewrootMount, SUBSYSTEMS, }, subsystems::hooks::HooksSubsystem, + ExitKind, }; #[cfg(feature = "grpc-dangerous")] use crate::{grpc, GrpcSender}; @@ -35,7 +36,7 @@ pub(crate) fn update( state: &mut DataStore, allowed_operations: &Operations, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, -) -> Result<(), TridentError> { +) -> Result { info!("Starting update"); let mut subsystems = SUBSYSTEMS.lock().unwrap(); @@ -84,7 +85,7 @@ pub(crate) fn update( .unwrap_or(ServicingType::NoActiveServicing); // Never None b/c select_servicing_type() returns a value if servicing_type == ServicingType::NoActiveServicing { info!("No update servicing required"); - return Ok(()); + return Ok(ExitKind::Done); } debug!( "Update of servicing type '{:?}' is required", @@ -137,6 +138,7 @@ pub(crate) fn update( None, state.host_status().servicing_state, ); + Ok(ExitKind::Done) } else { finalize_update( state, @@ -145,10 +147,8 @@ pub(crate) fn update( #[cfg(feature = "grpc-dangerous")] sender, ) - .message("Failed to finalize update")?; + .message("Failed to finalize update") } - - Ok(()) } ServicingType::NormalUpdate | ServicingType::HotPatch => { state.with_host_status(|host_status| { @@ -165,7 +165,7 @@ pub(crate) fn update( ); info!("Update of servicing type '{:?}' succeeded", servicing_type); - Ok(()) + Ok(ExitKind::Done) } ServicingType::CleanInstall => Err(TridentError::new( InvalidInputError::CleanInstallOnProvisionedHost, @@ -282,7 +282,7 @@ pub(crate) fn finalize_update( #[cfg(feature = "grpc-dangerous")] sender: &mut Option< mpsc::UnboundedSender>, >, -) -> Result<(), TridentError> { +) -> Result { info!("Finalizing update"); if servicing_type != ServicingType::AbUpdate { @@ -345,12 +345,12 @@ pub(crate) fn finalize_update( .internal_params .get_flag(NO_TRANSITION) { - engine::reboot() + Ok(ExitKind::NeedsReboot) } else { warn!( "Skipping reboot as requested by internal parameter '{}'", NO_TRANSITION ); - Ok(()) + Ok(ExitKind::Done) } } diff --git a/src/lib.rs b/src/lib.rs index 5e53813f8..d96e815da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ use engine::{rollback, storage::rebuild}; use harpoon_hc::HostConfigUpdate; pub use datastore::DataStore; -pub use engine::provisioning_network; +pub use engine::{provisioning_network, reboot}; pub use logging::{ background_log::BackgroundLog, logstream::Logstream, multilog::MultiLogger, tracestream::TraceStream, @@ -81,6 +81,14 @@ const SAFETY_OVERRIDE_CHECK_PATH: &str = "/override-trident-safety-check"; /// Temporary location of the datastore for multiboot install scenarios. const TEMPORARY_DATASTORE_PATH: &str = "/tmp/trident-datastore.sqlite"; +#[must_use] +pub enum ExitKind { + /// Requested operation completed successfully. + Done, + /// Reboot is needed to complete the operation. + NeedsReboot, +} + pub struct Trident { host_config: Option, orchestrator: Option, @@ -290,12 +298,14 @@ impl Trident { } => { info!("Server replied with new Host configuration v{version}, applying..."); self.host_config = Some(*host_config); - self.update( + if let ExitKind::NeedsReboot = self.update( datastore, Operations::all(), #[cfg(feature = "grpc-dangerous")] &mut None, - )?; + )? { + reboot().message("Failed to reboot after harpoon update")?; + } } HostConfigUpdate::NoUpdate => { warn!("No update available. No action will be taken."); @@ -313,20 +323,24 @@ impl Trident { if let Some((host_config, allowed_operations, sender)) = receiver.blocking_recv() { self.host_config = Some(host_config); - self.update(datastore, allowed_operations, &mut Some(sender))?; + if let ExitKind::NeedsReboot = + self.update(datastore, allowed_operations, &mut Some(sender))? + { + reboot().message("Failed to reboot after grpc update")?; + } } } Ok(()) } - fn execute_and_record_error( + fn execute_and_record_error( &mut self, datastore: &mut DataStore, f: F, - ) -> Result<(), TridentError> + ) -> Result where - F: FnOnce(&mut DataStore) -> Result<(), TridentError>, + F: FnOnce(&mut DataStore) -> Result, { datastore.with_host_status(|host_status| { if let Some(e) = host_status.last_error.take() { @@ -335,36 +349,40 @@ impl Trident { } })?; - if let Err(e) = f(datastore) { - // Record error in datastore. - let error = serde_yaml::to_value(&e).structured(InternalError::SerializeError)?; - if let Err(e2) = datastore.with_host_status(|status| status.last_error = Some(error)) { - error!("Failed to record error in datastore: {e2:?}"); - } + match f(datastore) { + Ok(t) => Ok(t), + Err(e) => { + // Record error in datastore. + let error = serde_yaml::to_value(&e).structured(InternalError::SerializeError)?; + if let Err(e2) = + datastore.with_host_status(|status| status.last_error = Some(error)) + { + error!("Failed to record error in datastore: {e2:?}"); + } - // Report error via phonehome. - if let Some(ref orchestrator) = self.orchestrator { - orchestrator.report_error( - format!("{e:?}"), - Some( - serde_yaml::to_string(&datastore.host_status()) - .unwrap_or("Failed to serialize Host Status".into()), - ), - ); - } + // Report error via phonehome. + if let Some(ref orchestrator) = self.orchestrator { + orchestrator.report_error( + format!("{e:?}"), + Some( + serde_yaml::to_string(&datastore.host_status()) + .unwrap_or("Failed to serialize Host Status".into()), + ), + ); + } - // Report error to Harpoon if enabled. - harpoon_hc::on_harpoon_enabled_event( - &datastore.host_status().spec, - harpoon::EventType::Install, - harpoon::EventResult::Error, - ); + // Report error to Harpoon if enabled. + harpoon_hc::on_harpoon_enabled_event( + &datastore.host_status().spec, + harpoon::EventType::Install, + harpoon::EventResult::Error, + ); - // TODO: report gPRC error + // TODO: report gPRC error - return Err(e); + Err(e) + } } - Ok(()) } /// Rebuilds RAID devices on replaced disks on the host @@ -432,7 +450,7 @@ impl Trident { allowed_operations: Operations, multiboot: bool, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, - ) -> Result<(), TridentError> { + ) -> Result { let host_config = self .host_config .clone() @@ -502,7 +520,7 @@ impl Trident { 'stage'. Add 'stage' and re-run to stage the clean install" ); - Ok(()) + Ok(ExitKind::Done) } } else { debug!("Host Configuration has not been updated"); @@ -528,7 +546,7 @@ impl Trident { to finalize the clean install" ); - Ok(()) + Ok(ExitKind::Done) } } ServicingState::NotProvisioned => { @@ -560,7 +578,7 @@ impl Trident { datastore: &mut DataStore, allowed_operations: Operations, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, - ) -> Result<(), TridentError> { + ) -> Result { let mut host_config = self .host_config .clone() @@ -596,7 +614,7 @@ impl Trident { engine::update(&host_config, datastore, &allowed_operations, #[cfg(feature = "grpc-dangerous")] sender).message("Failed to execute an update") } else { warn!("Host Configuration has been updated but allowed operations do not include 'stage'. Add 'stage' and re-run to stage the update"); - Ok(()) + Ok(ExitKind::Done) } } else { debug!("Host Configuration has not been updated"); @@ -616,7 +634,7 @@ impl Trident { .message("Failed to finalize update") } else { warn!("There is an update staged on the host, but allowed operations do not include 'finalize'. Add 'finalize' and re-run to finalize the update"); - Ok(()) + Ok(ExitKind::Done) } } ServicingState::AbUpdateFinalized | ServicingState::Provisioned => { diff --git a/src/main.rs b/src/main.rs index d7b9c4a04..8ea6f5aef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ use log::{error, info, LevelFilter}; use trident::{ cli::{self, Cli, Commands, GetKind}, - offline_init, validation, BackgroundLog, DataStore, Logstream, MultiLogger, TraceStream, - Trident, TRIDENT_BACKGROUND_LOG_PATH, + offline_init, validation, BackgroundLog, DataStore, ExitKind, Logstream, MultiLogger, + TraceStream, Trident, TRIDENT_BACKGROUND_LOG_PATH, }; use trident_api::{ config::HostConfigurationSource, @@ -39,29 +39,30 @@ fn run_trident( mut logstream: Logstream, mut tracestream: TraceStream, args: &Cli, -) -> Result<(), TridentError> { +) -> Result { // Log version ASAP info!("Trident version: {}", trident::TRIDENT_VERSION); // Catch exit fast commands match &args.command { Commands::Validate { config } => { - return validation::validate_host_config_file(config); + return validation::validate_host_config_file(config).map(|()| ExitKind::Done); } #[cfg(feature = "pytest-generator")] Commands::Pytest => { pytest::generate_functional_test_manifest(); - return Ok(()); + return Ok(ExitKind::Done); } Commands::OfflineInitialize { hs_path } => { - return offline_init::execute(hs_path.as_deref()); + return offline_init::execute(hs_path.as_deref()).map(|()| ExitKind::Done); } Commands::Get { kind, outfile } => { return Trident::get(&load_agent_config()?.datastore, outfile, *kind) - .message("Failed to retrieve Host Status"); + .message("Failed to retrieve Host Status") + .map(|()| ExitKind::Done); } Commands::StartNetwork { config } => { @@ -70,7 +71,8 @@ fn run_trident( logstream.disable(); tracestream.disable(); - return Trident::start_network(HostConfigurationSource::File(config.clone())); + return Trident::start_network(HostConfigurationSource::File(config.clone())) + .map(|()| ExitKind::Done); } _ => (), @@ -139,9 +141,15 @@ fn run_trident( #[cfg(feature = "grpc-dangerous")] &mut None, ), - Commands::Commit { .. } => trident.commit(&mut datastore), - Commands::Listen { .. } => trident.listen(&mut datastore), - Commands::RebuildRaid { .. } => trident.rebuild_raid(&mut datastore), + Commands::Commit { .. } => { + trident.commit(&mut datastore).map(|()| ExitKind::Done) + } + Commands::Listen { .. } => { + trident.listen(&mut datastore).map(|()| ExitKind::Done) + } + Commands::RebuildRaid { .. } => trident + .rebuild_raid(&mut datastore) + .map(|()| ExitKind::Done), _ => Err(TridentError::internal("Invalid command")), }; @@ -167,12 +175,10 @@ fn run_trident( } } - res.message(format!("Failed to execute '{}' command", args.command))?; + res.message(format!("Failed to execute '{}' command", args.command)) } _ => unreachable!(), } - - Ok(()) }); match res { @@ -259,10 +265,18 @@ fn main() -> ExitCode { } // Invoke Trident - if let Err(e) = run_trident(logstream.unwrap(), tracestream.unwrap(), &args) { - error!("Trident failed: {e:?}"); - return ExitCode::from(2); + match run_trident(logstream.unwrap(), tracestream.unwrap(), &args) { + Ok(ExitKind::Done) => {} + Err(e) => { + error!("Trident failed: {e:?}"); + return ExitCode::from(2); + } + Ok(ExitKind::NeedsReboot) => { + if let Err(e) = trident::reboot() { + error!("Failed to reboot: {e:?}"); + return ExitCode::from(3); + } + } } - ExitCode::SUCCESS } From 31b2145a97c42b080e6fde5c327b1e8c8d28732b Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 13 May 2025 18:04:42 +0000 Subject: [PATCH 09/99] Merged PR 23111: docs: Trident Install Flow Diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Medium-level diagram about how trident works, ![image (2).png](https://dev.azure.com/mariner-org/2311650c-e79e-4301-b4d2-96543fdd84ff/_apis/git/repositories/895b6b3d-5077-488a-8001-ab6b5a14c1a3/pullRequests/23111/attachments/image%20%282%29.png) ---- #### AI description (iteration 1) #### PR Classification This pull request adds a new visual install flow diagram along with supporting tooling to generate Trident architecture diagrams. #### PR Summary The changes introduce an SVG-based install flow diagram and implement a new diagram rendering module driven by YAML definitions, while extending the CLI and build configuration to support these features. - **`docs/resources/trident-install.svg`**: Added a new SVG file illustrating the Trident Install Flow. - **`docbuilder/src/trident_arch/`**: Introduced new files (`render.rs`, `nodes.rs`, `diagrams/install.yaml`, and `mod.rs`) to parse YAML and render architecture diagrams. - **`docbuilder/src/main.rs`**: Extended the CLI with a new `TridentArch` command and related options for generating diagrams. - **Build configuration**: Updated `Cargo.toml`, `Cargo.lock`, and `Makefile` to include dependencies (e.g., `svg`, `textwrap`) and targets for diagram generation. - **`docs/Explanation/Install-Flow.md`**: Added documentation referencing the new install flow diagram. Related work items: #12162 --- .vscode/settings.json | 4 +- Cargo.lock | 37 ++ Makefile | 6 +- docbuilder/Cargo.toml | 2 + docbuilder/src/main.rs | 52 +- .../src/trident_arch/diagrams/install.yaml | 135 +++++ docbuilder/src/trident_arch/mod.rs | 37 ++ docbuilder/src/trident_arch/nodes.rs | 75 +++ docbuilder/src/trident_arch/render.rs | 288 +++++++++++ docs/Explanation/Install-Flow.md | 3 + docs/resources/trident-install.svg | 469 ++++++++++++++++++ 11 files changed, 1103 insertions(+), 5 deletions(-) create mode 100644 docbuilder/src/trident_arch/diagrams/install.yaml create mode 100644 docbuilder/src/trident_arch/mod.rs create mode 100644 docbuilder/src/trident_arch/nodes.rs create mode 100644 docbuilder/src/trident_arch/render.rs create mode 100644 docs/Explanation/Install-Flow.md create mode 100644 docs/resources/trident-install.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a9f2a999..e44a7dd52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.formatOnSave": true, - "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.check.command": "clippy", "rust-analyzer.cargo.features": "all", "black-formatter.args": [], "[python]": { @@ -17,5 +17,5 @@ "editor.insertSpaces": true, "editor.tabSize": 2, "prettier.tabWidth": 2, - } + }, } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d6c4ab6c2..d77412eae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,7 +548,9 @@ dependencies = [ "serde_yaml", "setsail", "strum", + "svg", "tera", + "textwrap", "trident", "trident_api", ] @@ -2462,6 +2464,12 @@ dependencies = [ "syn", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.8" @@ -2531,6 +2539,12 @@ dependencies = [ "syn", ] +[[package]] +name = "svg" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3" + [[package]] name = "syn" version = "2.0.90" @@ -2674,6 +2688,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3093,12 +3118,24 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Makefile b/Makefile index 16bcbbeee..dd1f005fa 100644 --- a/Makefile +++ b/Makefile @@ -193,6 +193,7 @@ TRIDENT_API_HC_EXAMPLE_FILE := docs/Reference/Host-Configuration/Sample-Host-Con TRIDENT_API_HC_EXAMPLE_YAML := docs/Reference/Host-Configuration/sample-host-configuration.yaml TRIDENT_API_HC_STORAGE_RULES_FILES := docs/Reference/Host-Configuration/Storage-Rules.md TRIDENT_API_CLI_DOC := docs/Reference/Trident-CLI.md +TRIDENT_ARCH_INSTALL_SVG := docs/resources/trident-install.svg target/trident-api-docs: mkdir -p target/trident-api-docs @@ -224,6 +225,9 @@ build-api-docs: build-api-schema docbuilder $(DOCBUILDER_BIN) trident-cli -o $(TRIDENT_API_CLI_DOC) @echo Wrote CLI docs to $(TRIDENT_API_CLI_DOC) + $(DOCBUILDER_BIN) trident-arch install > $(TRIDENT_ARCH_INSTALL_SVG) + @echo Wrote install diagram to $(TRIDENT_ARCH_INSTALL_SVG) + # This target is meant to be used by CI to ensure that the API schema is up to date. @@ -299,7 +303,7 @@ generate-functional-test-manifest: .cargo/config .PHONY: validate-configs validate-configs: bin/trident - $(eval DETECTED_HC_FILES := $(shell grep -R 'storage:' . --include '*.yaml' --exclude-dir=trident-mos --exclude-dir=target --exclude-dir=dev --exclude-dir=azure-linux-image-tools -l)) + $(eval DETECTED_HC_FILES := $(shell grep -R 'storage:' . --include '*.yaml' --exclude-dir=trident-mos --exclude-dir=target --exclude-dir=dev --exclude-dir=azure-linux-image-tools --exclude-dir=docbuilder -l)) @for file in $(DETECTED_HC_FILES); do \ echo "Validating $$file"; \ $< validate $$file || exit 1; \ diff --git a/docbuilder/Cargo.toml b/docbuilder/Cargo.toml index a05165e83..5d811450b 100644 --- a/docbuilder/Cargo.toml +++ b/docbuilder/Cargo.toml @@ -20,7 +20,9 @@ serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9.34" strum = "0.26.3" +svg = "0.18.0" tera = { version = "1.20.0" } +textwrap = "0.16.2" setsail = { path = "../setsail" } diff --git a/docbuilder/src/main.rs b/docbuilder/src/main.rs index cb9c653e8..bdb5ed57a 100644 --- a/docbuilder/src/main.rs +++ b/docbuilder/src/main.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Error}; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use log::info; use crate::schema_renderer::SchemaDocSettings; @@ -10,6 +10,7 @@ mod host_config; mod markdown; mod schema_renderer; mod setsail; +mod trident_arch; mod trident_cli; #[derive(Parser, Debug)] @@ -28,6 +29,9 @@ enum Commands { /// Output documentation for Trident's CLI TridentCli(TridentCliOpts), + + /// Output a Trident arch diagram + TridentArch(TridentArchOpts), } #[derive(Args, Debug)] @@ -48,12 +52,31 @@ struct TridentCliOpts { output: Option, } -#[derive(Parser, Debug)] +#[derive(Args, Debug)] struct HostConfigCli { #[clap(subcommand)] command: HostConfigCommands, } +#[derive(Args, Debug)] +struct TridentArchOpts { + /// Optional output file + /// + /// If not specified, will print to stdout. + #[clap(short, long)] + output: Option, + + /// Arch diagram to output + selected: TridentArchSelection, +} + +#[derive(Debug, ValueEnum, Clone, Copy)] +#[clap(rename_all = "kebab-case")] +enum TridentArchSelection { + Install, + Update, +} + #[derive(Subcommand, Debug)] enum HostConfigCommands { /// Build markdown docs for Host Configuration @@ -145,6 +168,9 @@ fn main() -> Result<(), Error> { Commands::TridentCli(opts) => { build_tricent_cli_docs(opts).context("Failed to build CLI docs") } + Commands::TridentArch(opts) => { + build_trident_arch_diagram(opts).context("Failed to build arch diagram") + } } } @@ -208,3 +234,25 @@ fn build_tricent_cli_docs(opts: TridentCliOpts) -> Result<(), Error> { Ok(()) } + +fn build_trident_arch_diagram(opts: TridentArchOpts) -> Result<(), Error> { + info!("Building trident arch diagram"); + + let diagram = trident_arch::build_arch_diagram(opts.selected) + .context("Failed to build trident arch diagram")?; + + if let Some(output) = opts.output { + let parent = output.parent().context("Failed to get parent directory")?; + std::fs::create_dir_all(parent).context(format!( + "Failed to create parent directory {}", + parent.display() + ))?; + + std::fs::write(&output, diagram) + .context(format!("Failed to write to file {}", output.display()))?; + } else { + println!("{}", diagram); + } + + Ok(()) +} diff --git a/docbuilder/src/trident_arch/diagrams/install.yaml b/docbuilder/src/trident_arch/diagrams/install.yaml new file mode 100644 index 000000000..43ca0c7ee --- /dev/null +++ b/docbuilder/src/trident_arch/diagrams/install.yaml @@ -0,0 +1,135 @@ +legends: + verb: + friendly: Trident Invocation Verb + background: "#b03e00" + border: "#5a1f00" + operation: + friendly: Operation + background: "#023c57" + border: "#042433" + subsystem: + friendly: Subsystem + background: "#4EA72E" + border: "#1C440D" + step: + friendly: Step + background: "#A02B93" + border: "#410C3B" + hook: + friendly: Script Hook + background: "#FF5757" + border: "#C00000" + system: + friendly: System Action + background: "#9c2403" + border: "#4d1101" + storage: + friendly: Storage Configuration + background: "#01523c" + border: "#00241a" + mount: + friendly: Mount + background: "#324a02" + border: "#1b2901" +root: + - name: install + legend: verb + children: + - name: Static Validation + comment: "Context-free validation of Host Config" + - name: Safety Check + - name: Stage + legend: operation + children: + - name: Load COSI + - name: "Pre-servicing Script Hook" + legend: hook + - name: "Dynamic Validation" + legend: step + - name: Prepare + legend: step + children: + - name: MOS Config + legend: subsystem + comment: "Changes to current OS" + - name: Hooks + legend: subsystem + comment: Stage addtl. files & scripts + - name: Block Device Creation + legend: storage + children: + - name: Close pre-existing devices + children: + - name: Close verity + - name: Close Encrypted Volumes + - name: Close RAID + - name: Create partitions + - name: Create RAID + - name: Create Encrypted Volumes + - name: Block Device Initialization + legend: storage + children: + - name: Deploy Images + - name: Create Filesystems + - name: Create swap + - name: Open dm-verity devices + - name: New OS Mount + legend: mount + children: + - name: Provision + legend: step + children: + - name: Boot + legend: subsystem + comment: ESP Deployment + - name: Hooks + legend: subsystem + children: + - name: "Post-provision Script Hook" + legend: hook + - name: | + Configure + (chroot) + legend: step + children: + - name: Storage + legend: subsystem + comment: "Regenerate fstab, crypttab, mdadm.conf" + - name: Boot + legend: subsystem + comment: "Update grub config when in use" + - name: Network + legend: subsystem + comment: "Write network config" + - name: OS Config + legend: subsystem + comment: Enact OS config changes + - name: Hooks + legend: subsystem + children: + - name: Post-configure Script Hook + legend: hook + - name: Initrd + legend: subsystem + comment: "Regenerate initrd when not UKI" + - name: SELinux + legend: subsystem + comment: "Relabel filesystems when enabled" + - name: "Finalize" + legend: operation + children: + - name: "Boot Entry Configuration" + children: + - name: Insert/Update Boot Entry + comment: "Label depends on active volume" + - name: "Set Boot Order" + comment: "New entry is first" + - name: "Set NextBoot" + - name: Trigger Reboot + - name: "" + legend: system + - name: "commit" + legend: verb + children: + - name: "Success/Rollback Detection" + comment: "Update is finalized on success" diff --git a/docbuilder/src/trident_arch/mod.rs b/docbuilder/src/trident_arch/mod.rs new file mode 100644 index 000000000..f36a6ca29 --- /dev/null +++ b/docbuilder/src/trident_arch/mod.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +use anyhow::{Context, Error, Ok}; + +use crate::TridentArchSelection; + +mod nodes; +mod render; + +use nodes::Diagram; + +fn get_diagram_base(selected: TridentArchSelection) -> Result { + let file = match selected { + TridentArchSelection::Install => "install.yaml", + TridentArchSelection::Update => "update.yaml", + }; + + let full_path = PathBuf::from(file!()) + .parent() + .context("Failed to get parent directory")? + .join("diagrams") + .join(file); + + let yaml = std::fs::read_to_string(&full_path) + .with_context(|| format!("Failed to read diagram file: {:?}", full_path))?; + + serde_yaml::from_str(&yaml) + .with_context(|| format!("Failed to parse YAML for diagram '{:?}'", selected)) +} + +pub(super) fn build_arch_diagram(selected: TridentArchSelection) -> Result { + let diag = get_diagram_base(selected).context("Failed to get diagram base")?; + + let svg = render::render(diag).context("Failed to render diagram")?; + + Ok(svg.to_string()) +} diff --git a/docbuilder/src/trident_arch/nodes.rs b/docbuilder/src/trident_arch/nodes.rs new file mode 100644 index 000000000..4be450560 --- /dev/null +++ b/docbuilder/src/trident_arch/nodes.rs @@ -0,0 +1,75 @@ +use serde::{de::Visitor, Deserialize, Deserializer}; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(super) struct Diagram { + #[serde(default, deserialize_with = "deserialize_legends")] + pub legends: Vec, + + pub root: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(super) struct Legend { + #[serde(skip)] + pub id: String, + + #[serde(default)] + pub friendly: Option, + + #[serde(default)] + pub background: Option, + + #[serde(default)] + pub border: Option, + + #[serde(default)] + pub text: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(super) struct DiagramNode { + pub name: String, + + #[serde(default)] + pub children: Vec, + + #[serde(default)] + pub comment: Option, + + #[serde(default)] + pub legend: Option, +} + +fn deserialize_legends<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct LegendMapVisitor; + impl<'de> Visitor<'de> for LegendMapVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map of legends") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut legends = Vec::new(); + while let Some((key, value)) = map.next_entry::()? { + let mut legend = value; + legend.id = key; + legends.push(legend); + } + Ok(legends) + } + } + + deserializer + .deserialize_map(LegendMapVisitor) + .map_err(serde::de::Error::custom) +} diff --git a/docbuilder/src/trident_arch/render.rs b/docbuilder/src/trident_arch/render.rs new file mode 100644 index 000000000..900817964 --- /dev/null +++ b/docbuilder/src/trident_arch/render.rs @@ -0,0 +1,288 @@ +use std::{cmp::max, collections::HashMap}; + +use anyhow::{bail, Context, Error}; +use svg::{ + node::element::{Group, Path, Rectangle, Style, TSpan, Text}, + Document, Node, +}; +use textwrap::Options; + +use super::{ + nodes::{DiagramNode, Legend}, + Diagram, +}; + +const MARGIN: u32 = 10; +const AXIS_WIDTH: u32 = 30; +const DEFAULT_FILL: &str = "#008492"; +const DEFAULT_STROKE: &str = "#003F46"; +const DEFAULT_TEXT: &str = "#FFFFFF"; + +struct NodeGroup { + group: Group, + height: u32, + width: u32, +} + +type Legends<'a> = HashMap<&'a str, &'a Legend>; + +pub(super) fn render(diagram: Diagram) -> Result { + let legends = generate_legends(&diagram); + let axis_legend = time_axis().translated(legends.width as i32, 0); + + let annotations = Group::new() + .add(axis_legend) + .add(legends.group.translated(MARGIN as i32, MARGIN as i32)); + + let legend_map = diagram + .legends + .iter() + .map(|legend| (legend.id.as_str(), legend)) + .collect::(); + + let root_node = render_children(&legend_map, diagram.root.iter())? + .translated((MARGIN + legends.width + AXIS_WIDTH) as i32, MARGIN as i32); + + let width = MARGIN + legends.width + AXIS_WIDTH + root_node.width + MARGIN; + let height = root_node.height + 2 * MARGIN; + + Ok(Document::new() + .set("width", width) + .set("height", height) + .set("font-family", "Aptos,Aptos_MSFontService,sans-serif") + .set("viewBox", (0, 0, width, height)) + .add(Style::new(".caption { fill: white; }")) + .add( + Rectangle::new() + .set("x", 0) + .set("y", 0) + .set("width", "100%") + .set("height", "100%") + .set("fill", "white") + .set("stroke", "#000000") + .set("stroke-width", 2), + ) + .add(annotations) + .add(root_node.group)) +} + +fn render_children<'a>( + legends: &Legends, + children: impl Iterator, +) -> Result { + let mut children_group = Group::new(); + let mut y_pos: u32 = 0; + let mut child_width: u32 = 0; + for child in children { + if has_children(&children_group) { + y_pos += MARGIN; // Add some space between nodes + } + + let child_node = render_node(legends, child)?.translated(0, y_pos as i32); + child_width = max(child_width, child_node.width); + + y_pos += child_node.height; + children_group.append(child_node.group); + } + + Ok(NodeGroup { + group: children_group, + height: y_pos, + width: child_width, + }) +} + +fn render_node(legends: &Legends, node: &DiagramNode) -> Result { + if node.comment.is_some() && !node.children.is_empty() { + bail!( + "Only leaf nodes can have comments, but {} has both", + node.name + ); + } + + let box_width = 100; + + let children = + render_children(legends, node.children.iter())?.translated((box_width + MARGIN) as i32, 0); + + let wrapped_name = textwrap::wrap(&node.name, Options::new(20)); + let text_height = wrapped_name.len() as u32 * 10; // Approximate height of text + + let height = max(children.height, text_height + 10); + + let mut self_box = Rectangle::new() + .set("x", 0) + .set("y", 0) + .set("width", box_width) + .set("height", height); + + let mut self_text = Text::new("") + .set("x", box_width / 2) + .set("y", height / 2 - (wrapped_name.len() - 1) as u32 * 5) + .set("text-anchor", "middle") + .set("dominant-baseline", "middle") + .set("class", "caption"); + + apply_legend(legends, node, &mut self_box, &mut self_text) + .with_context(|| format!("Failed to apply legend for node {}", node.name))?; + + for (i, line) in wrapped_name.iter().enumerate() { + let mut span = TSpan::new(line.to_string()) + .set("x", box_width / 2) + .set("text-anchor", "middle") + .set("dominant-baseline", "middle") + .set("font-size", "10px"); + if i > 0 { + span.assign("dy", "12px"); + } + self_text = self_text.add(span); + } + + let mut width = box_width; + + let mut self_group = Group::new().add(self_box).add(self_text); + + // If we have children, add a margin and translate the group + // to the right of the box with. + if has_children(&children.group) { + width += MARGIN + children.width; + self_group.append(children.group); + } + + if let Some(comment) = &node.comment { + width += MARGIN; + let comment_text = Text::new(comment.clone()) + .set("x", width) + .set("y", height / 2) + .set("text-anchor", "start") + .set("dominant-baseline", "middle") + .set("font-size", "7px"); + self_group = self_group.add(comment_text); + width += comment.len() as u32 * 10 / 3; // Approximate width of comment + } + + Ok(NodeGroup { + group: self_group, + height, + width, + }) +} + +/// Trait to add transformations to SVG elements. +trait Transform { + fn translated(self, x: i32, y: i32) -> Self; +} + +impl Transform for Group { + fn translated(self, x: i32, y: i32) -> Self { + self.set("transform", format!("translate({}, {})", x, y)) + } +} + +impl Transform for NodeGroup { + fn translated(self, x: i32, y: i32) -> Self { + NodeGroup { + group: self.group.translated(x, y), + height: self.height, + width: self.width, + } + } +} + +fn generate_legends(diagram: &Diagram) -> NodeGroup { + let mut legend_width = 0; + let mut legend_y = 0; + let mut legend_group = Group::new(); + let square_size = 10; + let text_start = square_size + 2; + for legend in &diagram.legends { + let rect = Rectangle::new() + .set("x", 0) + .set("y", 0) + .set("width", square_size) + .set("height", square_size) + .set("stroke-width", 2) + .set("fill", legend.background.as_deref().unwrap_or(DEFAULT_FILL)) + .set("stroke", legend.border.as_deref().unwrap_or(DEFAULT_STROKE)); + + let display_text = legend.friendly.as_ref().unwrap_or(&legend.id); + + let text = Text::new(display_text) + .set("x", text_start) + .set("y", square_size / 2) + .set("text-anchor", "start") + .set("dominant-baseline", "middle") + .set("font-size", "10px"); + + legend_group.append( + Group::new() + .add(rect) + .add(text) + .translated(0, legend_y as i32), + ); + + legend_y += square_size + MARGIN; + legend_width = max( + legend_width, + text_start + display_text.len() as u32 * 12 / 3 + text_start + MARGIN, + ); + } + + NodeGroup { + group: legend_group, + height: legend_y, + width: legend_width, + } +} + +fn time_axis() -> Group { + Group::new() + .add( + Path::new().set("d", "M1641.94 1436.5 1641.94 1838.22 1635.06 1838.22 1635.06 1436.5ZM1652.25 1833.63 1638.5 1861.13 1624.75 1833.63Z") + .set("fill", "#7F7F7F") + .set("transform", "translate(-145,-135) scale(0.1)"), + ) + .add( + Text::new("Time") + .set("fill", "#7F7F7F") + .set("font-size", "10px") + .set("text-anchor", "start") + .set("transform", "translate(15,30) rotate(-90)") + ) +} + +fn apply_legend( + legends: &Legends, + node: &DiagramNode, + rect: &mut Rectangle, + text: &mut Text, +) -> Result<(), Error> { + let legend = node + .legend + .as_ref() + .map(|id| { + legends.get(id.as_str()).with_context(|| { + format!("Legend with ID '{}' not found for node {}", id, node.name) + }) + }) + .transpose()?; + + if let Some(legend) = legend { + rect.assign("fill", legend.background.as_deref().unwrap_or(DEFAULT_FILL)); + rect.assign("stroke", legend.border.as_deref().unwrap_or(DEFAULT_STROKE)); + text.assign("fill", legend.text.as_deref().unwrap_or(DEFAULT_TEXT)); + } else { + rect.assign("fill", DEFAULT_FILL); + rect.assign("stroke", DEFAULT_STROKE); + text.assign("fill", DEFAULT_TEXT); + } + + Ok(()) +} + +fn has_children(node: &impl Node) -> bool { + match node.get_children() { + Some(children) => !children.is_empty(), + None => false, + } +} diff --git a/docs/Explanation/Install-Flow.md b/docs/Explanation/Install-Flow.md new file mode 100644 index 000000000..fb8cf00b5 --- /dev/null +++ b/docs/Explanation/Install-Flow.md @@ -0,0 +1,3 @@ +# Install Flow + +![Install Flow](../resources/trident-install.svg) diff --git a/docs/resources/trident-install.svg b/docs/resources/trident-install.svg new file mode 100644 index 000000000..847e89b2e --- /dev/null +++ b/docs/resources/trident-install.svg @@ -0,0 +1,469 @@ + + + + + + + +Time + + + + + + +Trident Invocation Verb + + + + + +Operation + + + + + +Subsystem + + + + + +Step + + + + + +Script Hook + + + + + +System Action + + + + + +Storage Configuration + + + + + +Mount + + + + + + + + + +install + + + + + + +Static Validation + + +Context-free validation of Host Config + + + + + + +Safety Check + + + + + + +Stage + + + + + + +Load COSI + + + + + + +Pre-servicing Script +Hook + + + + + + +Dynamic Validation + + + + + + +Prepare + + + + + + +MOS Config + + +Changes to current OS + + + + + + +Hooks + + +Stage addtl. files & scripts + + + + + + + + +Block Device +Creation + + + + + + +Close pre-existing +devices + + + + + + +Close verity + + + + + + +Close Encrypted +Volumes + + + + + + +Close RAID + + + + + + + + +Create partitions + + + + + + +Create RAID + + + + + + +Create Encrypted +Volumes + + + + + + + + +Block Device +Initialization + + + + + + +Deploy Images + + + + + + +Create Filesystems + + + + + + +Create swap + + + + + + +Open dm-verity +devices + + + + + + + + +New OS Mount + + + + + + +Provision + + + + + + +Boot + + +ESP Deployment + + + + + + +Hooks + + + + + + +Post-provision +Script Hook + + + + + + + + + + +Configure +(chroot) + + + + + + + +Storage + + +Regenerate fstab, crypttab, mdadm.conf + + + + + + +Boot + + +Update grub config when in use + + + + + + +Network + + +Write network config + + + + + + +OS Config + + +Enact OS config changes + + + + + + +Hooks + + + + + + +Post-configure +Script Hook + + + + + + + + +Initrd + + +Regenerate initrd when not UKI + + + + + + +SELinux + + +Relabel filesystems when enabled + + + + + + + + + + + + +Finalize + + + + + + +Boot Entry +Configuration + + + + + + +Insert/Update Boot +Entry + + +Label depends on active volume + + + + + + +Set Boot Order + + +New entry is first + + + + + + +Set NextBoot + + + + + + + + +Trigger Reboot + + + + + + + + + + +<System Reboot> + + + + + + +commit + + + + + + +Success/Rollback +Detection + + +Update is finalized on success + + + + + + \ No newline at end of file From 5ca9ceca085584cde8928ce626d304269df40bea Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 13 May 2025 22:47:09 +0000 Subject: [PATCH 10/99] Merged PR 23118: engineering: try to fix dev-azl based builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description trident-cicd-for-azl-preview is broken in non-installer image builds. seems that the azl version is not being passed along to download base image or download rpms. ---- #### AI description (iteration 1) #### PR Classification Bug fix for build configuration in dev-azl based builds. #### PR Summary This pull request fixes the build pipeline configuration by updating the image version handling in the runtime build stage. - `/.pipelines/templates/stages/build_image/build-runtime.yml`: Changes `baseimgVersion` to use a variable substitution (`$(baseimgVersion)`) and adds `baseimgAzureLinuxVersion` to retain the original Azure Linux version reference. Related work items: #12138 --- .pipelines/templates/stages/build_image/build-runtime.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pipelines/templates/stages/build_image/build-runtime.yml b/.pipelines/templates/stages/build_image/build-runtime.yml index 85e408862..d742ded27 100644 --- a/.pipelines/templates/stages/build_image/build-runtime.yml +++ b/.pipelines/templates/stages/build_image/build-runtime.yml @@ -56,6 +56,7 @@ stages: variables: ob_outputDirectory: $(Pipeline.Workspace)/s/output ob_artifactBaseName: ${{ parameters.imageName }} + BASEIMG_AZURE_LINUX_VERSION: "3.0" steps: - task: PipAuthenticate@1 @@ -81,6 +82,7 @@ stages: imageName: ${{ parameters.imageName }} clones: 2 baseimgBuildType: ${{ parameters.baseimgBuildType }} - baseimgVersion: ${{ variables.BASEIMG_AZURE_LINUX_VERSION }} + baseimgVersion: $(baseimgVersion) + azureLinuxVersion: ${{ variables.BASEIMG_AZURE_LINUX_VERSION }} micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} From 4d5fa601b0433c3dd04d17f65e784be66bc937a3 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 14 May 2025 18:26:41 +0000 Subject: [PATCH 11/99] Merged PR 23086: engineering: e2e usr-verity tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Add simple e2e usr verity tests. Will add AB and other features in smaller follow ups. Depends on !22700 ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new feature by adding configurations for e2e usr-verity tests. #### PR Summary The changes add dedicated configuration files and update pipeline targets to support usr-verity testing during end-to-end runs. - `e2e_tests/trident_configurations/usr-verity/trident-config.yaml`: Added a new configuration file specifying storage, filesystem, and OS parameters for usr-verity. - `/.pipelines/trident-pr-e2e.yml`: Updated the branch reference for test images to target a usr-verity specific branch. - `e2e_tests/target-configurations.yaml`: Extended multiple test groups by appending the `usr-verity` target. - `e2e_tests/trident_configurations/usr-verity/test-selection.yaml`: Added a new file listing compatible test bases for usr-verity. Related work items: #12114 --- .pipelines/templates/e2e-template.yml | 18 ++++++ .../stages/common_tasks/build-osmodifier.yml | 14 +++++ .../templates/stages/common_tasks/os-info.yml | 21 +++++++ .../testing_baremetal/baremetal-testing.yml | 7 +++ .../testing_common/download-test-images.yml | 21 +++++++ .../stages/testing_vm/netlaunch-testing.yml | 3 + .../stages/trident_rpms/build-source.yml | 39 +++--------- .../stages/trident_rpms/release2.yml | 60 ------------------- Makefile | 2 +- azure-linux-image-tools | 2 +- e2e_tests/pytest.ini | 1 + e2e_tests/target-configurations.yaml | 16 +++++ .../usr-verity/test-selection.yaml | 3 + .../usr-verity/trident-config.yaml | 60 +++++++++++++++++++ .../suites/trident/helpers/prepare_images.go | 43 +++++++++---- 15 files changed, 205 insertions(+), 105 deletions(-) create mode 100644 .pipelines/templates/stages/common_tasks/os-info.yml delete mode 100644 .pipelines/templates/stages/trident_rpms/release2.yml create mode 100644 e2e_tests/trident_configurations/usr-verity/test-selection.yaml create mode 100644 e2e_tests/trident_configurations/usr-verity/trident-config.yaml diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index 1f7653f7e..8b3b9f84a 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -156,6 +156,24 @@ stages: micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} + # Build Trident test image for usr-verity (host) + - template: stages/build_image/build-runtime.yml + parameters: + imageName: trident-usrverity-testimage + baseimgBuildType: ${{ parameters.baseimgBuildType }} + baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} + micBuildType: ${{ parameters.micBuildType }} + micVersion: ${{ parameters.micVersion }} + + # Build Trident test image for usr-verity (host) + - template: stages/build_image/build-runtime.yml + parameters: + imageName: trident-container-usrverity-testimage + baseimgBuildType: ${{ parameters.baseimgBuildType }} + baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} + micBuildType: ${{ parameters.micBuildType }} + micVersion: ${{ parameters.micVersion }} + - ${{ if or(parameters.forceTridentRebuild, eq(parameters.stageType, 'pr-e2e'), eq(parameters.stageType, 'ci'), eq(parameters.stageType, 'pr-e2e-azure')) }}: # Build USB-ISO - template: stages/trident_usb_iso/trident-usb-iso.yml diff --git a/.pipelines/templates/stages/common_tasks/build-osmodifier.yml b/.pipelines/templates/stages/common_tasks/build-osmodifier.yml index 8a087ee7a..51802c8c0 100644 --- a/.pipelines/templates/stages/common_tasks/build-osmodifier.yml +++ b/.pipelines/templates/stages/common_tasks/build-osmodifier.yml @@ -1,4 +1,18 @@ steps: + - bash: | + set -ex + if command -v tdnf; then + # Use msft-golang since it has latest versions supported + sudo tdnf remove golang -y + sudo tdnf install msft-golang -y + else + echo "Go Update not needed in ubuntu 22.04" + fi + + # Verify installation + go version + displayName: "Install golang to build Image Customizer" + retryCountOnTaskFailure: 3 - bash: | set -ex git submodule init diff --git a/.pipelines/templates/stages/common_tasks/os-info.yml b/.pipelines/templates/stages/common_tasks/os-info.yml new file mode 100644 index 000000000..ff97d8521 --- /dev/null +++ b/.pipelines/templates/stages/common_tasks/os-info.yml @@ -0,0 +1,21 @@ +steps: + - script: | + echo "OS RELEASE:" + cat /etc/os-release + echo "KERNEL RELEASE:" + uname -r + + echo "" + echo "Installed packages:" + echo "=====================" + if command -v rpm; then + echo "Installed RPMs:" + rpm -qa + fi + + if command -v dpkg; then + echo "Installed DEBs:" + dpkg -l + fi + + displayName: os-release diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index 903916dc2..288024730 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -122,6 +122,12 @@ stages: ${{ else }}: value: "trident-verity-testimage" + - name: USRVERITY_IMAGE_NAME + ${{ if eq(parameters.runtimeEnv, 'container') }}: + value: "trident-container-usrverity-testimage" + ${{ else }}: + value: "trident-usrverity-testimage" + - group: baremetal_controller steps: @@ -165,6 +171,7 @@ stages: installerISO: ${{ variables.INSTALLER_ISO_NAME }} tridentTestImage: ${{ variables.IMAGE_NAME }} tridentTestImageVerity: ${{ variables.VERITY_IMAGE_NAME }} + tridentTestImageUsrVerity: ${{ variables.USRVERITY_IMAGE_NAME }} ${{ if eq(parameters.runtimeEnv, 'container') }}: downloadTridentContainer: true diff --git a/.pipelines/templates/stages/testing_common/download-test-images.yml b/.pipelines/templates/stages/testing_common/download-test-images.yml index 7fd76ac02..dcf8b5e40 100644 --- a/.pipelines/templates/stages/testing_common/download-test-images.yml +++ b/.pipelines/templates/stages/testing_common/download-test-images.yml @@ -26,6 +26,14 @@ parameters: - trident-verity-testimage - trident-container-verity-testimage + - name: tridentTestImageUsrVerity + displayName: "Image used the verity runtime OS, source of Trident (container vs host)" + type: string + default: trident-usrverity-testimage + values: + - trident-usrverity-testimage + - trident-container-usrverity-testimage + - name: downloadTridentContainer displayName: "Download Trident container" type: boolean @@ -51,6 +59,10 @@ parameters: type: string default: "$(System.ArtifactsDirectory)/verity-testimage" + - name: testImageDirUsrVerity + type: string + default: "$(System.ArtifactsDirectory)/usrverity-testimage" + steps: - bash: | set -eux @@ -100,6 +112,13 @@ steps: artifactName: "${{ parameters.tridentTestImageVerity }}" targetPath: "${{ parameters.testImageDirVerity }}" + - task: DownloadPipelineArtifact@2 + displayName: "Download ${{ parameters.tridentTestImageUsrVerity }}" + inputs: + buildType: current + artifactName: "${{ parameters.tridentTestImageUsrVerity }}" + targetPath: "${{ parameters.testImageDirUsrVerity }}" + - task: DownloadPipelineArtifact@2 displayName: "Download go-tools" inputs: @@ -133,8 +152,10 @@ steps: helper prepare-images -a -- \ "${{ parameters.testImageDir }}" \ "${{ parameters.testImageDirVerity }}" \ + "${{ parameters.testImageDirUsrVerity }}" \ "${{ parameters.tridentTestImage }}" \ "${{ parameters.tridentTestImageVerity }}" \ + "${{ parameters.tridentTestImageUsrVerity }}" \ "${{ parameters.targetDirectory }}" \ -v 4 diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index f95287084..24dada50a 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -70,11 +70,13 @@ stages: installerISOName: trident-container-installer-testimage testImageName: trident-container-testimage verityTestImageName: trident-container-verity-testimage + usrVerityTestImageName: trident-container-usrverity-testimage downloadTridentContainer: true ${{ else }}: installerISOName: trident-installer-testimage testImageName: trident-testimage verityTestImageName: trident-verity-testimage + usrVerityTestImageName: trident-usrverity-testimage downloadTridentContainer: false ob_outputDirectory: $(tridentSourceDirectory)/deployment_logs @@ -101,6 +103,7 @@ stages: tridentTestImage: ${{ variables.testImageName }} tridentTestImageVerity: ${{ variables.verityTestImageName }} downloadTridentContainer: ${{ variables.downloadTridentContainer }} + tridentTestImageUsrVerity: ${{ variables.usrVerityTestImageName }} - template: netlaunch-prep.yml diff --git a/.pipelines/templates/stages/trident_rpms/build-source.yml b/.pipelines/templates/stages/trident_rpms/build-source.yml index 8701e0985..cb30138fe 100644 --- a/.pipelines/templates/stages/trident_rpms/build-source.yml +++ b/.pipelines/templates/stages/trident_rpms/build-source.yml @@ -41,7 +41,7 @@ stages: - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + artifactFeeds: "mariner/Mariner-Pypi-Feed" - template: check.yml @@ -58,7 +58,7 @@ stages: - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + artifactFeeds: "mariner/Mariner-Pypi-Feed" # need newer rust for cargo-nextest, use rustup.yml to install - template: ../common_tasks/rustup.yml @@ -75,11 +75,11 @@ stages: ob_outputDirectory: "$(Build.SourcesDirectory)/trident/out" steps: + - template: ../common_tasks/os-info.yml - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' - + artifactFeeds: "mariner/Mariner-Pypi-Feed" - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/coverage.yml parameters: @@ -100,11 +100,11 @@ stages: buildType: auto steps: + - template: ../common_tasks/os-info.yml - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' - + artifactFeeds: "mariner/Mariner-Pypi-Feed" - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/build-osmodifier.yml - template: release.yml @@ -133,7 +133,7 @@ stages: - task: PipAuthenticate@1 displayName: Provision - Authenticate Pip inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + artifactFeeds: "mariner/Mariner-Pypi-Feed" - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/build-osmodifier.yml @@ -142,28 +142,3 @@ stages: baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImageArm64PipelineBuildId }} previewContainerPipeline: "[ARM64-6-OneBranch]-Prod-BuildImages" - - - job: BuildTrident2 - displayName: Build Trident 2.0 RPMs - pool: - type: linux - - variables: - - name: ob_artifactBaseName - value: ${{ parameters.tridentArtifactName }}2 - - name: ob_outputDirectory - value: "$(Build.SourcesDirectory)/out" - - template: common/setup-registries-vars-template.yaml@platform-pipelines - parameters: - buildType: auto - - steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' - - - template: ../common_tasks/rustup.yml - - template: ../common_tasks/cargo-auth.yml - - template: ../common_tasks/build-osmodifier.yml - - template: release2.yml diff --git a/.pipelines/templates/stages/trident_rpms/release2.yml b/.pipelines/templates/stages/trident_rpms/release2.yml deleted file mode 100644 index 0731cb57a..000000000 --- a/.pipelines/templates/stages/trident_rpms/release2.yml +++ /dev/null @@ -1,60 +0,0 @@ -parameters: - - name: minimumSystemdVersion - type: string - default: '254' - -steps: - - script: | - set -eux - TRIDENT_VERSION=$(python3 ./scripts/get-version.py "$(Build.BuildNumber)" --commit) - echo "##vso[task.setvariable variable=trident_version]$TRIDENT_VERSION" - displayName: "Setting Trident version" - - - task: onebranch.pipeline.version@1 - displayName: "Set build number" - inputs: - system: "Custom" - customVersion: $(trident_version) - - - task: CopyFiles@2 - inputs: - sourceFolder: "./artifacts" - targetFolder: "/usr/src/mariner/SOURCES" - contents: "*" - displayName: Copy EMU to SOURCES - - - task: CopyFiles@2 - inputs: - sourceFolder: "./" - targetFolder: "/usr/src/mariner/SOURCES" - contents: "trident-selinuxpolicies.cil" - displayName: Copy Trident selinux policy to SOURCES - - - script: sudo tdnf install -y protobuf protobuf-c openssl-devel clang-devel rust p7zip p7zip-plugins zstd moby-buildx - displayName: Install native dependencies - retryCountOnTaskFailure: 3 - - - script: | - set -eux - full_version=$(trident_version) - - # Separate into version and prerelease identifier - # for the RPM build. - version=$(echo $full_version | cut -d'-' -f1) - prerelease=$(echo $full_version | cut -d'-' -f2-) - - # Edit the trident.spec file to not require 3.0 dependencies - sed -i 's/systemd >= 255/systemd >= ${{ parameters.minimumSystemdVersion }}/g' trident.spec - sed -i 's/Requires:[[:blank:]]*systemd-udev//g' trident.spec - - rpmbuild -bb --build-in-place trident.spec \ - --define="trident_version $full_version" \ - --define="rpm_ver $version" \ - --define="rpm_rel $prerelease" - displayName: Build 2.0 RPMs - - - task: CopyFiles@2 - inputs: - sourceFolder: "/usr/src/mariner/RPMS/x86_64" - targetFolder: "$(ob_outputDirectory)" - displayName: Copy RPM file to output diff --git a/Makefile b/Makefile index dd1f005fa..e1b6f1d5a 100644 --- a/Makefile +++ b/Makefile @@ -306,7 +306,7 @@ validate-configs: bin/trident $(eval DETECTED_HC_FILES := $(shell grep -R 'storage:' . --include '*.yaml' --exclude-dir=trident-mos --exclude-dir=target --exclude-dir=dev --exclude-dir=azure-linux-image-tools --exclude-dir=docbuilder -l)) @for file in $(DETECTED_HC_FILES); do \ echo "Validating $$file"; \ - $< validate $$file || exit 1; \ + $< validate $$file -v info || exit 1; \ done .PHONY: generate-mermaid-diagrams diff --git a/azure-linux-image-tools b/azure-linux-image-tools index c60e9422d..77cd059eb 160000 --- a/azure-linux-image-tools +++ b/azure-linux-image-tools @@ -1 +1 @@ -Subproject commit c60e9422d312c0a8af1844d3e954dcf4858da644 +Subproject commit 77cd059eb0ce7e665206613e9c50c2b87db22564 diff --git a/e2e_tests/pytest.ini b/e2e_tests/pytest.ini index 41f0d1b9c..29ba7bea1 100644 --- a/e2e_tests/pytest.ini +++ b/e2e_tests/pytest.ini @@ -3,6 +3,7 @@ junit_suite_name = trident_e2e_tests markers = base: Tests designed to verify the fundamental operations of Trident. verity: Tests designed to verify the verity feature ops in Trident. + usr_verity: Tests designed to verify the usr verity feature ops in Trident. 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. diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 14a21e8bc..ab2ba1ee0 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -5,6 +5,7 @@ bareMetal: - combined - encrypted-partition - split + - usr-verity validation: - base - combined @@ -19,6 +20,7 @@ bareMetal: - rerun - simple - split + - usr-verity - verity - verity-raid weekly: @@ -35,6 +37,7 @@ bareMetal: - rerun - simple - split + - usr-verity - verity - verity-raid container: @@ -50,6 +53,7 @@ bareMetal: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid validation: @@ -64,6 +68,7 @@ bareMetal: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid weekly: @@ -78,6 +83,7 @@ bareMetal: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid virtualMachine: @@ -96,6 +102,7 @@ virtualMachine: - rerun - simple - split + - usr-verity - verity - verity-raid post_merge: @@ -112,6 +119,7 @@ virtualMachine: - rerun - simple - split + - usr-verity - verity - verity-raid pullrequest: @@ -122,6 +130,7 @@ virtualMachine: - rerun - simple - split + - usr-verity - verity-raid validation: - base @@ -137,6 +146,7 @@ virtualMachine: - rerun - simple - split + - usr-verity - verity - verity-raid weekly: @@ -153,6 +163,7 @@ virtualMachine: - rerun - simple - split + - usr-verity - verity - verity-raid container: @@ -168,6 +179,7 @@ virtualMachine: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid post_merge: @@ -182,6 +194,7 @@ virtualMachine: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid pullrequest: @@ -193,6 +206,7 @@ virtualMachine: - raid-mirrored - raid-resync-small - rerun + - usr-verity - simple - verity-raid validation: @@ -207,6 +221,7 @@ virtualMachine: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid weekly: @@ -221,5 +236,6 @@ virtualMachine: - raid-resync-small - rerun - simple + - usr-verity - verity - verity-raid diff --git a/e2e_tests/trident_configurations/usr-verity/test-selection.yaml b/e2e_tests/trident_configurations/usr-verity/test-selection.yaml new file mode 100644 index 000000000..9cdb5a029 --- /dev/null +++ b/e2e_tests/trident_configurations/usr-verity/test-selection.yaml @@ -0,0 +1,3 @@ +compatible: + - base + - usr_verity diff --git a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml new file mode 100644 index 000000000..ed3aeedc0 --- /dev/null +++ b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml @@ -0,0 +1,60 @@ +internalParams: + uki: true +image: + url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi + sha384: ignored +trident: + selfUpgrade: false +storage: + disks: + - id: os + device: /dev/sda + partitionTableType: gpt + partitions: + - id: esp + size: 50M + type: esp + - id: boot + size: 250M + type: xbootldr + - id: usr-data + size: 5G + type: usr + - id: usr-hash + size: 1G + type: usr-verity + - id: root + size: 5G + type: root + verity: + - id: usr + name: usr + dataDeviceId: usr-data + hashDeviceId: usr-hash + filesystems: + - deviceId: esp + mountPoint: + path: /boot/efi + options: umask=0077 + - deviceId: boot + mountPoint: /boot + - deviceId: root + mountPoint: / + - deviceId: usr + mountPoint: + path: /usr + options: ro +os: + network: + version: 2 + ethernets: + vmeths: + match: + name: enp* + dhcp4: true + users: + - name: testing-user + sshPublicKeys: [] + secondaryGroups: + - wheel + sshMode: key-only diff --git a/storm/suites/trident/helpers/prepare_images.go b/storm/suites/trident/helpers/prepare_images.go index a3317e6c1..7dd34652b 100644 --- a/storm/suites/trident/helpers/prepare_images.go +++ b/storm/suites/trident/helpers/prepare_images.go @@ -11,19 +11,22 @@ import ( ) const ( - COSI_EXTENSION = "cosi" - REGULAR_IMAGE_NAME = "regular" - VERITY_IMAGE_NAME = "verity" + COSI_EXTENSION = "cosi" + OUTPUT_REGULAR_IMAGE_NAME = "regular" + OUTPUT_VERITY_IMAGE_NAME = "verity" + OUTPUT_USRVERITY_IMAGE_NAME = "usrverity" ) type PrepareImages struct { args struct { - RegularTestImageDir string `arg:"" help:"Directory containing the regular test images" type:"path"` - VerityTestImageDir string `arg:"" help:"Directory containing the verity test images" type:"path"` - RegularImageName string `arg:"" help:"Name of the regular test image"` - VerityImageName string `arg:"" help:"Name of the verity test image"` - OutputDir string `arg:"" help:"Directory in which to place the prepared images" type:"path"` - Versions uint `short:"v" help:"Number of versions to create of each image type" default:"1"` + RegularTestImageDir string `arg:"" help:"Directory containing the regular test images" type:"path"` + VerityTestImageDir string `arg:"" help:"Directory containing the verity test images" type:"path"` + UsrVerityTestImageDir string `arg:"" help:"Directory containing the verity test images" type:"path"` + RegularImageName string `arg:"" help:"Name of the regular test image"` + VerityImageName string `arg:"" help:"Name of the verity test image"` + UsrVerityImageName string `arg:"" help:"Name of the verity test image"` + OutputDir string `arg:"" help:"Directory in which to place the prepared images" type:"path"` + Versions uint `short:"v" help:"Number of versions to create of each image type" default:"1"` } } @@ -38,6 +41,7 @@ func (h *PrepareImages) Args() any { func (h *PrepareImages) RegisterTestCases(r storm.TestRegistrar) error { r.RegisterTestCase("copy-regular", h.copyRegularImages) r.RegisterTestCase("copy-verity", h.copyVerityImages) + r.RegisterTestCase("copy-usrverity", h.copyUsrVerityImages) return nil } @@ -53,7 +57,7 @@ func (h *PrepareImages) copyRegularImages(tc storm.TestCase) error { h.args.OutputDir, h.args.RegularImageName, COSI_EXTENSION, - REGULAR_IMAGE_NAME, + OUTPUT_REGULAR_IMAGE_NAME, h.args.Versions, ) } @@ -70,7 +74,24 @@ func (h *PrepareImages) copyVerityImages(tc storm.TestCase) error { h.args.OutputDir, h.args.VerityImageName, COSI_EXTENSION, - VERITY_IMAGE_NAME, + OUTPUT_VERITY_IMAGE_NAME, + h.args.Versions, + ) +} + +func (h *PrepareImages) copyUsrVerityImages(tc storm.TestCase) error { + // Skip test if the path doesn't exist + if _, err := os.Stat(h.args.UsrVerityTestImageDir); os.IsNotExist(err) { + tc.Skip(fmt.Sprintf("Directory %s does not exist", h.args.UsrVerityTestImageDir)) + } + + return copyImages( + tc.Logger(), + h.args.UsrVerityTestImageDir, + h.args.OutputDir, + h.args.UsrVerityImageName, + COSI_EXTENSION, + OUTPUT_USRVERITY_IMAGE_NAME, h.args.Versions, ) } From f7979eaaa265ed81280140c6ff317a1ed2d4a739 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 15 May 2025 06:27:19 +0000 Subject: [PATCH 12/99] Merged PR 23139: engineering: storm: better output capturing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description I was annoyed at having to pass around a logger everywhere, so now we do proper stderr/stdout capturing instead. Officially supported: - Anything that writes to stderr - Anything that writes to stdout (eg. `fmt.PrintX` func family) - logrus Other log providers may or may not be supported depending on how they resolve stderr. Logrus, for example, stores a reference to os.StdErr on startup and always writes to that. We have to manually swap it from underneath to capture it, which requires deliberate action. Other loggers may require similar support. Related work items: #12187 --- storm/internal/cli/run/helper.go | 3 +- storm/internal/cli/run/scenario.go | 3 +- storm/internal/reporter/reporter.go | 16 +-- storm/internal/runner/runner.go | 101 +++++++++++++++++- storm/internal/testmgr/status.go | 1 + storm/internal/testmgr/testcase.go | 47 +++----- storm/pkg/storm/core/testcases.go | 2 - storm/pkg/storm/suite/suite.go | 8 ++ storm/suites/helloworld/helloworld.go | 4 +- storm/suites/helloworld/helper.go | 13 ++- storm/suites/trident/e2e/trident.go | 6 +- storm/suites/trident/helpers/ab_update.go | 47 ++++---- storm/suites/trident/helpers/check_ssh.go | 21 ++-- .../suites/trident/helpers/prepare_images.go | 13 +-- storm/suites/trident/utils/ssh.go | 8 +- storm/suites/trident/utils/trident.go | 14 +-- 16 files changed, 199 insertions(+), 108 deletions(-) diff --git a/storm/internal/cli/run/helper.go b/storm/internal/cli/run/helper.go index d1a9edc2b..abb31ae49 100644 --- a/storm/internal/cli/run/helper.go +++ b/storm/internal/cli/run/helper.go @@ -7,6 +7,7 @@ import ( type HelperCmd struct { Helper string `arg:"" name:"helper" help:"Name of the helper to run"` + Watch bool `short:"w" help:"Watch the output of the helper live"` HelperArgs []string `arg:"" passthrough:"all" help:"Arguments to pass to the helper, you may use '--' to force passthrough." optional:""` } @@ -16,5 +17,5 @@ func (cmd *HelperCmd) Run(suite core.SuiteContext) error { helper := suite.Helper(cmd.Helper) - return runner.RegisterAndRunTests(suite, helper, cmd.HelperArgs) + return runner.RegisterAndRunTests(suite, helper, cmd.HelperArgs, cmd.Watch) } diff --git a/storm/internal/cli/run/scenario.go b/storm/internal/cli/run/scenario.go index 2807fd92b..f9e9189aa 100644 --- a/storm/internal/cli/run/scenario.go +++ b/storm/internal/cli/run/scenario.go @@ -7,6 +7,7 @@ import ( type ScenarioCmd struct { Scenario string `arg:"" name:"scenario" help:"Name of the scenario to run"` + Watch bool `short:"w" help:"Watch the output of the scenario live"` ScenarioArgs []string `arg:"" passthrough:"all" help:"Arguments to pass to the scenario, you may use '--' to force passthrough." optional:""` } @@ -16,5 +17,5 @@ func (cmd *ScenarioCmd) Run(suite core.SuiteContext) error { scenario := suite.Scenario(cmd.Scenario) - return runner.RegisterAndRunTests(suite, scenario, cmd.ScenarioArgs) + return runner.RegisterAndRunTests(suite, scenario, cmd.ScenarioArgs, cmd.Watch) } diff --git a/storm/internal/reporter/reporter.go b/storm/internal/reporter/reporter.go index 44444646e..f7923e9d0 100644 --- a/storm/internal/reporter/reporter.go +++ b/storm/internal/reporter/reporter.go @@ -1,7 +1,6 @@ package reporter import ( - "bytes" "fmt" "storm/internal/devops" "storm/internal/stormerror" @@ -168,7 +167,7 @@ func (tr *TestReporter) printFailureReport() { fmt.Printf("Stack trace:\n%s\n", err.Stack) } - logLines := getLogLinesFromTestCase(testCase) + logLines := testCase.CollectedOutput() // Check if there are any log lines if len(logLines) == 0 { @@ -203,16 +202,3 @@ func (tr *TestReporter) printFailureReport() { } } } - -func getLogLinesFromTestCase(testCase *testmgr.TestCase) []string { - lines := make([]string, 0) - rawLines := testCase.Buffer().Bytes() - for _, line := range bytes.Split(rawLines, []byte("\n")) { - if len(line) == 0 { - continue - } - lines = append(lines, string(line)) - } - - return lines -} diff --git a/storm/internal/runner/runner.go b/storm/internal/runner/runner.go index bbf1aa50b..dd53085bc 100644 --- a/storm/internal/runner/runner.go +++ b/storm/internal/runner/runner.go @@ -1,7 +1,10 @@ package runner import ( + "bufio" "fmt" + "io" + "os" "runtime/debug" "slices" "storm/internal/reporter" @@ -9,6 +12,8 @@ import ( "storm/internal/testmgr" "storm/pkg/storm/core" "sync" + + "github.com/sirupsen/logrus" ) func RegisterAndRunTests(suite core.SuiteContext, @@ -17,6 +22,7 @@ func RegisterAndRunTests(suite core.SuiteContext, core.TestRegistrant }, args []string, + watch bool, ) error { // Create a new runnable instance registrantInstance := &runnableInstance{ @@ -37,7 +43,7 @@ func RegisterAndRunTests(suite core.SuiteContext, } // Actually run the thing - err = executeTestCases(suite, registrantInstance, testMgr) + err = executeTestCases(suite, registrantInstance, testMgr, watch) if err != nil { switch err.(type) { @@ -67,6 +73,7 @@ func executeTestCases(suite core.SuiteContext, core.TestRegistrant }, testManager *testmgr.StormTestManager, + watch bool, ) error { ctx := &runnableContext{ @@ -90,8 +97,24 @@ func executeTestCases(suite core.SuiteContext, for _, testCase := range testManager.TestCases() { if !bail { suite.Logger().Infof("%s (started)", testCase.Name()) + // Run the test case. - executeTestCase(testCase) + captured, err := captureOutput(func() { + executeTestCase(testCase) + }, func(w io.Writer, s string) { + if suite.AzureDevops() || watch { + fmt.Fprintf(w, " ├ %s\n", s) + } + }) + + // Store the captured output in the test case. + testCase.SetCollectedOutput(captured) + + // If we failed to collect the output, return an error. This means + // that we didn't even run. + if err != nil { + return fmt.Errorf("failed to capture output for '%s': %w", testCase.Name(), err) + } // Grab and store the cleanup functions for this test case. cleanupFuncs = append(cleanupFuncs, testCase.SuiteCleanupList()...) @@ -135,6 +158,7 @@ func executeTestCase(testCase *testmgr.TestCase) { wg.Add(1) go func() { defer wg.Done() + err = runCatchPanic(func() error { return testCase.Execute() }) @@ -160,3 +184,76 @@ func runCatchPanic(f func() error) (err error) { return f() } + +func captureOutput(f func(), forward func(io.Writer, string)) ([]string, error) { + oldStdout := os.Stdout + oldStderr := os.Stderr + + rOut, wOut, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("failed to create stdout capture pipe: %w", err) + } + + rErr, wErr, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("failed to create stderr capture pipe: %w", err) + } + + os.Stdout = wOut + os.Stderr = wErr + + logrusOutput := logrus.StandardLogger().Out + logrusFormatter := logrus.StandardLogger().Formatter + logrusLevel := logrus.StandardLogger().Level + + // Logrust's standard logger is created on startup and stores a reference to + // the real stderr then, so our clever redirection does not work. To enable it, we + // need to set the output of the logger to our pipe as well. + logrus.SetOutput(os.Stderr) + + // Trick logrus into treating our pipe as the real stderr and force it to TRACE level. + logrus.SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + }) + logrus.SetLevel(logrus.TraceLevel) + + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + + // Restore the original logrus configuration + logrus.SetOutput(logrusOutput) + logrus.SetFormatter(logrusFormatter) + logrus.SetLevel(logrusLevel) + }() + + var combinedOutput []string + var outMutex sync.Mutex + var wg sync.WaitGroup + + var streamReader = func(r io.Reader, w io.Writer) { + defer wg.Done() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + outMutex.Lock() + combinedOutput = append(combinedOutput, line) + outMutex.Unlock() + forward(w, line) + } + } + + wg.Add(2) + + go streamReader(rOut, oldStdout) + go streamReader(rErr, oldStderr) + + f() + + wOut.Close() + wErr.Close() + + wg.Wait() + + return combinedOutput, nil +} diff --git a/storm/internal/testmgr/status.go b/storm/internal/testmgr/status.go index 9d14dba1b..e470bc85e 100644 --- a/storm/internal/testmgr/status.go +++ b/storm/internal/testmgr/status.go @@ -45,6 +45,7 @@ func (tcs TestCaseStatus) String() string { } func (tcs TestCaseStatus) ColorString() string { + color.NoColor = false // Force colors switch tcs { case TestCaseStatusPassed: return color.GreenString(tcs.String()) diff --git a/storm/internal/testmgr/testcase.go b/storm/internal/testmgr/testcase.go index 109fe0b53..ef782ce63 100644 --- a/storm/internal/testmgr/testcase.go +++ b/storm/internal/testmgr/testcase.go @@ -1,27 +1,23 @@ package testmgr import ( - "bytes" "fmt" "runtime" "storm/pkg/storm/core" "time" - - "github.com/sirupsen/logrus" ) type TestCase struct { - registrant core.TestRegistrantMetadata - name string - startTime time.Time - endTime time.Time - status TestCaseStatus - reason string - err error - log *logrus.Logger - logBuffer bytes.Buffer - f core.TestCaseFunction - suiteCleanup []func() + registrant core.TestRegistrantMetadata + name string + startTime time.Time + endTime time.Time + status TestCaseStatus + reason string + err error + collectedOutput []string + f core.TestCaseFunction + suiteCleanup []func() } func newTestCase(name string, f core.TestCaseFunction) *TestCase { @@ -29,16 +25,8 @@ func newTestCase(name string, f core.TestCaseFunction) *TestCase { name: name, f: f, status: TestCaseStatusPending, - log: logrus.New(), } - tc.log.SetLevel(logrus.TraceLevel) - tc.log.SetOutput(&tc.logBuffer) - tc.log.SetFormatter(&logrus.TextFormatter{ - ForceColors: true, - DisableTimestamp: false, - }) - return tc } @@ -93,9 +81,9 @@ func (t *TestCase) IsBailCondition() bool { return t.status.IsBad() } -// Returns the log buffer of the test case. -func (t *TestCase) Buffer() *bytes.Buffer { - return &t.logBuffer +// Returns the collected output of the test case. +func (t *TestCase) CollectedOutput() []string { + return t.collectedOutput } // Returns the reason for the test case closure. @@ -114,6 +102,10 @@ func (t *TestCase) MarkNotRun(reason string) { t.close(TestCaseStatusNotRun, reason, nil) } +func (t *TestCase) SetCollectedOutput(val []string) { + t.collectedOutput = val +} + // Mark a test as errored. This is used when the test case panics or returns an // error. func (t *TestCase) MarkError(err error) { @@ -171,11 +163,6 @@ func (t *TestCase) Skip(reason string) { runtime.Goexit() } -// Logger implements core.TestCase. -func (t *TestCase) Logger() *logrus.Logger { - return t.log -} - // Name implements core.TestCase. func (t *TestCase) Name() string { return t.name diff --git a/storm/pkg/storm/core/testcases.go b/storm/pkg/storm/core/testcases.go index af0c29f2f..bd7b83b48 100644 --- a/storm/pkg/storm/core/testcases.go +++ b/storm/pkg/storm/core/testcases.go @@ -5,8 +5,6 @@ import "time" type TestCase interface { Named - LoggerProvider - // Returns information about the registrant that created this test case. Registrant() TestRegistrantMetadata diff --git a/storm/pkg/storm/suite/suite.go b/storm/pkg/storm/suite/suite.go index d447900cc..d70cbb188 100644 --- a/storm/pkg/storm/suite/suite.go +++ b/storm/pkg/storm/suite/suite.go @@ -2,6 +2,7 @@ package suite import ( "fmt" + "os" "slices" "storm/internal/cli" @@ -24,12 +25,19 @@ type StormSuite struct { func CreateSuite(name string) StormSuite { name = fmt.Sprintf("storm-%s", name) ctx, global := cli.ParseCommandLine(name) + logger := logrus.New() logger.SetLevel(global.Verbosity) logger.SetFormatter(&logrus.TextFormatter{ ForceColors: true, }) + // Create a copy of stdErr and set it as the output for the logger. This + // means that regardless of any changes to os.Stderr we will still log + // correctly. + stdErrCopy := os.Stderr + logger.SetOutput(stdErrCopy) + logger.Infof("Creating suite '%s'", name) return StormSuite{ diff --git a/storm/suites/helloworld/helloworld.go b/storm/suites/helloworld/helloworld.go index 152022b58..0287647c7 100644 --- a/storm/suites/helloworld/helloworld.go +++ b/storm/suites/helloworld/helloworld.go @@ -4,6 +4,8 @@ package helloworld import ( "storm/pkg/storm" "storm/pkg/storm/core" + + "github.com/sirupsen/logrus" ) type HelloWorldScenario struct { @@ -16,7 +18,7 @@ func (s *HelloWorldScenario) Name() string { func (h *HelloWorldScenario) RegisterTestCases(r storm.TestRegistrar) error { r.RegisterTestCase("myPassingTestCase", func(tc core.TestCase) error { - tc.Logger().Info("This message will be logged in the test case!") + logrus.Info("This message will be logged in the test case!") // Do something here! // ... diff --git a/storm/suites/helloworld/helper.go b/storm/suites/helloworld/helper.go index f4e44b2ff..65e98754d 100644 --- a/storm/suites/helloworld/helper.go +++ b/storm/suites/helloworld/helper.go @@ -3,6 +3,8 @@ package helloworld import ( "fmt" "storm/pkg/storm" + + "github.com/sirupsen/logrus" ) // This is a simple implementation of the storm.Helper interface. It is @@ -32,7 +34,12 @@ func (h *HelloWorldHelper) RegisterTestCases(r storm.TestRegistrar) error { } func (h *HelloWorldHelper) myPasssingTestCase(tc storm.TestCase) error { - tc.Logger().Info("This message will be logged in the test case!") + // It is recommended to use the logrus logger for logging in your test cases. + // This will be captured by storm and stored in the test case. + logrus.Info("This message will be captured by storm and stored in the test case!") + + // If desired, you can also use the standard fmt package to print messages. + fmt.Println("This message will also be captured!") // Do something here! // ... @@ -50,7 +57,7 @@ func (h *HelloWorldHelper) mySkippedTestCase(tc storm.TestCase) error { } func (h *HelloWorldHelper) myFailingTestCase(tc storm.TestCase) error { - tc.Logger().Info("This message will be shown in the failure report!") + logrus.Info("This message will be shown in the failure report!") // A failure will stop execution of this test case here, mark it as failed, // and stop execution of the entire test suite. // time.Sleep(time.Second * 10) @@ -66,7 +73,7 @@ func (h *HelloWorldHelper) myFailingTestCase(tc storm.TestCase) error { } func (h *HelloWorldHelper) myErrorTestCase(tc storm.TestCase) error { - tc.Logger().Info("This test case will never run because we fail before," + + logrus.Info("This test case will never run because we fail before," + "but we'll use it to demonstrate error handling.") // Storm treats failures an errors differently. Both generally imply that a diff --git a/storm/suites/trident/e2e/trident.go b/storm/suites/trident/e2e/trident.go index 1abaf7988..7bec78962 100644 --- a/storm/suites/trident/e2e/trident.go +++ b/storm/suites/trident/e2e/trident.go @@ -3,6 +3,8 @@ package trident import ( "fmt" "storm/pkg/storm" + + "github.com/sirupsen/logrus" ) type TridentE2EScenario struct { @@ -56,8 +58,8 @@ func (s *TridentE2EScenario) RegisterTestCases(r storm.TestRegistrar) error { } func (s TridentE2EScenario) Run(tc storm.TestCase) error { - tc.Logger().Infof("Hello from '%s'!", s.Name()) - tc.Logger().Infof("Running stage '%s'", s.args.StagePath) + logrus.Infof("Hello from '%s'!", s.Name()) + logrus.Infof("Running stage '%s'", s.args.StagePath) fmt.Println(s.config) diff --git a/storm/suites/trident/helpers/ab_update.go b/storm/suites/trident/helpers/ab_update.go index 044303573..85aaa4832 100644 --- a/storm/suites/trident/helpers/ab_update.go +++ b/storm/suites/trident/helpers/ab_update.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v3" ) @@ -70,14 +71,14 @@ func (h *AbUpdateHelper) getHostConfig(tc storm.TestCase) error { return fmt.Errorf("failed to run trident to get host config: %w", err) } - tc.Logger().Debugf("Trident stdout:\n%s", out.Stdout) - tc.Logger().Debugf("Trident stderr:\n%s", out.Stderr) + logrus.Debugf("Trident stdout:\n%s", out.Stdout) + logrus.Debugf("Trident stderr:\n%s", out.Stderr) err = yaml.Unmarshal([]byte(out.Stdout), &h.config) if err != nil { return fmt.Errorf("failed to unmarshal YAML: %w", err) } - tc.Logger().Infof("Trident configuration: %v", h.config) + logrus.Infof("Trident configuration: %v", h.config) return nil } @@ -93,7 +94,7 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { return fmt.Errorf("failed to get old image URL from configuration") } - tc.Logger().Infof("Old image URL: %s", oldUrl) + logrus.Infof("Old image URL: %s", oldUrl) base := path.Base(oldUrl) @@ -110,7 +111,7 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { newCosiPath := path.Join(h.args.DestinationDirectory, newCosiName) tridentCosiPath := path.Join(h.args.Env.HostPath(), newCosiPath) newUrl := fmt.Sprintf("file://%s", tridentCosiPath) - tc.Logger().Infof("New image URL: %s", newUrl) + logrus.Infof("New image URL: %s", newUrl) // Update the image URL in the configuration h.config["image"].(map[string]any)["url"] = newUrl @@ -139,7 +140,7 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { defer sftpClient.Close() // Ensure the cosi file exists - tc.Logger().Infof("Checking if new COSI file exists at %s", newCosiPath) + logrus.Infof("Checking if new COSI file exists at %s", newCosiPath) _, err = sftpClient.Stat(newCosiPath) if err != nil { fmt.Println("Yielding to the error") @@ -179,12 +180,12 @@ func (h *AbUpdateHelper) triggerTridentUpdate(tc storm.TestCase) error { allowedOperations := make([]string, 0) if h.args.StageAbUpdate { - tc.Logger().Infof("Allowed operations: stage") + logrus.Infof("Allowed operations: stage") allowedOperations = append(allowedOperations, "stage") } if h.args.FinalizeAbUpdate { - tc.Logger().Infof("Allowed operations: finalize") + logrus.Infof("Allowed operations: finalize") allowedOperations = append(allowedOperations, "finalize") } @@ -194,41 +195,41 @@ func (h *AbUpdateHelper) triggerTridentUpdate(tc storm.TestCase) error { strings.Join(allowedOperations, ","), ) - file, err := utils.CommandOutput(h.client, tc.Logger(), fmt.Sprintf("sudo cat %s", h.args.TridentConfig)) + file, err := utils.CommandOutput(h.client, fmt.Sprintf("sudo cat %s", h.args.TridentConfig)) if err != nil { return fmt.Errorf("failed to read new Host Config file: %w", err) } - tc.Logger().Debugf("Trident config file:\n%s", file) + logrus.Debugf("Trident config file:\n%s", file) for i := 1; ; i++ { - tc.Logger().Infof("Invoking Trident attempt #%d with args: %s", i, args) + logrus.Infof("Invoking Trident attempt #%d with args: %s", i, args) out, err := utils.InvokeTrident(h.args.Env, h.client, args) if err != nil { if err, ok := err.(*ssh.ExitMissingError); ok && strings.Contains(out.Stderr, "Rebooting system") { // The connection closed without an exit code, and the output contains "Rebooting system". // This indicates that the host has rebooted. - tc.Logger().Infof("Host rebooted successfully") + logrus.Infof("Host rebooted successfully") break } else { // Some unknown error occurred. - tc.Logger().Errorf("Failed to invoke Trident: %s; %s", err, out.Report()) + logrus.Errorf("Failed to invoke Trident: %s; %s", err, out.Report()) return fmt.Errorf("failed to invoke Trident: %w", err) } } if out.Status == 0 && strings.Contains(out.Stderr, "Staging of update 'AbUpdate' succeeded") { - tc.Logger().Infof("Staging of update 'AbUpdate' succeeded") + logrus.Infof("Staging of update 'AbUpdate' succeeded") break } if out.Status == 2 && strings.Contains(out.Stderr, "Failed to run post-configure script 'fail-on-the-first-run'") { - tc.Logger().Infof("Detected intentional failure. Re-running...") + logrus.Infof("Detected intentional failure. Re-running...") continue } - tc.Logger().Errorf("Trident update failed %s", out.Report()) + logrus.Errorf("Trident update failed %s", out.Report()) tc.Fail(fmt.Sprintf("Trident update failed with status %d", out.Status)) } @@ -245,7 +246,7 @@ func (h *AbUpdateHelper) checkTridentService(tc storm.TestCase) error { tc.Skip("No Trident environment specified") } - tc.Logger().Infof("Waiting for the host to reboot and come back online...") + logrus.Infof("Waiting for the host to reboot and come back online...") time.Sleep(time.Second * 10) // Reconnect via SSH to the updated OS @@ -253,23 +254,23 @@ func (h *AbUpdateHelper) checkTridentService(tc storm.TestCase) error { h.args.TimeoutDuration(), time.Second*5, func(attempt int) (*bool, error) { - tc.Logger().Infof("SSH dial to '%s' (attempt %d)", h.args.SshCliSettings.FullHost(), attempt) + logrus.Infof("SSH dial to '%s' (attempt %d)", h.args.SshCliSettings.FullHost(), attempt) client, err := utils.OpenSshClient(h.args.SshCliSettings) if err != nil { - tc.Logger().Warnf("Failed to dial SSH server '%s': %s", h.args.SshCliSettings.FullHost(), err) + logrus.Warnf("Failed to dial SSH server '%s': %s", h.args.SshCliSettings.FullHost(), err) return nil, err } defer client.Close() - tc.Logger().Infof("SSH dial to '%s' succeeded", h.args.SshCliSettings.FullHost()) + logrus.Infof("SSH dial to '%s' succeeded", h.args.SshCliSettings.FullHost()) - err = utils.CheckTridentService(client, tc.Logger(), h.args.Env, h.args.TimeoutDuration()) + err = utils.CheckTridentService(client, h.args.Env, h.args.TimeoutDuration()) if err != nil { - tc.Logger().Warnf("Trident service is not in expected state: %s", err) + logrus.Warnf("Trident service is not in expected state: %s", err) return nil, err } - tc.Logger().Infof("Trident service is in expected state") + logrus.Infof("Trident service is in expected state") return nil, nil }, ) diff --git a/storm/suites/trident/helpers/check_ssh.go b/storm/suites/trident/helpers/check_ssh.go index 32ec0990d..4bddaeaee 100644 --- a/storm/suites/trident/helpers/check_ssh.go +++ b/storm/suites/trident/helpers/check_ssh.go @@ -6,6 +6,7 @@ import ( "storm/suites/trident/utils" "time" + "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v3" ) @@ -35,14 +36,14 @@ func (h *CheckSshHelper) RegisterTestCases(r storm.TestRegistrar) error { } func (h *CheckSshHelper) sshDial(tc storm.TestCase) error { - tc.Logger().Infof("Checking SSH connection to '%s' as user '%s'", h.args.Host, h.args.User) + logrus.Infof("Checking SSH connection to '%s' as user '%s'", h.args.Host, h.args.User) var err error h.client, err = utils.Retry( time.Second*time.Duration(h.args.Timeout), time.Second*5, func(attempt int) (*ssh.Client, error) { - tc.Logger().Infof("SSH dial to '%s' (attempt %d)", h.args.SshCliSettings.FullHost(), attempt) + logrus.Infof("SSH dial to '%s' (attempt %d)", h.args.SshCliSettings.FullHost(), attempt) return utils.OpenSshClient(h.args.SshCliSettings) }, ) @@ -66,7 +67,7 @@ func (h *CheckSshHelper) checkTridentService(tc storm.TestCase) error { tc.Skip("No Trident environment specified") } - err := utils.CheckTridentService(h.client, tc.Logger(), h.args.Env, h.args.TimeoutDuration()) + err := utils.CheckTridentService(h.client, h.args.Env, h.args.TimeoutDuration()) if err != nil { // Log this as a test failure tc.FailFromError(err) @@ -84,8 +85,8 @@ func (h *CheckSshHelper) checkActiveVolume(tc storm.TestCase) error { time.Second*5, time.Second, func(attempt int) (*ssh.Client, error) { - tc.Logger().Infof("Checking active volume (attempt %d)", attempt) - return nil, checkActiveVolumeInner(tc, h.client, h.args.CheckActiveVolume) + logrus.Infof("Checking active volume (attempt %d)", attempt) + return nil, checkActiveVolumeInner(h.client, h.args.CheckActiveVolume) }, ) @@ -97,7 +98,7 @@ func (h *CheckSshHelper) checkActiveVolume(tc storm.TestCase) error { return nil } -func checkActiveVolumeInner(lp storm.LoggerProvider, client *ssh.Client, expectedActiveVolume string) error { +func checkActiveVolumeInner(client *ssh.Client, expectedActiveVolume string) error { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create SSH session: %w", err) @@ -111,7 +112,7 @@ func checkActiveVolumeInner(lp storm.LoggerProvider, client *ssh.Client, expecte outputStr := string(output) - lp.Logger().Debugf("Host Status:\n%s", outputStr) + logrus.Debugf("Host Status:\n%s", outputStr) hostStatus := make(map[string]interface{}) if err = yaml.Unmarshal([]byte(outputStr), &hostStatus); err != nil { @@ -121,13 +122,15 @@ func checkActiveVolumeInner(lp storm.LoggerProvider, client *ssh.Client, expecte if hostStatus["servicingState"] != "provisioned" { return fmt.Errorf("trident state is not 'provisioned'") } - lp.Logger().Info("Host is in provisioned state") + + logrus.Info("Host is in provisioned state") hsActiveVol := hostStatus["abActiveVolume"] if hsActiveVol != expectedActiveVolume { return fmt.Errorf("expected active volume '%s', got '%s'", expectedActiveVolume, hsActiveVol) } - lp.Logger().Infof("Active volume is '%s'", hsActiveVol) + + logrus.Infof("Active volume is '%s'", hsActiveVol) return nil } diff --git a/storm/suites/trident/helpers/prepare_images.go b/storm/suites/trident/helpers/prepare_images.go index 7dd34652b..ae3e5c0fa 100644 --- a/storm/suites/trident/helpers/prepare_images.go +++ b/storm/suites/trident/helpers/prepare_images.go @@ -52,7 +52,6 @@ func (h *PrepareImages) copyRegularImages(tc storm.TestCase) error { } return copyImages( - tc.Logger(), h.args.RegularTestImageDir, h.args.OutputDir, h.args.RegularImageName, @@ -69,7 +68,6 @@ func (h *PrepareImages) copyVerityImages(tc storm.TestCase) error { } return copyImages( - tc.Logger(), h.args.VerityTestImageDir, h.args.OutputDir, h.args.VerityImageName, @@ -86,7 +84,6 @@ func (h *PrepareImages) copyUsrVerityImages(tc storm.TestCase) error { } return copyImages( - tc.Logger(), h.args.UsrVerityTestImageDir, h.args.OutputDir, h.args.UsrVerityImageName, @@ -96,7 +93,7 @@ func (h *PrepareImages) copyUsrVerityImages(tc storm.TestCase) error { ) } -func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ext string, outputFilename string, versions uint) error { +func copyImages(srcDir, destDir string, imageName string, ext string, outputFilename string, versions uint) error { srcDir, err := filepath.Abs(srcDir) if err != nil { return fmt.Errorf("failed to get absolute path of source directory %s: %v", srcDir, err) @@ -116,7 +113,7 @@ func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ex return fmt.Errorf("no '%s' files found in directory %s", glob, srcDir) } - log.Infof("Found %d files in %s matching glob %s", len(files), srcDir, glob) + logrus.Infof("Found %d files in %s matching glob %s", len(files), srcDir, glob) singleFilePattern := fmt.Sprintf("%s.%s", imageName, ext) multipleFilePattern := fmt.Sprintf(`%s_(\d+).%s`, regexp.QuoteMeta(imageName), regexp.QuoteMeta(ext)) @@ -140,7 +137,7 @@ func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ex // Create output directory if it doesn't exist if _, err := os.Stat(destDir); os.IsNotExist(err) { - log.Debugf("Creating directory %s", destDir) + logrus.Debugf("Creating directory %s", destDir) err := os.MkdirAll(destDir, 0755) if err != nil { return fmt.Errorf("failed to create directory %s: %v", destDir, err) @@ -158,7 +155,7 @@ func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ex newFileName = fmt.Sprintf("%s_v%d.%s", outputFilename, i+1, ext) } - log.Infof("Moving file '%s' to '%s'", file, newFileName) + logrus.Infof("Moving file '%s' to '%s'", file, newFileName) newFilePath := filepath.Join(destDir, newFileName) err := os.Rename(file, newFilePath) @@ -175,7 +172,7 @@ func copyImages(log *logrus.Logger, srcDir, destDir string, imageName string, ex baseFile := outputFiles[v%len(outputFiles)] // Create a hard link to the base file newFilePath := filepath.Join(destDir, newFileName) - log.Infof("Linking file '%s' to '%s'", baseFile, newFilePath) + logrus.Infof("Linking file '%s' to '%s'", baseFile, newFilePath) err := os.Link(baseFile, newFilePath) if err != nil { return fmt.Errorf("failed to link file %s to %s: %v", baseFile, newFilePath, err) diff --git a/storm/suites/trident/utils/ssh.go b/storm/suites/trident/utils/ssh.go index 473551ab9..59154e584 100644 --- a/storm/suites/trident/utils/ssh.go +++ b/storm/suites/trident/utils/ssh.go @@ -160,16 +160,16 @@ func RunCommand(client *ssh.Client, command string) (*SshCmdOutput, error) { return out, nil } -func CommandOutput(client *ssh.Client, log *logrus.Logger, command string) (string, error) { - log.WithField("command", command).Debug("Executing command") +func CommandOutput(client *ssh.Client, command string) (string, error) { + logrus.WithField("command", command).Debug("Executing command") output, err := RunCommand(client, command) if err != nil { - log.Errorf("Failed to run command: %s", err) + logrus.Errorf("Failed to run command: %s", err) return "", fmt.Errorf("failed to run command: %w", err) } if err := output.Check(); err != nil { - log.Errorf("Command failed: %s", output.Report()) + logrus.Errorf("Command failed: %s", output.Report()) return "", fmt.Errorf("command failed: %w", err) } diff --git a/storm/suites/trident/utils/trident.go b/storm/suites/trident/utils/trident.go index a8f4c44ca..6cedca2ba 100644 --- a/storm/suites/trident/utils/trident.go +++ b/storm/suites/trident/utils/trident.go @@ -82,7 +82,7 @@ func LoadTridentContainer(client *ssh.Client) error { return nil } -func CheckTridentService(client *ssh.Client, log *logrus.Logger, env TridentEnvironment, timeout time.Duration) error { +func CheckTridentService(client *ssh.Client, env TridentEnvironment, timeout time.Duration) error { if client == nil { return fmt.Errorf("SSH client is nil") } @@ -101,10 +101,10 @@ func CheckTridentService(client *ssh.Client, log *logrus.Logger, env TridentEnvi timeout, time.Second*5, func(attempt int) (*bool, error) { - log.Infof("Checking Trident service status (attempt %d)", attempt) - err := checkTridentServiceInner(log, client, serviceName) + logrus.Infof("Checking Trident service status (attempt %d)", attempt) + err := checkTridentServiceInner(client, serviceName) if err != nil { - log.Warnf("Trident service is not in expected state: %s", err) + logrus.Warnf("Trident service is not in expected state: %s", err) return nil, err } @@ -118,7 +118,7 @@ func CheckTridentService(client *ssh.Client, log *logrus.Logger, env TridentEnvi return nil } -func checkTridentServiceInner(log *logrus.Logger, client *ssh.Client, serviceName string) error { +func checkTridentServiceInner(client *ssh.Client, serviceName string) error { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create SSH session: %w", err) @@ -140,7 +140,7 @@ func checkTridentServiceInner(log *logrus.Logger, client *ssh.Client, serviceNam outputStr := string(output) - log.Debugf("Trident service status:\n%s", outputStr) + logrus.Debugf("Trident service status:\n%s", outputStr) if !strings.Contains(outputStr, "Active: inactive (dead)") { return fmt.Errorf("expected to find 'Active: inactive (dead)' in Trident service status") @@ -163,7 +163,7 @@ func checkTridentServiceInner(log *logrus.Logger, client *ssh.Client, serviceNam return fmt.Errorf("expected to find '(code=exited, status=0/SUCCESS)' in Trident service status") } - log.Info("Trident service ran successfully") + logrus.Info("Trident service ran successfully") return nil } From 267770da3f3c6aab86e9894d6b42b57dfb13ebd9 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 15 May 2025 17:02:25 +0000 Subject: [PATCH 13/99] Merged PR 23141: engineering: storm: refactor into independent module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description DEPENDS ON: !23139 First step to move it out of trident, make it an independent module. This also means tools and the test suite now exist in the same module and can easily share code. No behavior changes, just moving files around and updating imports. ---- #### AI description (iteration 1) #### PR Classification This pull request refactors storm’s testing and logging infrastructure to decouple it into an independent module while enhancing output capturing. #### PR Summary The changes introduce a new mechanism in the test runner for capturing and forwarding output, simplify test case logging, and reorganize the project structure for module independence. - **`storm/internal/runner/runner.go`**: Added a new `captureOutput` function to redirect stdout/stderr and integrate live output forwarding (via the new watch flag) into test executions. - **`storm/internal/testmgr/testcase.go`**: Removed the buffered logger in favor of a `collectedOutput` field and updated accessors to return captured output. - **`storm/internal/cli/run`**: Updated helper and scenario commands to support a `watch` flag, passing it to `RegisterAndRunTests` for live output. - **File Movements**: Moved multiple helpers, utilities, and scenario files from storm directories to the tools structure to establish module independence. - **Dependency Updates**: Revised go.mod and go.sum entries, including module renaming from argus_toolkit to tridenttools and updating several dependency versions. Related work items: #12194 --- Makefile | 4 +- storm/cmd/storm-helloworld/main.go | 5 +- storm/go.mod | 8 +-- storm/go.sum | 43 --------------- storm/{suites => }/helloworld/helper.go | 2 +- .../helloworld.go => helloworld/scenario.go} | 5 +- storm/internal/suite/context.go | 26 ---------- storm/{pkg/storm/init.go => storm.go} | 0 tools/cmd/mkcosi/builder/builder.go | 3 +- tools/cmd/mkcosi/builder/common.go | 4 +- tools/cmd/mkcosi/builder/regular.go | 4 +- tools/cmd/mkcosi/builder/verity.go | 4 +- tools/cmd/mkcosi/cmd/random_uuid.go | 6 +-- tools/cmd/mkcosi/cmd/read_metadata.go | 2 +- tools/cmd/mkcosi/cosi/cosi.go | 2 +- tools/cmd/mkcosi/cosi/scan.go | 2 +- tools/cmd/mkcosi/main.go | 4 +- tools/cmd/netlaunch/main.go | 6 +-- tools/cmd/netlisten/main.go | 2 +- {storm => tools}/cmd/storm-trident/main.go | 6 +-- tools/go.mod | 23 +++++--- tools/go.sum | 52 ++++++++++++++++--- .../storm}/e2e/configurations/.gitignore | 0 .../trident => tools/storm}/e2e/discover.go | 2 +- .../trident => tools/storm}/e2e/stages.go | 0 .../trident => tools/storm}/e2e/trident.go | 3 +- .../storm}/helpers/ab_update.go | 6 ++- .../storm}/helpers/check_ssh.go | 6 ++- .../trident => tools/storm}/helpers/init.go | 2 +- .../storm}/helpers/prepare_images.go | 3 +- .../trident => tools/storm}/utils/env.go | 0 .../trident => tools/storm}/utils/retry.go | 0 .../trident => tools/storm}/utils/ssh.go | 0 .../trident => tools/storm}/utils/trident.go | 0 34 files changed, 109 insertions(+), 126 deletions(-) rename storm/{suites => }/helloworld/helper.go (99%) rename storm/{suites/helloworld/helloworld.go => helloworld/scenario.go} (80%) delete mode 100644 storm/internal/suite/context.go rename storm/{pkg/storm/init.go => storm.go} (100%) rename {storm => tools}/cmd/storm-trident/main.go (80%) rename {storm/suites/trident => tools/storm}/e2e/configurations/.gitignore (100%) rename {storm/suites/trident => tools/storm}/e2e/discover.go (98%) rename {storm/suites/trident => tools/storm}/e2e/stages.go (100%) rename {storm/suites/trident => tools/storm}/e2e/trident.go (98%) rename {storm/suites/trident => tools/storm}/helpers/ab_update.go (99%) rename {storm/suites/trident => tools/storm}/helpers/check_ssh.go (98%) rename {storm/suites/trident => tools/storm}/helpers/init.go (82%) rename {storm/suites/trident => tools/storm}/helpers/prepare_images.go (99%) rename {storm/suites/trident => tools/storm}/utils/env.go (100%) rename {storm/suites/trident => tools/storm}/utils/retry.go (100%) rename {storm/suites/trident => tools/storm}/utils/ssh.go (100%) rename {storm/suites/trident => tools/storm}/utils/trident.go (100%) diff --git a/Makefile b/Makefile index e1b6f1d5a..d82acfc87 100644 --- a/Makefile +++ b/Makefile @@ -343,8 +343,8 @@ bin/mkcosi: tools/cmd/mkcosi/* tools/go.sum tools/pkg/* tools/cmd/mkcosi/**/* bin/storm-trident: $(shell find storm -type f) tools/go.sum @mkdir -p bin - cd storm && go generate suites/trident/e2e/discover.go - cd storm && go build -o ../bin/storm-trident ./cmd/storm-trident/main.go + cd tools && go generate storm/e2e/discover.go + cd tools && go build -o ../bin/storm-trident ./cmd/storm-trident/main.go .PHONY: validate validate: $(TRIDENT_CONFIG) bin/trident diff --git a/storm/cmd/storm-helloworld/main.go b/storm/cmd/storm-helloworld/main.go index f167d3298..8e3219d72 100644 --- a/storm/cmd/storm-helloworld/main.go +++ b/storm/cmd/storm-helloworld/main.go @@ -1,8 +1,9 @@ package main import ( - "storm/pkg/storm" - "storm/suites/helloworld" + "storm" + + "storm/helloworld" ) func main() { diff --git a/storm/go.mod b/storm/go.mod index 829ca37d2..478db0af7 100644 --- a/storm/go.mod +++ b/storm/go.mod @@ -5,19 +5,13 @@ go 1.23.5 require ( github.com/alecthomas/kong v1.8.1 github.com/fatih/color v1.18.0 - github.com/pkg/sftp v1.13.9 github.com/sirupsen/logrus v1.9.3 - golang.org/x/crypto v0.37.0 golang.org/x/term v0.31.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/stretchr/testify v1.8.0 // indirect golang.org/x/sys v0.32.0 // indirect ) - -// Deal with CVE-2024-45338, CVE-2025-22870, CVE-2025-22872 -replace golang.org/x/net => golang.org/x/net v0.39.0 diff --git a/storm/go.sum b/storm/go.sum index 5c0d49497..a42c16ddf 100644 --- a/storm/go.sum +++ b/storm/go.sum @@ -9,18 +9,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= -github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -31,51 +26,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/storm/suites/helloworld/helper.go b/storm/helloworld/helper.go similarity index 99% rename from storm/suites/helloworld/helper.go rename to storm/helloworld/helper.go index 65e98754d..dff236200 100644 --- a/storm/suites/helloworld/helper.go +++ b/storm/helloworld/helper.go @@ -2,7 +2,7 @@ package helloworld import ( "fmt" - "storm/pkg/storm" + "storm" "github.com/sirupsen/logrus" ) diff --git a/storm/suites/helloworld/helloworld.go b/storm/helloworld/scenario.go similarity index 80% rename from storm/suites/helloworld/helloworld.go rename to storm/helloworld/scenario.go index 0287647c7..2af87c2b0 100644 --- a/storm/suites/helloworld/helloworld.go +++ b/storm/helloworld/scenario.go @@ -2,8 +2,7 @@ package helloworld import ( - "storm/pkg/storm" - "storm/pkg/storm/core" + "storm" "github.com/sirupsen/logrus" ) @@ -17,7 +16,7 @@ func (s *HelloWorldScenario) Name() string { } func (h *HelloWorldScenario) RegisterTestCases(r storm.TestRegistrar) error { - r.RegisterTestCase("myPassingTestCase", func(tc core.TestCase) error { + r.RegisterTestCase("myPassingTestCase", func(tc storm.TestCase) error { logrus.Info("This message will be logged in the test case!") // Do something here! diff --git a/storm/internal/suite/context.go b/storm/internal/suite/context.go deleted file mode 100644 index 2a081c4e2..000000000 --- a/storm/internal/suite/context.go +++ /dev/null @@ -1,26 +0,0 @@ -package suite - -import ( - "storm/pkg/storm" - "storm/pkg/storm/core" -) - -type SuiteContext interface { - core.Named - - core.LoggerProvider - - // Returns a list of all scenarios - Scenarios() []storm.Scenario - - // Returns a scenario by name, will exit with an error if the scenario is - // not found. - Scenario(name string) storm.Scenario - - // Returns a list of all helpers - Helpers() []storm.Helper - - // Returns a helper by name, will exit with an error if the helper is - // not found. - Helper(name string) storm.Helper -} diff --git a/storm/pkg/storm/init.go b/storm/storm.go similarity index 100% rename from storm/pkg/storm/init.go rename to storm/storm.go diff --git a/tools/cmd/mkcosi/builder/builder.go b/tools/cmd/mkcosi/builder/builder.go index 9263c7b04..f51d2a4db 100644 --- a/tools/cmd/mkcosi/builder/builder.go +++ b/tools/cmd/mkcosi/builder/builder.go @@ -2,13 +2,14 @@ package builder import ( "archive/tar" - "argus_toolkit/cmd/mkcosi/metadata" "bytes" "encoding/json" "fmt" "io" "os" + "tridenttools/cmd/mkcosi/metadata" + log "github.com/sirupsen/logrus" ) diff --git a/tools/cmd/mkcosi/builder/common.go b/tools/cmd/mkcosi/builder/common.go index 35b342842..846e87be5 100644 --- a/tools/cmd/mkcosi/builder/common.go +++ b/tools/cmd/mkcosi/builder/common.go @@ -1,8 +1,6 @@ package builder import ( - "argus_toolkit/cmd/mkcosi/metadata" - "argus_toolkit/pkg/ref" "crypto/sha512" _ "embed" "encoding/json" @@ -13,6 +11,8 @@ import ( "os/exec" "path" "strings" + "tridenttools/cmd/mkcosi/metadata" + "tridenttools/pkg/ref" "github.com/google/uuid" "github.com/klauspost/compress/zstd" diff --git a/tools/cmd/mkcosi/builder/regular.go b/tools/cmd/mkcosi/builder/regular.go index 295b22c56..a9980c634 100644 --- a/tools/cmd/mkcosi/builder/regular.go +++ b/tools/cmd/mkcosi/builder/regular.go @@ -1,8 +1,8 @@ package builder import ( - "argus_toolkit/cmd/mkcosi/metadata" - "argus_toolkit/pkg/ref" + "tridenttools/cmd/mkcosi/metadata" + "tridenttools/pkg/ref" ) type BuildRegular struct { diff --git a/tools/cmd/mkcosi/builder/verity.go b/tools/cmd/mkcosi/builder/verity.go index 190b8d4ac..9f8f5d6c2 100644 --- a/tools/cmd/mkcosi/builder/verity.go +++ b/tools/cmd/mkcosi/builder/verity.go @@ -1,8 +1,8 @@ package builder import ( - "argus_toolkit/cmd/mkcosi/metadata" - "argus_toolkit/pkg/ref" + "tridenttools/cmd/mkcosi/metadata" + "tridenttools/pkg/ref" ) type BuildVerity struct { diff --git a/tools/cmd/mkcosi/cmd/random_uuid.go b/tools/cmd/mkcosi/cmd/random_uuid.go index 89fff4371..aa78cab96 100644 --- a/tools/cmd/mkcosi/cmd/random_uuid.go +++ b/tools/cmd/mkcosi/cmd/random_uuid.go @@ -1,15 +1,15 @@ package cmd import ( - "argus_toolkit/cmd/mkcosi/builder" - "argus_toolkit/cmd/mkcosi/cosi" - "argus_toolkit/cmd/mkcosi/metadata" "fmt" "io" "os" "os/exec" "slices" "strings" + "tridenttools/cmd/mkcosi/builder" + "tridenttools/cmd/mkcosi/cosi" + "tridenttools/cmd/mkcosi/metadata" "github.com/google/uuid" "github.com/klauspost/compress/zstd" diff --git a/tools/cmd/mkcosi/cmd/read_metadata.go b/tools/cmd/mkcosi/cmd/read_metadata.go index 77e545057..90051bb84 100644 --- a/tools/cmd/mkcosi/cmd/read_metadata.go +++ b/tools/cmd/mkcosi/cmd/read_metadata.go @@ -1,9 +1,9 @@ package cmd import ( - "argus_toolkit/cmd/mkcosi/cosi" "encoding/json" "fmt" + "tridenttools/cmd/mkcosi/cosi" log "github.com/sirupsen/logrus" ) diff --git a/tools/cmd/mkcosi/cosi/cosi.go b/tools/cmd/mkcosi/cosi/cosi.go index c8a6b69f9..f36860eca 100644 --- a/tools/cmd/mkcosi/cosi/cosi.go +++ b/tools/cmd/mkcosi/cosi/cosi.go @@ -1,11 +1,11 @@ package cosi import ( - "argus_toolkit/cmd/mkcosi/metadata" "fmt" "io" "os" "path/filepath" + "tridenttools/cmd/mkcosi/metadata" log "github.com/sirupsen/logrus" ) diff --git a/tools/cmd/mkcosi/cosi/scan.go b/tools/cmd/mkcosi/cosi/scan.go index 3d80245f4..a998f4ef0 100644 --- a/tools/cmd/mkcosi/cosi/scan.go +++ b/tools/cmd/mkcosi/cosi/scan.go @@ -2,11 +2,11 @@ package cosi import ( "archive/tar" - "argus_toolkit/cmd/mkcosi/metadata" "encoding/json" "fmt" "io" "os" + "tridenttools/cmd/mkcosi/metadata" log "github.com/sirupsen/logrus" "golang.org/x/exp/slices" diff --git a/tools/cmd/mkcosi/main.go b/tools/cmd/mkcosi/main.go index 59d5c8a73..d9845e44c 100644 --- a/tools/cmd/mkcosi/main.go +++ b/tools/cmd/mkcosi/main.go @@ -1,8 +1,8 @@ package main import ( - "argus_toolkit/cmd/mkcosi/builder" - "argus_toolkit/cmd/mkcosi/cmd" + "tridenttools/cmd/mkcosi/builder" + "tridenttools/cmd/mkcosi/cmd" "github.com/alecthomas/kong" log "github.com/sirupsen/logrus" diff --git a/tools/cmd/netlaunch/main.go b/tools/cmd/netlaunch/main.go index 9b2c10ee5..b4db4f982 100644 --- a/tools/cmd/netlaunch/main.go +++ b/tools/cmd/netlaunch/main.go @@ -4,10 +4,10 @@ Copyright Š 2023 Microsoft Corporation package main import ( - "argus_toolkit/pkg/netfinder" - "argus_toolkit/pkg/phonehome" - "argus_toolkit/pkg/serial" "sync" + "tridenttools/pkg/netfinder" + "tridenttools/pkg/phonehome" + "tridenttools/pkg/serial" "bytes" "context" diff --git a/tools/cmd/netlisten/main.go b/tools/cmd/netlisten/main.go index c32e6c07a..ff358570f 100644 --- a/tools/cmd/netlisten/main.go +++ b/tools/cmd/netlisten/main.go @@ -24,11 +24,11 @@ Then start the provisioning using the patched Trident config file. package main import ( - "argus_toolkit/pkg/phonehome" "fmt" "net" "os/signal" "syscall" + "tridenttools/pkg/phonehome" "context" "net/http" diff --git a/storm/cmd/storm-trident/main.go b/tools/cmd/storm-trident/main.go similarity index 80% rename from storm/cmd/storm-trident/main.go rename to tools/cmd/storm-trident/main.go index 0446cd5ca..6695a50eb 100644 --- a/storm/cmd/storm-trident/main.go +++ b/tools/cmd/storm-trident/main.go @@ -1,9 +1,9 @@ package main import ( - "storm/pkg/storm" - trident "storm/suites/trident/e2e" - "storm/suites/trident/helpers" + "storm" + trident "tridenttools/storm/e2e" + "tridenttools/storm/helpers" ) func main() { diff --git a/tools/go.mod b/tools/go.mod index c40ae630a..9e491d000 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,26 +1,37 @@ -module argus_toolkit +module tridenttools -go 1.23.0 +go 1.23.5 toolchain go1.24.2 +replace storm => ../storm + +// Deal with CVE-2024-45338, CVE-2025-22870, CVE-2025-22872 +replace golang.org/x/net => golang.org/x/net v0.39.0 + require ( github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230530141715-da28e42c453f - github.com/fatih/color v1.17.0 + github.com/fatih/color v1.18.0 github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 + github.com/pkg/sftp v1.13.9 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 gopkg.in/yaml.v2 v2.4.0 + storm v1.0.0 ) -require github.com/vishvananda/netns v0.0.4 // indirect +require ( + github.com/kr/fs v0.1.0 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + golang.org/x/term v0.31.0 // indirect +) require ( github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 // indirect github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 // indirect - github.com/alecthomas/kong v1.2.1 + github.com/alecthomas/kong v1.8.1 github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -53,5 +64,5 @@ require ( golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/tools/go.sum b/tools/go.sum index 1b9a25fb3..2a8626afb 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -2,10 +2,10 @@ github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 h1:t95Grn2 github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230/go.mod h1:t2EzW1qybnPDQ3LR/GgeF0GOzHUXT5IVMLP2gkW1cmc= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 h1:a0MBqYm44o0NcthLKCljZHe1mxlN6oahCQHHThnSwB4= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22/go.mod h1:/B7V22rcz4860iDqstGvia/2+IYWXf3/JdQCVd/1D2A= -github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= -github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= -github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= +github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230530141715-da28e42c453f h1:5xXPluhAvpNCmrgVhQesx+GcpPX1pXRyDFtj3JmxT+g= @@ -19,8 +19,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -48,6 +48,8 @@ github.com/jacobweinstock/registrar v0.4.7 h1:s4dOExccgD+Pc7rJC+f3Mc3D+NXHcXUaOi github.com/jacobweinstock/registrar v0.4.7/go.mod h1:PWmkdGFG5/ZdCqgMo7pvB3pXABOLHc5l8oQ0sgmBNDU= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -65,6 +67,8 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= +github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -94,7 +98,10 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+ github.com/stmcginnis/gofish v0.19.0 h1:fmxdRZ5WHfs+4ExArMYoeRfoh+SAxLELKtmoVplBkU4= github.com/stmcginnis/gofish v0.19.0/go.mod h1:lq2jHj2t8Krg0Gx02ABk8MbK7Dz9jvWpO/TGnVksn00= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -103,27 +110,60 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/storm/suites/trident/e2e/configurations/.gitignore b/tools/storm/e2e/configurations/.gitignore similarity index 100% rename from storm/suites/trident/e2e/configurations/.gitignore rename to tools/storm/e2e/configurations/.gitignore diff --git a/storm/suites/trident/e2e/discover.go b/tools/storm/e2e/discover.go similarity index 98% rename from storm/suites/trident/e2e/discover.go rename to tools/storm/e2e/discover.go index 60bb6967c..78bbe927e 100644 --- a/storm/suites/trident/e2e/discover.go +++ b/tools/storm/e2e/discover.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" ) -//go:generate cp -r ../../../../e2e_tests/trident_configurations configurations +//go:generate cp -r ../../../e2e_tests/trident_configurations configurations //go:embed configurations/* var content embed.FS diff --git a/storm/suites/trident/e2e/stages.go b/tools/storm/e2e/stages.go similarity index 100% rename from storm/suites/trident/e2e/stages.go rename to tools/storm/e2e/stages.go diff --git a/storm/suites/trident/e2e/trident.go b/tools/storm/e2e/trident.go similarity index 98% rename from storm/suites/trident/e2e/trident.go rename to tools/storm/e2e/trident.go index 7bec78962..be4832596 100644 --- a/storm/suites/trident/e2e/trident.go +++ b/tools/storm/e2e/trident.go @@ -2,7 +2,8 @@ package trident import ( "fmt" - "storm/pkg/storm" + + "storm" "github.com/sirupsen/logrus" ) diff --git a/storm/suites/trident/helpers/ab_update.go b/tools/storm/helpers/ab_update.go similarity index 99% rename from storm/suites/trident/helpers/ab_update.go rename to tools/storm/helpers/ab_update.go index 85aaa4832..ac2488863 100644 --- a/storm/suites/trident/helpers/ab_update.go +++ b/tools/storm/helpers/ab_update.go @@ -4,14 +4,16 @@ import ( "fmt" "path" "regexp" - "storm/pkg/storm" - "storm/suites/trident/utils" "strings" "time" + "storm" + "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v3" + + "tridenttools/storm/utils" ) type AbUpdateHelper struct { diff --git a/storm/suites/trident/helpers/check_ssh.go b/tools/storm/helpers/check_ssh.go similarity index 98% rename from storm/suites/trident/helpers/check_ssh.go rename to tools/storm/helpers/check_ssh.go index 4bddaeaee..62ee23676 100644 --- a/storm/suites/trident/helpers/check_ssh.go +++ b/tools/storm/helpers/check_ssh.go @@ -2,13 +2,15 @@ package helpers import ( "fmt" - "storm/pkg/storm" - "storm/suites/trident/utils" "time" + "storm" + "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v3" + + "tridenttools/storm/utils" ) type CheckSshHelper struct { diff --git a/storm/suites/trident/helpers/init.go b/tools/storm/helpers/init.go similarity index 82% rename from storm/suites/trident/helpers/init.go rename to tools/storm/helpers/init.go index 5a41fa966..dae59b1f7 100644 --- a/storm/suites/trident/helpers/init.go +++ b/tools/storm/helpers/init.go @@ -1,6 +1,6 @@ package helpers -import "storm/pkg/storm" +import "storm" var TRIDENT_HELPERS = []storm.Helper{ &CheckSshHelper{}, diff --git a/storm/suites/trident/helpers/prepare_images.go b/tools/storm/helpers/prepare_images.go similarity index 99% rename from storm/suites/trident/helpers/prepare_images.go rename to tools/storm/helpers/prepare_images.go index ae3e5c0fa..346e41c47 100644 --- a/storm/suites/trident/helpers/prepare_images.go +++ b/tools/storm/helpers/prepare_images.go @@ -5,7 +5,8 @@ import ( "os" "path/filepath" "regexp" - "storm/pkg/storm" + + "storm" "github.com/sirupsen/logrus" ) diff --git a/storm/suites/trident/utils/env.go b/tools/storm/utils/env.go similarity index 100% rename from storm/suites/trident/utils/env.go rename to tools/storm/utils/env.go diff --git a/storm/suites/trident/utils/retry.go b/tools/storm/utils/retry.go similarity index 100% rename from storm/suites/trident/utils/retry.go rename to tools/storm/utils/retry.go diff --git a/storm/suites/trident/utils/ssh.go b/tools/storm/utils/ssh.go similarity index 100% rename from storm/suites/trident/utils/ssh.go rename to tools/storm/utils/ssh.go diff --git a/storm/suites/trident/utils/trident.go b/tools/storm/utils/trident.go similarity index 100% rename from storm/suites/trident/utils/trident.go rename to tools/storm/utils/trident.go From b9dd94097aad556ca6e51a885c35c6dca86d6c9c Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 15 May 2025 19:04:16 +0000 Subject: [PATCH 14/99] Merged PR 23146: Add currently unused LocalVmUuid field to netlaunch We currently fail if there's any unrecognized entries in the netlaunch config, so this is needed before https://dev.azure.com/mariner-org/ECF/_git/argus-toolkit/pullrequest/23145 lands Related work items: #12199 --- tools/cmd/netlaunch/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/cmd/netlaunch/main.go b/tools/cmd/netlaunch/main.go index b4db4f982..3557bf34d 100644 --- a/tools/cmd/netlaunch/main.go +++ b/tools/cmd/netlaunch/main.go @@ -52,6 +52,7 @@ type NetLaunchConfig struct { Output string } } + LocalVmUuid *string } Iso struct { PreTridentScript *string From 9cf5eb34bb3cfa4cea1788ba81d0ba3bb2b4c559 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Fri, 16 May 2025 00:48:11 +0000 Subject: [PATCH 15/99] Merged PR 23016: doc: Trident Feature Matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Add a feature matrix to our docs landing page. ---- #### AI description (iteration 1) #### PR Classification Documentation update to include a feature matrix for Trident. #### PR Summary This pull request adds a detailed feature matrix to the Trident documentation, outlining the capabilities and planned features for managing Azure Linux systems. - `docs/Trident.md`: Added a comprehensive feature matrix table detailing various categories such as Runtime, Bootloader, Lifecycle, Integrity, Storage, OS Config, SELinux, Customization, and Development, along with their support status across different lifecycle stages (Install, VM-Init, Update). Related work items: #11446 --- docs/Trident.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/Trident.md b/docs/Trident.md index fbba9b2b3..7c412ec19 100644 --- a/docs/Trident.md +++ b/docs/Trident.md @@ -32,3 +32,78 @@ preview: - UKI - Encryption with PCR sealing --> + +# Trident + +Trident is a tool for managing the lifecycle of Azure Linux systems. + +## Feature Matrix + +Legend: + +- ✅: Fully supported. +- â˜‘ī¸: In preview or partially supported. +- 🔜: Planned feature. Not implemented yet. +- âš ī¸: Refer to relevant notes for details. +- ❌: Not supported. + +### Servicing Features + +| Category | Feature | Install | VM-Init | Update | +| --------------- | --------------------------------------- | ------- | ------- | ------ | +| 🚀 Runtime | Native binary | ✅ | ✅ | ✅ | +| 🚀 Runtime | Containerized | ✅ | ❌ | ✅ | +| âš™ī¸ Bootloader | UEFI [1] | ✅ | ✅ | ✅ | +| âš™ī¸ Bootloader | GPT partitioning | ✅ | ✅ | ✅ | +| âš™ī¸ Bootloader | Grub2 | ✅ | ✅ | ✅ | +| âš™ī¸ Bootloader | Systemd-boot | â˜‘ī¸ | â˜‘ī¸ | â˜‘ī¸ | +| 🔄 Lifecycle | Onboard system for updates | ✅ | ✅ | ✅ | +| 🔄 Lifecycle | Rollback (grub) | ✅ | ✅ | ✅ | +| 🔄 Lifecycle | Rollback (systemd-boot/UKI) | 🔜 | 🔜 | 🔜 | +| 🔏 Integrity | Secure boot | ✅ | ✅ | ✅ | +| 🔏 Integrity | UKI | â˜‘ī¸ | â˜‘ī¸ | â˜‘ī¸ | +| 🔏 Integrity | Root verity (grub) | âš ī¸[2] | âš ī¸[2] | âš ī¸[2] | +| 🔏 Integrity | Root verity (UKI) | â˜‘ī¸ | â˜‘ī¸ | â˜‘ī¸ | +| 🔏 Integrity | User verity (UKI) | â˜‘ī¸ | â˜‘ī¸ | â˜‘ī¸ | +| đŸ’Ŋ Storage | Block device creation | ✅ | 🔜 | ❌ | +| đŸ’Ŋ Storage | Image streaming (local) | ✅ | 🔜 | ✅ | +| đŸ’Ŋ Storage | Image streaming (HTTPS) | ✅ | 🔜 | ✅ | +| đŸ’Ŋ Storage | Multiboot | â˜‘ī¸ | ❌ | ✅[3] | +| đŸ’Ŋ Storage | Partition adoption | â˜‘ī¸ | ❌ | ✅[3] | +| đŸ’Ŋ Storage | Software RAID | ✅ | ❌ | ✅[3] | +| đŸ’Ŋ Storage | ESP redundancy | ✅ | ❌ | ✅[3] | +| đŸ’Ŋ Storage | Encryption with secure boot PCR sealing | ✅ | 🔜 | ✅[3] | +| đŸ’Ŋ Storage | Encryption with OS PCR sealing | 🔜[4] | 🔜 | ✅[3] | +| 📝 OS Config | Network configuration | ✅ | ❌ | ✅ | +| 📝 OS Config | Hostname configuration | ✅[5] | ❌ | ✅[5] | +| 📝 OS Config | User configuration | ✅[5] | ❌ | ✅[5] | +| 📝 OS Config | SSH configuration | ✅[5] | ❌ | ✅[5] | +| 📝 OS Config | Initrd regeneration (grub) | ✅ | ❌ | ✅ | +| 📝 OS Config | Initrd regeneration (UKI) | ❌ | ❌ | ❌ | +| đŸ›Ąī¸ Security | SELinux Configuration | ✅ | ❌ | ✅ | +| đŸĒ› Customization | User provided-scripts | ✅ | ❌ | ✅ | +| đŸ› ī¸ Development | Offline validation | ✅ | 🔜 | 🔜 | +| đŸ› ī¸ Development | Debugging log | ✅ | ✅ | ✅ | + +_Notes:_ + +- [1] Trident exclusively supports UEFI booting. BIOS booting is not supported. +- [2] Root verity is supported with grub, but support for this feature + will be deprecated soon. +- [3] A system installed with these features can be updated, but the features + themselves cannot be activated during an update. +- [4] Currently, only PCR 7 is supported. Sealing against other PCRs is + planned for a future release. +- [5] These feature cannot be used in conjunction with root verity. + +### Out-of-Band Features + +These are features that exist outside of the normal servicing flows in Trident. + +| Category | Feature | Status | Notes | +| --------- | ------------ | ------ | ----------------------------------------------------------------- | +| đŸ’Ŋ Storage | RAID Rebuild | ✅ | Rebuild a software RAID array after a physical drive replacement. | + +## Subpages + +[[_TOSP_]] \ No newline at end of file From c0901190fae02e35c86ba91a2304c6bb23fe2d2f Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 16 May 2025 17:52:31 +0000 Subject: [PATCH 16/99] Merged PR 23033: engineering: try getting kernel/initrd/userspace/etc boot times from systemd-analyze and publish as metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Use storm to create new metric using `systemd-analyze` to capture kernel/initrd/userspace/etc times. Related work items: #12142 --- .../testing_baremetal/baremetal-testing.yml | 11 + .../e2e-ab-update-stage-finalize-test-run.yml | 12 + .../stages/testing_common/e2e-test-run.yml | 24 ++ .../stages/testing_vm/netlaunch-testing.yml | 12 + tools/storm/helpers/boot_metrics.go | 224 ++++++++++++++++++ tools/storm/helpers/init.go | 1 + 6 files changed, 284 insertions(+) create mode 100644 tools/storm/helpers/boot_metrics.go diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index 288024730..fb5da07fb 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -271,6 +271,17 @@ stages: displayName: "Output serial log" condition: always() + - bash: | + set -eux + ./bin/storm-trident helper boot-metrics \ + "$(Build.SourcesDirectory)/e2e_tests/helpers/key" \ + "${{ variables.BAREMETAL_OAM_IP }}" \ + "testing-user" \ + "${{ parameters.runtimeEnv }}" \ + --metrics-file $(tridentSourceDirectory)/trident-clean-install-metrics.jsonl \ + --metrics-operation install + displayName: "Create boot metrics for booting into runtime OS" + - template: ../testing_common/trident-metrics.yml parameters: tridentSourceDirectory: $(TRIDENT_SOURCE_DIR) diff --git a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml index 71d801870..b3182dccb 100644 --- a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml @@ -160,6 +160,18 @@ steps: displayName: "🤝 Check SSH connection after booting into runtime OS B" condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) + - bash: | + set -eux + ./bin/storm-trident helper boot-metrics \ + "${{ parameters.sshKeyPath }}" \ + "${{ parameters.hostIp }}" \ + "${{ parameters.userName }}" \ + "${{ parameters.runtimeEnv }}" \ + --metrics-file $(Build.SourcesDirectory)/trident-finalize-update-metrics.jsonl \ + --metrics-operation update1 + displayName: "Create boot metrics for booting into runtime OS B" + condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) + - template: ../testing_common/trident-metrics.yml parameters: tridentSourceDirectory: $(Build.SourcesDirectory) diff --git a/.pipelines/templates/stages/testing_common/e2e-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-test-run.yml index 3273765d2..a02bd95b1 100644 --- a/.pipelines/templates/stages/testing_common/e2e-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-test-run.yml @@ -179,6 +179,18 @@ steps: deploymentLogPath: $(Build.SourcesDirectory)/logstream-full.log displayName: "📄 [TRACE] Display A/B update deployment logs for runtime OS B" + - bash: | + set -eux + ./bin/storm-trident helper boot-metrics \ + "${{ parameters.sshKeyPath }}" \ + "${{ parameters.hostIp }}" \ + "${{ parameters.userName }}" \ + "${{ parameters.runtimeEnv }}" \ + --metrics-file $(Build.SourcesDirectory)/trident-ab-update-metrics-runtime-os-B.jsonl \ + --metrics-operation update1 + displayName: "Create boot metrics for booting into runtime OS B" + condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) + - template: ../testing_common/trident-metrics.yml parameters: tridentSourceDirectory: $(Build.SourcesDirectory) @@ -273,6 +285,18 @@ steps: deploymentLogPath: $(Build.SourcesDirectory)/logstream-full.log displayName: "📄 [TRACE] Display A/B update deployment logs for runtime OS A" + - bash: | + set -eux + ./bin/storm-trident helper boot-metrics \ + "${{ parameters.sshKeyPath }}" \ + "${{ parameters.hostIp }}" \ + "${{ parameters.userName }}" \ + "${{ parameters.runtimeEnv }}" \ + --metrics-file $(Build.SourcesDirectory)/trident-ab-update-metrics-runtime-os-A.jsonl \ + --metrics-operation update2 + displayName: "Create boot metrics for booting into runtime OS A" + condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) + - template: ../testing_common/trident-metrics.yml parameters: tridentSourceDirectory: $(Build.SourcesDirectory) diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index 24dada50a..29e5f9241 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -198,6 +198,18 @@ stages: condition: eq('${{ parameters.runtimeEnv }}', 'container') displayName: "📄 Display Container logs" + - bash: | + set -eux + HOST_IP=$(jq -r '.virtualmachines[0].ip' $(argusToolkitSourceDirectory)/virt-deploy-metadata.json) + ./bin/storm-trident helper boot-metrics \ + "$(tridentSourceDirectory)/e2e_tests/helpers/key" \ + "$HOST_IP" \ + "testing-user" \ + "${{ parameters.runtimeEnv }}" \ + --metrics-file $(tridentSourceDirectory)/trident-clean-install-metrics.jsonl \ + --metrics-operation install + displayName: "Create boot metrics for booting into runtime OS" + - template: ../testing_common/trident-metrics.yml parameters: tridentSourceDirectory: $(tridentSourceDirectory) diff --git a/tools/storm/helpers/boot_metrics.go b/tools/storm/helpers/boot_metrics.go new file mode 100644 index 000000000..abb214368 --- /dev/null +++ b/tools/storm/helpers/boot_metrics.go @@ -0,0 +1,224 @@ +package helpers + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "regexp" + "storm" + "strconv" + "strings" + "time" + "tridenttools/storm/utils" + + "github.com/sirupsen/logrus" +) + +type BootMetricsHelper struct { + args struct { + utils.SshCliSettings `embed:""` + utils.EnvCliSettings `embed:""` + MetricsFile string `required:"" help:"Metrics file." type:"path"` + MetricsOperation string `required:"" help:"Metrics operation."` + } +} + +type BootMetric struct { + Operation string `json:"operation"` + FirmwareMs float64 `json:"firmware,omitempty"` + LoaderMs float64 `json:"loader,omitempty"` + KernelMs float64 `json:"kernel,omitempty"` + InitrdMs float64 `json:"initrd,omitempty"` + UserspaceMs float64 `json:"userspace,omitempty"` +} + +type BootMetrics struct { + Timestamp string `json:"timestamp"` + MetricName string `json:"metric_name"` + Value BootMetric `json:"value"` + AdditionalFields map[string]interface{} `json:"additional_fields"` + PlatformInfo map[string]interface{} `json:"platform_info"` +} + +func (h BootMetricsHelper) Name() string { + return "boot-metrics" +} + +func (h *BootMetricsHelper) Args() any { + return &h.args +} + +func (h *BootMetricsHelper) RegisterTestCases(r storm.TestRegistrar) error { + r.RegisterTestCase("collect-boot-metrics", h.collectBootMetrics) + return nil +} + +func (h *BootMetricsHelper) collectBootMetrics(tc storm.TestCase) error { + if h.args.Env == utils.TridentEnvironmentNone { + tc.Skip("No Trident environment specified") + } + logrus.Infof("Waiting for the host to reboot and come back online...") + + result, err := h.initializeBootMetrics(tc, h.args.MetricsFile) + if err != nil { + tc.FailFromError(err) + } + + value, err := utils.Retry( + time.Second*time.Duration(h.args.Timeout), + time.Second*5, + func(attempt int) (*BootMetric, error) { + var err error = nil + result := BootMetric{} + client, err := utils.OpenSshClient(h.args.SshCliSettings) + if err != nil { + return &result, err + } + + // Expect output in the form of: + // Startup finished in [13.022s (firmware) + 2.552s (loader) + ]? 4.740s (kernel) + 1.267s (initrd) + 15.249s (userspace) = 35.565s + // graphical.target reached after 13.272s in userspace + systemdAnalzeBootResult, err := utils.RunCommand(client, "systemd-analyze | head -n 1") + if err != nil { + return &result, err + } + systemdAnalzeBootOutput := systemdAnalzeBootResult.Stdout + + result.Operation = h.args.MetricsOperation + + if firmwareBoot, units, firmwareBootExists := h.findWordBeforeMatch(tc, systemdAnalzeBootOutput, "(firmware)"); firmwareBootExists { + result.FirmwareMs, err = h.ensureMilliseconds(tc, firmwareBoot, units) + if err != nil { + return &result, err + } + } + if loaderBoot, units, loaderBootExists := h.findWordBeforeMatch(tc, systemdAnalzeBootOutput, "(loader)"); loaderBootExists { + result.LoaderMs, err = h.ensureMilliseconds(tc, loaderBoot, units) + if err != nil { + return &result, err + } + } + if kernelBoot, units, kernelBootExists := h.findWordBeforeMatch(tc, systemdAnalzeBootOutput, "(kernel)"); kernelBootExists { + result.KernelMs, err = h.ensureMilliseconds(tc, kernelBoot, units) + if err != nil { + return &result, err + } + } + if initrdBoot, units, initrdBootExists := h.findWordBeforeMatch(tc, systemdAnalzeBootOutput, "(initrd)"); initrdBootExists { + result.InitrdMs, err = h.ensureMilliseconds(tc, initrdBoot, units) + if err != nil { + return &result, err + } + } + if userspaceBoot, units, userspaceBootExists := h.findWordBeforeMatch(tc, systemdAnalzeBootOutput, "(userspace)"); userspaceBootExists { + result.UserspaceMs, err = h.ensureMilliseconds(tc, userspaceBoot, units) + if err != nil { + return &result, err + } + } + return &result, err + }, + ) + if err != nil { + // Log this as a test failure + tc.FailFromError(err) + } + + result.Value = *value + + jsonBytes, err := json.Marshal(result) + if err != nil { + // Log this as a test failure + tc.FailFromError(err) + } + + file, err := os.OpenFile(h.args.MetricsFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(string(jsonBytes) + "\n") + if err != nil { + return err + } + + return nil +} + +func (h *BootMetricsHelper) ensureMilliseconds(tc storm.TestCase, value string, unit string) (float64, error) { + valueAsDouble, err := strconv.ParseFloat(value, 64) + if err != nil { + logrus.Infof("Failed to parse value: %s", value) + return 0, fmt.Errorf("failed to parse value: %s", value) + } + + switch unit { + case "s": + return valueAsDouble * 1000, nil + case "ms": + return valueAsDouble, nil + case "m": + return valueAsDouble * 60 * 1000, nil + case "ns": + return valueAsDouble / 1000000, nil + } + + return 0, fmt.Errorf("unknown time unit: %s", unit) +} + +func (h *BootMetricsHelper) initializeBootMetrics(tc storm.TestCase, metricsFile string) (BootMetrics, error) { + result := BootMetrics{} + + // Open metrics file + file, err := os.OpenFile(h.args.MetricsFile, os.O_RDONLY, os.ModeAppend) + if err != nil { + return result, err + } + defer file.Close() + + // Read only the first line of the file and parse it as JSON + // This is to ensure that the file is not empty and contains valid JSON + // Read the first line + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + return result, fmt.Errorf("failed to read the first line of the file") + } + + // Decode the first line as JSON + decoder := json.NewDecoder(strings.NewReader(scanner.Text())) + var firstRecord map[string]interface{} + err = decoder.Decode(&firstRecord) + if err != nil { + return result, err + } + + result.Timestamp = time.Now().Format(time.RFC3339) + result.MetricName = "boot_info" + + // Get additional fields from first record + if firstRecord["additional_fields"] != nil { + additionalFields, ok := firstRecord["additional_fields"].(map[string]interface{}) + if ok { + result.AdditionalFields = additionalFields + } + } + // Get platform info from first record + if firstRecord["platform_info"] != nil { + platformInfo, ok := firstRecord["platform_info"].(map[string]interface{}) + if ok { + result.PlatformInfo = platformInfo + } + } + return result, nil +} + +func (h *BootMetricsHelper) findWordBeforeMatch(tc storm.TestCase, text, target string) (string, string, bool) { + re := regexp.MustCompile(`([-+]?\d*\.?\d+)(.)\s+` + regexp.QuoteMeta(target)) + match := re.FindStringSubmatch(text) + if len(match) >= 3 { + return match[1], match[2], true + } + return "", "", false +} diff --git a/tools/storm/helpers/init.go b/tools/storm/helpers/init.go index dae59b1f7..95ac7b424 100644 --- a/tools/storm/helpers/init.go +++ b/tools/storm/helpers/init.go @@ -6,4 +6,5 @@ var TRIDENT_HELPERS = []storm.Helper{ &CheckSshHelper{}, &AbUpdateHelper{}, &PrepareImages{}, + &BootMetricsHelper{}, } From 35820066db63082982ec3e29cf65d295539a7955 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 16 May 2025 22:19:30 +0000 Subject: [PATCH 17/99] Merged PR 23095: engineering: SFI: disable pypi.org with /etc/hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description We continue to get notifications about pypi.org. Try using adding `127.0.0.1 pypi.org` to /etc/hosts to try to disable any process from getting to pypi.org. Maybe this will help determine what is reaching out to pypi.org? requires: * (merged) platform-pipelines: https://dev.azure.com/mariner-org/ECF/_git/platform-pipelines/pullrequest/23152 related to : * (merged) test-images: https://dev.azure.com/mariner-org/ECF/_git/test-images/pullrequest/23151h validation: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=811288&view=results Related work items: #10524 --- .../build_docker_image/trident-container.yml | 2 ++ .../stages/build_image/build-installer.yml | 5 +--- .../stages/build_image/build-runtime.yml | 5 +--- .../stages/building_tools/building-tools.yml | 5 +--- .../stages/common_tasks/avoid-pypi-usage.yml | 7 +++++ .../stages/testing_servicing/build-image.yml | 5 +--- .../testing_servicing/generate-ssh-keys.yml | 5 +--- .../stages/trident_rpms/build-source.yml | 28 ++++--------------- .../templates/stages/trident_rpms/check.yml | 6 +--- .../trident_usb_iso/trident-usb-iso.yml | 5 +--- .../stages/validate_makefile/dev-build.yml | 6 +--- 11 files changed, 22 insertions(+), 57 deletions(-) create mode 100644 .pipelines/templates/stages/common_tasks/avoid-pypi-usage.yml diff --git a/.pipelines/templates/stages/build_docker_image/trident-container.yml b/.pipelines/templates/stages/build_docker_image/trident-container.yml index 710036ee4..40cc7ae13 100644 --- a/.pipelines/templates/stages/build_docker_image/trident-container.yml +++ b/.pipelines/templates/stages/build_docker_image/trident-container.yml @@ -28,6 +28,8 @@ stages: ob_artifactBaseName: "trident-docker-image" steps: + - template: ../common_tasks/avoid-pypi-usage.yml + - script: | set -eux mkdir -p bin/RPMS diff --git a/.pipelines/templates/stages/build_image/build-installer.yml b/.pipelines/templates/stages/build_image/build-installer.yml index 6460160ef..aef73cc85 100644 --- a/.pipelines/templates/stages/build_image/build-installer.yml +++ b/.pipelines/templates/stages/build_image/build-installer.yml @@ -62,10 +62,7 @@ stages: BASEIMG_AZURE_LINUX_VERSION: "3.0" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" + - template: ../common_tasks/avoid-pypi-usage.yml - task: DownloadPipelineArtifact@2 inputs: diff --git a/.pipelines/templates/stages/build_image/build-runtime.yml b/.pipelines/templates/stages/build_image/build-runtime.yml index d742ded27..56c853d8b 100644 --- a/.pipelines/templates/stages/build_image/build-runtime.yml +++ b/.pipelines/templates/stages/build_image/build-runtime.yml @@ -59,10 +59,7 @@ stages: BASEIMG_AZURE_LINUX_VERSION: "3.0" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" + - template: ../common_tasks/avoid-pypi-usage.yml - task: DownloadPipelineArtifact@2 inputs: diff --git a/.pipelines/templates/stages/building_tools/building-tools.yml b/.pipelines/templates/stages/building_tools/building-tools.yml index 2bc80fbc0..ff5e6c694 100644 --- a/.pipelines/templates/stages/building_tools/building-tools.yml +++ b/.pipelines/templates/stages/building_tools/building-tools.yml @@ -37,10 +37,7 @@ stages: ob_artifactBaseName: go-tools steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + - template: ../common_tasks/avoid-pypi-usage.yml - ${{ if eq(parameters.buildNetlaunch, true) }}: - bash: | diff --git a/.pipelines/templates/stages/common_tasks/avoid-pypi-usage.yml b/.pipelines/templates/stages/common_tasks/avoid-pypi-usage.yml new file mode 100644 index 000000000..2059ce9c1 --- /dev/null +++ b/.pipelines/templates/stages/common_tasks/avoid-pypi-usage.yml @@ -0,0 +1,7 @@ +steps: + - task: PipAuthenticate@1 + displayName: Provision - Authenticate Pip + inputs: + artifactFeeds: "mariner/Mariner-Pypi-Feed" + + - template: common/sfi-enforce-isolation-with-etc-hosts.yaml@platform-pipelines diff --git a/.pipelines/templates/stages/testing_servicing/build-image.yml b/.pipelines/templates/stages/testing_servicing/build-image.yml index 2d0dfa9e8..d9a62072d 100644 --- a/.pipelines/templates/stages/testing_servicing/build-image.yml +++ b/.pipelines/templates/stages/testing_servicing/build-image.yml @@ -63,10 +63,7 @@ jobs: ob_artifactBaseName: "image-${{ parameters.label }}" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + - template: ../common_tasks/avoid-pypi-usage.yml - task: DownloadPipelineArtifact@2 inputs: diff --git a/.pipelines/templates/stages/testing_servicing/generate-ssh-keys.yml b/.pipelines/templates/stages/testing_servicing/generate-ssh-keys.yml index 63df1c68d..12f1da76c 100644 --- a/.pipelines/templates/stages/testing_servicing/generate-ssh-keys.yml +++ b/.pipelines/templates/stages/testing_servicing/generate-ssh-keys.yml @@ -10,10 +10,7 @@ jobs: ob_artifactBaseName: "ssh-keys" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + - template: ../common_tasks/avoid-pypi-usage.yml - bash: | set -eux diff --git a/.pipelines/templates/stages/trident_rpms/build-source.yml b/.pipelines/templates/stages/trident_rpms/build-source.yml index cb30138fe..49f14614a 100644 --- a/.pipelines/templates/stages/trident_rpms/build-source.yml +++ b/.pipelines/templates/stages/trident_rpms/build-source.yml @@ -38,11 +38,7 @@ stages: value: "$(Build.SourcesDirectory)/trident/out" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" - + - template: ../common_tasks/avoid-pypi-usage.yml - template: check.yml - job: TestTrident @@ -55,11 +51,7 @@ stages: value: "$(Build.SourcesDirectory)/trident/out" steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" - + - template: ../common_tasks/avoid-pypi-usage.yml # need newer rust for cargo-nextest, use rustup.yml to install - template: ../common_tasks/rustup.yml - template: ../common_tasks/cargo-auth.yml @@ -76,10 +68,7 @@ stages: steps: - template: ../common_tasks/os-info.yml - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" + - template: ../common_tasks/avoid-pypi-usage.yml - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/coverage.yml parameters: @@ -101,10 +90,7 @@ stages: steps: - template: ../common_tasks/os-info.yml - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" + - template: ../common_tasks/avoid-pypi-usage.yml - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/build-osmodifier.yml - template: release.yml @@ -130,11 +116,7 @@ stages: buildType: auto steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: "mariner/Mariner-Pypi-Feed" - + - template: ../common_tasks/avoid-pypi-usage.yml - template: ../common_tasks/cargo-auth.yml - template: ../common_tasks/build-osmodifier.yml - template: release.yml diff --git a/.pipelines/templates/stages/trident_rpms/check.yml b/.pipelines/templates/stages/trident_rpms/check.yml index 7c4e95e23..6ad8a39e8 100644 --- a/.pipelines/templates/stages/trident_rpms/check.yml +++ b/.pipelines/templates/stages/trident_rpms/check.yml @@ -5,11 +5,7 @@ steps: - template: ../common_tasks/rustup.yml - template: ../common_tasks/cargo-auth.yml - - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + - template: ../common_tasks/avoid-pypi-usage.yml - script: | set -eux diff --git a/.pipelines/templates/stages/trident_usb_iso/trident-usb-iso.yml b/.pipelines/templates/stages/trident_usb_iso/trident-usb-iso.yml index 16cc77e86..f3067f8a9 100644 --- a/.pipelines/templates/stages/trident_usb_iso/trident-usb-iso.yml +++ b/.pipelines/templates/stages/trident_usb_iso/trident-usb-iso.yml @@ -55,10 +55,7 @@ stages: fetchDepth: 1 path: s/test-images - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' + - template: ../common_tasks/avoid-pypi-usage.yml - task: DownloadPipelineArtifact@2 inputs: diff --git a/.pipelines/templates/stages/validate_makefile/dev-build.yml b/.pipelines/templates/stages/validate_makefile/dev-build.yml index 1c1e7b752..3f9db059a 100644 --- a/.pipelines/templates/stages/validate_makefile/dev-build.yml +++ b/.pipelines/templates/stages/validate_makefile/dev-build.yml @@ -15,11 +15,7 @@ stages: ob_outputDirectory: $(Build.SourcesDirectory)/build steps: - - task: PipAuthenticate@1 - displayName: Provision - Authenticate Pip - inputs: - artifactFeeds: 'mariner/Mariner-Pypi-Feed' - + - template: ../common_tasks/avoid-pypi-usage.yml - template: ../common_tasks/build-osmodifier.yml - script: | From 9c834fa03512f380082082e5adb900afb132d62a Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Fri, 16 May 2025 23:05:36 +0000 Subject: [PATCH 18/99] Merged PR 23166: Fix 'make download-runtime-images' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description What is this PR about? feature/doc/engineering/bug? # 🤔 Rationale Why is this PR needed? # 📝 Checks - [ ] Check [dev-docs/manual-validation.md](/dev-docs/manual-validation.md) # 📌 Follow-ups TODO: - #0000 # đŸ—’ī¸ Notes Fix 'make download-runtime-images' Related work items: #12207 --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d82acfc87..d4431b574 100644 --- a/Makefile +++ b/Makefile @@ -444,7 +444,8 @@ download-runtime-images: rm -rf ./artifacts/test-image mkdir -p ./artifacts/test-image # Move regular COSI image - mv $(DOWNLOAD_DIR)/*.cosi ./artifacts/test-image/regular.cosi + mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/regular.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/regular2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) @@ -458,7 +459,8 @@ download-runtime-images: --artifact-name 'trident-verity-testimage' # Move verity COSI image - mv $(DOWNLOAD_DIR)/*.cosi ./artifacts/test-image/verity.cosi + mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/verity.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/verity2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) @@ -472,7 +474,8 @@ download-runtime-images: --artifact-name 'trident-container-testimage' # Move container COSI image - mv $(DOWNLOAD_DIR)/*.cosi ./artifacts/test-image/container.cosi + mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/container.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/container2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) From 1d59a3e122a78e862094a570b70dc43cae136880 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 19 May 2025 17:07:09 +0000 Subject: [PATCH 19/99] Merged PR 23171: engineering: Stream Update Test Images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Previously we were scp'ing the images onto the host, but we already have netlisten running anyway, so we can simplify and use that instead. Related work items: #12213 --- .../e2e-ab-update-stage-finalize-test-run.yml | 21 ++------ .../stages/testing_common/e2e-test-run.yml | 50 +------------------ .../scripts/transfer-update-os-image.sh | 35 ------------- Makefile | 38 ++++++++++---- tools/storm/helpers/ab_update.go | 45 +++++++++++++---- 5 files changed, 69 insertions(+), 120 deletions(-) delete mode 100644 .pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh diff --git a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml index b3182dccb..712e04cbc 100644 --- a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml @@ -53,31 +53,21 @@ parameters: default: 4000 steps: - - bash: | - set -eux - "$(Build.SourcesDirectory)/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh" \ - "${{ parameters.sshKeyPath }}" \ - "${{ parameters.userName }}" \ - "${{ parameters.hostIp }}" \ - "${{ parameters.artifactsDirectory }}" \ - "$(destinationDirectory)" \ - "$(version)" \ - "$(verityRequired)" - displayName: "Transfer updated OS image to the host for A/B update testing" - condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) - - bash: | set -eux # If there is a netlisten process, kill it so there is no port clash in the instance if pgrep netlisten > /dev/null; then pkill netlisten; fi - ./bin/netlisten -m $(Build.SourcesDirectory)/trident-stage-update-metrics.jsonl -p ${{ parameters.netlistenPort }} > ./stage-ab-update-deployment.log 2>&1 & + + ./bin/netlisten -m $(Build.SourcesDirectory)/trident-stage-update-metrics.jsonl \ + -p ${{ parameters.netlistenPort }} \ + -s "${{ parameters.artifactsDirectory }}" > ./stage-ab-update-deployment.log 2>&1 & + echo "Running script to stage A/B update..." ./bin/storm-trident helper ab-update \ "${{ parameters.sshKeyPath }}" \ "${{ parameters.hostIp }}" \ "${{ parameters.userName }}" \ "${{ parameters.runtimeEnv }}" \ - --destination-directory $(destinationDirectory) \ --trident-config $(tridentConfigFile) \ --version $(version) \ --stage-ab-update @@ -130,7 +120,6 @@ steps: "${{ parameters.hostIp }}" \ "${{ parameters.userName }}" \ "${{ parameters.runtimeEnv }}" \ - --destination-directory $(destinationDirectory) \ --trident-config $(tridentConfigFile) \ --version $(version) \ --finalize-ab-update diff --git a/.pipelines/templates/stages/testing_common/e2e-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-test-run.yml index a02bd95b1..0ff32f532 100644 --- a/.pipelines/templates/stages/testing_common/e2e-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-test-run.yml @@ -75,25 +75,6 @@ steps: workingDirectory: $(Build.SourcesDirectory)/e2e_tests displayName: "Check if Trident config requires A/B update testing" - # Check if Trident config uses verity. If yes, we need to copy the verity COSI - # to a writable directory /run on the VM. Otherwise, copy the regular COSI to - # /abupdate. - - bash: | - set -eu - verityRequired=$(sudo yq e '.storage.verity != null' "${{ parameters.tridentConfigPath }}/trident-config.yaml") - if [ "$verityRequired" == "true" ]; then - echo "Trident config requires verity runtime OS images" - echo "##vso[task.setvariable variable=verityRequired]true" - echo "##vso[task.setvariable variable=destinationDirectory]/run" - else - echo "Trident config does not require verity runtime OS images" - echo "##vso[task.setvariable variable=verityRequired]false" - echo "##vso[task.setvariable variable=destinationDirectory]/abupdate" - fi - workingDirectory: $(Build.SourcesDirectory)/e2e_tests - displayName: "Check if Trident config requires verity runtime OS images" - condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) - - bash: | $(Build.SourcesDirectory)/bin/storm-trident helper check-ssh \ "${{ parameters.sshKeyPath }}" \ @@ -123,20 +104,6 @@ steps: testRunTitle: ${{ parameters.deploymentEnvironment }}_trident_e2e_tests_${{ parameters.tridentConfigurationName }}_clean_install_$(System.JobAttempt) displayName: "Publish test results for clean install of runtime OS" - - bash: | - set -eux - chmod +x $(Build.SourcesDirectory)/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh - "$(Build.SourcesDirectory)/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh" \ - "${{ parameters.sshKeyPath }}" \ - "${{ parameters.userName }}" \ - "${{ parameters.hostIp }}" \ - "${{ parameters.artifactsDirectory }}" \ - "$(destinationDirectory)" \ - "$(version)" \ - "$(verityRequired)" - displayName: "Transfer update OS image to the host for A/B update testing" - condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) - # If current config requires A/B update testing, execute script to ssh into the host, update # images in the custom Trident config, and re-run Trident to both stage and finalize A/B update. - bash: | @@ -144,6 +111,7 @@ steps: ./bin/netlisten --force-color \ -m $(Build.SourcesDirectory)/trident-ab-update-metrics-runtime-os-B.jsonl \ --full-logstream ./logstream-full.log \ + -s "${{ parameters.artifactsDirectory }}" \ -p ${{ parameters.netlistenPort }} > ./stage-finalize-ab-update-runtime-os-B.log 2>&1 & echo "Running script to stage and finalize A/B update..." @@ -152,7 +120,6 @@ steps: "${{ parameters.hostIp }}" \ "${{ parameters.userName }}" \ "${{ parameters.runtimeEnv }}" \ - --destination-directory $(destinationDirectory) \ --trident-config $(tridentConfigFile) \ --version $(version) \ --stage-ab-update \ @@ -229,19 +196,6 @@ steps: testRunTitle: ${{ parameters.deploymentEnvironment }}_trident_e2e_tests_${{ parameters.tridentConfigurationName }}_ab_update_B_$(System.JobAttempt) displayName: "Publish test results for A/B update into runtime OS B" - - bash: | - set -eux - "$(Build.SourcesDirectory)/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh" \ - "${{ parameters.sshKeyPath }}" \ - "${{ parameters.userName }}" \ - "${{ parameters.hostIp }}" \ - "${{ parameters.artifactsDirectory }}" \ - "$(destinationDirectory)" \ - "$(version)" \ - "$(verityRequired)" - displayName: "Transfer update OS image to the host for A/B update testing" - condition: and(succeeded(), ne(variables['abActiveVolume'], 'null')) - - bash: | set -eux # If there is a netlisten process, kill it so there is no port clash in the instance @@ -250,6 +204,7 @@ steps: ./bin/netlisten --force-color \ -m $(Build.SourcesDirectory)/trident-ab-update-metrics-runtime-os-A.jsonl \ --full-logstream ./logstream-full.log \ + -s "${{ parameters.artifactsDirectory }}" \ -p ${{ parameters.netlistenPort }} > ./stage-finalize-ab-update-runtime-os-A.log 2>&1 & echo "Running script to stage and finalize A/B update..." @@ -258,7 +213,6 @@ steps: "${{ parameters.hostIp }}" \ "${{ parameters.userName }}" \ "${{ parameters.runtimeEnv }}" \ - --destination-directory $(destinationDirectory) \ --trident-config $(tridentConfigFile) \ --version $(version) \ --stage-ab-update \ diff --git a/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh b/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh deleted file mode 100644 index 64e8aa493..000000000 --- a/.pipelines/templates/stages/testing_common/scripts/transfer-update-os-image.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# transfer-update-os-image.sh - -set -eux - -# Arguments -SSH_KEY_PATH=$1 -USER_NAME=$2 -HOST_IP=$3 -ARTIFACTS_DIR=$4 -DESTINATION_DIR=$5 -VERSION=$6 -VERITY_REQUIRED=$7 - -FILE_NAME_BASE="regular" - -if [ "$VERITY_REQUIRED" = "true" ]; then - FILE_NAME_BASE="verity" -fi - -COSI_FILE_NAME="${FILE_NAME_BASE}_v${VERSION}.cosi" -LOCAL_FILE="$ARTIFACTS_DIR/$COSI_FILE_NAME" -REMOTE_FILE="$DESTINATION_DIR/$COSI_FILE_NAME" - -echo "Transferring $LOCAL_FILE to the host into $REMOTE_FILE" - -# Create destination directory on the host it it doesn't exist -ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$USER_NAME"@"$HOST_IP" "sudo mkdir -p '$DESTINATION_DIR'" - -# Prepare destination directory on the host -ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$USER_NAME"@"$HOST_IP" "sudo chown '$USER_NAME:$USER_NAME' '$DESTINATION_DIR' && sudo chmod 755 '$DESTINATION_DIR'" - -# SCP the file onto the host -scp -o StrictHostKeyChecking=no -i "$SSH_KEY_PATH" "$LOCAL_FILE" "$USER_NAME"@"$HOST_IP":"$REMOTE_FILE" -echo "Transferred COSI file to the host" diff --git a/Makefile b/Makefile index d4431b574..88deede44 100644 --- a/Makefile +++ b/Makefile @@ -432,6 +432,12 @@ download-runtime-images: --top 1 \ --query '[0].id')) @echo PIPELINE RUN ID: $(RUN_ID) + +# Clean & create artifacts dir + rm -rf ./artifacts/test-image + mkdir -p ./artifacts/test-image + +# Get regular image $(eval DOWNLOAD_DIR := $(shell mktemp -d)) az pipelines runs artifact download \ --org 'https://dev.azure.com/mariner-org' \ @@ -440,16 +446,28 @@ download-runtime-images: --path $(DOWNLOAD_DIR) \ --artifact-name 'trident-testimage' -# Clean & create artifacts dir - rm -rf ./artifacts/test-image - mkdir -p ./artifacts/test-image -# Move regular COSI image +# Move COSI images mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/regular.cosi - mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/regular2.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/regular_v2.cosi +# Clean temp dir + rm -rf $(DOWNLOAD_DIR) + +# Get usr-verity image + $(eval DOWNLOAD_DIR := $(shell mktemp -d)) + az pipelines runs artifact download \ + --org 'https://dev.azure.com/mariner-org' \ + --project "ECF" \ + --run-id $(RUN_ID) \ + --path $(DOWNLOAD_DIR) \ + --artifact-name 'trident-usrverity-testimage' + +# Move COSI images + mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/usrverity.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/usrverity_v2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) -# Get verity image +# Get root-verity image $(eval DOWNLOAD_DIR := $(shell mktemp -d)) az pipelines runs artifact download \ --org 'https://dev.azure.com/mariner-org' \ @@ -458,9 +476,9 @@ download-runtime-images: --path $(DOWNLOAD_DIR) \ --artifact-name 'trident-verity-testimage' -# Move verity COSI image +# Move COSI images mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/verity.cosi - mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/verity2.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/verity_v2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) @@ -473,9 +491,9 @@ download-runtime-images: --path $(DOWNLOAD_DIR) \ --artifact-name 'trident-container-testimage' -# Move container COSI image +# Move COSI images mv $(DOWNLOAD_DIR)/*_0.cosi ./artifacts/test-image/container.cosi - mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/container2.cosi + mv $(DOWNLOAD_DIR)/*_1.cosi ./artifacts/test-image/container_v2.cosi # Clean temp dir rm -rf $(DOWNLOAD_DIR) diff --git a/tools/storm/helpers/ab_update.go b/tools/storm/helpers/ab_update.go index ac2488863..06f0abf64 100644 --- a/tools/storm/helpers/ab_update.go +++ b/tools/storm/helpers/ab_update.go @@ -2,6 +2,7 @@ package helpers import ( "fmt" + "net/http" "path" "regexp" "strings" @@ -20,7 +21,6 @@ type AbUpdateHelper struct { args struct { utils.SshCliSettings `embed:""` utils.EnvCliSettings `embed:""` - DestinationDirectory string `short:"d" required:"" help:"Read-write directory on the host that contains the runtime OS images for the A/B update."` TridentConfig string `short:"c" required:"" help:"File name of the custom read-write Trident config on the host to point Trident to."` Version string `short:"v" required:"" help:"Version of the Trident image to use for the A/B update."` StageAbUpdate bool `short:"s" help:"Controls whether A/B update should be staged."` @@ -98,7 +98,19 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { logrus.Infof("Old image URL: %s", oldUrl) + // Extract the base name of the image URL base := path.Base(oldUrl) + if base == "" { + return fmt.Errorf("failed to get base name from URL: %s", oldUrl) + } + + // Then extract everything but the base by removing it as a suffix + urlPath, ok := strings.CutSuffix(oldUrl, base) + if !ok { + return fmt.Errorf("failed to remove suffix '%s' from URL '%s'", base, oldUrl) + } + + logrus.Debugf("Base name: %s", base) matches := regexp.MustCompile(`^(.*?)(_v\d+)?\.(.+)$`).FindStringSubmatch(base) @@ -110,11 +122,17 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { ext := matches[3] newCosiName := fmt.Sprintf("%s_v%s.%s", name, h.args.Version, ext) - newCosiPath := path.Join(h.args.DestinationDirectory, newCosiName) - tridentCosiPath := path.Join(h.args.Env.HostPath(), newCosiPath) - newUrl := fmt.Sprintf("file://%s", tridentCosiPath) + newUrl := fmt.Sprintf("%s/%s", urlPath, newCosiName) logrus.Infof("New image URL: %s", newUrl) + logrus.Infof("Checking if new image URL is accessible...") + err := checkUrlIsAccessible(newUrl) + if err != nil { + logrus.WithError(err).Errorf("New image URL is not accessible: %s (continuing)", newUrl) + } else { + logrus.Infof("New image URL is accessible") + } + // Update the image URL in the configuration h.config["image"].(map[string]any)["url"] = newUrl @@ -142,13 +160,6 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { defer sftpClient.Close() // Ensure the cosi file exists - logrus.Infof("Checking if new COSI file exists at %s", newCosiPath) - _, err = sftpClient.Stat(newCosiPath) - if err != nil { - fmt.Println("Yielding to the error") - return fmt.Errorf("failed to stat new COSI file at %s: %w", newCosiPath, err) - } - err = sftpClient.MkdirAll(path.Dir(h.args.TridentConfig)) if err != nil { return fmt.Errorf("failed to create directory for new Host Config file: %w", err) @@ -283,3 +294,15 @@ func (h *AbUpdateHelper) checkTridentService(tc storm.TestCase) error { return nil } + +func checkUrlIsAccessible(url string) error { + resp, err := http.Head(url) + if err != nil { + return fmt.Errorf("failed to check new image URL: %w", err) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("new image URL is not accessible: %s, got HTTP code: %d", url, resp.StatusCode) + } + + return nil +} From 709b5ffc3b6edd8f57cff8cd3526534e7e17d80d Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 19 May 2025 17:09:04 +0000 Subject: [PATCH 20/99] Merged PR 23178: bug: use bm variable for metrics path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description PR last week introduced bug where metrics file path is specified incorrectly. ---- #### AI description (iteration 1) #### PR Classification Bug fix addressing an incorrect variable reference in the metrics file path. #### PR Summary This pull request corrects the variable used for specifying the metrics file path in the baremetal testing pipeline configuration, ensuring that the correct environment variable is referenced. - `.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml`: Changed the metrics file parameter from `$(tridentSourceDirectory)` to `$(TRIDENT_SOURCE_DIR)`. Related work items: #12214 --- .../templates/stages/testing_baremetal/baremetal-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index fb5da07fb..693c13ac5 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -278,7 +278,7 @@ stages: "${{ variables.BAREMETAL_OAM_IP }}" \ "testing-user" \ "${{ parameters.runtimeEnv }}" \ - --metrics-file $(tridentSourceDirectory)/trident-clean-install-metrics.jsonl \ + --metrics-file $(TRIDENT_SOURCE_DIR)/trident-clean-install-metrics.jsonl \ --metrics-operation install displayName: "Create boot metrics for booting into runtime OS" From fa18bafc86074043c311dd5ed8e26393a9e3217c Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 20 May 2025 20:15:05 +0000 Subject: [PATCH 21/99] Merged PR 23158: engineering: Extensive testing of usr-verity sans UKI+RAID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Swap places between root-verity and usr-verity to test the latter more thoroughly and reduce the scope of root-verity to just a limited scenario in preparation for incoming changes to the feature. Depends on: !23171 ---- #### AI description (iteration 1) #### PR Classification This PR implements extensive test configuration updates for usr-verity, replacing previous verity settings with new usr-verity definitions. #### PR Summary The pull request revises multiple e2e test configuration files to support usr-verity testing by updating storage IDs, RAID groups, mount points, and image URLs. - `e2e_tests/trident_configurations/combined/trident-config.yaml`: Renamed storage and RAID identifiers from root/verity to usr-based names and updated the image URL to usrverity.cosi. - `e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml`: Added a new configuration file with usr-verity RAID settings, including updated partition and RAID definitions. - `e2e_tests/trident_configurations/usr-verity/trident-config.yaml`: Revised partition layouts, volume pairs, and verity settings to align with the usr-verity naming convention. - `e2e_tests/target-configurations.yaml`: Adjusted test suite mappings to replace legacy verity and verity-raid entries with usr-verity and root-verity targets. - Various test-selection and other configuration files: Updated internal parameters and image URLs from verity.cosi to usrverity.cosi for consistent usr-verity testing. Related work items: #12233 --- e2e_tests/target-configurations.yaml | 200 +++++++++++------- .../combined/test-selection.yaml | 2 +- .../combined/trident-config.yaml | 178 +++++++--------- .../test-selection.yaml | 2 +- .../trident-config.yaml | 12 +- .../rerun/test-selection.yaml | 2 +- .../rerun/trident-config.yaml | 179 +++++++--------- .../test-selection.yaml | 0 .../trident-config.yaml | 74 ++++--- .../test-selection.yaml | 2 +- .../usr-verity-raid/trident-config.yaml | 97 +++++++++ .../usr-verity/trident-config.yaml | 46 +++- .../verity-raid/trident-config.yaml | 113 ---------- tools/cmd/storm-trident/main.go | 11 +- trident_api/src/error.rs | 2 +- 15 files changed, 478 insertions(+), 442 deletions(-) rename e2e_tests/trident_configurations/{verity-raid => root-verity}/test-selection.yaml (100%) rename e2e_tests/trident_configurations/{verity => root-verity}/trident-config.yaml (70%) rename e2e_tests/trident_configurations/{verity => usr-verity-raid}/test-selection.yaml (58%) create mode 100644 e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml delete mode 100644 e2e_tests/trident_configurations/verity-raid/trident-config.yaml diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index ab2ba1ee0..20458ce3e 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -2,240 +2,286 @@ bareMetal: host: daily: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - split - usr-verity validation: - base - - combined - - raid-big - - raid-resync-small + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - misc + - raid-big - raid-mirrored - - rerun + - raid-resync-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid weekly: - base - - combined - - raid-big - - raid-resync-small + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - misc + - raid-big - raid-mirrored - - rerun + - raid-resync-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid container: daily: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid validation: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid weekly: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid virtualMachine: host: daily: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - - raid-mirrored - misc - - raid-small + - raid-mirrored - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid post_merge: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - - raid-mirrored - misc - - raid-small + - raid-mirrored - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid pullrequest: - base - - combined + # TODO(12232): enable + # - combined - raid-mirrored - raid-resync-small - - rerun + # TODO(12232): enable + # - rerun - simple - split - usr-verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid validation: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid weekly: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - memory-constraint-combined - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - split - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid container: daily: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid post_merge: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid pullrequest: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - raid-mirrored - raid-resync-small - - rerun - - usr-verity + # TODO(12232): enable + # - rerun - simple - - verity-raid + - usr-verity + # TODO(12232): enable + # - usr-verity-raid validation: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid weekly: - base - - combined + # TODO(12232): enable + # - combined - encrypted-partition - encrypted-raid - encrypted-swap - misc - raid-mirrored - - raid-small - raid-resync-small - - rerun + - raid-small + # TODO(12232): enable + # - rerun + - root-verity - simple - usr-verity - - verity - - verity-raid + # TODO(12232): enable + # - usr-verity-raid diff --git a/e2e_tests/trident_configurations/combined/test-selection.yaml b/e2e_tests/trident_configurations/combined/test-selection.yaml index a1d3d4ad0..bb3138711 100644 --- a/e2e_tests/trident_configurations/combined/test-selection.yaml +++ b/e2e_tests/trident_configurations/combined/test-selection.yaml @@ -1,4 +1,4 @@ compatible: - base - - verity + - usr_verity - encryption diff --git a/e2e_tests/trident_configurations/combined/trident-config.yaml b/e2e_tests/trident_configurations/combined/trident-config.yaml index 3e49d8f35..8c97e0664 100644 --- a/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -1,5 +1,7 @@ +internalParams: + uki: true image: - url: http://NETLAUNCH_HOST_ADDRESS/files/verity.cosi + url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored storage: disks: @@ -16,30 +18,38 @@ storage: - id: boot-b type: xbootldr size: 200M - - id: root-data-a-1 - type: root + - id: root-a-1 size: 4G - - id: root-data-b-1 - type: root + - id: root-b-1 size: 4G - - id: root-data-a-2 - type: root + - id: root-a-2 size: 4G - - id: root-data-b-2 - type: root + - id: root-b-2 size: 4G - - id: root-hash-a-1 + - id: usr-data-a-1 + type: root + size: 2G + - id: usr-data-b-1 + type: root + size: 2G + - id: usr-data-a-2 + type: root + size: 2G + - id: usr-data-b-2 + type: root + size: 2G + - id: usr-hash-a-1 type: root-verity - size: 1G - - id: root-hash-b-1 + size: 256M + - id: usr-hash-b-1 type: root-verity - size: 1G - - id: root-hash-a-2 + size: 256M + - id: usr-hash-a-2 type: root-verity - size: 1G - - id: root-hash-b-2 + size: 256M + - id: usr-hash-b-2 type: root-verity - size: 1G + size: 256M - id: web-a-e-1 type: linux-generic size: 1G @@ -52,21 +62,6 @@ storage: - id: web-b-e-2 type: linux-generic size: 1G - - id: home - type: linux-generic - size: 100M - - id: trident-overlay-a-1 - type: linux-generic - size: 100M - - id: trident-overlay-b-1 - type: linux-generic - size: 100M - - id: trident-overlay-a-2 - type: linux-generic - size: 100M - - id: trident-overlay-b-2 - type: linux-generic - size: 100M - id: trident type: linux-generic size: 500M @@ -87,6 +82,18 @@ storage: deviceId: web-b-e raid: software: + - id: root-a + name: root-a + level: raid1 + devices: + - root-a-1 + - root-a-2 + - id: root-b + name: root-b + level: raid1 + devices: + - root-b-1 + - root-b-2 - id: web-a-e name: web-a-e level: raid1 @@ -99,103 +106,72 @@ storage: devices: - web-b-e-1 - web-b-e-2 - - id: root-data-a - name: root-data-a - level: raid1 - devices: - - root-data-a-1 - - root-data-a-2 - - id: root-data-b - name: root-data-b - level: raid1 - devices: - - root-data-b-1 - - root-data-b-2 - - id: root-hash-a - name: root-hash-a + - id: usr-data-a + name: usr-data-a level: raid1 devices: - - root-hash-a-1 - - root-hash-a-2 - - id: root-hash-b - name: root-hash-b + - usr-data-a-1 + - usr-data-a-2 + - id: usr-data-b + name: usr-data-b level: raid1 devices: - - root-hash-b-1 - - root-hash-b-2 - - id: trident-overlay-a - name: trident-overlay-a + - usr-data-b-1 + - usr-data-b-2 + - id: usr-hash-a + name: usr-hash-a level: raid1 devices: - - trident-overlay-a-1 - - trident-overlay-a-2 - - id: trident-overlay-b - name: trident-overlay-b + - usr-hash-a-1 + - usr-hash-a-2 + - id: usr-hash-b + name: usr-hash-b level: raid1 devices: - - trident-overlay-b-1 - - trident-overlay-b-2 + - usr-hash-b-1 + - usr-hash-b-2 abUpdate: volumePairs: - id: boot volumeAId: boot-a volumeBId: boot-b - - id: root-data - volumeAId: root-data-a - volumeBId: root-data-b - - id: root-hash - volumeAId: root-hash-a - volumeBId: root-hash-b - - id: trident-overlay - volumeAId: trident-overlay-a - volumeBId: trident-overlay-b + - id: root + volumeAId: root-a + volumeBId: root-b + - id: usr-data + volumeAId: usr-data-a + volumeBId: usr-data-b + - id: usr-hash + volumeAId: usr-hash-a + volumeBId: usr-hash-b - id: web volumeAId: web-a volumeBId: web-b verity: - - id: root - name: root - dataDeviceId: root-data - hashDeviceId: root-hash + - id: usr + name: usr + dataDeviceId: usr-data + hashDeviceId: usr-hash filesystems: - - deviceId: web - source: new - mountPoint: - path: /web - options: defaults - - deviceId: home - source: new - mountPoint: /home - deviceId: esp mountPoint: path: /boot/efi options: umask=0077 - - deviceId: trident-overlay - source: new - mountPoint: /var/lib/trident-overlay - deviceId: boot mountPoint: /boot - - deviceId: trident - source: new - mountPoint: /var/lib/trident - - deviceId: var - mountPoint: /var - deviceId: root + mountPoint: / + - deviceId: usr mountPoint: - path: / + path: /usr options: defaults,ro -scripts: - postConfigure: - - name: overlay - runOn: - - clean-install - - ab-update - content: | - mkdir -p /var/lib/trident-overlay/etc-rw/upper - mkdir -p /var/lib/trident-overlay/etc-rw/work + - deviceId: web + source: new + mountPoint: /web + - deviceId: trident + source: new + mountPoint: /var/lib/trident os: - selinux: - mode: permissive network: version: 2 ethernets: diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/test-selection.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/test-selection.yaml index a1d3d4ad0..bb3138711 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/test-selection.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/test-selection.yaml @@ -1,4 +1,4 @@ compatible: - base - - verity + - usr_verity - encryption diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index cafb4c278..1bda5738e 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -1,5 +1,7 @@ +internalParams: + uki: true image: - url: http://NETLAUNCH_HOST_ADDRESS/files/verity.cosi + url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored storage: disks: @@ -198,14 +200,6 @@ scripts: touch "/run/already-run" exit 1 fi - postConfigure: - - name: overlay - runOn: - - clean-install - - ab-update - content: | - mkdir -p /var/lib/trident-overlay/etc-rw/upper - mkdir -p /var/lib/trident-overlay/etc-rw/work os: selinux: mode: permissive diff --git a/e2e_tests/trident_configurations/rerun/test-selection.yaml b/e2e_tests/trident_configurations/rerun/test-selection.yaml index a1d3d4ad0..bb3138711 100644 --- a/e2e_tests/trident_configurations/rerun/test-selection.yaml +++ b/e2e_tests/trident_configurations/rerun/test-selection.yaml @@ -1,4 +1,4 @@ compatible: - base - - verity + - usr_verity - encryption diff --git a/e2e_tests/trident_configurations/rerun/trident-config.yaml b/e2e_tests/trident_configurations/rerun/trident-config.yaml index d36e68214..b38ea5369 100644 --- a/e2e_tests/trident_configurations/rerun/trident-config.yaml +++ b/e2e_tests/trident_configurations/rerun/trident-config.yaml @@ -1,5 +1,7 @@ +internalParams: + uki: true image: - url: http://NETLAUNCH_HOST_ADDRESS/files/verity.cosi + url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored storage: disks: @@ -13,31 +15,39 @@ storage: - id: boot-a type: xbootldr size: 200M + - id: root-a-1 + size: 4G + - id: root-a-2 + size: 4G + - id: root-b-1 + size: 4G + - id: root-b-2 + size: 4G - id: boot-b type: xbootldr size: 200M - - id: root-data-a-1 + - id: usr-data-a-1 type: root - size: 4G - - id: root-data-b-1 + size: 2G + - id: usr-data-b-1 type: root - size: 4G - - id: root-data-a-2 + size: 2G + - id: usr-data-a-2 type: root - size: 4G - - id: root-data-b-2 + size: 2G + - id: usr-data-b-2 type: root - size: 4G - - id: root-hash-a-1 + size: 2G + - id: usr-hash-a-1 type: root-verity size: 1G - - id: root-hash-b-1 + - id: usr-hash-b-1 type: root-verity size: 1G - - id: root-hash-a-2 + - id: usr-hash-a-2 type: root-verity size: 1G - - id: root-hash-b-2 + - id: usr-hash-b-2 type: root-verity size: 1G - id: home-a-e-1 @@ -52,81 +62,66 @@ storage: - id: home-b-e-2 type: linux-generic size: 100M - - id: trident-overlay-a-1 - type: linux-generic - size: 100M - - id: trident-overlay-b-1 - type: linux-generic - size: 100M - - id: trident-overlay-a-2 - type: linux-generic - size: 100M - - id: trident-overlay-b-2 - type: linux-generic - size: 100M - id: trident type: linux-generic size: 500M - id: srv-e type: linux-generic size: 1G - - id: var - type: linux-generic - size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt partitions: [] raid: software: - - id: home-a-e - name: home-a-e + - id: usr-data-a + name: usr-data-a level: raid1 devices: - - home-a-e-1 - - home-a-e-2 - - id: home-b-e - name: home-b-e + - usr-data-a-1 + - usr-data-a-2 + - id: usr-data-b + name: usr-data-b level: raid1 devices: - - home-b-e-1 - - home-b-e-2 - - id: root-data-a - name: root-data-a + - usr-data-b-1 + - usr-data-b-2 + - id: usr-hash-a + name: usr-hash-a level: raid1 devices: - - root-data-a-1 - - root-data-a-2 - - id: root-data-b - name: root-data-b + - usr-hash-a-1 + - usr-hash-a-2 + - id: usr-hash-b + name: usr-hash-b level: raid1 devices: - - root-data-b-1 - - root-data-b-2 - - id: root-hash-a - name: root-hash-a + - usr-hash-b-1 + - usr-hash-b-2 + - id: root-a + name: root-a level: raid1 devices: - - root-hash-a-1 - - root-hash-a-2 - - id: root-hash-b - name: root-hash-b + - root-a-1 + - root-a-2 + - id: root-b + name: root-b level: raid1 devices: - - root-hash-b-1 - - root-hash-b-2 - - id: trident-overlay-a - name: trident-overlay-a + - root-b-1 + - root-b-2 + - id: home-a-e + name: home-a-e level: raid1 devices: - - trident-overlay-a-1 - - trident-overlay-a-2 - - id: trident-overlay-b - name: trident-overlay-b + - home-a-e-1 + - home-a-e-2 + - id: home-b-e + name: home-b-e level: raid1 devices: - - trident-overlay-b-1 - - trident-overlay-b-2 + - home-b-e-1 + - home-b-e-2 encryption: volumes: - id: srv @@ -143,52 +138,45 @@ storage: - id: boot volumeAId: boot-a volumeBId: boot-b - - id: root-data - volumeAId: root-data-a - volumeBId: root-data-b - - id: root-hash - volumeAId: root-hash-a - volumeBId: root-hash-b - - id: trident-overlay - volumeAId: trident-overlay-a - volumeBId: trident-overlay-b + - id: usr-data + volumeAId: usr-data-a + volumeBId: usr-data-b + - id: usr-hash + volumeAId: usr-hash-a + volumeBId: usr-hash-b + - id: root + volumeAId: root-a + volumeBId: root-b - id: home volumeAId: home-a volumeBId: home-b verity: - - id: root - name: root - dataDeviceId: root-data - hashDeviceId: root-hash + - id: usr + name: usr + dataDeviceId: usr-data + hashDeviceId: usr-hash filesystems: - - deviceId: trident - source: new - mountPoint: - path: /var/lib/trident - options: defaults - - deviceId: srv - source: new - mountPoint: - path: /srv - options: defaults - - deviceId: var - mountPoint: /var - - deviceId: home - source: new - mountPoint: /home - deviceId: esp mountPoint: path: /boot/efi options: umask=0077 - deviceId: boot mountPoint: /boot - - deviceId: trident-overlay - source: new - mountPoint: /var/lib/trident-overlay - deviceId: root + mountPoint: / + - deviceId: usr mountPoint: - path: / + path: /usr options: defaults,ro + - deviceId: trident + source: new + mountPoint: /var/lib/trident + - deviceId: srv + source: new + mountPoint: /srv + - deviceId: home + source: new + mountPoint: /home scripts: postProvision: - name: rerun-trident @@ -213,13 +201,6 @@ scripts: trap "umount /tmp/var" EXIT echo fail-on-first-run exit 1 - - name: overlay - runOn: - - clean-install - - ab-update - content: | - mkdir -p /var/lib/trident-overlay/etc-rw/upper - mkdir -p /var/lib/trident-overlay/etc-rw/work os: selinux: mode: permissive diff --git a/e2e_tests/trident_configurations/verity-raid/test-selection.yaml b/e2e_tests/trident_configurations/root-verity/test-selection.yaml similarity index 100% rename from e2e_tests/trident_configurations/verity-raid/test-selection.yaml rename to e2e_tests/trident_configurations/root-verity/test-selection.yaml diff --git a/e2e_tests/trident_configurations/verity/trident-config.yaml b/e2e_tests/trident_configurations/root-verity/trident-config.yaml similarity index 70% rename from e2e_tests/trident_configurations/verity/trident-config.yaml rename to e2e_tests/trident_configurations/root-verity/trident-config.yaml index 96a3d5842..4dc9ac23e 100644 --- a/e2e_tests/trident_configurations/verity/trident-config.yaml +++ b/e2e_tests/trident_configurations/root-verity/trident-config.yaml @@ -7,27 +7,39 @@ storage: device: /dev/disk/by-path/pci-0000:00:1f.2-ata-2 partitionTableType: gpt partitions: - - id: boot + - id: esp + type: esp + size: 1G + - id: boot-a type: xbootldr size: 200M - - id: root-data + - id: boot-b + type: xbootldr + size: 200M + - id: root-data-a + type: root + size: 4G + - id: root-data-b type: root - size: 8G - - id: root-hash + size: 4G + - id: root-hash-a type: root-verity size: 1G - - id: esp - type: esp + - id: root-hash-b + type: root-verity size: 1G - - id: trident + - id: home type: linux-generic - size: 500M - - id: trident-overlay + size: 100M + - id: trident-overlay-a type: linux-generic size: 100M - - id: home + - id: trident-overlay-b type: linux-generic size: 100M + - id: trident + type: linux-generic + size: 500M - id: var type: linux-generic size: 1G @@ -35,9 +47,33 @@ storage: device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt partitions: [] + abUpdate: + volumePairs: + - id: boot + volumeAId: boot-a + volumeBId: boot-b + - id: root-data + volumeAId: root-data-a + volumeBId: root-data-b + - id: root-hash + volumeAId: root-hash-a + volumeBId: root-hash-b + - id: trident-overlay + volumeAId: trident-overlay-a + volumeBId: trident-overlay-b + verity: + - id: root + name: root + dataDeviceId: root-data + hashDeviceId: root-hash filesystems: - - deviceId: var - mountPoint: /var + - deviceId: home + source: new + mountPoint: /home + - deviceId: esp + mountPoint: + path: /boot/efi + options: umask=0077 - deviceId: trident-overlay source: new mountPoint: /var/lib/trident-overlay @@ -46,22 +82,12 @@ storage: - deviceId: trident source: new mountPoint: /var/lib/trident - - deviceId: home - source: new - mountPoint: /home - - deviceId: esp - mountPoint: - path: /boot/efi - options: umask=0077 + - deviceId: var + mountPoint: /var - deviceId: root mountPoint: path: / options: defaults,ro - verity: - - id: root - name: root - dataDeviceId: root-data - hashDeviceId: root-hash scripts: postConfigure: - name: overlay diff --git a/e2e_tests/trident_configurations/verity/test-selection.yaml b/e2e_tests/trident_configurations/usr-verity-raid/test-selection.yaml similarity index 58% rename from e2e_tests/trident_configurations/verity/test-selection.yaml rename to e2e_tests/trident_configurations/usr-verity-raid/test-selection.yaml index e14974f13..9cdb5a029 100644 --- a/e2e_tests/trident_configurations/verity/test-selection.yaml +++ b/e2e_tests/trident_configurations/usr-verity-raid/test-selection.yaml @@ -1,3 +1,3 @@ compatible: - base - - verity + - usr_verity diff --git a/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml new file mode 100644 index 000000000..06d0e902c --- /dev/null +++ b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml @@ -0,0 +1,97 @@ +internalParams: + uki: true +image: + url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi + sha384: ignored +storage: + disks: + - id: os + device: /dev/disk/by-path/pci-0000:00:1f.2-ata-2 + partitionTableType: gpt + partitions: + - id: esp + type: esp + size: 1G + - id: boot + size: 200M + - id: root-1 + size: 4G + - id: usr-data-1 + size: 2G + - id: usr-hash-1 + size: 1G + - id: trident-1 + size: 128M + - id: disk2 + device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 + partitionTableType: gpt + partitions: + - id: root-2 + size: 4G + - id: usr-data-2 + size: 2G + - id: usr-hash-2 + size: 1G + - id: trident-2 + size: 128M + raid: + software: + - id: root + name: root + level: raid1 + devices: + - root-1 + - root-2 + - id: usr-data + name: usr-data + level: raid1 + devices: + - usr-data-1 + - usr-data-2 + - id: usr-hash + name: usr-hash + level: raid1 + devices: + - usr-hash-1 + - usr-hash-2 + - id: trident + name: trident + level: raid1 + devices: + - trident-1 + - trident-2 + verity: + - id: usr + name: usr + dataDeviceId: usr-data + hashDeviceId: usr-hash + filesystems: + - deviceId: esp + mountPoint: + path: /boot/efi + options: umask=0077 + - deviceId: boot + mountPoint: /boot + - deviceId: root + mountPoint: / + - deviceId: usr + mountPoint: + path: /usr + options: defaults,ro + - deviceId: trident + source: new + mountPoint: /var/lib/trident +os: + network: + version: 2 + ethernets: + vmeths: + match: + name: enp* + dhcp4: true + users: + - name: testing-user + sshPublicKeys: [] + secondaryGroups: + - wheel + sshMode: key-only diff --git a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml index ed3aeedc0..8889990c8 100644 --- a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml +++ b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml @@ -12,20 +12,47 @@ storage: partitionTableType: gpt partitions: - id: esp - size: 50M + size: 1G type: esp - - id: boot + - id: boot-a + size: 250M + - id: boot-b size: 250M - type: xbootldr - - id: usr-data + - id: root-a + size: 5G + type: root + - id: root-b + size: 5G + type: root + - id: usr-data-a size: 5G type: usr - - id: usr-hash + - id: usr-data-b + size: 5G + type: usr + - id: usr-hash-a size: 1G type: usr-verity - - id: root - size: 5G - type: root + - id: usr-hash-b + size: 1G + type: usr-verity + - id: trident + type: linux-generic + size: 1G + abUpdate: + volumePairs: + - id: boot + volumeAId: boot-a + volumeBId: boot-b + - id: root + volumeAId: root-a + volumeBId: root-b + - id: usr-data + volumeAId: usr-data-a + volumeBId: usr-data-b + - id: usr-hash + volumeAId: usr-hash-a + volumeBId: usr-hash-b verity: - id: usr name: usr @@ -44,6 +71,9 @@ storage: mountPoint: path: /usr options: ro + - deviceId: trident + source: new + mountPoint: /var/lib/trident os: network: version: 2 diff --git a/e2e_tests/trident_configurations/verity-raid/trident-config.yaml b/e2e_tests/trident_configurations/verity-raid/trident-config.yaml deleted file mode 100644 index 92be42f2c..000000000 --- a/e2e_tests/trident_configurations/verity-raid/trident-config.yaml +++ /dev/null @@ -1,113 +0,0 @@ -image: - url: http://NETLAUNCH_HOST_ADDRESS/files/verity.cosi - sha384: ignored -storage: - disks: - - id: os - device: /dev/disk/by-path/pci-0000:00:1f.2-ata-2 - partitionTableType: gpt - partitions: - - id: esp - type: esp - size: 1G - - id: boot - size: 200M - - id: root-data-1 - size: 4G - - id: root-hash-1 - size: 1G - - id: trident - size: 500M - - id: trident-overlay-1 - size: 100M - - id: home - size: 100M - - id: var - size: 1G - - id: run - size: 1G - - id: disk2 - device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 - partitionTableType: gpt - partitions: - - id: root-data-2 - size: 4G - - id: root-hash-2 - size: 1G - - id: trident-overlay-2 - size: 100M - raid: - software: - - id: root-data - name: root-data - level: raid1 - devices: - - root-data-1 - - root-data-2 - - id: root-hash - name: root-hash - level: raid1 - devices: - - root-hash-1 - - root-hash-2 - - id: trident-overlay - name: trident-overlay - level: raid1 - devices: - - trident-overlay-1 - - trident-overlay-2 - verity: - - id: root - name: root - dataDeviceId: root-data - hashDeviceId: root-hash - filesystems: - - deviceId: trident-overlay - source: new - mountPoint: /var/lib/trident-overlay - - deviceId: home - source: new - mountPoint: /home - - deviceId: var - mountPoint: /var - - deviceId: boot - mountPoint: /boot - - deviceId: trident - source: new - mountPoint: /var/lib/trident - - deviceId: run - source: new - mountPoint: /run - - deviceId: esp - mountPoint: - path: /boot/efi - options: umask=0077 - - deviceId: root - mountPoint: - path: / - options: defaults,ro -scripts: - postConfigure: - - name: overlay - runOn: - - clean-install - - ab-update - content: | - mkdir -p /var/lib/trident-overlay/etc-rw/upper - mkdir -p /var/lib/trident-overlay/etc-rw/work -os: - selinux: - mode: permissive - network: - version: 2 - ethernets: - vmeths: - match: - name: enp* - dhcp4: true - users: - - name: testing-user - sshPublicKeys: [] - secondaryGroups: - - wheel - sshMode: key-only diff --git a/tools/cmd/storm-trident/main.go b/tools/cmd/storm-trident/main.go index 6695a50eb..77a032a9c 100644 --- a/tools/cmd/storm-trident/main.go +++ b/tools/cmd/storm-trident/main.go @@ -2,18 +2,17 @@ package main import ( "storm" - trident "tridenttools/storm/e2e" "tridenttools/storm/helpers" ) func main() { storm := storm.CreateSuite("trident") - // Add Trident E2E scenarios - scenarios := trident.DiscoverTridentScenarios(storm.Log) - for _, scenario := range scenarios { - storm.AddScenario(&scenario) - } + // Add Trident E2E scenarios (disabled for now) + // scenarios := trident.DiscoverTridentScenarios(storm.Log) + // for _, scenario := range scenarios { + // storm.AddScenario(&scenario) + // } // Register Trident helpers for _, helper := range helpers.TRIDENT_HELPERS { diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index ac414b4e4..7448f51cd 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -261,7 +261,7 @@ pub enum InvalidInputError { #[error( "Filesystem at '{mount_point}' in OS Image is not being used by the provided Host \ - Configuration. This could mean that the Host COnfiguration is missing a filesystem \ + Configuration. This could mean that the Host Configuration is missing a filesystem \ definition." )] UnusedOsImageFilesystem { mount_point: String }, From 66dfec44f6ab7075ad8b1894450e6e9aae8323ac Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Tue, 20 May 2025 21:53:31 +0000 Subject: [PATCH 22/99] Merged PR 23155: engineering: Have netlaunch directly interact with local VMs without using BMC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a storm utility code for interacting with libvirt VMs and changes netlaunch to use it, rather than interacting over an emulated BMC.  It avoids the boot order hacks that we previously relied on and will make netlaunch VM testing closer to how baremetal machines behave. In the future, we will have the option to configure secureboot or set other UEFI variables from outside the VM before it starts. ---- #### AI description (iteration 1) #### PR Classification This PR implements a new feature to bypass the BMC emulator and have netlaunch interact directly with local VMs. #### PR Summary The pull request updates netlaunch to use local VMs via libvirt for testing, reducing reliance on the BMC emulator. - `tools/cmd/netlaunch/main.go`: Introduced a conditional branch to use a local VM (via config.Netlaunch.LocalVmUuid) for HTTP boot setup and VM start; retains the BMC code path as fallback. - `tools/storm/utils/libvirt.go`: Added a new file with utilities to initialize, set boot URI, start, and disconnect from a VM using libvirt. - Updated dependency versions in `tools/go.sum` and `tools/go.mod` to include libvirt packages and newer versions for related modules. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml`: Adjusted the testing command to run netlaunch with the appropriate privileges for libvirt. Related work items: #12199 --- .../stages/testing_common/trident-rebuild.yml | 17 --- .../stages/testing_vm/netlaunch-prep.yml | 21 +++ .../stages/testing_vm/netlaunch-testing.yml | 4 +- .../stages/testing_vm/update-vm-bootorder.py | 53 -------- dev-docs/prerequisites.md | 1 + osutils/src/mount.rs | 18 ++- osutils/src/testutils/repart.rs | 15 +- src/engine/newroot.rs | 48 ++++--- tools/cmd/netlaunch/main.go | 128 +++++++++++------- tools/go.mod | 10 +- tools/go.sum | 12 ++ tools/storm/utils/libvirt.go | 113 ++++++++++++++++ 12 files changed, 280 insertions(+), 160 deletions(-) delete mode 100755 .pipelines/templates/stages/testing_vm/update-vm-bootorder.py create mode 100644 tools/storm/utils/libvirt.go diff --git a/.pipelines/templates/stages/testing_common/trident-rebuild.yml b/.pipelines/templates/stages/testing_common/trident-rebuild.yml index 247089fdc..c125501bb 100644 --- a/.pipelines/templates/stages/testing_common/trident-rebuild.yml +++ b/.pipelines/templates/stages/testing_common/trident-rebuild.yml @@ -70,23 +70,6 @@ steps: displayName: "Replace the test disk with a new disk" condition: and(succeeded(),eq(variables['TEST_REBUILD_RAID'], 'True')) - - bash: | - set -eux - echo "Dump the VM before boot order change." - until sudo virsh dumpxml virtdeploy-vm-0; do - sudo virsh list - sleep 0.1 - done - echo "Changing the boot order of the VM to boot from sda." - python3 $(Build.SourcesDirectory)/.pipelines/templates/stages/testing_vm/update-vm-bootorder.py - echo "Dump the VM after boot order change." - until sudo virsh dumpxml virtdeploy-vm-0; do - sudo virsh list - sleep 0.1 - done - displayName: "Change boot order" - condition: and(succeeded(),eq(variables['TEST_REBUILD_RAID'], 'True')) - - bash: | set -eux diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-prep.yml b/.pipelines/templates/stages/testing_vm/netlaunch-prep.yml index 0a86d0715..affd3d04e 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-prep.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-prep.yml @@ -58,6 +58,8 @@ steps: python3-bcrypt \ python3-jinja2 \ zstd + + sudo pip3 install virt-firmware displayName: Install virt-deploy dependencies retryCountOnTaskFailure: 3 @@ -71,4 +73,23 @@ steps: cat << EOF > ~/.config/libvirt/libvirt.conf uri_default = "qemu:///system" EOF + + sudo mkdir -p /etc/systemd/system/docker.socket.d + echo "[Socket] + SocketMode=0666" | sudo tee /etc/systemd/system/docker.socket.d/mode.conf > /dev/null + + sudo mkdir -p /etc/systemd/system/libvirtd.socket.d + echo "[Socket] + SocketMode=0666" | sudo tee /etc/systemd/system/libvirtd.socket.d/mode.conf > /dev/null + + sudo systemctl daemon-reload + + sudo systemctl stop docker.service + sudo systemctl restart docker.socket + sudo systemctl start docker.service + + sudo systemctl stop libvirtd.service + sudo systemctl restart libvirtd.socket + sudo systemctl start libvirtd.service + displayName: "Configure virt-deploy" diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index 29e5f9241..c9a3f15e7 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -115,8 +115,8 @@ stages: - bash: | set -eux - sg libvirt "./virt-deploy create --mem 12 --disks 32,32" - sg libvirt "./virt-deploy run" + ./virt-deploy create --mem 12 --disks 32,32 + ./virt-deploy run workingDirectory: $(argusToolkitSourceDirectory) displayName: "Creating virt-deploy VM" diff --git a/.pipelines/templates/stages/testing_vm/update-vm-bootorder.py b/.pipelines/templates/stages/testing_vm/update-vm-bootorder.py deleted file mode 100755 index 1800d5982..000000000 --- a/.pipelines/templates/stages/testing_vm/update-vm-bootorder.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import xml.etree.ElementTree as ET - -VM_NAME = "virtdeploy-vm-0" -XML_FILE = f"/tmp/{VM_NAME}.xml" - - -def run_command(command): - result = subprocess.run(command, shell=True, capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"Command '{command}' failed with error: {result.stderr}") - return result.stdout - - -# Dump the current XML configuration to a temporary file -run_command(f"sudo virsh dumpxml {VM_NAME} > {XML_FILE}") - -# Parse the XML file -tree = ET.parse(XML_FILE) -root = tree.getroot() - -# Remove the line from the cdrom device -for disk in root.findall("./devices/disk"): - if disk.get("device") == "cdrom": - boot = disk.find("boot") - if boot is not None and boot.get("order") == "1": - disk.remove(boot) - -# Add to the sda device -for disk in root.findall("./devices/disk"): - source = disk.find("source") - if ( - disk.get("device") == "disk" - and source is not None - and source.get("file") - == "/var/lib/libvirt/images/virtdeploy-pool/virtdeploy-vm-0-0-volume.qcow2" - ): - boot = ET.Element("boot", order="1") - disk.append(boot) - -# Write the updated XML back to the file -tree.write(XML_FILE) - -# Define the updated XML configuration -run_command(f"sudo virsh define {XML_FILE}") - -# Cleanup -os.remove(XML_FILE) - -print(f"Boot order updated successfully for VM: {VM_NAME}") diff --git a/dev-docs/prerequisites.md b/dev-docs/prerequisites.md index 5d696272e..f44bede76 100644 --- a/dev-docs/prerequisites.md +++ b/dev-docs/prerequisites.md @@ -28,6 +28,7 @@ - Install `build-essential`, `pkg-config`, `libssl-dev`, `libclang-dev`, and `protobuf-compiler`. E.g. `sudo apt install build-essential pkg-config libssl-dev libclang-dev protobuf-compiler`. +- Install the `virt-firmware` Python package: `sudo pip3 install virt-firmware`. - Clone the [Trident repository](https://mariner-org@dev.azure.com/mariner-org/ECF/_git/trident): `git clone https://mariner-org@dev.azure.com/mariner-org/ECF/_git/trident`. diff --git a/osutils/src/mount.rs b/osutils/src/mount.rs index 949f4ff7a..720090f89 100644 --- a/osutils/src/mount.rs +++ b/osutils/src/mount.rs @@ -100,15 +100,15 @@ mod functional_test { use pytest_gen::functional_test; use trident_api::constants::MOUNT_OPTION_READ_ONLY; - use crate::mountpoint; + use crate::{filesystems::MkfsFileSystemType, mkfs, mountpoint, testutils::repart}; #[functional_test(feature = "helpers")] fn test_mount_and_umount() { - // CDROM device to be mounted - let device = Path::new("/dev/sr0"); - // Mount point - let mount_point = Path::new("/mnt/cdrom"); + let loopback = repart::make_loopback_filesystem(MkfsFileSystemType::Vfat); + let device = loopback.path(); + // Mount point + let mount_point = Path::new("/mnt/tmpmount"); if mountpoint::check_is_mountpoint(mount_point).unwrap() { umount(mount_point, false).unwrap(); } @@ -117,7 +117,7 @@ mod functional_test { fs::create_dir_all(mount_point).unwrap(); // Test mount_file function - mount(device, mount_point, MountFileSystemType::Iso9660, &[]).unwrap(); + mount(device, mount_point, MountFileSystemType::Vfat, &[]).unwrap(); // If device is a file, fetch the name of loop device that was mounted at mount point; // otherwise, use the device path itself @@ -145,6 +145,10 @@ mod functional_test { #[functional_test(feature = "helpers")] fn test_recursive_unmount() { + let loopback = NamedTempFile::new().unwrap(); + loopback.as_file().set_len(1024 * 1024).unwrap(); + mkfs::run(loopback.path(), MkfsFileSystemType::Ext4).unwrap(); + let tmp_mount = Path::new("/mnt/tmpfs"); fs::create_dir_all(tmp_mount).unwrap(); mount( @@ -158,7 +162,7 @@ mod functional_test { let cdrom_mount = tmp_mount.join("cdrom"); fs::create_dir_all(&cdrom_mount).unwrap(); mount( - Path::new("/dev/sr0"), + loopback.path(), &cdrom_mount, MountFileSystemType::Auto, &[], diff --git a/osutils/src/testutils/repart.rs b/osutils/src/testutils/repart.rs index 63bf852cc..9700b91af 100644 --- a/osutils/src/testutils/repart.rs +++ b/osutils/src/testutils/repart.rs @@ -3,8 +3,11 @@ use std::{ffi::OsString, path::Path}; use anyhow::Error; use sysdefs::partition_types::DiscoverablePartitionType; +use tempfile::NamedTempFile; -use crate::{dependencies::Dependency, repart::RepartPartitionEntry}; +use crate::{ + dependencies::Dependency, filesystems::MkfsFileSystemType, mkfs, repart::RepartPartitionEntry, +}; pub const DISK_SIZE: u64 = 16 * 1024 * 1024 * 1024; // 16 GiB pub const PART1_SIZE: u64 = 50 * 1024 * 1024; // 50 MiB @@ -15,8 +18,6 @@ pub const SIZE_100MIB: u64 = 100 * 1024 * 1024; pub const OS_DISK_DEVICE_PATH: &str = "/dev/sda"; pub const TEST_DISK_DEVICE_PATH: &str = "/dev/sdb"; -pub const CDROM_DEVICE_PATH: &str = "/dev/sr0"; -pub const CDROM_MOUNT_PATH: &str = "/mnt/cdrom"; pub fn generate_partition_definition_esp_generic() -> Vec { vec![ @@ -191,3 +192,11 @@ pub fn clear_disk(disk_path: impl AsRef) -> Result<(), Error> { .run_and_check()?; Ok(()) } + +/// Generate a temporary file with the given filesystem type on it. +pub fn make_loopback_filesystem(filesystem_type: MkfsFileSystemType) -> NamedTempFile { + let loopback = NamedTempFile::new().unwrap(); + loopback.as_file().set_len(5 * 1024 * 1024).unwrap(); + mkfs::run(loopback.path(), filesystem_type).unwrap(); + loopback +} diff --git a/src/engine/newroot.rs b/src/engine/newroot.rs index b65425ce9..6578c49e3 100644 --- a/src/engine/newroot.rs +++ b/src/engine/newroot.rs @@ -697,10 +697,7 @@ mod functional_test { mkfs, mountpoint, repart::{RepartEmptyMode, RepartPartitionEntry, SystemdRepartInvoker}, testutils::{ - repart::{ - self, CDROM_DEVICE_PATH, CDROM_MOUNT_PATH, OS_DISK_DEVICE_PATH, - TEST_DISK_DEVICE_PATH, - }, + repart::{self, OS_DISK_DEVICE_PATH, TEST_DISK_DEVICE_PATH}, tmp_mount, }, udevadm, @@ -718,10 +715,19 @@ mod functional_test { #[functional_test(feature = "engine")] fn test_mount_and_umount() { - // CDROM device to be mounted - let device = Path::new(CDROM_DEVICE_PATH); + let loopback = repart::make_loopback_filesystem(MkfsFileSystemType::Vfat); + + let loop_device = Dependency::Losetup + .cmd() + .arg("-f") + .arg("--show") + .arg(loopback.path()) + .output_and_check() + .unwrap(); + let loop_device = loop_device.trim(); + // Mount point - let mount_point = Path::new(CDROM_MOUNT_PATH); + let mount_point = Path::new("/mnt/mountpoint"); if mountpoint::check_is_mountpoint(mount_point).unwrap() { mount::umount(mount_point, false).unwrap(); @@ -757,7 +763,7 @@ mod functional_test { }, partition_paths: btreemap! { "os".into() => PathBuf::from("/dev/sr"), - "sr0".into() => PathBuf::from(CDROM_DEVICE_PATH) + "sr0".into() => PathBuf::from(&loop_device), }, ..Default::default() }; @@ -767,17 +773,9 @@ mod functional_test { .mount_newroot_partitions(&ctx.spec, &ctx.partition_paths, AbVolumeSelection::VolumeA) .unwrap(); - // If device is a file, fetch the name of loop device that was mounted at mount point; - // otherwise, use the device path itself - let loop_device = if device.is_file() { - find_loop_device(device).unwrap() - } else { - device.to_string_lossy().to_string() - }; - // Validate that the device has been successfully mounted assert!( - is_device_mounted_at(&loop_device, mount_point), + is_device_mounted_at(loop_device, mount_point), "Device not mounted at the expected mount point" ); @@ -881,7 +879,7 @@ mod functional_test { // Validate that the device has been successfully unmounted assert!( - !is_device_mounted_at(&loop_device, mount_point), + !is_device_mounted_at(loop_device, mount_point), "Device '{loop_device}' still mounted at '{}'", mount_point.display() ); @@ -926,6 +924,8 @@ mod functional_test { #[functional_test(feature = "engine", negative = true)] fn test_mount_failure() { + let loopback = repart::make_loopback_filesystem(MkfsFileSystemType::Ext4); + let temp_mount_dir = TempDir::new().unwrap(); // bad mount path @@ -956,7 +956,7 @@ mod functional_test { }, partition_paths: btreemap! { "os".into() => PathBuf::from("/dev/sr"), - "sr0".into() => PathBuf::from(CDROM_DEVICE_PATH) + "sr0".into() => loopback.path().to_owned(), }, ..Default::default() }; @@ -1284,19 +1284,17 @@ mod functional_test { /// Attempt to prepare a directory within a read-only mounted filesystem #[functional_test(feature = "engine", negative = true)] fn test_prepare_mount_directory_ro() { - let temp_dir = TempDir::new().unwrap(); - - // CDROM device to be mounted for testing - let device = Path::new(CDROM_DEVICE_PATH); + let loopback = repart::make_loopback_filesystem(MkfsFileSystemType::Vfat); // Target path for the mount + let temp_dir = TempDir::new().unwrap(); let mount_point = temp_dir.path().join("mount_point"); fs::create_dir_all(&mount_point).unwrap(); // Mount the CDROM device and attempt to prepare a directory inside the read-only mount tmp_mount::mount( - device, - MountFileSystemType::Iso9660, + loopback.path(), + MountFileSystemType::Vfat, &["ro".into()], |mount_dir| { // Target path within the read-only mount diff --git a/tools/cmd/netlaunch/main.go b/tools/cmd/netlaunch/main.go index 3557bf34d..178d61365 100644 --- a/tools/cmd/netlaunch/main.go +++ b/tools/cmd/netlaunch/main.go @@ -8,6 +8,7 @@ import ( "tridenttools/pkg/netfinder" "tridenttools/pkg/phonehome" "tridenttools/pkg/serial" + "tridenttools/storm/utils" "bytes" "context" @@ -27,6 +28,7 @@ import ( "gopkg.in/yaml.v2" bmclib "github.com/bmc-toolbox/bmclib/v2" + "github.com/google/uuid" ) // `MagicString` is used to locate placeholder files in the initrd. Each placeholder file will be @@ -41,7 +43,7 @@ type NetLaunchConfig struct { Netlaunch struct { AnnounceIp *string AnnouncePort *uint16 - Bmc struct { + Bmc *struct { Ip string Port *string Username string @@ -289,65 +291,69 @@ var rootCmd = &cobra.Command{ // Start the HTTP server go server.Serve(listen) log.WithField("address", listen.Addr().String()).Info("Listening...") + iso_location := fmt.Sprintf("http://%s/provision.iso", announceAddress) - if config.Netlaunch.Bmc.SerialOverSsh != nil { - serial, err := serial.NewSerialOverSshSession(serial.SerialOverSSHSettings{ - Host: config.Netlaunch.Bmc.Ip, - Port: config.Netlaunch.Bmc.SerialOverSsh.SshPort, - Username: config.Netlaunch.Bmc.Username, - Password: config.Netlaunch.Bmc.Password, - ComPort: config.Netlaunch.Bmc.SerialOverSsh.ComPort, - Output: config.Netlaunch.Bmc.SerialOverSsh.Output, - }) - if err != nil { - log.WithError(err).Fatalf("Failed to open serial over SSH session") + if config.Netlaunch.LocalVmUuid != nil { + startLocalVm(*config.Netlaunch.LocalVmUuid, iso_location) + } else { + if config.Netlaunch.Bmc.SerialOverSsh != nil { + serial, err := serial.NewSerialOverSshSession(serial.SerialOverSSHSettings{ + Host: config.Netlaunch.Bmc.Ip, + Port: config.Netlaunch.Bmc.SerialOverSsh.SshPort, + Username: config.Netlaunch.Bmc.Username, + Password: config.Netlaunch.Bmc.Password, + ComPort: config.Netlaunch.Bmc.SerialOverSsh.ComPort, + Output: config.Netlaunch.Bmc.SerialOverSsh.Output, + }) + if err != nil { + log.WithError(err).Fatalf("Failed to open serial over SSH session") + } + defer serial.Close() } - defer serial.Close() - } - // Deploy ISO to BMC + // Deploy ISO to BMC - // Default to port 443 - port := "443" - if config.Netlaunch.Bmc.Port != nil { - port = *config.Netlaunch.Bmc.Port - } + // Default to port 443 + port := "443" + if config.Netlaunch.Bmc.Port != nil { + port = *config.Netlaunch.Bmc.Port + } - client := bmclib.NewClient( - config.Netlaunch.Bmc.Ip, - config.Netlaunch.Bmc.Username, - config.Netlaunch.Bmc.Password, - bmclib.WithRedfishPort(port), - ) + client := bmclib.NewClient( + config.Netlaunch.Bmc.Ip, + config.Netlaunch.Bmc.Username, + config.Netlaunch.Bmc.Password, + bmclib.WithRedfishPort(port), + ) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() - log.Info("Connecting to BMC") - client.Registry.Drivers = client.Registry.For("gofish") - if err := client.Open(context.Background()); err != nil { - log.WithError(err).Fatalf("failed to open connection to BMC") - } + log.Info("Connecting to BMC") + client.Registry.Drivers = client.Registry.For("gofish") + if err := client.Open(context.Background()); err != nil { + log.WithError(err).Fatalf("failed to open connection to BMC") + } - log.Info("Shutting down machine") - if _, err = client.SetPowerState(ctx, "off"); err != nil { - log.WithError(err).Fatalf("failed to turn off machine") - } + log.Info("Shutting down machine") + if _, err = client.SetPowerState(ctx, "off"); err != nil { + log.WithError(err).Fatalf("failed to turn off machine") + } - iso_location := fmt.Sprintf("http://%s/provision.iso", announceAddress) - log.WithField("url", iso_location).Info("Setting virtual media to ISO") - if _, err = client.SetVirtualMedia(ctx, string(redfish.CDMediaType), iso_location); err != nil { - log.WithError(err).Fatalf("failed to set virtual media") - } + log.WithField("url", iso_location).Info("Setting virtual media to ISO") + if _, err = client.SetVirtualMedia(ctx, string(redfish.CDMediaType), iso_location); err != nil { + log.WithError(err).Fatalf("failed to set virtual media") + } - log.Info("Setting boot media") - if _, err = client.SetBootDevice(ctx, "cdrom", false, true); err != nil { - log.WithError(err).Fatalf("failed to set boot media") - } + log.Info("Setting boot media") + if _, err = client.SetBootDevice(ctx, "cdrom", false, true); err != nil { + log.WithError(err).Fatalf("failed to set boot media") + } - log.Info("Turning on machine") - if _, err = client.SetPowerState(ctx, "on"); err != nil { - log.WithError(err).Fatalf("failed to turn on machine") + log.Info("Turning on machine") + if _, err = client.SetPowerState(ctx, "on"); err != nil { + log.WithError(err).Fatalf("failed to turn on machine") + } } log.Info("ISO deployed!") @@ -368,6 +374,30 @@ var rootCmd = &cobra.Command{ }, } +func startLocalVm(localVmUuidStr string, isoLocation string) { + log.Info("Using local VM") + + // TODO: Parse the UUID directly when reading the config file + vmUuid, err := uuid.Parse(localVmUuidStr) + if err != nil { + log.WithError(err).Fatalf("failed to parse LocalVmUuid as UUID") + } + + vm, err := utils.InitializeVm(vmUuid) + if err != nil { + log.WithError(err).Fatalf("failed to initialize VM") + } + defer vm.Disconnect() + + if err = vm.SetVmHttpBootUri(isoLocation); err != nil { + log.WithError(err).Fatalf("failed to set VM HTTP boot URI") + } + + if err = vm.Start(); err != nil { + log.WithError(err).Fatalf("failed to start VM") + } +} + func init() { rootCmd.PersistentFlags().StringVarP(&netlaunchConfigFile, "config", "c", "netlaunch.yaml", "Netlaunch config file") rootCmd.PersistentFlags().StringVarP(&tridentConfigFile, "trident", "t", "", "Trident local config file") diff --git a/tools/go.mod b/tools/go.mod index 9e491d000..50e45ea61 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -23,9 +23,11 @@ require ( ) require ( + github.com/digitalocean/go-libvirt v0.0.0-20250512231903-57024326652b // indirect github.com/kr/fs v0.1.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect - golang.org/x/term v0.31.0 // indirect + golang.org/x/term v0.32.0 // indirect + libvirt.org/libvirt-go-xml v7.4.0+incompatible // indirect ) require ( @@ -58,11 +60,11 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/vishvananda/netlink v1.3.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.38.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/tools/go.sum b/tools/go.sum index 2a8626afb..369ff8cec 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -19,6 +19,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitalocean/go-libvirt v0.0.0-20250512231903-57024326652b h1:o/RoLbHmKtibc3lMpuPcYGUjnboEORpLFnqtC89tfqY= +github.com/digitalocean/go-libvirt v0.0.0-20250512231903-57024326652b/go.mod h1:B2R8mtJc0BNx0NvvfOajL5no+MaFDumyD5sHsxll62g= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -119,6 +121,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -148,16 +152,22 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -176,3 +186,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +libvirt.org/libvirt-go-xml v7.4.0+incompatible h1:NaCRjbtz//xuTZOp1nDHbe0eu5BQlhIy5PPuc09EWtU= +libvirt.org/libvirt-go-xml v7.4.0+incompatible/go.mod h1:FL+H1+hKNWDdkKQGGS4sGCZJ3pGWcjt6VbxZvPlQJkY= diff --git a/tools/storm/utils/libvirt.go b/tools/storm/utils/libvirt.go new file mode 100644 index 000000000..fc2003490 --- /dev/null +++ b/tools/storm/utils/libvirt.go @@ -0,0 +1,113 @@ +package utils + +import ( + "fmt" + "net/url" + "os" + "os/exec" + + libvirtxml "libvirt.org/libvirt-go-xml" + + "github.com/digitalocean/go-libvirt" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +type LibvirtVm struct { + libvirt *libvirt.Libvirt + domain libvirt.Domain +} + +func InitializeVm(vmUuid uuid.UUID) (*LibvirtVm, error) { + logrus.Infof("Initializing VM with UUID '%s'", vmUuid.String()) + + uri, _ := url.Parse(string(libvirt.QEMUSession)) + l, err := libvirt.ConnectToURI(uri) + if err != nil { + return nil, fmt.Errorf("failed to connect: %v", err) + } + + var uuidSlice [16]byte + copy(uuidSlice[:], vmUuid[:]) + + domain, err := l.DomainLookupByUUID(uuidSlice) + if err != nil { + return nil, fmt.Errorf("failed to lookup domain by UUID '%s': %w", vmUuid.String(), err) + } + + domainState, _, err := l.DomainGetState(domain, 0) + if err != nil { + return nil, fmt.Errorf("failed to get domain state: %w", err) + } + + // Shutdown the VM if necessary + if domainState != int32(libvirt.DomainShutoff) { + logrus.Infof("Shutting down VM '%s'", domain.Name) + if err = l.DomainDestroy(domain); err != nil { + logrus.Warnf("failed to reset domain '%s': %s", domain.Name, err.Error()) + } + } + + return &LibvirtVm{l, domain}, nil +} + +func (vm *LibvirtVm) SetVmHttpBootUri(url string) error { + // Get the domain XML + domainXml, err := vm.libvirt.DomainGetXMLDesc(vm.domain, libvirt.DomainXMLUpdateCPU) + if err != nil { + return fmt.Errorf("failed to get XML description of domain '%s': %w", vm.domain.Name, err) + } + logrus.Tracef("Domain XML:\n%s", domainXml) + + // Parse the domain XML + parsedDomainXml := &libvirtxml.Domain{} + if err := parsedDomainXml.Unmarshal(domainXml); err != nil { + return fmt.Errorf("failed to parse domain XML: %w", err) + } + + // Find the NVRAM path + nvram := parsedDomainXml.OS.NVRam + if nvram != nil { + logrus.Debugf("Extracted NVRAM path: %s", nvram.NVRam) + } else { + return fmt.Errorf("no node found in domain XML") + } + + // Check if a file exists at the NVRAM path + if _, err := os.Stat(nvram.NVRam); err != nil { + // If not, start the VM in a paused state and then immediately stop it. + // This will cause libvirt to create the NVRAM file. + if vm.domain, err = vm.libvirt.DomainCreateWithFlags(vm.domain, uint32(libvirt.DomainStartPaused)); err != nil { + return fmt.Errorf("failed to create domain '%s': %w", vm.domain.Name, err) + } + if err = vm.libvirt.DomainDestroy(vm.domain); err != nil { + return fmt.Errorf("failed to destroy domain '%s': %w", vm.domain.Name, err) + } + } + + cmd := exec.Command("virt-fw-vars", "--inplace", nvram.NVRam, "--set-boot-uri", url) + if output, err := cmd.CombinedOutput(); err != nil { + logrus.Debugf("virt-fw-vars output:\n%s\n", output) + return fmt.Errorf("failed to set boot URI: %w", err) + } + logrus.Infof("Set boot URI to %s", url) + + return nil +} + +func (vm *LibvirtVm) Start() error { + logrus.Infof("Starting VM '%s'", vm.domain.Name) + + if err := vm.libvirt.DomainCreate(vm.domain); err != nil { + logrus.Errorf("failed to start domain '%s'", vm.domain.Name) + return err + } + + return nil +} + +func (vm *LibvirtVm) Disconnect() { + if err := vm.libvirt.Disconnect(); err != nil { + logrus.Errorf("failed to disconnect from libvirt: %s", err.Error()) + } +} From 7b7f803cfa5ca85e80065623e7c853e3dcfac23b Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 20 May 2025 23:47:41 +0000 Subject: [PATCH 23/99] Merged PR 23209: engineering: Disable memory-constraint-combined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Missed this in the last PR ---- #### AI description (iteration 1) #### PR Classification This PR disables a failing test configuration by removing the memory-constraint-combined test from active runs. #### PR Summary The pull request addresses the broken usr-verity configuration issue by updating and then disabling the memory-constraint-combined test setup. - In `e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml`, RAID array IDs, volume pair mappings, and filesystem mounts are revised. - In `e2e_tests/target-configurations.yaml`, the memory-constraint-combined test is commented out with a TODO for re-enabling, ensuring it does not execute during CI. Related work items: #12241 --- e2e_tests/target-configurations.yaml | 18 +- .../trident-config.yaml | 185 ++++++++---------- 2 files changed, 92 insertions(+), 111 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 20458ce3e..87d07f80b 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -14,7 +14,8 @@ bareMetal: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-big - raid-mirrored @@ -34,7 +35,8 @@ bareMetal: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-big - raid-mirrored @@ -111,7 +113,8 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -131,7 +134,8 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -164,7 +168,8 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -184,7 +189,8 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12232): enable + # - memory-constraint-combined - misc - raid-mirrored - raid-resync-small diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index 1bda5738e..0e2545738 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -18,62 +18,41 @@ storage: - id: boot-b type: xbootldr size: 200M - - id: root-data-a-1 - type: root - size: 4G - - id: root-data-b-1 - type: root - size: 4G - - id: root-data-a-2 - type: root - size: 4G - - id: root-data-b-2 - type: root - size: 4G - - id: root-hash-a-1 - type: root-verity - size: 1G - - id: root-hash-b-1 - type: root-verity - size: 1G - - id: root-hash-a-2 - type: root-verity - size: 1G - - id: root-hash-b-2 - type: root-verity - size: 1G + - id: root-a-1 + size: 2G + - id: root-a-2 + size: 2G + - id: root-b-1 + size: 2G + - id: root-b-2 + size: 2G + - id: usr-data-a-1 + size: 2G + - id: usr-data-b-1 + size: 2G + - id: usr-data-a-2 + size: 2G + - id: usr-data-b-2 + size: 2G + - id: usr-hash-a-1 + size: 500M + - id: usr-hash-b-1 + size: 500M + - id: usr-hash-a-2 + size: 500M + - id: usr-hash-b-2 + size: 500M - id: web-a-e-1 - type: linux-generic size: 1G - id: web-a-e-2 - type: linux-generic size: 1G - id: web-b-e-1 - type: linux-generic size: 1G - id: web-b-e-2 - type: linux-generic size: 1G - - id: home - type: linux-generic - size: 100M - - id: trident-overlay-a-1 - type: linux-generic - size: 100M - - id: trident-overlay-b-1 - type: linux-generic - size: 100M - - id: trident-overlay-a-2 - type: linux-generic - size: 100M - - id: trident-overlay-b-2 - type: linux-generic - size: 100M - id: trident - type: linux-generic size: 100M - id: var - type: linux-generic size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 @@ -89,103 +68,99 @@ storage: deviceId: web-b-e raid: software: - - id: web-a-e - name: web-a-e + - id: root-a + name: root-a level: raid1 devices: - - web-a-e-1 - - web-a-e-2 - - id: web-b-e - name: web-b-e + - root-a-1 + - root-a-2 + - id: root-b + name: root-b level: raid1 devices: - - web-b-e-1 - - web-b-e-2 - - id: root-data-a - name: root-data-a + - root-b-1 + - root-b-2 + + - id: usr-data-a + name: usr-data-a level: raid1 devices: - - root-data-a-1 - - root-data-a-2 - - id: root-data-b - name: root-data-b + - usr-data-a-1 + - usr-data-a-2 + - id: usr-data-b + name: usr-data-b level: raid1 devices: - - root-data-b-1 - - root-data-b-2 - - id: root-hash-a - name: root-hash-a + - usr-data-b-1 + - usr-data-b-2 + - id: usr-hash-a + name: usr-hash-a level: raid1 devices: - - root-hash-a-1 - - root-hash-a-2 - - id: root-hash-b - name: root-hash-b + - usr-hash-a-1 + - usr-hash-a-2 + - id: usr-hash-b + name: usr-hash-b level: raid1 devices: - - root-hash-b-1 - - root-hash-b-2 - - id: trident-overlay-a - name: trident-overlay-a + - usr-hash-b-1 + - usr-hash-b-2 + + - id: web-a-e + name: web-a-e level: raid1 devices: - - trident-overlay-a-1 - - trident-overlay-a-2 - - id: trident-overlay-b - name: trident-overlay-b + - web-a-e-1 + - web-a-e-2 + - id: web-b-e + name: web-b-e level: raid1 devices: - - trident-overlay-b-1 - - trident-overlay-b-2 + - web-b-e-1 + - web-b-e-2 abUpdate: volumePairs: - id: boot volumeAId: boot-a volumeBId: boot-b - - id: root-data - volumeAId: root-data-a - volumeBId: root-data-b - - id: root-hash - volumeAId: root-hash-a - volumeBId: root-hash-b - - id: trident-overlay - volumeAId: trident-overlay-a - volumeBId: trident-overlay-b + - id: root + volumeAId: root-a + volumeBId: root-b + - id: usr-data + volumeAId: usr-data-a + volumeBId: usr-data-b + - id: usr-hash + volumeAId: usr-hash-a + volumeBId: usr-hash-b - id: web volumeAId: web-a volumeBId: web-b verity: - - id: root - name: root - dataDeviceId: root-data - hashDeviceId: root-hash + - id: usr + name: usr + dataDeviceId: usr-data + hashDeviceId: usr-hash filesystems: - - deviceId: web - source: new - mountPoint: - path: /web - options: defaults - - deviceId: home - source: new - mountPoint: /home - deviceId: esp mountPoint: path: /boot/efi options: umask=0077 - - deviceId: trident-overlay - source: new - mountPoint: /var/lib/trident-overlay - deviceId: boot mountPoint: /boot + - deviceId: root + mountPoint: / + - deviceId: usr + mountPoint: + path: /usr + options: defaults,ro + - deviceId: web + source: new + mountPoint: /web - deviceId: trident source: new mountPoint: /var/lib/trident - deviceId: var mountPoint: /var - - deviceId: root - mountPoint: - path: / - options: defaults,ro scripts: preServicing: - name: rerun-trident-with-memory-limit From ce522b532409a5d0828fdb40899a17f07dfce775 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Wed, 21 May 2025 23:52:03 +0000 Subject: [PATCH 24/99] Merged PR 23222: bug: Fix parsing of prism history when disks or filesystems isn't present Related work items: #12032 --- src/offline_init/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/offline_init/mod.rs b/src/offline_init/mod.rs index cc61fc5c1..4e2b06033 100644 --- a/src/offline_init/mod.rs +++ b/src/offline_init/mod.rs @@ -75,7 +75,10 @@ struct PrismVerity { #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "camelCase")] struct PrismStorage { + #[serde(default)] disks: Vec, + + #[serde(default)] filesystems: Vec, #[serde(default)] @@ -110,7 +113,7 @@ fn generate_host_status( .iter() .rev() .map(|entry| entry.config.storage.as_ref()) - .find(|storage| storage.is_some()) + .find(|storage| storage.is_some_and(|s| !s.disks.is_empty())) .flatten() else { return Err(TridentError::new(InvalidInputError::ParsePrismHistory)) From 3ec37af0ea6304731cde3509fbd2acbbeb347d5b Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 22 May 2025 02:33:14 +0000 Subject: [PATCH 25/99] Merged PR 23230: bug: Stop disabling the sshd service for misc test Related work items: #12244 --- e2e_tests/trident_configurations/misc/trident-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/e2e_tests/trident_configurations/misc/trident-config.yaml b/e2e_tests/trident_configurations/misc/trident-config.yaml index 5b0ffdef1..2a902e7fe 100644 --- a/e2e_tests/trident_configurations/misc/trident-config.yaml +++ b/e2e_tests/trident_configurations/misc/trident-config.yaml @@ -59,10 +59,7 @@ os: "option2": "value2" services: enable: - - sshd - cron - disable: - - sshd kernelCommandLine: extraCommandLine: - param1=value From 0f029e79a7c5d1054dd8fd9332a553be27ce73fa Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 22 May 2025 21:41:31 +0000 Subject: [PATCH 26/99] Merged PR 23218: engineering: Delete virt-deploy run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Matching changes for !23217, SHOULD MERGE IMMEDIATELY AFTER THAT PR. ---- #### AI description (iteration 1) #### PR Classification This pull request is a cleanup that removes obsolete `virt-deploy run` commands. #### PR Summary The changes eliminate redundant calls to `virt-deploy run` across pipeline configuration, documentation, and test setup, aligning with the "Cleanup virt-deploy" work item. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml`: Removed the `./virt-deploy run` command during the VM creation stage. - `/dev-docs/validating-container.md`: Deleted the `./virt-deploy run` command from the container validation example. - `/functional_tests/test_setup.py`: Eliminated the redundant execution of `virt-deploy run` in the test setup. Related work items: #12247 --- .pipelines/templates/stages/testing_vm/netlaunch-testing.yml | 1 - dev-docs/validating-container.md | 1 - functional_tests/test_setup.py | 1 - 3 files changed, 3 deletions(-) diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index c9a3f15e7..c31091583 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -116,7 +116,6 @@ stages: - bash: | set -eux ./virt-deploy create --mem 12 --disks 32,32 - ./virt-deploy run workingDirectory: $(argusToolkitSourceDirectory) displayName: "Creating virt-deploy VM" diff --git a/dev-docs/validating-container.md b/dev-docs/validating-container.md index 884a94970..7975ff5e2 100644 --- a/dev-docs/validating-container.md +++ b/dev-docs/validating-container.md @@ -100,7 +100,6 @@ validating an image running Trident from a container. ```bash ./virt-deploy create --mem 11 - ./virt-deploy run ``` Note that at least 11GB of RAM is necessary to run Trident in a container, diff --git a/functional_tests/test_setup.py b/functional_tests/test_setup.py index a52391cbf..352c9cedc 100644 --- a/functional_tests/test_setup.py +++ b/functional_tests/test_setup.py @@ -24,7 +24,6 @@ def create_vm(create_params): """Creates a VM with the given parameters, using virt-deploy.""" argus_runcmd([ARGUS_REPO_DIR_PATH / "virt-deploy", "create"] + create_params) - argus_runcmd([ARGUS_REPO_DIR_PATH / "virt-deploy", "run"]) def disable_phonehome(ssh_node: SshNode): From 12c54c67955f55f4ec9923b7406c05c03db02f6f Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Fri, 23 May 2025 00:30:13 +0000 Subject: [PATCH 27/99] Merged PR 23234: Update osmodifier Fixes a SFI issue with an outdated dependency: https://dev.azure.com/mariner-org/ECF/_componentGovernance/trident/alert/10132178?typeId=18851394 Related work items: #12263 --- .pipelines/templates/stages/common_tasks/build-osmodifier.yml | 2 +- azure-linux-image-tools | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/templates/stages/common_tasks/build-osmodifier.yml b/.pipelines/templates/stages/common_tasks/build-osmodifier.yml index 51802c8c0..0da3f8d3b 100644 --- a/.pipelines/templates/stages/common_tasks/build-osmodifier.yml +++ b/.pipelines/templates/stages/common_tasks/build-osmodifier.yml @@ -6,7 +6,7 @@ steps: sudo tdnf remove golang -y sudo tdnf install msft-golang -y else - echo "Go Update not needed in ubuntu 22.04" + sudo snap install --classic go fi # Verify installation diff --git a/azure-linux-image-tools b/azure-linux-image-tools index 77cd059eb..3db1167c3 160000 --- a/azure-linux-image-tools +++ b/azure-linux-image-tools @@ -1 +1 @@ -Subproject commit 77cd059eb0ce7e665206613e9c50c2b87db22564 +Subproject commit 3db1167c3b59aaf02ef9ba35647c2ef7b769607d From ae1ba5a76083925baa2ddad2cefa4c81afc96923 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Fri, 23 May 2025 21:53:09 +0000 Subject: [PATCH 28/99] Merged PR 23246: engineering: Pre-generate test matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description First stab at a solution to generate test lists out of the critical path. Not ideal, but this solution requires minimal changes, will iterate on it in the future. ---- #### AI description (iteration 1) #### PR Classification This pull request implements an engineering enhancement to pre-generate the test matrix in the virtual machine testing pipeline. #### PR Summary The PR introduces a new stage that generates the test list for virtual machine testing and updates subsequent steps to depend on this pre-generated data, streamlining test configuration retrieval. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml`: Added the `DefineTests_VM_${{ parameters.runtimeEnv }}` stage that invokes the `get-tests.yml` template with appropriate parameters for VM testing. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml`: Modified the deployment testing stage to depend on the new test definition stage and updated the matrix reference to use outputs from the pre-generated test list. --- .../stages/testing_vm/netlaunch-testing.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index c31091583..6dfd870b1 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -23,9 +23,19 @@ parameters: - container stages: + - stage: DefineTests_VM_${{ parameters.runtimeEnv }} + displayName: Test List for VM:${{ parameters.runtimeEnv }} + jobs: + - template: ../testing_common/get-tests.yml + parameters: + buildPurpose: ${{ parameters.buildPurpose }} + deploymentEnvironment: virtualMachine + runtimeEnv: ${{ parameters.runtimeEnv }} + - stage: DeploymentTesting_${{ parameters.runtimeEnv }} displayName: Deployment VM ${{ parameters.runtimeEnv }} Testing dependsOn: + - DefineTests_VM_${{ parameters.runtimeEnv }} - ${{ if eq(parameters.testingRun, true) }}: - DownloadTestingElements - ${{ else }}: @@ -42,14 +52,7 @@ stages: - TridentTestImg_trident_verity_testimage jobs: - - template: ../testing_common/get-tests.yml - parameters: - buildPurpose: ${{ parameters.buildPurpose }} - deploymentEnvironment: virtualMachine - runtimeEnv: ${{ parameters.runtimeEnv }} - - job: Testing - dependsOn: DefineTests timeoutInMinutes: 50 pool: type: linux @@ -57,7 +60,7 @@ stages: hostArchitecture: amd64 strategy: - matrix: $[ dependencies.DefineTests.outputs['setConfigurations.matrixConfigurations'] ] + matrix: $[ stageDependencies.DefineTests_VM_${{ parameters.runtimeEnv }}.DefineTests.outputs['setConfigurations.matrixConfigurations'] ] variables: tridentConfigurationName: $(configuration) From dbd871ca4ea28c53d35979bac716367e151f91df Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Sat, 24 May 2025 00:49:15 +0000 Subject: [PATCH 29/99] Merged PR 23203: feature: Enable UKI+RAID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description When using UKI, create RAID Arrays with `homehost=any` so that the runtime OS will adopt the arrays as its own without caring for the homehost in the metadata. Follow ups: - #12276 - #12277 ---- #### AI description (iteration 1) #### PR Classification New feature to enable RAID array creation with UKI support. #### PR Summary This PR introduces a new function for RAID array creation that accepts a homehost parameter and updates the RAID initialization logic to support UKI. - `osutils/src/mdadm.rs`: Added a `create_homehost` function and modified `create_inner` to pass a `--homehost` argument when provided. - `src/engine/storage/raid.rs`: Updated the RAID creation routine to conditionally invoke `create_homehost` based on the UKI support flag and imported the corresponding constant. Related work items: #12232 --- .pipelines/templates/e2e-template.yml | 3 +- .../testing_baremetal/baremetal-testing.yml | 1 + .../stages/testing_common/e2e-test-run.yml | 1 + .../stages/testing_common/get-tests.yml | 17 +- .../stages/testing_common/trident-rebuild.yml | 7 +- .../stages/testing_vm/netlaunch-testing.yml | 4 +- .../helpers/read_target_configurations.py | 40 +- e2e_tests/target-configurations.yaml | 141 ++---- .../combined/trident-config.yaml | 50 +- .../rerun/trident-config.yaml | 54 +- .../usr-verity-raid/trident-config.yaml | 4 +- osutils/src/mdadm.rs | 24 + scripts/compare-cosi.py | 475 ++++++++++++++++++ src/engine/storage/raid.rs | 15 +- 14 files changed, 645 insertions(+), 191 deletions(-) create mode 100755 scripts/compare-cosi.py diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index 8b3b9f84a..70994b370 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -165,10 +165,11 @@ stages: micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} - # Build Trident test image for usr-verity (host) + # Build Trident test image for usr-verity (container) - template: stages/build_image/build-runtime.yml parameters: imageName: trident-container-usrverity-testimage + runtimeEnv: "container" baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} micBuildType: ${{ parameters.micBuildType }} diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index 693c13ac5..aa07de171 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -83,6 +83,7 @@ stages: - name: BAREMETAL_LAB_CANARY_IP value: "10.8.6.1" + # Sourced from the matrix - name: TRIDENT_CONFIGURATION_NAME value: $(configuration) diff --git a/.pipelines/templates/stages/testing_common/e2e-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-test-run.yml index 0ff32f532..42edc1809 100644 --- a/.pipelines/templates/stages/testing_common/e2e-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-test-run.yml @@ -314,3 +314,4 @@ steps: tridentSourceDirectory: $(Build.SourcesDirectory) tridentConfigPath: ${{ parameters.tridentConfigPath }} deploymentEnvironment: ${{ parameters.deploymentEnvironment }} + tridentConfigurationName: ${{ parameters.tridentConfigurationName }} diff --git a/.pipelines/templates/stages/testing_common/get-tests.yml b/.pipelines/templates/stages/testing_common/get-tests.yml index 349e2b62c..da4efbff0 100644 --- a/.pipelines/templates/stages/testing_common/get-tests.yml +++ b/.pipelines/templates/stages/testing_common/get-tests.yml @@ -36,16 +36,11 @@ jobs: steps: - bash: | - set -eux - matrix=$( - python3 ./e2e_tests/helpers/read_target_configurations.py \ - --configurations ./e2e_tests/target-configurations.yaml \ - --env ${{ parameters.deploymentEnvironment }} \ - --runtimeEnv ${{ parameters.runtimeEnv }} \ - --purpose ${{ parameters.buildPurpose }} \ - ) - - set +x - echo "##vso[task.setvariable variable=matrixConfigurations;isOutput=true]$matrix" + python3 ./e2e_tests/helpers/read_target_configurations.py \ + --configurations ./e2e_tests/target-configurations.yaml \ + --env ${{ parameters.deploymentEnvironment }} \ + --runtimeEnv ${{ parameters.runtimeEnv }} \ + --purpose ${{ parameters.buildPurpose }} \ + --matrix-name matrixConfigurations name: setConfigurations displayName: Matrix of Trident configurations for E2E Tests diff --git a/.pipelines/templates/stages/testing_common/trident-rebuild.yml b/.pipelines/templates/stages/testing_common/trident-rebuild.yml index c125501bb..e25425a43 100644 --- a/.pipelines/templates/stages/testing_common/trident-rebuild.yml +++ b/.pipelines/templates/stages/testing_common/trident-rebuild.yml @@ -31,11 +31,16 @@ parameters: - bareMetal - virtualMachine + - name: tridentConfigurationName + type: string + steps: - bash: | set -eu raidExists=$(sudo yq e '.storage.raid != null' "${{ parameters.tridentConfigPath }}/trident-config.yaml") - if [ "$raidExists" == "true" ]; then + + # TODO (12277): Support for UKI + Rebuild + if [ "$raidExists" == "true" ] && [ "${{ parameters.tridentConfigurationName }}" != "usr-verity-raid" ]; then echo "Trident config requires Rebuild testing" echo "##vso[task.setvariable variable=TEST_REBUILD_RAID]True" else diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index 6dfd870b1..fd19e13fc 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -63,7 +63,9 @@ stages: matrix: $[ stageDependencies.DefineTests_VM_${{ parameters.runtimeEnv }}.DefineTests.outputs['setConfigurations.matrixConfigurations'] ] variables: + # Sourced from the matrix tridentConfigurationName: $(configuration) + tridentConfigPath: $(tridentSourceDirectory)/e2e_tests/trident_configurations/$(tridentConfigurationName) tridentSourceDirectory: $(Build.SourcesDirectory) @@ -152,7 +154,7 @@ stages: --port ${{variables.netlaunchPort}} 2>&1 | tee ./clean-install-deployment.log workingDirectory: $(tridentSourceDirectory) displayName: "🚀 Run netlaunch for testing" - timeoutInMinutes: 15 + timeoutInMinutes: 20 - template: ../testing_common/display-deployment-logs.yml parameters: diff --git a/e2e_tests/helpers/read_target_configurations.py b/e2e_tests/helpers/read_target_configurations.py index c97b29953..1343760a9 100755 --- a/e2e_tests/helpers/read_target_configurations.py +++ b/e2e_tests/helpers/read_target_configurations.py @@ -1,10 +1,12 @@ import argparse +from pathlib import Path import sys import yaml +import json +import logging - -def format_matrix(configurations): - return {directory: {"configuration": directory} for directory in configurations} +logging.basicConfig(level=logging.INFO) +log = logging.getLogger("read_target_configurations") def main(): @@ -17,7 +19,7 @@ def main(): parser.add_argument( "-c", "--configurations", - type=str, + type=Path, required=True, help="File path to the YAML that contains the configurations for the E2E testing.", ) @@ -44,9 +46,22 @@ def main(): choices=["host", "container"], help="The runtime environment of Trident (e.g., host or container).", ) + parser.add_argument( + "--matrix-name", + type=str, + required=True, + help="Name of the ADO variable to write the matrix to.", + ) args = parser.parse_args() - with open(args.configurations, "r") as file: + log.info( + f"Reading target configurations from '{args.configurations}' for '{args.env}' " + f"with purpose '{args.purpose}' and runtime environment '{args.runtimeEnv}'." + ) + + configurations_file: Path = args.configurations.absolute() + + with open(configurations_file, "r") as file: target_configurations = yaml.safe_load(file) if args.env not in target_configurations: @@ -61,11 +76,16 @@ def main(): sys.exit( f"Build purpose {args.purpose} not found in {args.configurations} for {args.env} and {args.runtimeEnv}." ) - else: - matrix = format_matrix( - target_configurations[args.env][args.runtimeEnv][args.purpose] - ) - print(matrix) + + configurations = target_configurations[args.env][args.runtimeEnv][args.purpose] + + matrix = {name: {"configuration": name} for name in configurations} + + log.info(f"Matrix:\n{json.dumps(matrix, indent=4)}") + + print( + f"##vso[task.setvariable variable={args.matrix_name};isOutput=true]{json.dumps(matrix)}" + ) if __name__ == "__main__": diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 87d07f80b..89a2d61df 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -2,36 +2,30 @@ bareMetal: host: daily: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - split - usr-verity validation: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + # TODO(12276) re-enable: #- memory-constraint-combined - misc - raid-big - raid-mirrored - raid-resync-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid weekly: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -41,19 +35,16 @@ bareMetal: - raid-big - raid-mirrored - raid-resync-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid container: daily: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -61,17 +52,14 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid validation: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -79,17 +67,14 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid weekly: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -97,19 +82,16 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid virtualMachine: host: daily: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -119,18 +101,15 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid post_merge: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -140,31 +119,25 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid pullrequest: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - raid-mirrored - raid-resync-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid validation: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -174,18 +147,15 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid weekly: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -195,19 +165,16 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - split - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid container: daily: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -215,17 +182,14 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid post_merge: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -233,32 +197,26 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid pullrequest: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap - raid-mirrored - raid-resync-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid validation: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -266,17 +224,14 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid weekly: - base - # TODO(12232): enable - # - combined + # TODO(12276) re-enable: #- combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -284,10 +239,8 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12232): enable - # - rerun + # TODO(12276) re-enable: #- rerun - root-verity - simple - usr-verity - # TODO(12232): enable - # - usr-verity-raid + - usr-verity-raid diff --git a/e2e_tests/trident_configurations/combined/trident-config.yaml b/e2e_tests/trident_configurations/combined/trident-config.yaml index 8c97e0664..64a7978a4 100644 --- a/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -22,56 +22,40 @@ storage: size: 4G - id: root-b-1 size: 4G - - id: root-a-2 - size: 4G - - id: root-b-2 - size: 4G - id: usr-data-a-1 - type: root size: 2G - id: usr-data-b-1 - type: root - size: 2G - - id: usr-data-a-2 - type: root - size: 2G - - id: usr-data-b-2 - type: root size: 2G - id: usr-hash-a-1 - type: root-verity size: 256M - id: usr-hash-b-1 - type: root-verity - size: 256M - - id: usr-hash-a-2 - type: root-verity - size: 256M - - id: usr-hash-b-2 - type: root-verity size: 256M - id: web-a-e-1 - type: linux-generic - size: 1G - - id: web-a-e-2 - type: linux-generic size: 1G - id: web-b-e-1 - type: linux-generic - size: 1G - - id: web-b-e-2 - type: linux-generic size: 1G - id: trident - type: linux-generic size: 500M - - id: var - type: linux-generic - size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt - partitions: [] + partitions: + - id: root-a-2 + size: 4G + - id: root-b-2 + size: 4G + - id: usr-data-a-2 + size: 2G + - id: usr-data-b-2 + size: 2G + - id: usr-hash-a-2 + size: 256M + - id: usr-hash-b-2 + size: 256M + - id: web-a-e-2 + size: 1G + - id: web-b-e-2 + size: 1G encryption: volumes: - id: web-a diff --git a/e2e_tests/trident_configurations/rerun/trident-config.yaml b/e2e_tests/trident_configurations/rerun/trident-config.yaml index b38ea5369..2ae8840bd 100644 --- a/e2e_tests/trident_configurations/rerun/trident-config.yaml +++ b/e2e_tests/trident_configurations/rerun/trident-config.yaml @@ -13,65 +13,49 @@ storage: type: esp size: 1G - id: boot-a - type: xbootldr + size: 200M + - id: boot-b size: 200M - id: root-a-1 size: 4G - - id: root-a-2 - size: 4G - id: root-b-1 size: 4G - - id: root-b-2 - size: 4G - - id: boot-b - type: xbootldr - size: 200M - id: usr-data-a-1 - type: root size: 2G - id: usr-data-b-1 - type: root - size: 2G - - id: usr-data-a-2 - type: root - size: 2G - - id: usr-data-b-2 - type: root size: 2G - id: usr-hash-a-1 - type: root-verity size: 1G - id: usr-hash-b-1 - type: root-verity - size: 1G - - id: usr-hash-a-2 - type: root-verity - size: 1G - - id: usr-hash-b-2 - type: root-verity size: 1G - id: home-a-e-1 - type: linux-generic - size: 100M - - id: home-a-e-2 - type: linux-generic size: 100M - id: home-b-e-1 - type: linux-generic - size: 100M - - id: home-b-e-2 - type: linux-generic size: 100M - id: trident - type: linux-generic size: 500M - id: srv-e - type: linux-generic size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt - partitions: [] + partitions: + - id: root-a-2 + size: 4G + - id: root-b-2 + size: 4G + - id: usr-data-a-2 + size: 2G + - id: usr-data-b-2 + size: 2G + - id: usr-hash-a-2 + size: 1G + - id: usr-hash-b-2 + size: 1G + - id: home-a-e-2 + size: 100M + - id: home-b-e-2 + size: 100M raid: software: - id: usr-data-a diff --git a/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml index 06d0e902c..329b35ba4 100644 --- a/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml +++ b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml @@ -21,7 +21,7 @@ storage: - id: usr-hash-1 size: 1G - id: trident-1 - size: 128M + size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt @@ -33,7 +33,7 @@ storage: - id: usr-hash-2 size: 1G - id: trident-2 - size: 128M + size: 1G raid: software: - id: root diff --git a/osutils/src/mdadm.rs b/osutils/src/mdadm.rs index 235726729..17b6fe8a8 100644 --- a/osutils/src/mdadm.rs +++ b/osutils/src/mdadm.rs @@ -11,10 +11,30 @@ use crate::{dependencies::Dependency, lsblk}; pub const METADATA_VERSION: &str = "1.0"; +/// Creates a RAID array using `mdadm` with the specified level and devices. pub fn create( raid_path: &PathBuf, level: &RaidLevel, device_paths: Vec, +) -> Result<(), Error> { + create_inner(raid_path, level, device_paths, None) +} + +/// Creates a RAID array using `mdadm` with the specified level, devices, and homehost. +pub fn create_homehost( + raid_path: &PathBuf, + level: &RaidLevel, + device_paths: Vec, + homehost: &str, +) -> Result<(), Error> { + create_inner(raid_path, level, device_paths, Some(homehost)) +} + +fn create_inner( + raid_path: &PathBuf, + level: &RaidLevel, + device_paths: Vec, + homehost: Option<&str>, ) -> Result<(), Error> { trace!("Creating RAID array '{}'", &raid_path.display()); @@ -36,6 +56,10 @@ pub fn create( .args(&device_paths) .arg(format!("--metadata={METADATA_VERSION}")); + if let Some(homehost) = homehost { + mdadm_command.arg(format!("--homehost={}", homehost)); + } + mdadm_command .run_and_check() .context("Failed to run mdadm create") diff --git a/scripts/compare-cosi.py b/scripts/compare-cosi.py new file mode 100755 index 000000000..b836523ac --- /dev/null +++ b/scripts/compare-cosi.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 + +""" +A script to thoroughly compare two COSI files. +It extracts the contents of the COSI files, compares the files and directories, +and generates a report of the differences. + +If it finds a UKI file in both with different content, it will extract the initrd +and compare the contents of the initrd files. + +It optionally outputs the diff trees to a specified directory for further analysis. + +Requires: +- Python 3.12 (I think) or higher +- pefile + +Usage: + python3 compare-cosi.py [-o ] +""" + +import argparse +from contextlib import contextmanager +from dataclasses import dataclass +import gzip +import hashlib +import logging +import json +import shutil +from pathlib import Path +import tarfile +import tempfile +from typing import Dict, Generator, List, Optional, Tuple +import subprocess +from io import StringIO + +# Set up logging +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger("compare-cosi") + +try: + import pefile +except ImportError: + log.critical( + "pefile is not installed. Please install it using 'pip install pefile'." + ) + exit(1) + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Compare two COSI files and optionally write results to a file." + ) + parser.add_argument("cosi_file_a", type=Path, help="Path to the first COSI file.") + parser.add_argument("cosi_file_b", type=Path, help="Path to the second COSI file.") + parser.add_argument( + "-o", + "--output", + type=Path, + default=None, + help="Optional path to write the comparison results.", + ) + return parser.parse_args() + + +@contextmanager +def cosi_extractor(cosi_file_path: Path) -> "Generator[Path, None, None]": + """ + Extracts the given COSI file (assumed to be a zip archive) to the specified mount path. + If the mount path does not exist, it will be created. + """ + + with tempfile.TemporaryDirectory(prefix=f"{cosi_file_path.name}-") as work_dir: + work_path = Path(work_dir) + log.debug(f"Temporary work directory for '{cosi_file_path}': '{work_path}'") + + mounted = [] + + try: + yield extract_and_mount_cosi(mounted, cosi_file_path, work_path) + finally: + for mount_point in reversed(mounted): + log.debug(f"Unmounting {mount_point}") + # Unmount the image + unmount_command = ["umount", str(mount_point)] + subprocess.run(unmount_command, check=True) + + +def extract_and_mount_cosi( + mounted: List[Path], + cosi_file_path: Path, + work_path: Path, +) -> Path: + if not work_path.exists(): + raise FileNotFoundError(f"Work path does not exist: {work_path}") + + extract_path = work_path / "extracted" + decompression_path = work_path / "decompressed" + + with open(cosi_file_path, "rb") as cosi_file: + with tarfile.open(fileobj=cosi_file, mode="r:*") as tar: + tar.extractall(path=extract_path, filter="data") + + metadata_path = extract_path / "metadata.json" + if not metadata_path.exists(): + raise FileNotFoundError(f"Metadata file does not exist: {metadata_path}") + log.info(f"Extracted COSI file to: {extract_path}") + + with open(metadata_path, "r") as metadata_file: + metadata = json.load(metadata_file) + + root_path = work_path / "root" + root_path.mkdir(parents=True, exist_ok=True) + + # Mount all images + image_mount: List[Tuple[Path, Path]] = [ + (Path(image["mountPoint"]), Path(image["image"]["path"])) + for image in metadata["images"] + ] + + image_mount.sort(key=lambda x: len(x[0].parts)) + + for mount_point, image_path in image_mount: + effective_mount_point = root_path / mount_point.relative_to("/") + + # VERY IMPORTANT SAFETY CHECKS TO AVOID WEIRD ISSUES! + assert effective_mount_point.is_absolute() + # Python 3.8 compatibility: check if effective_mount_point is under root_path + try: + effective_mount_point.relative_to(root_path) + except ValueError: + raise AssertionError("Mount point must be inside root_path") + + mount_point.mkdir(parents=True, exist_ok=True) + image_path = extract_path / image_path + if not image_path.exists(): + raise FileNotFoundError(f"Image path does not exist: {image_path}") + + # Decompress the image file using zstd + decompressed_image_path = decompression_path / image_path.relative_to( + extract_path + ).with_suffix(".raw") + + decompressed_image_path.parent.mkdir(parents=True, exist_ok=True) + subprocess.run( + ["zstd", "-d", "-f", str(image_path), "-o", str(decompressed_image_path)], + check=True, + ) + + log.debug(f"Mounting {decompressed_image_path} to {effective_mount_point}") + + if not effective_mount_point.exists(): + effective_mount_point.mkdir(parents=True, exist_ok=True) + + # Mount the decompressed image + mount_command = [ + "mount", + "-o", + "loop", + str(decompressed_image_path), + str(effective_mount_point), + ] + log.debug(f"Mounting {decompressed_image_path} to {effective_mount_point}") + subprocess.run(mount_command, check=True) + mounted.append(effective_mount_point) + + return root_path + + +def hash_file(path: Path) -> str: + """Return SHA256 hash of file contents.""" + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +class TreeCompare: + def __init__(self, tree_a: Path, tree_b: Path): + self.tree_a = tree_a + self.tree_b = tree_b + self.same_files: List[Path] = [] + self.only_in_tree_a: List[Path] = [] + self.only_in_tree_b: List[Path] = [] + self.diff_files: List[Path] = [] + + for root, _, files in tree_a.walk(): + rel_root = Path(root).relative_to(tree_a) + for file in files: + rel_path = rel_root / file + path_a = tree_a / rel_path + path_b = tree_b / rel_path + + if path_b.exists(follow_symlinks=False): + if path_b.is_file(): + if hash_file(path_a) == hash_file(path_b): + self.same_files.append(rel_path) + else: + self.diff_files.append(rel_path) + elif path_a.is_file(): + self.only_in_tree_a.append(rel_path) + else: + # This is a directory that only exists in tree A. + # We don't have to do anything as we only care about files. + pass + + for root, _, files in tree_b.walk(): + rel_root = Path(root).relative_to(tree_b) + for file in files: + rel_path = rel_root / file + path_a = tree_a / rel_path + if not path_a.exists(follow_symlinks=False): + self.only_in_tree_b.append(rel_path) + + def diftree_a(self) -> List[Path]: + return [file for file in self.diff_files + self.only_in_tree_a] + + def diftree_b(self) -> List[Path]: + return [file for file in self.diff_files + self.only_in_tree_b] + + def __copy_diftree(self, base: Path, files: List[Path], dest: Path): + for file in files: + source = base / file + dest_file = dest / file + dest_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(source, dest_file, follow_symlinks=False) + + def copy_diftree_a(self, dest: Path): + """Copy files only in tree A and files with different content to the destination.""" + log.info(f"Copying diftree A to '{dest}' from '{self.tree_a}'") + self.__copy_diftree(self.tree_a, self.diftree_a(), dest) + + def copy_diftree_b(self, dest: Path): + """Copy files only in tree B and files with different content to the destination.""" + log.info(f"Copying diftree B to '{dest}' from '{self.tree_b}'") + self.__copy_diftree(self.tree_b, self.diftree_b(), dest) + + def copy_diftrees(self, a_dest: Path, b_dest: Path): + """Copy files only in tree A and B and files with different content to the respective destinations.""" + a_dest.mkdir(parents=True, exist_ok=True) + b_dest.mkdir(parents=True, exist_ok=True) + + self.copy_diftree_a(a_dest) + self.copy_diftree_b(b_dest) + log.info(f"Copied differing files to:\n - A: {a_dest}/\n - B: {b_dest}/") + + def report(self, a_name: str = None, b_name: str = None) -> str: + """Generate a report of the differences.""" + if a_name is None: + a_name = str(self.tree_a) + if b_name is None: + b_name = str(self.tree_b) + + report = StringIO() + report.write(f"Files only in '{a_name}':\n") + for file in self.only_in_tree_a: + report.write(f" {file}\n") + report.write(f"Files only in '{b_name}':\n") + for file in self.only_in_tree_b: + report.write(f" {file}\n") + report.write(f"Files with different content:\n") + for file in self.diff_files: + report.write(f" {file}\n") + return report.getvalue() + + +@dataclass +class UkiData: + path: Path + sections: List[str] + initrd: Path + + +@contextmanager +def uki_extractor(uki_path: Path) -> Generator[UkiData, None, None]: + with tempfile.TemporaryDirectory() as work_dir: + work_path = Path(work_dir) + yield extract_uki(uki_path, work_path) + # Cleanup is handled by the context manager + + +def extract_uki_section( + sections: Dict[str, pefile.SectionStructure], section_name: str, target: Path +): + """ + Extract a specific section from the PE file. + """ + if section_name not in sections: + log.error(f"Section {section_name} not found in PE file.") + raise ValueError(f"Section {section_name} not found in PE file.") + + section = sections[section_name] + log.info(f"Extracting section '{section_name}' to '{target}'") + with open(target, "wb") as f: + f.write(section.get_data()) + return target + + +def detect_compression(path: Path) -> str: + with open(path, "rb") as f: + magic = f.read(4) + if magic.startswith(b"\x1f\x8b"): + return "gzip" + elif magic == b"\x28\xb5\x2f\xfd": + return "zstd" + else: + return "unknown" + + +def extract_initrd(path: Path, target: Path): + """ + Extract the initrd file from the given path. + """ + compression = detect_compression(path) + log.info(f"Detected compression: {compression}") + + target.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as work_dir: + work_path = Path(work_dir) + staging_file = work_path / "staging_file" + if compression == "gzip": + with gzip.open(path, "rb") as gzf: + with open(staging_file, "wb") as out_f: + shutil.copyfileobj(gzf, out_f) + elif compression == "zstd": + subprocess.run( + ["zstd", "-d", str(path), "-o", str(staging_file)], check=True + ) + else: + raise ValueError(f"Unsupported compression type: {compression}") + + # Extract the cpio archive from staging_file into target + subprocess.run( + ["cpio", "-idmv"], + cwd=target, + input=staging_file.read_bytes(), + check=True, + capture_output=True, + ) + + +def extract_uki(uki_path: Path, workdir: Path) -> UkiData: + log.info(f"Extracting UKI file: {uki_path}") + with open(uki_path, "rb") as f: + data = f.read() + pe = pefile.PE(data=data) + + available_sections = {} + section: pefile.SectionStructure + for section in pe.sections: + name: str = section.Name.decode("utf-8").strip("\x00") + available_sections[name] = section + log.debug(f"Sections in UKI file: {','.join(available_sections.keys())}") + + # Extract all sections + initrd_img = workdir / "initrd.img" + extract_uki_section(available_sections, ".initrd", initrd_img) + initrd_extracted = workdir / "initrd" + extract_initrd(initrd_img, workdir / "initrd") + + pe.close() + return UkiData( + path=uki_path, + sections=list(available_sections.keys()), + initrd=initrd_extracted, + ) + + +def compare_ukis( + *, + uki_path: Path, + cosi_a_path: Path, + cosi_a_root: Path, + cosi_b_path: Path, + cosi_b_root: Path, + output: Optional[Path] = None, +): + """ + Compare the UKI files in the two COSI files. + This is a placeholder function and should be implemented based on your requirements. + """ + with uki_extractor(cosi_a_root / uki_path) as uki_a, uki_extractor( + cosi_b_root / uki_path + ) as uki_b: + # Compare the two UKI files + log.info(f"Comparing UKI files: {uki_a.path} and {uki_b.path}") + # Implement your comparison logic here + initrd_compare = TreeCompare(uki_a.initrd, uki_b.initrd) + + report = initrd_compare.report(a_name=cosi_a_path.name, b_name=cosi_b_path.name) + print(report) + + if output: + initrd_compare.copy_diftrees( + output / f"{cosi_a_path.name}-initrd", + output / f"{cosi_b_path.name}-initrd", + ) + + with open(output / "initrd-report.txt", "w") as report_file: + report_file.write(report) + + +def main(): + args = parse_args() + cosi_file_a: Path = args.cosi_file_a + cosi_file_b: Path = args.cosi_file_b + output: Optional[Path] = args.output + + if not cosi_file_a.exists(): + log.error(f"COSI file 1 does not exist: {cosi_file_a}") + exit(1) + + if not cosi_file_b.exists(): + log.error(f"COSI file 2 does not exist: {cosi_file_b}") + exit(1) + + if output: + if output.exists(): + shutil.rmtree(output) + output.mkdir(parents=True, exist_ok=True) + log.info(f"Results will be written to: {output}") + else: + log.info("No output file specified.") + + log.info(f"COSI file 1: {cosi_file_a}") + log.info(f"COSI file 2: {cosi_file_b}") + + with tempfile.TemporaryDirectory() as work_dir: + log.info(f"Temporary work directory: {work_dir}") + work_dir = Path(work_dir) + cosi_a_workdir = work_dir / "cosi_a" + cosi_b_workdir = work_dir / "cosi_b" + + cosi_a_workdir.mkdir(parents=True, exist_ok=True) + cosi_b_workdir.mkdir(parents=True, exist_ok=True) + + with cosi_extractor(cosi_file_a) as cosi_a_root, cosi_extractor( + cosi_file_b + ) as cosi_b_root: + # Compare the two COSI files + log.info(f"Comparing {cosi_file_a} and {cosi_file_b}") + log.debug(f"COSI A root: {cosi_a_root}") + log.debug(f"COSI B root: {cosi_b_root}") + + # Compare the two directories + compare = TreeCompare(cosi_a_root, cosi_b_root) + + report = compare.report() + print(report) + + if output: + with open(output / "report.txt", "w") as report_file: + report_file.write(report) + + compare.copy_diftrees( + output / cosi_file_a.name, + output / cosi_file_b.name, + ) + + for file in compare.diff_files: + if file.suffix == ".efi": + log.info(f"UKI file found: {file}") + compare_ukis( + uki_path=file, + cosi_a_path=cosi_file_a, + cosi_a_root=cosi_a_root, + cosi_b_path=cosi_file_b, + cosi_b_root=cosi_b_root, + output=args.output, + ) + + +if __name__ == "__main__": + main() diff --git a/src/engine/storage/raid.rs b/src/engine/storage/raid.rs index e6f62ad41..d53a3a5f4 100644 --- a/src/engine/storage/raid.rs +++ b/src/engine/storage/raid.rs @@ -14,7 +14,7 @@ use strum_macros::{Display, EnumString}; use osutils::{block_devices, dependencies::Dependency, mdadm, udevadm}; use trident_api::{ config::{HostConfiguration, SoftwareRaidArray}, - constants::MDSTAT_PATH, + constants::{internal_params::ENABLE_UKI_SUPPORT, MDSTAT_PATH}, BlockDeviceId, }; @@ -42,8 +42,17 @@ fn create(config: SoftwareRaidArray, ctx: &EngineContext) -> Result<(), Error> { let device_paths = get_device_paths(ctx, devices).context("Failed to get device paths")?; info!("Initializing '{}': creating RAID array", config.id); - mdadm::create(&config.device_path(), &config.level, device_paths) - .context("Failed to create RAID array")?; + + if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + // If UKI support is enabled, we need to create the RAID array with the + // homehost=any option to ensure that the RAID array can be opened by the + // runtime OS. + mdadm::create_homehost(&config.device_path(), &config.level, device_paths, "any") + } else { + mdadm::create(&config.device_path(), &config.level, device_paths) + } + .context("Failed to create RAID array")?; + Ok(()) } From dba8941b707e4099d48a43d44985e0bc189116a2 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 27 May 2025 20:47:23 +0000 Subject: [PATCH 30/99] Merged PR 23261: bug: Disable usr-verity-e2e on baremetal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Turns out that the test does not work on bare metal, so disabling it for now until it gets fixes to stop failing the pipeline. Work to re-enable: - #12291 Related work items: #12289 --- e2e_tests/target-configurations.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 89a2d61df..8feb96fd5 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -5,7 +5,7 @@ bareMetal: # TODO(12276) re-enable: #- combined - encrypted-partition - split - - usr-verity + # TODO(12291) re-enable: #- usr-verity validation: - base # TODO(12276) re-enable: #- combined @@ -21,8 +21,8 @@ bareMetal: - root-verity - simple - split - - usr-verity - - usr-verity-raid + # TODO(12291) re-enable: #- usr-verity + # TODO(12291) re-enable: #- usr-verity-raid weekly: - base # TODO(12276) re-enable: #- combined @@ -39,8 +39,8 @@ bareMetal: - root-verity - simple - split - - usr-verity - - usr-verity-raid + # TODO(12291) re-enable: #- usr-verity + # TODO(12291) re-enable: #- usr-verity-raid container: daily: - base @@ -55,8 +55,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - - usr-verity - - usr-verity-raid + # TODO(12291) re-enable: #- usr-verity + # TODO(12291) re-enable: #- usr-verity-raid validation: - base # TODO(12276) re-enable: #- combined @@ -70,8 +70,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - - usr-verity - - usr-verity-raid + # TODO(12291) re-enable: #- usr-verity + # TODO(12291) re-enable: #- usr-verity-raid weekly: - base # TODO(12276) re-enable: #- combined @@ -85,8 +85,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - - usr-verity - - usr-verity-raid + # TODO(12291) re-enable: #- usr-verity + # TODO(12291) re-enable: #- usr-verity-raid virtualMachine: host: daily: From ddc5dae827d10e6992cc7441d88d1c509b79c9a7 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Tue, 27 May 2025 21:46:01 +0000 Subject: [PATCH 31/99] Merged PR 23264: Fixes for clippy 1.87 #### AI description (iteration 1) #### PR Classification This PR is a code cleanup that fixes clippy lint warnings introduced in version 1.87. #### PR Summary The changes refine range extraction logic and update lint suppression to ensure compliance with clippy's recommendations. - `src/osimage/cosi/reader.rs`: Replaces chained option handling with explicit if-else for parsing range indices. - `src/osimage/mod.rs`: Adds a clippy suppression attribute for large enum variants during tests. Related work items: #12304 --- src/osimage/cosi/reader.rs | 20 +++++++++++--------- src/osimage/mod.rs | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/osimage/cosi/reader.rs b/src/osimage/cosi/reader.rs index b604b5b7d..eef653395 100644 --- a/src/osimage/cosi/reader.rs +++ b/src/osimage/cosi/reader.rs @@ -515,15 +515,17 @@ mod tests { .unwrap() .split('-') .collect::>(); - let start = ranges[0] - .is_empty() - .then_some(0) - .unwrap_or_else(|| ranges[0].parse::().expect("Failed to parse start")); - let end = ranges[1] - .is_empty() - .then_some(body.len()) - .unwrap_or_else(|| ranges[1].parse::().expect("Failed to parse end")) - .min(body.len() - 1); + let start = if ranges[0].is_empty() { + 0 + } else { + ranges[0].parse::().expect("Failed to parse start") + }; + let end = if ranges[1].is_empty() { + body.len() + } else { + ranges[1].parse::().expect("Failed to parse end") + } + .min(body.len() - 1); trace!("Mocking range {} to {}", start, end); body.as_bytes()[start..=end].to_vec() }) diff --git a/src/osimage/mod.rs b/src/osimage/mod.rs index a078d8596..d3b855a3b 100644 --- a/src/osimage/mod.rs +++ b/src/osimage/mod.rs @@ -34,6 +34,7 @@ use mock::MockOsImage; #[derive(Debug, Clone)] pub struct OsImage(OsImageInner); +#[cfg_attr(test, allow(clippy::large_enum_variant))] #[derive(Debug, Clone)] enum OsImageInner { /// Composable OS Image (COSI) From 13736e1ae7f13592def95d14d11d018fb3699dc2 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 28 May 2025 14:58:55 +0000 Subject: [PATCH 32/99] Merged PR 23250: engineering: SFI: set up code coverage phase 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description SFI wants this magic file set up ---- #### AI description (iteration 1) #### PR Classification New feature: introducing a code coverage configuration for pull requests. #### PR Summary This pull request adds a new configuration file to enable code coverage reporting in the CI pipeline. - `azurepipelines-coverage.yml`: New file that sets up code coverage status with diff coverage and a 50% target. Related work items: #12303 --- azurepipelines-coverage.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 azurepipelines-coverage.yml diff --git a/azurepipelines-coverage.yml b/azurepipelines-coverage.yml new file mode 100644 index 000000000..716200487 --- /dev/null +++ b/azurepipelines-coverage.yml @@ -0,0 +1,5 @@ +coverage: + status: # Code coverage status will be posted to pull requests based on targets defined below. + comments: on # Off by default. When on, details about coverage for each file changed will be posted as a pull request comment. + diff: # diff coverage is code coverage only for the lines changed in a pull request. + target: 50% # set this to a desired %. Default is 50% \ No newline at end of file From e2eecec70adea791a5b0b0ab8e6eefc5a46c1d4b Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Wed, 28 May 2025 18:01:50 +0000 Subject: [PATCH 33/99] Merged PR 23221: engineering: Introduce systemd-pcrlock utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR introduces the systemd-pcrlock utilities that are necessary for implementing PCR-based encryption. TODO: 1. Implement helper methods to generate the two remaining pcrlock files, 2. Use the statically defined pcrlock files once the shiproom approves the PR, and it's merged: https://github.com/microsoft/azurelinux/pull/13821, 3. Actually update the encryption logic to use PCR-based encryption. ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new feature that adds systemd-pcrlock based utilities for PCR-based encryption. #### PR Summary The PR implements a new module to generate TPM2 access policies and create PCR lock files by integrating systemd-pcrlock commands, directly addressing work items for PCR-based encryption in Trident. - `osutils/src/pcrlock.rs`: Adds functionality for generating TPM2 policies, converting PCR flags, and wrapping various `lock-*` commands via a common trait and enum. - `osutils/src/dependencies.rs`: Introduces a new dependency for systemd-pcrlock. - `osutils/src/lib.rs`: Registers the newly added `pcrlock` module. Related work items: #12123 --- osutils/src/dependencies.rs | 3 + osutils/src/lib.rs | 1 + osutils/src/pcrlock.rs | 446 ++++++++++++++++++++++++++++++++++++ sysdefs/src/tpm2.rs | 54 +++++ trident_api/src/error.rs | 3 + 5 files changed, 507 insertions(+) create mode 100644 osutils/src/pcrlock.rs diff --git a/osutils/src/dependencies.rs b/osutils/src/dependencies.rs index 055df95d4..97e422822 100644 --- a/osutils/src/dependencies.rs +++ b/osutils/src/dependencies.rs @@ -119,6 +119,8 @@ pub enum Dependency { SystemdCryptenroll, #[strum(serialize = "systemd-firstboot")] SystemdFirstboot, + #[strum(serialize = "systemd-pcrlock")] + SystemdPcrlock, #[strum(serialize = "systemd-repart")] SystemdRepart, Touch, @@ -152,6 +154,7 @@ impl Dependency { fn path_override(&self) -> Option { Some(PathBuf::from(match self { Self::Netplan => "/usr/sbin/netplan", + Self::SystemdPcrlock => "/usr/lib/systemd/systemd-pcrlock", _ => return None, })) } diff --git a/osutils/src/lib.rs b/osutils/src/lib.rs index c0b71bc40..6a5fc2aa9 100644 --- a/osutils/src/lib.rs +++ b/osutils/src/lib.rs @@ -30,6 +30,7 @@ pub mod osmodifier; pub mod osrelease; pub mod overlay; pub mod path; +pub mod pcrlock; pub mod repart; pub mod resize2fs; pub mod scripts; diff --git a/osutils/src/pcrlock.rs b/osutils/src/pcrlock.rs new file mode 100644 index 000000000..c00a1764c --- /dev/null +++ b/osutils/src/pcrlock.rs @@ -0,0 +1,446 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{Context, Error, Result}; +use enumflags2::{make_bitflags, BitFlags}; +use log::{debug, error, trace, warn}; +use serde::Deserialize; + +use trident_api::error::{ReportError, ServicingError, TridentError}; + +use sysdefs::tpm2::Pcr; + +use crate::dependencies::Dependency; + +/// Path to the pcrlock directory where .pcrlock files are stored. +#[allow(dead_code)] +const PCRLOCK_DIR: &str = "/var/lib/pcrlock.d"; + +/// Path to the PCR policy JSON file. +const PCR_POLICY_PATH: &str = "/var/lib/systemd/pcrlock.json"; + +/// Dir-s for dynamically generated .pcrlock files that might contain 1+ .pcrlock files, for the +/// current and updated images: +/// 1. /var/lib/pcrlock.d/600-gpt.pcrlock.d, where `lock-gpt` measures the GPT partition table of +/// the booted medium, as recorded to PCR 5 by the firmware, +#[allow(dead_code)] +const GPT_PCRLOCK_DIR: &str = "600-gpt.pcrlock.d"; + +/// 2. /var/lib/pcrlock.d/610-boot-loader-code.pcrlock.d, where Trident measures the bootx64.efi +/// binary, as recorded into PCR 4 following Microsoft's Authenticode hash spec, +#[allow(dead_code)] +const BOOT_LOADER_CODE_PCRLOCK_DIR: &str = "610-boot-loader-code.pcrlock.d"; + +/// 3. /var/lib/pcrlock.d/630-boot-loader-conf.pcrlock.d, where `lock-raw` measures the boot loader +/// configuration file, as recorded into PCR 5, +#[allow(dead_code)] +const BOOT_LOADER_CONF_PCRLOCK_DIR: &str = "630-boot-loader-conf.pcrlock.d"; + +/// 4. /var/lib/pcrlock.d/650-uki.pcrlock.d, where `lock-uki` measures the UKI binary, as recorded +/// into PCR 4, +#[allow(dead_code)] +const UKI_PCRLOCK_DIR: &str = "650-uki.pcrlock.d"; + +/// 5. /var/lib/pcrlock.d/710-kernel-cmdline.pcrlock.d, where `lock-kernel-cmdline` measures the +/// kernel command line, as recorded into PCR 9, +#[allow(dead_code)] +const KERNEL_CMDLINE_PCRLOCK_DIR: &str = "710-kernel-cmdline.pcrlock.d"; + +/// 6. /var/lib/pcrlock.d/720-kernel-initrd.pcrlock.d, where Trident measures the initrd section of +/// the UKI binary, as recorded into PCR 9. +#[allow(dead_code)] +const KERNEL_INITRD_PCRLOCK_DIR: &str = "720-kernel-initrd.pcrlock.d"; + +/// Valid PCRs for TPM2 policy generation, following the `systemd-pcrlock` spec. +/// +/// https://www.man7.org/linux/man-pages/man8/systemd-pcrlock.8.html. +const ALLOWED_PCRS: BitFlags = make_bitflags!(Pcr::{Pcr0 | Pcr1 | Pcr2 | Pcr3 | Pcr4 | Pcr5 | Pcr7 | Pcr11 | Pcr12 | Pcr13 | Pcr14 | Pcr15}); + +#[derive(Debug, Deserialize)] +struct PcrValue { + pcr: Pcr, +} + +#[derive(Debug, Deserialize)] +struct PcrPolicy { + #[serde(rename = "pcrValues")] + pcr_values: Vec, +} + +/// Validates the PCR input and calls a helper function to generate the TPM 2.0 access policy. +/// Parses the output of the helper function to validate that the policy has been updated as +/// expected. +/// +/// If PCRs are not specified, the command defaults to PCRs 0-5, 7, 11-15. +pub fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), TridentError> { + debug!( + "Generating a new TPM 2.0 access policy for the following PCRs: {:?}", + pcrs.iter().map(|pcr| pcr.to_value()).collect::>() + ); + + // Validate that all requested PCRs are allowed by systemd-pcrlock + let filtered_pcrs = pcrs & ALLOWED_PCRS; + + if pcrs != filtered_pcrs { + let ignored = pcrs & !filtered_pcrs; + warn!( + "Ignoring unsupported PCRs while generating a new TPM 2.0 access policy: {:?}", + ignored.iter().collect::>() + ); + } + + let output = make_policy(pcrs).structured(ServicingError::GenerateTpm2AccessPolicy)?; + + // Validate that TPM 2.0 access policy has been updated + if !output.contains("Calculated new PCR policy") || !output.contains("Updated NV index") { + warn!("TPM 2.0 access policy has not been updated:\n{}", output); + } + + // Log PCR policy JSON contents + let pcrlock_policy = + fs::read_to_string(PCR_POLICY_PATH).structured(ServicingError::GenerateTpm2AccessPolicy)?; + trace!( + "Contents of PCR policy JSON at '{PCR_POLICY_PATH}':\n{}", + pcrlock_policy + ); + + // Parse the policy JSON to validate that all requested PCRs are present + let policy: PcrPolicy = serde_json::from_str(&pcrlock_policy) + .structured(ServicingError::GenerateTpm2AccessPolicy)?; + // Extract PCRs from the policy, and filter for PCRs that were requested yet are missing + // from the policy + let policy_pcrs: Vec = policy.pcr_values.iter().map(|pv| pv.pcr).collect(); + let missing_pcrs: Vec = pcrs + .iter() + .filter(|pcr| !policy_pcrs.contains(pcr)) + .collect(); + + if !missing_pcrs.is_empty() { + error!( + "Some requested PCRs are missing from the generated PCR policy: '{:?}'", + missing_pcrs + .iter() + .map(|pcr| pcr.to_value()) + .collect::>() + ); + return Err(TridentError::new(ServicingError::GenerateTpm2AccessPolicy)); + } + + Ok(()) +} + +/// Runs `systemd-pcrlock log` command to view the combined TPM 2.0 event log matched against the +/// current PCR values, output in a tabular format. +#[allow(dead_code)] +fn log() -> Result { + Dependency::SystemdPcrlock + .cmd() + .arg("log") + .output_and_check() + .context("Failed to run systemd-pcrlock log") +} + +/// Runs `systemd-pcrlock make-policy` command to predict the PCR state for future boots and then +/// generate a TPM 2.0 access policy, stored in a TPM 2.0 NV index. The prediction and info about +/// the used TPM 2.0 and its NV index are written to PCR_POLICY_PATH. +fn make_policy(pcrs: BitFlags) -> Result { + Dependency::SystemdPcrlock + .cmd() + .arg("make-policy") + .arg(to_pcr_arg(pcrs)) + .output_and_check() + .context("Failed to run systemd-pcrlock make-policy") +} + +/// Converts the provided PCR bitflags into the `--pcr=` argument for `systemd-pcrlock`. Returns a +/// string with the PCR indices separated by `,`. +fn to_pcr_arg(pcrs: BitFlags) -> String { + format!( + "--pcr={}", + pcrs.iter() + .map(|flag| flag.to_value().to_string()) + .collect::>() + .join(",") + ) +} + +/// Represents the `systemd-pcrlock lock-*` commands. Each command generates or removes specific +/// .pcrlock files based on the TPM 2.0 event log of the current/next boot covering all records for +/// a specific set of PCRs. +/// +/// For more info, see the official documentation for the `systemd-pcrlock` tool: +/// https://www.man7.org/linux/man-pages/man8/systemd-pcrlock.8.html. +enum LockCommand { + /// Generates .pcrlock files covering all records for PCRs 0 ("platform-code") and 2 + /// ("external-code"). Allows locking the boot process to the current version of the firmware + /// of the system and its extension cards. + FirmwareCode, + + /// Locks down the firmware configuration, i.e. PCRs 1 ("platform-config") and 3 + /// ("external-config"). + FirmwareConfig, + + /// Generates a .pcrlock file based on the SecureBoot policy currently enforced. Looks at + /// SecureBoot, PK, KEK, db, dbx, dbt, dbr EFI variables and predicts their measurements to PCR + /// 7 ("secure-boot-policy") on the next boot. + SecureBootPolicy, + + /// Generates a .pcrlock file based on the SecureBoot authorities used to validate the boot + /// path. Uses relevant measurements on PCR 7 ("secure-boot-policy"). + SecureBootAuthority, + + /// Generates a .pcrlock file based on the GPT partition table of the specified disk. If no + /// disk is specified automatically determines the block device backing the root file system. + /// Locks the state of the disk partitioning, which firmware measures to PCR 5 + /// ("boot-loader-config"). + Gpt { + path: Option, + pcrlock_file: PathBuf, + }, + + /// Generates a .pcrlock file based on the specified PE binary. Useful for predicting + /// measurements the firmware makes to PCR 4 ("boot-loader-code") if the specified + /// binary is part of the UEFI boot process. + /// + /// Used for non-UKI images only; UKI binaries must be locked with lock-uki. + #[allow(dead_code)] + Pe { + path: PathBuf, + pcrlock_file: PathBuf, + }, + + /// Generates a .pcrlock file based on the specified UKI PE binary. Useful for predicting + /// measurements the firmware makes to PCR 4 ("boot-loader-code"), and systemd-stub makes to + /// PCR 11 ("kernel-boot"). Used for UKI images only; non-UKI binaries must be locked with + /// lock-pe. + Uki { + path: PathBuf, + pcrlock_file: PathBuf, + }, + + /// Generates a .pcrlock file based on /etc/machine-id. Useful for predicting measurements + /// systemd-pcrmachine.service makes to PCR 15 ("system-identity"). + MachineId, + + /// Generates a .pcrlock file based on file system identity. Useful for predicting measurements + /// systemd-pcrfs@.service makes to PCR 15 ("system-identity") for the root and var + /// filesystems. + FileSystem, + + /// Generates a .pcrlock file based on /proc/cmdline (or the specified file if given). Useful + /// for predicting measurements the Linux kernel makes to PCR 9 ("kernel-initrd"). + KernelCmdline { + path: Option, + pcrlock_file: PathBuf, + }, + + /// Generates a .pcrlock file based on a kernel initrd cpio archive. Useful for predicting + /// measurements the Linux kernel makes to PCR 9 ("kernel-initrd"). Should not be used for + /// systemd-stub UKIs, as the initrd is combined dynamically from various sources and hence + /// does not take a single input, like this command. + #[allow(dead_code)] + KernelInitrd { + path: PathBuf, + pcrlock_file: PathBuf, + }, + + /// Generates/removes a .pcrlock file based on raw binary data. The data is either read from + /// the specified file or from STDIN. Requires that --pcrs= is specified. The generated + /// .pcrlock file is written to the file specified via --pcrlock=. + Raw { + path: PathBuf, + pcrs: BitFlags, + pcrlock_file: PathBuf, + }, +} + +impl LockCommand { + /// Returns the name of the subcommand for the `systemd-pcrlock` tool. + fn subcmd_name(&self) -> &'static str { + match self { + Self::FirmwareCode => "lock-firmware-code", + Self::FirmwareConfig => "lock-firmware-config", + Self::SecureBootPolicy => "lock-secureboot-policy", + Self::SecureBootAuthority => "lock-secureboot-authority", + Self::MachineId => "lock-machine-id", + Self::FileSystem => "lock-file-system", + Self::Gpt { .. } => "lock-gpt", + Self::Pe { .. } => "lock-pe", + Self::Uki { .. } => "lock-uki", + Self::KernelCmdline { .. } => "lock-kernel-cmdline", + Self::KernelInitrd { .. } => "lock-kernel-initrd", + Self::Raw { .. } => "lock-raw", + } + } + + /// Runs a `systemd-pcrlock` command. + /// + /// Primarily designed for running the `lock-*` commands. + fn run(&self) -> Result<(), Error> { + let (path, pcrlock_file, pcrs) = { + let mut cmd_path: Option = None; + let mut cmd_pcrlock_file: Option = None; + let mut cmd_pcrs: Option> = None; + + match self { + Self::FirmwareCode + | Self::FirmwareConfig + | Self::SecureBootPolicy + | Self::SecureBootAuthority + | Self::MachineId + | Self::FileSystem => (), + + Self::Gpt { path, pcrlock_file } | Self::KernelCmdline { path, pcrlock_file } => { + cmd_path = path.clone(); + cmd_pcrlock_file = Some(pcrlock_file.clone()); + } + + Self::Pe { path, pcrlock_file } + | Self::Uki { path, pcrlock_file } + | Self::KernelInitrd { path, pcrlock_file } => { + cmd_path = Some(path.clone()); + cmd_pcrlock_file = Some(pcrlock_file.clone()); + } + + Self::Raw { + path, + pcrs: raw_pcrs, + pcrlock_file, + } => { + cmd_path = Some(path.clone()); + cmd_pcrlock_file = Some(pcrlock_file.clone()); + cmd_pcrs = Some(*raw_pcrs); + } + } + + (cmd_path, cmd_pcrlock_file, cmd_pcrs) + }; + + let mut cmd = Dependency::SystemdPcrlock.cmd(); + cmd.arg(self.subcmd_name()); + + if let Some(path) = path { + cmd.arg(path); + } + + if let Some(pcrs) = pcrs { + cmd.arg(to_pcr_arg(pcrs)); + } + + if let Some(pcrlock_file) = pcrlock_file { + cmd.arg(format!("--pcrlock={}", pcrlock_file.display())); + } + + cmd.run_and_check() + .with_context(|| format!("Failed to run systemd-pcrlock {}", self.subcmd_name())) + } +} + +/// Generates dynamically defined .pcrlock files for either (1) the current boot only or (2) the +/// current and the next boots. Calls the `systemd-pcrlock lock-*` commands to generate the +/// .pcrlock files, as well as native logic to generate the remaining .pcrlock files. +pub fn generate_pcrlock_files( + // lock-gpt -> path of partitioned disk, pcrlock_file to write to + gpt_disks: Vec<(Option, PathBuf)>, + // lock-pe -> path of PE binary, pcrlock_file to write to + _pe_binaries: Vec<(PathBuf, PathBuf)>, + // lock-uki -> path of UKI PE binary, pcrlock_file to write to + uki_binaries: Vec<(PathBuf, PathBuf)>, + // lock-kernel-cmdline -> path of kernel cmdline, pcrlock_file to write to + kernel_cmdlines: Vec<(Option, PathBuf)>, + // lock-kernel-initrd -> path, pcrlock_file to write to + _kernel_initrds: Vec<(PathBuf, PathBuf)>, + // lock-raw -> path, pcrs, pcrlock_file to write to + raw_binaries: Vec<(PathBuf, BitFlags, PathBuf)>, +) -> Result<()> { + let basic_cmds: Vec = vec![ + LockCommand::FirmwareCode, + LockCommand::FirmwareConfig, + LockCommand::SecureBootPolicy, + LockCommand::SecureBootAuthority, + LockCommand::MachineId, + LockCommand::FileSystem, + ]; + + for cmd in basic_cmds { + cmd.run()?; + } + + for (path, pcrlock_file) in gpt_disks { + LockCommand::Gpt { path, pcrlock_file }.run()?; + } + + for (path, pcrlock_file) in uki_binaries { + LockCommand::Uki { path, pcrlock_file }.run()?; + } + + for (path, pcrlock_file) in kernel_cmdlines { + LockCommand::KernelCmdline { path, pcrlock_file }.run()?; + } + + // For now, needed to generate 630-boot-loader-conf.pcrlock.d, which measures the raw binary of + // /boot/efi/loader/loader.conf into PCR 5. + for (path, pcrs, pcrlock_file) in raw_binaries { + LockCommand::Raw { + path, + pcrs, + pcrlock_file, + } + .run()?; + } + + // TODO: Run helpers to generate remaining .pcrlock files, which cannot be generated via the + // lock-* commands. + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use enumflags2::make_bitflags; + + #[test] + fn test_to_pcr_arg() { + let pcrs = make_bitflags!(Pcr::{Pcr1 | Pcr4}); + assert_eq!(to_pcr_arg(pcrs), "--pcr=1,4".to_string()); + + let single_pcr = make_bitflags!(Pcr::{Pcr7}); + assert_eq!(to_pcr_arg(single_pcr), "--pcr=7".to_string()); + + let all_pcrs = BitFlags::::all(); + assert_eq!( + to_pcr_arg(all_pcrs), + "--pcr=0,1,2,3,4,5,7,9,10,11,12,13,14,15,16,23".to_string() + ); + } +} + +#[cfg(feature = "functional-test")] +#[cfg_attr(not(test), allow(unused_imports, dead_code))] +mod functional_test { + use super::*; + + use pytest_gen::functional_test; + + use trident_api::error::ErrorKind; + + #[functional_test(feature = "helpers")] + fn test_generate_tpm2_access_policy() { + // Test case #0. Since no pcrlock files have been generated yet, only 0-valued PCRs can be + // used to generate a TPM 2.0 access policy. + let zero_pcrs = make_bitflags!(Pcr::{Pcr11 | Pcr12 | Pcr13}); + generate_tpm2_access_policy(zero_pcrs).unwrap(); + + // Test case #1. Try to generate a TPM 2.0 access policy with all PCRs; should return an + // error since no pcrlock files have been generated yet. + let pcrs = BitFlags::::all(); + assert_eq!( + generate_tpm2_access_policy(pcrs).unwrap_err().kind(), + &ErrorKind::Servicing(ServicingError::GenerateTpm2AccessPolicy) + ); + + // TODO: Add other/more test cases once helpers are implemented and statically defined + // pcrlock files have been added. + } +} diff --git a/sysdefs/src/tpm2.rs b/sysdefs/src/tpm2.rs index feea2786c..01eb97757 100644 --- a/sysdefs/src/tpm2.rs +++ b/sysdefs/src/tpm2.rs @@ -1,4 +1,6 @@ +use anyhow::{bail, Error}; use enumflags2::bitflags; +use serde::{self, Deserialize}; /// Represents the Platform Configuration Registers (PCRs) in the TPM. #[bitflags] @@ -44,6 +46,43 @@ impl Pcr { pub fn to_value(&self) -> u32 { (*self as u32).trailing_zeros() } + + /// Returns the PCR for the given digit value. Needed for deserialization. + pub fn from_value(value: u32) -> Result { + match value { + 0 => Ok(Pcr::Pcr0), + 1 => Ok(Pcr::Pcr1), + 2 => Ok(Pcr::Pcr2), + 3 => Ok(Pcr::Pcr3), + 4 => Ok(Pcr::Pcr4), + 5 => Ok(Pcr::Pcr5), + 7 => Ok(Pcr::Pcr7), + 9 => Ok(Pcr::Pcr9), + 10 => Ok(Pcr::Pcr10), + 11 => Ok(Pcr::Pcr11), + 12 => Ok(Pcr::Pcr12), + 13 => Ok(Pcr::Pcr13), + 14 => Ok(Pcr::Pcr14), + 15 => Ok(Pcr::Pcr15), + 16 => Ok(Pcr::Pcr16), + 23 => Ok(Pcr::Pcr23), + _ => bail!( + "Failed to convert an invalid PCR value '{}' to a Pcr", + value + ), + } + } +} + +impl<'de> Deserialize<'de> for Pcr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let val = u32::deserialize(deserializer)?; + Pcr::from_value(val) + .map_err(|_| serde::de::Error::custom(format!("Failed to deserialize PCR: {}", val))) + } } #[cfg(test)] @@ -69,4 +108,19 @@ mod tests { assert_eq!(Pcr::Pcr16.to_value(), 16); assert_eq!(Pcr::Pcr23.to_value(), 23); } + + #[test] + fn test_from_value() { + // Test case #0: Convert a valid value to a PCR. + assert_eq!(Pcr::from_value(0).unwrap(), Pcr::Pcr0); + assert_eq!(Pcr::from_value(1).unwrap(), Pcr::Pcr1); + assert_eq!(Pcr::from_value(2).unwrap(), Pcr::Pcr2); + assert_eq!(Pcr::from_value(23).unwrap(), Pcr::Pcr23); + + // Test case #1: Convert an invalid value to a PCR. + assert_eq!( + Pcr::from_value(31).unwrap_err().root_cause().to_string(), + "Failed to convert an invalid PCR value '31' to a Pcr" + ); + } } diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index 7448f51cd..6a4d0e84f 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -419,6 +419,9 @@ pub enum ServicingError { #[error("Failed to generate recovery key file '{key_file}'")] GenerateRecoveryKeyFile { key_file: String }, + #[error("Failed to generate a new TPM 2.0 access policy")] + GenerateTpm2AccessPolicy, + #[error("Failed to get block device path for device '{device_id}'")] GetBlockDevicePath { device_id: String }, From b6fb8172cb57db4d4d785b03c51968e86d5d5530 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Wed, 28 May 2025 23:56:32 +0000 Subject: [PATCH 34/99] Merged PR 23276: engineering: Update pipelines to build installer ISOs from containerized MIC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description In order to do so, use new template which calls `testimages.py` script in test-images instead of using Make targets. In addition, update the name of the installer ISO files to omit `-testimage` for succinctness. This PR depends on !23267 in test-images repo. PR e2e run: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=821434&view=results ---- #### AI description (iteration 1) #### PR Classification This pull request refactors and updates the CI/CD pipeline configuration to build installer ISOs from containerized MIC. #### PR Summary The changes standardize and simplify the pipeline templates by replacing the old build-installer.yml with build-runtime.yml and updating naming conventions for installer artifacts. - `/.pipelines/templates/stages/build_image/build-installer.yml` was deleted and references were switched to `build-runtime.yml` in `/.pipelines/templates/e2e-template.yml`. - Multiple pipeline templates (in testing, artifact download, and netlaunch stages) were updated to remove the “-testimage” suffix from installer image names. - The `Makefile` was modified to update artifact paths and commands to match the new naming convention. - A new `clones` parameter was added in `build-runtime.yml` to control image generation. Related work items: #12296 --- .pipelines/templates/e2e-template.yml | 18 ++-- .../stages/build_image/build-installer.yml | 90 ------------------- .../stages/build_image/build-runtime.yml | 7 +- .../download_artifacts/get-artifacts.yml | 12 +-- .../testing_baremetal/baremetal-testing.yml | 8 +- .../testing_common/download-test-images.yml | 12 +-- .../testing_functional/functional-testing.yml | 6 +- .../stages/testing_vm/netlaunch-testing.yml | 12 +-- Makefile | 12 +-- 9 files changed, 46 insertions(+), 131 deletions(-) delete mode 100644 .pipelines/templates/stages/build_image/build-installer.yml diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index 70994b370..db832c6e0 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -88,35 +88,35 @@ stages: baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} # Build Trident installer ISO (host) - - template: stages/build_image/build-installer.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-installer-testimage.iso - outputArtifactName: trident-installer-testimage + imageName: trident-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} + clones: 1 # Build Trident split (stage and finalize separated) installer ISO (host) - - template: stages/build_image/build-installer.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-split-installer-testimage.iso - outputArtifactName: trident-split-installer-testimage + imageName: trident-split-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} + clones: 1 # Build Trident installer ISO (container) - - template: stages/build_image/build-installer.yml + - template: stages/build_image/build-runtime.yml parameters: - imageName: build/trident-container-installer-testimage.iso - outputArtifactName: trident-container-installer-testimage + imageName: trident-container-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} runtimeEnv: "container" micBuildType: ${{ parameters.micBuildType }} micVersion: ${{ parameters.micVersion }} + clones: 1 # Build Trident test image (regular) - template: stages/build_image/build-runtime.yml diff --git a/.pipelines/templates/stages/build_image/build-installer.yml b/.pipelines/templates/stages/build_image/build-installer.yml deleted file mode 100644 index aef73cc85..000000000 --- a/.pipelines/templates/stages/build_image/build-installer.yml +++ /dev/null @@ -1,90 +0,0 @@ -parameters: - - name: imageName - type: string - - - name: outputArtifactName - type: string - - - name: baseimgBuildType - displayName: Base Image build type - type: string - values: - - dev - - preview - - release - default: "release" - - - name: baseImagePipelineBuildId - displayName: "Build Id of the pipeline run, default will select latest successful run from pipeline 2116 ([AMD64-6-OneBranch]-Prod-BuildImages) with tag 3.0-preview" - type: string - default: "latestFromBranch" - - - name: runtimeEnv - displayName: "Runtime environment (host vs container)" - type: string - default: "host" - values: - - host - - container - - - name: micBuildType - displayName: MIC Build Type - type: string - values: - - dev - - preview - - release - default: release - - - name: micVersion - displayName: MIC Version - type: string - default: "*.*.*" - -stages: - - stage: TridentTestImg_${{ replace(parameters.outputArtifactName, '-', '_') }} - displayName: Build ${{ parameters.outputArtifactName }} - dependsOn: - - ${{ if eq(parameters.runtimeEnv, 'host') }}: - - GetTridentBinaries_rpms - - ${{ else }}: [] - - jobs: - - job: BuildTridentTestImg - displayName: Build - timeoutInMinutes: 20 - pool: - type: linux - - variables: - ob_outputDirectory: $(Pipeline.Workspace)/s/test-images/output - ob_artifactBaseName: ${{ parameters.outputArtifactName }} - BASEIMG_AZURE_LINUX_VERSION: "3.0" - - steps: - - template: ../common_tasks/avoid-pypi-usage.yml - - - task: DownloadPipelineArtifact@2 - inputs: - buildType: current - artifactName: trident-binaries - targetPath: "$(Build.ArtifactStagingDirectory)/trident" - displayName: Download Trident RPMs - condition: eq('${{ parameters.runtimeEnv }}', 'host') - - - template: ../common_tasks/find-base-image-version.yml - parameters: - baseimgBuildType: ${{ parameters.baseimgBuildType }} - baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} - - - template: .pipelines/templates/trident-testimg-template.yml@test-images - parameters: - target: ${{ parameters.imageName }} - outputDirectory: ${{ variables.ob_outputDirectory }} - testImagesRepo: test-images - micBuildType: ${{ parameters.micBuildType }} - micVersion: ${{ parameters.micVersion }} - baseimgBuildType: ${{ parameters.baseimgBuildType }} - baseimgVersion: $(baseimgVersion) - baseimgAzureLinuxVersion: ${{ variables.BASEIMG_AZURE_LINUX_VERSION }} - downloadTrident: false diff --git a/.pipelines/templates/stages/build_image/build-runtime.yml b/.pipelines/templates/stages/build_image/build-runtime.yml index 56c853d8b..2c021cb7d 100644 --- a/.pipelines/templates/stages/build_image/build-runtime.yml +++ b/.pipelines/templates/stages/build_image/build-runtime.yml @@ -38,6 +38,11 @@ parameters: type: string default: "*.*.*" + - name: clones + displayName: "Number of clones to generate" + type: number + default: 2 + stages: - stage: TridentTestImg_${{ replace(parameters.imageName, '-', '_') }} displayName: Build ${{ parameters.imageName }} @@ -77,7 +82,7 @@ stages: - template: .pipelines/templates/build-image.yml@test-images parameters: imageName: ${{ parameters.imageName }} - clones: 2 + clones: ${{ parameters.clones }} baseimgBuildType: ${{ parameters.baseimgBuildType }} baseimgVersion: $(baseimgVersion) azureLinuxVersion: ${{ variables.BASEIMG_AZURE_LINUX_VERSION }} diff --git a/.pipelines/templates/stages/download_artifacts/get-artifacts.yml b/.pipelines/templates/stages/download_artifacts/get-artifacts.yml index 7dd7f3361..a997db757 100644 --- a/.pipelines/templates/stages/download_artifacts/get-artifacts.yml +++ b/.pipelines/templates/stages/download_artifacts/get-artifacts.yml @@ -195,14 +195,14 @@ stages: targetPath: "$(System.ArtifactsDirectory)/container/" - job: TridentContainerInstallerTestimage - displayName: Download trident-container-installer-testimage + displayName: Download trident-container-installer timeoutInMinutes: 10 pool: type: linux variables: ob_outputDirectory: $(Build.SourcesDirectory)/build - ob_artifactBaseName: "trident-container-installer-testimage" + ob_artifactBaseName: "trident-container-installer" steps: - task: DownloadPipelineArtifact@2 @@ -267,14 +267,14 @@ stages: # Host images: - ${{ if eq(parameters.hostImages, true) }}: - job: TridentInstallerTestimage - displayName: Download trident-installer-testimage + displayName: Download trident-installer timeoutInMinutes: 10 pool: type: linux variables: ob_outputDirectory: $(Build.SourcesDirectory)/build - ob_artifactBaseName: "trident-installer-testimage" + ob_artifactBaseName: "trident-installer" steps: - task: DownloadPipelineArtifact@2 @@ -291,14 +291,14 @@ stages: targetPath: $(ob_outputDirectory) - job: TridentSplitInstallerTestimage - displayName: Download trident-split-installer-testimage + displayName: Download trident-split-installer timeoutInMinutes: 10 pool: type: linux variables: ob_outputDirectory: $(Build.SourcesDirectory)/build - ob_artifactBaseName: "trident-split-installer-testimage" + ob_artifactBaseName: "trident-split-installer" steps: - task: DownloadPipelineArtifact@2 diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index aa07de171..bb459cc6d 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -40,12 +40,12 @@ stages: - BuildingTools - ${{ if eq(parameters.runtimeEnv, 'container') }}: - BuildTridentContainerImage - - TridentTestImg_trident_container_installer_testimage + - TridentTestImg_trident_container_installer - TridentTestImg_trident_container_testimage - TridentTestImg_trident_container_verity_testimage - ${{ else }}: - TridentTestImg_trident_verity_testimage - - TridentTestImg_trident_installer_testimage + - TridentTestImg_trident_installer - TridentTestImg_trident_testimage jobs: @@ -107,9 +107,9 @@ stages: - name: INSTALLER_ISO_NAME ${{ if eq(parameters.runtimeEnv, 'container') }}: - value: "trident-container-installer-testimage" + value: "trident-container-installer" ${{ else }}: - value: "trident-installer-testimage" + value: "trident-installer" - name: IMAGE_NAME ${{ if eq(parameters.runtimeEnv, 'container') }}: diff --git a/.pipelines/templates/stages/testing_common/download-test-images.yml b/.pipelines/templates/stages/testing_common/download-test-images.yml index dcf8b5e40..a5e5e8107 100644 --- a/.pipelines/templates/stages/testing_common/download-test-images.yml +++ b/.pipelines/templates/stages/testing_common/download-test-images.yml @@ -2,13 +2,13 @@ parameters: - name: installerISO displayName: "Image used for the installer, source of Trident (container vs host)" type: string - default: trident-installer-testimage + default: trident-installer # Test selection is at runtime, and template parameters cannot be validated against runtime variables. # So, do validation in task below rather than specifying this here: # values: - # - trident-installer-testimage - # - trident-split-installer-testimage - # - trident-container-installer-testimage + # - trident-installer + # - trident-split-installer + # - trident-container-installer - name: tridentTestImage displayName: "Image used the runtime OS, source of Trident (container vs host)" @@ -71,11 +71,11 @@ steps: # So, do validation here. # case "${{ parameters.installerISO }}" in - trident-installer-testimage|trident-split-installer-testimage|trident-container-installer-testimage) + trident-installer|trident-split-installer|trident-container-installer) # Valid image_type, do nothing ;; *) - echo "installerISO should be either 'trident-installer-testimage', 'trident-split-installer-testimage', or 'trident-container-installer-testimage'." + echo "installerISO should be either 'trident-installer', 'trident-split-installer', or 'trident-container-installer'." exit 1 ;; esac diff --git a/.pipelines/templates/stages/testing_functional/functional-testing.yml b/.pipelines/templates/stages/testing_functional/functional-testing.yml index a70f21b3c..ff792d8fe 100644 --- a/.pipelines/templates/stages/testing_functional/functional-testing.yml +++ b/.pipelines/templates/stages/testing_functional/functional-testing.yml @@ -19,7 +19,7 @@ stages: - DownloadTestingElements - ${{ else }}: - BuildingTools - - TridentTestImg_trident_installer_testimage + - TridentTestImg_trident_installer - TridentTestImg_trident_testimage - TridentTestImg_trident_verity_testimage @@ -78,7 +78,7 @@ stages: # rebuilding the test binaries and invoking pytest. The regular # target is meant for local use and does extra setup not required # here. - sg libvirt "make functional-test-core INSTALLER_ISO_PATH=./artifacts/iso/trident-installer-testimage.iso ARGUS_TOOLKIT_PATH=argus-toolkit" + sg libvirt "make functional-test-core INSTALLER_ISO_PATH=./artifacts/iso/trident-installer.iso ARGUS_TOOLKIT_PATH=argus-toolkit" displayName: Execute Functional Tests - template: ../common_tasks/coverage.yml @@ -97,6 +97,6 @@ stages: - bash: | set -eux - sg libvirt "make patch-functional-test INSTALLER_ISO_PATH=$(System.ArtifactsDirectory)/trident-installer-testimg.iso ARGUS_TOOLKIT_PATH=argus-toolkit" + sg libvirt "make patch-functional-test INSTALLER_ISO_PATH=$(System.ArtifactsDirectory)/trident-installer.iso ARGUS_TOOLKIT_PATH=argus-toolkit" condition: and(succeeded(), eq('${{ parameters.buildPurpose }}', 'post_merge')) displayName: Rerun Functional Tests diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index fd19e13fc..4f25ef64f 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -42,12 +42,12 @@ stages: - BuildingTools - ${{ if eq(parameters.runtimeEnv, 'container') }}: - BuildTridentContainerImage - - TridentTestImg_trident_container_installer_testimage + - TridentTestImg_trident_container_installer - TridentTestImg_trident_container_testimage - TridentTestImg_trident_container_verity_testimage - ${{ else }}: - - TridentTestImg_trident_split_installer_testimage - - TridentTestImg_trident_installer_testimage + - TridentTestImg_trident_split_installer + - TridentTestImg_trident_installer - TridentTestImg_trident_testimage - TridentTestImg_trident_verity_testimage @@ -72,13 +72,13 @@ stages: argusToolkitSourceDirectory: $(Build.SourcesDirectory)/argus-toolkit ${{ if eq(parameters.runtimeEnv, 'container') }}: - installerISOName: trident-container-installer-testimage + installerISOName: trident-container-installer testImageName: trident-container-testimage verityTestImageName: trident-container-verity-testimage usrVerityTestImageName: trident-container-usrverity-testimage downloadTridentContainer: true ${{ else }}: - installerISOName: trident-installer-testimage + installerISOName: trident-installer testImageName: trident-testimage verityTestImageName: trident-verity-testimage usrVerityTestImageName: trident-usrverity-testimage @@ -92,7 +92,7 @@ stages: steps: - bash: | if [ ${{ variables['tridentConfigurationName'] }} == 'split' ]; then - splitInstallerIsoName="trident-split-installer-testimage" + splitInstallerIsoName="trident-split-installer" echo "setting variable.installerISOName to $splitInstallerIsoName" echo "##vso[task.setvariable variable=installerISOName]$splitInstallerIsoName" fi diff --git a/Makefile b/Makefile index 88deede44..e017da6e1 100644 --- a/Makefile +++ b/Makefile @@ -378,12 +378,12 @@ run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlau run-netlaunch-container-images: \ validate \ $(NETLAUNCH_CONFIG) \ - artifacts/trident-container-installer-testimage.iso \ + artifacts/trident-container-installer.iso \ artifacts/test-image/trident-container.tar.gz \ $(TRIDENT_CONFIG) \ bin/netlaunch @bin/netlaunch \ - --iso artifacts/trident-container-installer-testimage.iso \ + --iso artifacts/trident-container-installer.iso \ $(if $(NETLAUNCH_PORT),--port $(NETLAUNCH_PORT)) \ --config $(NETLAUNCH_CONFIG) \ --trident $(TRIDENT_CONFIG) \ @@ -522,7 +522,7 @@ endif --project "ECF" \ --run-id $(RUN_ID) \ --path artifacts/ \ - --artifact-name 'trident-installer-testimage' + --artifact-name 'trident-installer' .PHONY: download-trident-container-installer-iso download-trident-container-installer-iso: @@ -544,11 +544,11 @@ download-trident-container-installer-iso: --project "ECF" \ --run-id $(RUN_ID) \ --path artifacts/ \ - --artifact-name 'trident-container-installer-testimage' + --artifact-name 'trident-container-installer' -artifacts/trident-container-installer-testimage.iso: +artifacts/trident-container-installer.iso: $(MAKE) download-trident-container-installer-iso; \ - ls -l artifacts/trident-container-installer-testimage.iso + ls -l artifacts/trident-container-installer.iso # Copies locally built runtime images from ../test-images/build to ./artifacts/test-image. # Expects that both the regular and verity Trident test images have been built. From 6eaa6093b7c52223615791aee9c3476e84b35341 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Thu, 29 May 2025 01:33:04 +0000 Subject: [PATCH 35/99] Merged PR 23278: engineering: (small follow-up) rename build-runtime to build-image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Rename `build-runtime.yml` to `build-image.yml` since the template now also is used to build installer ISOs. ---- #### AI description (iteration 1) #### PR Classification This PR performs a code cleanup by renaming template references. #### PR Summary This pull request updates YAML pipeline configuration by replacing all occurrences of `build-runtime.yml` with `build-image.yml` for clearer naming. - `/.pipelines/templates/e2e-template.yml`: Updated multiple pipeline stages to reference `build-image.yml` instead of `build-runtime.yml`. Related work items: #12296 --- .pipelines/templates/e2e-template.yml | 18 +++++++++--------- .../{build-runtime.yml => build-image.yml} | 0 2 files changed, 9 insertions(+), 9 deletions(-) rename .pipelines/templates/stages/build_image/{build-runtime.yml => build-image.yml} (100%) diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index db832c6e0..569bee10c 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -88,7 +88,7 @@ stages: baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} # Build Trident installer ISO (host) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -98,7 +98,7 @@ stages: clones: 1 # Build Trident split (stage and finalize separated) installer ISO (host) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-split-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -108,7 +108,7 @@ stages: clones: 1 # Build Trident installer ISO (container) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-container-installer baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -119,7 +119,7 @@ stages: clones: 1 # Build Trident test image (regular) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -128,7 +128,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image (container) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-container-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -138,7 +138,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image for verity (host) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-verity-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -147,7 +147,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image for verity (container) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-container-verity-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -157,7 +157,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image for usr-verity (host) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-usrverity-testimage baseimgBuildType: ${{ parameters.baseimgBuildType }} @@ -166,7 +166,7 @@ stages: micVersion: ${{ parameters.micVersion }} # Build Trident test image for usr-verity (container) - - template: stages/build_image/build-runtime.yml + - template: stages/build_image/build-image.yml parameters: imageName: trident-container-usrverity-testimage runtimeEnv: "container" diff --git a/.pipelines/templates/stages/build_image/build-runtime.yml b/.pipelines/templates/stages/build_image/build-image.yml similarity index 100% rename from .pipelines/templates/stages/build_image/build-runtime.yml rename to .pipelines/templates/stages/build_image/build-image.yml From a487fa6d5bb7b39ac208b6ac140ad3b861d8b386 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 29 May 2025 18:19:05 +0000 Subject: [PATCH 36/99] Merged PR 23274: engineering: Enable UKI + Encryption - Internal param to override sealing PCR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description UKIs seem to be changing the value of PCR 7, which we currently use for sealing encryption keys. This PR adds an internal param to override the default and updates config with UKI+encryption to use PCR 0 intead while we work on the overall encryption strategy. ---- #### AI description (iteration 1) #### PR Classification This pull request implements a new feature that enables overriding the default sealing PCR by introducing a new internal parameter. #### PR Summary The changes allow encryption provisioning to use a custom PCR binding instead of the default PCR7 by reading the `overrideEncryptionPcrs` internal parameter. This update increases flexibility for UKI+Encryption and includes corresponding adjustments in test configurations. - **`src/engine/storage/encryption.rs`**: Added logic to parse and apply the new `overrideEncryptionPcrs` parameter and adjusted the encryption workflow to pass custom PCR flags. - **`trident_api/src/constants.rs`**: Introduced the constant `OVERRIDE_ENCRYPTION_PCRS` for internal parameter support. - **e2e test configurations**: Updated various `trident-config.yaml` and `target-configurations.yaml` files to include the new parameter and re-enable related test cases. Related work items: #12276 --- .../stages/testing_common/trident-rebuild.yml | 5 +- e2e_tests/encryption_test.py | 6 +-- e2e_tests/target-configurations.yaml | 52 +++++++++---------- .../combined/trident-config.yaml | 1 + .../trident-config.yaml | 1 + .../rerun/trident-config.yaml | 1 + src/engine/storage/encryption.rs | 46 ++++++++++++---- trident_api/src/constants.rs | 3 ++ 8 files changed, 73 insertions(+), 42 deletions(-) diff --git a/.pipelines/templates/stages/testing_common/trident-rebuild.yml b/.pipelines/templates/stages/testing_common/trident-rebuild.yml index e25425a43..466fd4dc1 100644 --- a/.pipelines/templates/stages/testing_common/trident-rebuild.yml +++ b/.pipelines/templates/stages/testing_common/trident-rebuild.yml @@ -37,10 +37,13 @@ parameters: steps: - bash: | set -eu + + # For some reason the file is owned by root, so sudo is needed to read it. raidExists=$(sudo yq e '.storage.raid != null' "${{ parameters.tridentConfigPath }}/trident-config.yaml") + usrVerity=$(sudo yq e '.storage.verity[0].name == "usr"' "${{ parameters.tridentConfigPath }}/trident-config.yaml") # TODO (12277): Support for UKI + Rebuild - if [ "$raidExists" == "true" ] && [ "${{ parameters.tridentConfigurationName }}" != "usr-verity-raid" ]; then + if [ "$raidExists" == "true" ] && [ "$usrVerity" != "true" ]; then echo "Trident config requires Rebuild testing" echo "##vso[task.setvariable variable=TEST_REBUILD_RAID]True" else diff --git a/e2e_tests/encryption_test.py b/e2e_tests/encryption_test.py index a32f3987e..9df30db4a 100644 --- a/e2e_tests/encryption_test.py +++ b/e2e_tests/encryption_test.py @@ -470,10 +470,10 @@ def check_crypsetup_luks_dump(conn: fabric.Connection, cryptDevPath: str) -> Non ), f"Expected one TPM keyslot, got {len(dump['tokens'][0]['keyslots'])}" actual = dump["tokens"]["0"]["tpm2-pcrs"][0] - expectedInt = 7 + expectedInt = [0, 7] assert ( - actual == expectedInt - ), f"Expected TPM2 PCR to be {expected!r}, got {actual!r}" + actual in expectedInt + ), f"Expected TPM2 PCR to be in '{expectedInt}', got {actual}" assert ( len(dump["tokens"]["0"]["tpm2-pcrs"]) == 1 diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 8feb96fd5..1f8f68468 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -91,17 +91,16 @@ virtualMachine: host: daily: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -109,17 +108,16 @@ virtualMachine: - usr-verity-raid post_merge: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -127,27 +125,26 @@ virtualMachine: - usr-verity-raid pullrequest: - base - # TODO(12276) re-enable: #- combined + - combined - raid-mirrored - raid-resync-small - # TODO(12276) re-enable: #- rerun + - rerun - simple - split - usr-verity - usr-verity-raid validation: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -155,17 +152,16 @@ virtualMachine: - usr-verity-raid weekly: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -174,7 +170,7 @@ virtualMachine: container: daily: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -182,14 +178,14 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity - usr-verity-raid post_merge: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -197,26 +193,26 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity - usr-verity-raid pullrequest: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - raid-mirrored - raid-resync-small - # TODO(12276) re-enable: #- rerun + - rerun - simple - usr-verity - usr-verity-raid validation: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -224,14 +220,14 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity - usr-verity-raid weekly: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -239,7 +235,7 @@ virtualMachine: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity diff --git a/e2e_tests/trident_configurations/combined/trident-config.yaml b/e2e_tests/trident_configurations/combined/trident-config.yaml index 64a7978a4..83780b5ae 100644 --- a/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -1,5 +1,6 @@ internalParams: uki: true + overrideEncryptionPcrs: [0] image: url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index 0e2545738..72f50fb59 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -1,5 +1,6 @@ internalParams: uki: true + overrideEncryptionPcrs: [0] image: url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored diff --git a/e2e_tests/trident_configurations/rerun/trident-config.yaml b/e2e_tests/trident_configurations/rerun/trident-config.yaml index 2ae8840bd..5f132b635 100644 --- a/e2e_tests/trident_configurations/rerun/trident-config.yaml +++ b/e2e_tests/trident_configurations/rerun/trident-config.yaml @@ -1,5 +1,6 @@ internalParams: uki: true + overrideEncryptionPcrs: [0] image: url: http://NETLAUNCH_HOST_ADDRESS/files/usrverity.cosi sha384: ignored diff --git a/src/engine/storage/encryption.rs b/src/engine/storage/encryption.rs index 43dee68ff..adbc315dd 100644 --- a/src/engine/storage/encryption.rs +++ b/src/engine/storage/encryption.rs @@ -19,7 +19,9 @@ use osutils::{ use sysdefs::tpm2::Pcr; use trident_api::{ config::{HostConfiguration, HostConfigurationStaticValidationError, Partition, PartitionSize}, - constants::internal_params::{NO_CLOSE_ENCRYPTED_VOLUMES, REENCRYPT_ON_CLEAN_INSTALL}, + constants::internal_params::{ + NO_CLOSE_ENCRYPTED_VOLUMES, OVERRIDE_ENCRYPTION_PCRS, REENCRYPT_ON_CLEAN_INSTALL, + }, error::{InvalidInputError, ReportError, ServicingError, TridentError}, BlockDeviceId, }; @@ -134,20 +136,43 @@ pub(super) fn provision( }, )?; + let encryption_type = if host_config + .internal_params + .get_flag(REENCRYPT_ON_CLEAN_INSTALL) + { + EncryptionType::Reencrypt + } else { + EncryptionType::LuksFormat + }; + + let pcrs = ctx + .spec + .internal_params + // Extract the parameter as a `Vec`, which is a list of PCRs to bind the + // encryption key to. + .get::>(OVERRIDE_ENCRYPTION_PCRS) + // Get the result from under the option. + .transpose() + .structured(InvalidInputError::InvalidInternalParameter { + name: OVERRIDE_ENCRYPTION_PCRS.to_string(), + explanation: format!( + "Failed to parse internal parameter '{}' as BitFlags", + OVERRIDE_ENCRYPTION_PCRS + ), + })? + // Convert the `Vec` into a `BitFlags`, which is a bitmask of PCRs. + .map(|v| BitFlags::::from_iter(v.into_iter())) + // If the internal parameter is not set, default to PCR 7. + .unwrap_or(Pcr::Pcr7.into()); + // Check if `REENCRYPT_ON_CLEAN_INSTALL` internal param is set to true; if so, re-encrypt // the device in-place. Otherwise, initialize a new LUKS2 volume. encrypt_and_open_device( &device_path, &ev.device_name, &key_file_path, - if host_config - .internal_params - .get_flag(REENCRYPT_ON_CLEAN_INSTALL) - { - EncryptionType::Reencrypt - } else { - EncryptionType::LuksFormat - }, + encryption_type, + pcrs, ) .structured(ServicingError::EncryptBlockDevice { device_path: device_path.to_string_lossy().to_string(), @@ -177,6 +202,7 @@ fn encrypt_and_open_device( device_name: &String, key_file: &Path, encryption_type: EncryptionType, + pcrs: BitFlags, ) -> Result<(), Error> { match encryption_type { EncryptionType::Reencrypt => { @@ -208,7 +234,7 @@ fn encrypt_and_open_device( // Enroll the TPM 2.0 device for the underlying device. Currently, we bind the enrollment to // PCR 7 by default. - encryption::systemd_cryptenroll(key_file, device_path, BitFlags::from(Pcr::Pcr7))?; + encryption::systemd_cryptenroll(key_file, device_path, pcrs)?; debug!( "Opening underlying encrypted device '{}' as '{}'", diff --git a/trident_api/src/constants.rs b/trident_api/src/constants.rs index e84674208..ddb942294 100644 --- a/trident_api/src/constants.rs +++ b/trident_api/src/constants.rs @@ -212,4 +212,7 @@ pub mod internal_params { /// Allow unused images in a COSI file. pub const ALLOW_UNUSED_FILESYSTEMS_IN_COSI: &str = "allowUnusedFilesystems"; + + /// Overrides the default PCR registries to use when sealing encryption keys. + pub const OVERRIDE_ENCRYPTION_PCRS: &str = "overrideEncryptionPcrs"; } From 279e60bda16ac165e595c2224f21579be10a530c Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 29 May 2025 20:26:40 +0000 Subject: [PATCH 37/99] Merged PR 23291: bug: Fix memory-constraint-combined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Fix test ---- #### AI description (iteration 1) #### PR Classification Bug fix to update the filesystem configuration in the memory-constraint-combined test scenario. #### PR Summary This pull request corrects the filesystem layout in the memory-constraint-combined configuration to address CI failures. - In `e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml`, several disk partition entries (e.g., for root-a-2, root-b-2, usr-data-*, usr-hash-*, and web-*-e-2) have been relocated from the main storage list to the `disk2` partitions section. - The obsolete `var` mount configuration has been removed from the storage configuration. Related work items: #12346 --- .../trident-config.yaml | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index 72f50fb59..4edd2d828 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -21,44 +21,42 @@ storage: size: 200M - id: root-a-1 size: 2G - - id: root-a-2 - size: 2G - id: root-b-1 size: 2G - - id: root-b-2 - size: 2G - id: usr-data-a-1 size: 2G - id: usr-data-b-1 size: 2G - - id: usr-data-a-2 - size: 2G - - id: usr-data-b-2 - size: 2G - id: usr-hash-a-1 size: 500M - id: usr-hash-b-1 size: 500M - - id: usr-hash-a-2 - size: 500M - - id: usr-hash-b-2 - size: 500M - id: web-a-e-1 size: 1G - - id: web-a-e-2 - size: 1G - id: web-b-e-1 size: 1G - - id: web-b-e-2 - size: 1G - id: trident size: 100M - - id: var - size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt - partitions: [] + partitions: + - id: root-a-2 + size: 2G + - id: root-b-2 + size: 2G + - id: usr-data-a-2 + size: 2G + - id: usr-data-b-2 + size: 2G + - id: usr-hash-a-2 + size: 500M + - id: usr-hash-b-2 + size: 500M + - id: web-a-e-2 + size: 1G + - id: web-b-e-2 + size: 1G encryption: volumes: - id: web-a @@ -160,8 +158,6 @@ storage: - deviceId: trident source: new mountPoint: /var/lib/trident - - deviceId: var - mountPoint: /var scripts: preServicing: - name: rerun-trident-with-memory-limit From 764a90c16764c76bef3852657367795488e2300e Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 29 May 2025 20:35:59 +0000 Subject: [PATCH 38/99] Merged PR 23191: engineering: trace cpu/memory/network usage during trident MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Create tracing for cpu/memory/network utilization. ---- #### AI description (iteration 1) #### PR Classification New feature implementation for tracing CPU, memory, and network usage in the application. #### PR Summary This pull request introduces a monitoring module to log resource usage during execution, enhancing observability and performance analysis. Key changes include: - `src/monitor_metrics.rs`: Added a new module that creates separate threads to trace and log CPU, memory, and network metrics. - `src/engine/clean_install.rs`: Integrated the monitoring functionality into the clean install workflow and ensured proper thread stoppage. - `Cargo.toml` and `Cargo.lock`: Updated to include the new `procfs` dependency and its related crates. - `src/lib.rs`: Exposed the `monitor_metrics` module to the rest of the codebase. Related work items: #12242 --- Cargo.lock | 51 +++ Cargo.toml | 1 + src/engine/clean_install.rs | 17 + src/engine/update.rs | 17 + src/lib.rs | 1 + src/monitor_metrics.rs | 630 ++++++++++++++++++++++++++++++++++++ 6 files changed, 717 insertions(+) create mode 100644 src/monitor_metrics.rs diff --git a/Cargo.lock b/Cargo.lock index d77412eae..26b61cf62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,6 +454,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -709,6 +718,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -930,6 +949,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "home" version = "0.5.9" @@ -1887,6 +1912,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags", + "chrono", + "flate2", + "hex", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags", + "chrono", + "hex", +] + [[package]] name = "prost" version = "0.13.4" @@ -2986,6 +3036,7 @@ dependencies = [ "netplan-types", "nix", "osutils", + "procfs", "prost", "pytest", "pytest_gen", diff --git a/Cargo.toml b/Cargo.toml index 747f2e350..e84727e96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ nix = { version = "0.29.0", default-features = false, features = [ "fs", "user", ] } +procfs = "0.17.0" rayon = "1.10" regex = "1.11.1" reqwest = { version = "0.12.9", default-features = false, features = [ diff --git a/src/engine/clean_install.rs b/src/engine/clean_install.rs index a334f5a68..1c39ca9c9 100644 --- a/src/engine/clean_install.rs +++ b/src/engine/clean_install.rs @@ -26,6 +26,7 @@ use trident_api::{ use crate::{ datastore::DataStore, engine::{self, boot::esp, bootentries, osimage, storage, EngineContext, SUBSYSTEMS}, + monitor_metrics, subsystems::hooks::HooksSubsystem, ExitKind, SAFETY_OVERRIDE_CHECK_PATH, }; @@ -172,6 +173,15 @@ fn stage_clean_install( mpsc::UnboundedSender>, >, ) -> Result { + // Best effort to measure memory, CPU, and network usage during execution + let monitor = match monitor_metrics::MonitorMetrics::new("stage_clean_install".to_string()) { + Ok(monitor) => Some(monitor), + Err(e) => { + warn!("Failed to create metrics monitor: {e:?}"); + None + } + }; + // Initialize a copy of the Host Status with the changes that are planned. We make a copy // rather than modifying the datastore's version so that we can wait until the clean install is // staged before committing the changes. @@ -223,6 +233,13 @@ fn stage_clean_install( .message("Failed to enter chroot")? .execute_and_exit(|| engine::configure(subsystems, &ctx)); + if let Some(mut monitor) = monitor { + // If the monitor was created successfully, stop it after execution + if let Err(e) = monitor.stop() { + warn!("Failed to stop metrics monitor: {e:?}"); + } + } + if let Err(original_error) = result { if let Err(e) = newroot_mount.unmount_all() { warn!("While handling an earlier error: {e:?}"); diff --git a/src/engine/update.rs b/src/engine/update.rs index cfdc0fd42..30b8d5f30 100644 --- a/src/engine/update.rs +++ b/src/engine/update.rs @@ -22,6 +22,7 @@ use crate::{ storage::{self, verity}, EngineContext, NewrootMount, SUBSYSTEMS, }, + monitor_metrics, subsystems::hooks::HooksSubsystem, ExitKind, }; @@ -207,6 +208,15 @@ fn stage_update( } } + // Best effort to measure memory, CPU, and network usage during execution + let monitor = match monitor_metrics::MonitorMetrics::new("stage_update".to_string()) { + Ok(monitor) => Some(monitor), + Err(e) => { + warn!("Failed to create metrics monitor: {e:?}"); + None + } + }; + engine::prepare(subsystems, &ctx)?; if let ServicingType::AbUpdate = ctx.servicing_type { @@ -266,6 +276,13 @@ fn stage_update( #[cfg(feature = "grpc-dangerous")] grpc::send_host_status_state(sender, state)?; + if let Some(mut monitor) = monitor { + // If the monitor was created successfully, stop it after execution + if let Err(e) = monitor.stop() { + warn!("Failed to stop metrics monitor: {e:?}"); + } + } + info!("Staging of update '{:?}' succeeded", ctx.servicing_type); Ok(()) diff --git a/src/lib.rs b/src/lib.rs index d96e815da..67722d6dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ mod datastore; mod engine; mod harpoon_hc; mod logging; +mod monitor_metrics; pub mod offline_init; mod orchestrate; pub mod osimage; diff --git a/src/monitor_metrics.rs b/src/monitor_metrics.rs new file mode 100644 index 000000000..8b41c8555 --- /dev/null +++ b/src/monitor_metrics.rs @@ -0,0 +1,630 @@ +use anyhow::{Error, Result}; +use log::{error, trace}; +use procfs::{net::DeviceStatus, process::Process, ticks_per_second}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicU64}, + Arc, + }, + thread, + time::Duration, +}; + +// This constant defines the interval at which the monitoring thread will check for updates. +const MONITORING_INTERVAL_MS: u64 = 100; // in milliseconds + +/// `MonitorMetrics` is a struct with shareable fields providing methods to monitor and log CPU, memory, and network metrics. +pub struct MonitorMetrics { + metric_count: Arc, + stop: Arc, + join_handle: Option>, +} + +/// `CpuStat` is a struct that tracks CPU usage metrics. +/// It stores the phase, start CPU ticks, last CPU ticks, and ticks per second. +/// It provides methods to update CPU ticks and calculate CPU time. +/// It also provides a method to log the CPU time summary. +#[derive(Debug, Clone, Default)] +struct CpuStat { + phase: String, + start_cpu_ticks: f64, + last_cpu_ticks: f64, + ticks_per_second: f64, +} +impl CpuStat { + /// Creates a new `CpuStat` instance with the given phase, start CPU ticks, and ticks per second. + fn new(phase: String, start_cpu_ticks: f64, ticks_per_second: f64) -> Self { + Self { + phase, + start_cpu_ticks, + last_cpu_ticks: start_cpu_ticks, + ticks_per_second, + } + } + /// Updates the last CPU ticks with the given value. + fn update(&mut self, cpu_ticks: f64) { + self.last_cpu_ticks = cpu_ticks; + } + /// Calculates the CPU time based on the start and last CPU ticks. + fn get_cpu_time(&self) -> f64 { + (self.last_cpu_ticks - self.start_cpu_ticks) / self.ticks_per_second + } + /// Logs the CPU time summary. + fn summary_trace(&mut self) { + let total_cpu_time = self.get_cpu_time(); + tracing::info!( + metric_name = "total_cpu_time", + phase = &self.phase, + total_cpu_time = total_cpu_time, + ); + trace!("Total cpu time for {}: {}", &self.phase, total_cpu_time); + } +} + +/// `MemoryStat` is a struct that tracks memory usage metrics. +#[derive(Debug, Clone, Default)] +struct MemoryStat { + phase: String, + total_rss: u64, + peak_rss: u64, + number_measurements: u64, +} +impl MemoryStat { + /// Creates a new `MemoryStat` instance with the given phase. + fn new(phase: String) -> Self { + Self { + phase, + total_rss: 0, + peak_rss: 0, + number_measurements: 0, + } + } + /// Updates the memory usage metrics with the given RSS value. + fn update(&mut self, rss: u64) { + self.total_rss += rss; + if rss > self.peak_rss { + self.peak_rss = rss; + } + self.number_measurements += 1; + } + /// Calculates the average memory usage. + fn get_average_memory_usage(&self) -> f64 { + if self.number_measurements == 0 { + return 0.0; + } + self.total_rss as f64 / self.number_measurements as f64 + } + /// Returns the peak memory usage. + fn get_peak_memory_usage(&self) -> u64 { + self.peak_rss + } + /// Logs the memory usage summary. + /// It logs the average and peak memory usage. + fn summary_trace(&mut self) { + let average_memory_usage = self.get_average_memory_usage(); + let peak_memory_usage = self.get_peak_memory_usage(); + tracing::info!( + metric_name = "average_memory_usage", + phase = &self.phase, + average_memory_usage = average_memory_usage, + ); + trace!( + "Average memory usage for {}: {}", + &self.phase, + average_memory_usage + ); + tracing::info!( + metric_name = "peak_memory_usage", + phase = &self.phase, + peak_memory_usage = peak_memory_usage, + ); + trace!( + "Peak memory usage for {}: {}", + &self.phase, + peak_memory_usage + ); + } +} + +/// `NetworkStat` is a struct that tracks network usage metrics. +#[derive(Debug, Clone, Default)] +struct NetworkStat { + phase: String, + iface_start_bytes: HashMap, + iface_bytes: HashMap, +} +impl NetworkStat { + /// Creates a new `NetworkStat` instance with the given phase and initial network statistics. + fn new(phase: String, init_stats: HashMap) -> Self { + let mut iface_start_bytes = HashMap::new(); + for stat in init_stats.values() { + iface_start_bytes.insert(stat.name.clone(), (stat.recv_bytes, stat.sent_bytes)); + } + + Self { + phase: phase.clone(), + iface_start_bytes, + iface_bytes: HashMap::new(), + } + } + /// Updates the network usage metrics with the given interface name and received/sent bytes. + /// It calculates the difference from the initial values and stores it in `iface_bytes`. + /// If the interface name is not found in `iface_start_bytes`, it simply stores the received/sent bytes. + fn update(&mut self, name: String, recv_bytes: u64, sent_bytes: u64) { + let mut trace_measurement = (recv_bytes, sent_bytes); + if let Some(start_bytes) = self.iface_start_bytes.get(&name) { + trace_measurement = (recv_bytes - start_bytes.0, sent_bytes - start_bytes.1); + } + self.iface_bytes.insert(name.clone(), trace_measurement); + } + /// Logs the network usage summary for each interface. + fn summary_trace(&mut self) { + for (name, trace_measurement) in &self.iface_bytes { + tracing::info!( + metric_name = "total_network_usage", + phase = &self.phase, + iface_name = &name, + rx_bytes = trace_measurement.0, + tx_bytes = trace_measurement.1, + ); + trace!( + "Total network usage for {}: iface: {}, rx_bytes: {}, tx_bytes: {}", + &self.phase, + &name, + trace_measurement.0, + trace_measurement.1, + ); + } + } +} + +impl MonitorMetrics { + /// Creates a new `MonitorMetrics` instance that starts monitoring process and network metrics. + /// The `phase` parameter is used to tag the logs with the current phase of the application. + pub fn new(phase: String) -> Result { + let mut monitor = MonitorMetrics { + stop: Arc::new(AtomicBool::new(false)), + metric_count: Arc::new(AtomicU64::new(0)), + join_handle: None, + }; + monitor.start_monitoring(phase.clone())?; + Ok(monitor) + } + + /// Stops the monitoring thread by setting stop. + /// This method is called when the `MonitorMetrics` instance is dropped. + /// It ensures that the thread is stopped gracefully. + pub fn stop(&mut self) -> Result<(), Error> { + self.stop.store(true, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } + + /// Joins the monitoring thread. + #[allow(dead_code)] + pub fn join(&mut self) -> thread::Result<()> { + if let Some(h) = self.join_handle.take() { + h.join()?; + } else { + error!("No monitoring thread to join"); + } + Ok(()) + } + + /// Creates and starts the monitoring thread and returns the thread handle. + /// The thread will run in a loop, updating the metrics at regular intervals. + /// It will stop when stop is set. + fn start_monitoring(&mut self, phase: String) -> Result<(), Error> { + let polling_interval = Duration::from_millis(MONITORING_INTERVAL_MS); + + let local_stop = self.stop.clone(); + let local_metric_count = self.metric_count.clone(); + + let process = Process::myself()?; + let init_process_stats = process.stat()?; + let init_net_stats = procfs::net::dev_status()?; + + let mut cpu_stat = CpuStat::new( + phase.clone(), + (init_process_stats.utime + init_process_stats.stime) as f64, + ticks_per_second() as f64, + ); + let mut memory_stat = MemoryStat::new(phase.clone()); + let mut network_stat = NetworkStat::new(phase.clone(), init_net_stats); + + let join_handle = thread::spawn(move || { + loop { + // Update CPU and memory statistics + if let Ok(process) = Process::myself() { + if let Ok(stat) = process.stat() { + cpu_stat.update((stat.utime + stat.stime) as f64); + memory_stat.update(stat.rss); + } + } + // Update network statistics + if let Ok(dev_stats) = procfs::net::dev_status() { + let stats: Vec<_> = dev_stats.values().collect(); + for stat in stats { + network_stat.update(stat.name.clone(), stat.recv_bytes, stat.sent_bytes); + } + } + + local_metric_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + if local_stop.load(std::sync::atomic::Ordering::SeqCst) { + break; + } + + // Sleep for the polling interval + thread::sleep(polling_interval); + } + // Perform summary trace for CPU, memory, and network metrics + // after the monitoring thread is stopped. + cpu_stat.summary_trace(); + memory_stat.summary_trace(); + network_stat.summary_trace(); + }); + + self.join_handle = Some(join_handle); + Ok(()) + } +} + +impl Drop for MonitorMetrics { + fn drop(&mut self) { + if let Err(e) = self.stop() { + trace!("Failed to stop monitoring threads: {:?}", e); + } + } +} + +#[cfg(test)] +mod stat_tests { + use super::*; + use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + }; + use tracing_subscriber::{layer::SubscriberExt, Registry}; + + #[derive(Debug, Clone, Default)] + struct TestTraceWriter { + logs: Arc>>, + } + + impl std::io::Write for TestTraceWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let s = String::from_utf8_lossy(buf).to_string(); + self.logs.lock().unwrap().push(s); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + fn init_monitor_metrics_tracing_validation_for_thread( + ) -> (Arc>>, tracing::subscriber::DefaultGuard) { + let logs = Arc::new(Mutex::new(Vec::new())); + let writer = TestTraceWriter { logs: logs.clone() }; + let guard = tracing::subscriber::set_default( + Registry::default().with( + tracing_subscriber::fmt::layer() + .with_writer(move || writer.clone()) + .with_ansi(false) + .with_target(false) + .with_level(true), + ), + ); + (logs, guard) + } + + #[test] + fn test_cpu_stat_update() { + let phase = "test_phase".to_string(); + let start_cpu_ticks = 40.0; + let ticks_per_second = 70.0; + + let mut cpu_stat = CpuStat::new(phase, start_cpu_ticks, ticks_per_second); + assert_eq!(cpu_stat.last_cpu_ticks, start_cpu_ticks); + let first_update = 200.0; + cpu_stat.update(first_update); + assert_eq!(cpu_stat.last_cpu_ticks, first_update); + + let last_update = 600.0; + cpu_stat.update(last_update); + assert_eq!(cpu_stat.last_cpu_ticks, last_update); + + assert_eq!(cpu_stat.start_cpu_ticks, start_cpu_ticks); + assert_eq!(cpu_stat.last_cpu_ticks, last_update); + assert_eq!(cpu_stat.ticks_per_second, ticks_per_second); + + assert_eq!( + cpu_stat.get_cpu_time(), + (last_update - start_cpu_ticks) / ticks_per_second + ); + + let (trace_logs, _guard) = init_monitor_metrics_tracing_validation_for_thread(); + cpu_stat.summary_trace(); + + let logs = trace_logs.lock().unwrap().join(""); + println!("Trace logs: {}", logs); + let expected_log = format!( + "total_cpu_time={}", + (last_update - start_cpu_ticks) / ticks_per_second + ); + assert!(logs.contains(&expected_log)); + } + + #[test] + fn test_memory_stat_initialization() { + let phase = "test_phase".to_string(); + + let memory_stat = MemoryStat::new(phase.clone()); + + assert_eq!(memory_stat.phase, phase); + assert_eq!(memory_stat.total_rss, 0); + assert_eq!(memory_stat.peak_rss, 0); + assert_eq!(memory_stat.number_measurements, 0); + } + + #[test] + fn test_memory_stat_update() { + let phase = "test_phase".to_string(); + let mut memory_stat = MemoryStat::new(phase); + + let first_rss = 2048; + memory_stat.update(first_rss); + assert_eq!(memory_stat.total_rss, first_rss); + assert_eq!(memory_stat.peak_rss, first_rss); + assert_eq!(memory_stat.number_measurements, 1); + + let second_rss = 1024; + memory_stat.update(second_rss); + assert_eq!(memory_stat.total_rss, first_rss + second_rss); + assert_eq!(memory_stat.peak_rss, first_rss); + assert_eq!(memory_stat.number_measurements, 2); + + assert_eq!( + memory_stat.get_average_memory_usage(), + (first_rss + second_rss) as f64 / 2.0 + ); + assert_eq!(memory_stat.get_peak_memory_usage(), first_rss); + + let (trace_logs, _guard) = init_monitor_metrics_tracing_validation_for_thread(); + memory_stat.summary_trace(); + + let logs = trace_logs.lock().unwrap().join(""); + println!("Trace logs: {}", logs); + let expected_log = format!( + "average_memory_usage={}", + (first_rss + second_rss) as f64 / 2.0 + ); + assert!(logs.contains(&expected_log)); + let expected_log = format!("peak_memory_usage={}", first_rss); + assert!(logs.contains(&expected_log)); + } + + fn create_mock_device_status(name: &str, recv_bytes: u64, sent_bytes: u64) -> DeviceStatus { + DeviceStatus { + name: name.to_string(), + recv_bytes, + sent_bytes, + recv_packets: 0, + recv_errs: 0, + recv_drop: 0, + recv_fifo: 0, + recv_frame: 0, + recv_compressed: 0, + recv_multicast: 0, + sent_packets: 0, + sent_errs: 0, + sent_drop: 0, + sent_fifo: 0, + sent_colls: 0, + sent_carrier: 0, + sent_compressed: 0, + } + } + + #[test] + fn test_network_stat_initialization() { + let phase = "test_phase".to_string(); + let mut init_stats = HashMap::new(); + let mock_device_status = create_mock_device_status("eth0", 1000, 2000); + init_stats.insert("eth0".to_string(), mock_device_status); + + let network_stat = NetworkStat::new(phase.clone(), init_stats); + + assert_eq!(network_stat.phase, phase); + assert!(network_stat.iface_start_bytes.contains_key("eth0")); + assert_eq!(network_stat.iface_start_bytes["eth0"], (1000, 2000)); + } + + #[test] + fn test_network_stat_update() { + let phase = "test_phase".to_string(); + let mut init_stats = HashMap::new(); + let mock_device_status = create_mock_device_status("eth0", 1000, 2000); + init_stats.insert("eth0".to_string(), mock_device_status); + + let mut network_stat = NetworkStat::new(phase, init_stats); + network_stat.update("eth0".to_string(), 3000, 4000); + assert!(network_stat.iface_bytes.contains_key("eth0")); + assert_eq!(network_stat.iface_bytes["eth0"], (2000, 2000)); + network_stat.update("eth1".to_string(), 5000, 6000); + assert!(network_stat.iface_bytes.contains_key("eth1")); + assert_eq!(network_stat.iface_bytes["eth1"], (5000, 6000)); + assert!(network_stat.iface_bytes.contains_key("eth0")); + assert_eq!(network_stat.iface_bytes["eth0"], (2000, 2000)); + + let (trace_logs, _guard) = init_monitor_metrics_tracing_validation_for_thread(); + network_stat.summary_trace(); + + let logs = trace_logs.lock().unwrap().join(""); + println!("Trace logs: {}", logs); + let expected_log = "iface_name=\"eth0\" rx_bytes=2000 tx_bytes=2000".to_string(); + assert!(logs.contains(&expected_log)); + let expected_log = "iface_name=\"eth1\" rx_bytes=5000 tx_bytes=6000".to_string(); + assert!(logs.contains(&expected_log)); + } + + #[test] + fn test_stop() { + let monitor = MonitorMetrics { + stop: Arc::new(AtomicBool::new(false)), + metric_count: Arc::new(AtomicU64::new(0)), + join_handle: None, + }; + assert!(!monitor.stop.load(std::sync::atomic::Ordering::SeqCst)); + monitor + .stop + .store(true, std::sync::atomic::Ordering::SeqCst); + assert!(monitor.stop.load(std::sync::atomic::Ordering::SeqCst)); + assert_eq!( + monitor + .metric_count + .load(std::sync::atomic::Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_join() { + let mut monitor = MonitorMetrics { + stop: Arc::new(AtomicBool::new(false)), + metric_count: Arc::new(AtomicU64::new(0)), + join_handle: None, + }; + assert!(monitor.join_handle.is_none()); + // Validate that join without handle returns Ok + assert!(monitor.join().is_ok()); + + // Create thread and validate that join works + let started = Arc::new(AtomicBool::new(false)); + let ended = Arc::new(AtomicBool::new(false)); + let thread_started = started.clone(); + let thread_ended = ended.clone(); + monitor.join_handle = Some(thread::spawn(move || { + thread_started.store(true, std::sync::atomic::Ordering::SeqCst); + // Simulate some work + thread::sleep(Duration::from_millis(100)); + thread_ended.store(true, std::sync::atomic::Ordering::SeqCst); + })); + // Validate that join with handle returns Ok + assert!(monitor.join().is_ok()); + assert!(started.load(std::sync::atomic::Ordering::SeqCst)); + assert!(ended.load(std::sync::atomic::Ordering::SeqCst)); + assert!(monitor.join_handle.is_none()); + // Validate that join without handle returns Ok + assert!(monitor.join().is_ok()); + } + + #[test] + fn test_start_monitoring() { + let mut monitor = MonitorMetrics { + // Initialize monitor with stop=true, forcing thead to + // collect 1 set of metrics and exit + stop: Arc::new(AtomicBool::new(true)), + metric_count: Arc::new(AtomicU64::new(0)), + join_handle: None, + }; + + monitor.start_monitoring("test-phase".to_string()).unwrap(); + assert!(monitor.join_handle.is_some()); + + // Validate that join with handle returns Ok + assert!(monitor.join().is_ok()); + assert!(monitor.join_handle.is_none()); + // Validate that join without handle returns Ok + assert!(monitor.join().is_ok()); + assert_eq!( + monitor + .metric_count + .load(std::sync::atomic::Ordering::SeqCst), + 1 + ); + } +} + +#[cfg(feature = "functional-test")] +#[cfg_attr(not(test), allow(unused_imports, dead_code))] +mod functional_test { + use super::*; + + use pytest_gen::functional_test; + use std::sync::{Arc, Mutex}; + use tracing_subscriber::{layer::SubscriberExt, Registry}; + + #[derive(Debug, Clone, Default)] + struct TestTraceWriter { + logs: Arc>>, + } + + impl std::io::Write for TestTraceWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let s = String::from_utf8_lossy(buf).to_string(); + self.logs.lock().unwrap().push(s); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + fn init_monitor_metrics_tracing_validation_global() -> Arc>> { + let logs = Arc::new(Mutex::new(Vec::new())); + let writer = TestTraceWriter { logs: logs.clone() }; + assert!(tracing::subscriber::set_global_default( + Registry::default().with( + tracing_subscriber::fmt::layer() + .with_writer(move || writer.clone()) + .with_ansi(false) + .with_target(false) + .with_level(true), + ), + ) + .is_ok()); + logs + } + + #[functional_test] + fn test_monitor_metrics() { + let trace_logs = init_monitor_metrics_tracing_validation_global(); + + let mut test_metrics = MonitorMetrics::new("test_phase".to_string()).unwrap(); + + // Wait for a while to allow the thread to run and collect metrics + let sleep_ms = 1000; + thread::sleep(Duration::from_millis(sleep_ms)); + + // Tell monitor loop to stop + test_metrics.stop().unwrap(); + + // Join the thread to wait monitor loop to end + test_metrics.join().unwrap(); + + // Loop is stopped after X ms, each iteration waits Y ms, check + // that metric count is roughly (maybe within 20%) between 0 and + // (X / Y) + assert_ne!( + test_metrics + .metric_count + .load(std::sync::atomic::Ordering::SeqCst), + 0 + ); + assert!( + test_metrics + .metric_count + .load(std::sync::atomic::Ordering::SeqCst) + <= (1.2 * sleep_ms as f64 / MONITORING_INTERVAL_MS as f64) as u64 + ); + + // Validate that tracing output contains expected metrics + let logs = trace_logs.lock().unwrap().join(""); + assert!(logs.contains("total_cpu_time")); + assert!(logs.contains("average_memory_usage")); + assert!(logs.contains("peak_memory_usage")); + assert!(logs.contains("total_network_usage")); + } +} From e876dfbc80359e748b82ac972437b9ab34dd5d56 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 29 May 2025 20:57:45 +0000 Subject: [PATCH 39/99] Merged PR 23288: engineering: Misc Pipeline Fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description - Add missing stage dependencies - Stop running makefile validation in pr-e2e, it is already part of pr and not needed for anything else ---- #### AI description (iteration 1) #### PR Classification This pull request fixes pipeline configuration issues by adding missing test images and refining conditional logic for Makefile validation. #### PR Summary The changes adjust pipeline YAML templates to include new usrverity test images and restrict Makefile validation to appropriate CI stages. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml`: Added usrverity test image references to both primary and alternate testing stages. - `/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml`: Appended usrverity test image entries for baremetal testing. - `/.pipelines/templates/e2e-template.yml`: Modified the condition to run Makefile validation only when the stage type equals 'ci'. Related work items: #12348 --- .pipelines/templates/e2e-template.yml | 4 ++-- .../templates/stages/testing_baremetal/baremetal-testing.yml | 2 ++ .pipelines/templates/stages/testing_vm/netlaunch-testing.yml | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index 569bee10c..f38cfdc26 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -77,8 +77,8 @@ stages: # Build tools (Go stuff) - template: stages/building_tools/building-tools.yml - # Makefile validation, only for CI and PR-E2E - - ${{ if or(eq(parameters.stageType, 'ci'), eq(parameters.stageType, 'pr-e2e'), eq(parameters.stageType, 'pr-e2e-azure')) }}: + # Makefile validation, only for CI + - ${{ if eq(parameters.stageType, 'ci') }}: - template: stages/validate_makefile/dev-build.yml # Build Trident container image diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index bb459cc6d..cc60ed21e 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -43,10 +43,12 @@ stages: - TridentTestImg_trident_container_installer - TridentTestImg_trident_container_testimage - TridentTestImg_trident_container_verity_testimage + - TridentTestImg_trident_container_usrverity_testimage - ${{ else }}: - TridentTestImg_trident_verity_testimage - TridentTestImg_trident_installer - TridentTestImg_trident_testimage + - TridentTestImg_trident_usrverity_testimage jobs: - template: ../testing_common/get-tests.yml diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index 4f25ef64f..a32db6c12 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -45,11 +45,13 @@ stages: - 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 jobs: - job: Testing From 9b008a560cb6052eeff903b1e9ce8bd726a02fe4 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Torres Date: Thu, 29 May 2025 21:26:41 +0000 Subject: [PATCH 40/99] Merged PR 23292: engineering: Download missed usrverity images for trident-testing pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Add missed `usrverity` images (host and container) to the download template for trident-testing pipeline # Validation https://dev.azure.com/mariner-org/ECF/_build/results?buildId=822580&view=results Seems memory-constraint-combined test may timeout, but the image was downloaded correctly in all the tests. ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new feature to the trident-testing pipeline by adding steps to download missed usrverity images. #### PR Summary The changes add two new jobs in the pipeline YAML file to download usrverity image artifacts from a specific pipeline run, enhancing the artifact retrieval process for trident tests. - `/.pipelines/templates/stages/download_artifacts/get-artifacts.yml`: Added job `TridentContainerUserVerityTestimage` to download the "trident-container-usrverity-testimage" artifact. - `/.pipelines/templates/stages/download_artifacts/get-artifacts.yml`: Added job `TridentUserVerityTestimage` to download the "trident-usrverity-testimage" artifact. Related work items: #12233 --- .../download_artifacts/get-artifacts.yml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.pipelines/templates/stages/download_artifacts/get-artifacts.yml b/.pipelines/templates/stages/download_artifacts/get-artifacts.yml index a997db757..761d8c975 100644 --- a/.pipelines/templates/stages/download_artifacts/get-artifacts.yml +++ b/.pipelines/templates/stages/download_artifacts/get-artifacts.yml @@ -264,6 +264,29 @@ stages: artifactName: $(ob_artifactBaseName) targetPath: $(ob_outputDirectory) + - job: TridentContainerUsrVerityTestimage + displayName: "Download trident-container-usrverity-testimage" + timeoutInMinutes: 10 + pool: + type: linux + + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/build + ob_artifactBaseName: "trident-container-usrverity-testimage" + + steps: + - task: DownloadPipelineArtifact@2 + inputs: + source: specific + project: "ECF" + definition: ${{ parameters.definition }} + runVersion: ${{ parameters.runVersion }} + branchName: "refs/heads/${{ parameters.branch }}" + runId: ${{ parameters.tridentPipelineRunId }} + allowFailedBuilds: ${{ parameters.allowFailedBuilds }} + artifactName: $(ob_artifactBaseName) + targetPath: $(ob_outputDirectory) + # Host images: - ${{ if eq(parameters.hostImages, true) }}: - job: TridentInstallerTestimage @@ -359,3 +382,26 @@ stages: allowFailedBuilds: ${{ parameters.allowFailedBuilds }} artifactName: $(ob_artifactBaseName) targetPath: $(ob_outputDirectory) + + - job: TridentUsrVerityTestimage + displayName: "Download trident-usrverity-testimage" + timeoutInMinutes: 10 + pool: + type: linux + + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/build + ob_artifactBaseName: "trident-usrverity-testimage" + + steps: + - task: DownloadPipelineArtifact@2 + inputs: + source: specific + project: "ECF" + definition: ${{ parameters.definition }} + runVersion: ${{ parameters.runVersion }} + branchName: "refs/heads/${{ parameters.branch }}" + runId: ${{ parameters.tridentPipelineRunId }} + allowFailedBuilds: ${{ parameters.allowFailedBuilds }} + artifactName: $(ob_artifactBaseName) + targetPath: $(ob_outputDirectory) From 9420bc6e64d03d90e5e936a50ea6f8100914088c Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 29 May 2025 22:18:45 +0000 Subject: [PATCH 41/99] Merged PR 23293: bug: Disable mem-contrained test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Usr-verity is not working on the mem constrained scenario. Disabling to unblock and fix out-of-band. Related work items: #12350 --- e2e_tests/target-configurations.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 1f8f68468..60a6b9cbe 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -95,7 +95,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12349): fix #- memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -112,7 +112,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12349): fix #- memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -139,7 +139,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12349): fix #- memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -156,7 +156,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - - memory-constraint-combined + # TODO(12349): fix #- memory-constraint-combined - misc - raid-mirrored - raid-resync-small From 5c2be9ed0d017683d3ccaef98ecb2481cebebec4 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 30 May 2025 17:41:31 +0000 Subject: [PATCH 42/99] Merged PR 23299: engineering: functional tests periodically complain about \s escape sequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Functional tests periodically [fail](https://dev.azure.com/mariner-org/ECF/_build/results?buildId=822654&view=logs&j=4ce13d6b-835b-52c0-999f-6017e90e79af&t=442025c4-c2be-5dc6-7792-2440c387badc&s=c64e7540-96c6-5017-e157-cc59e3332132) with invalid escape sequence complaint: ``` functional_tests/test_setup.py:31 /mnt/vss/_work/1/s/functional_tests/test_setup.py:31: DeprecationWarning: invalid escape sequence '\s' ssh_node.execute("sudo sed -i 's/^\s*phonehome: .*//' /etc/trident/config.yaml") ``` Try using `\\s` in py string. ---- #### AI description (iteration 1) #### PR Classification Bug fix addressing a functional test failure due to an incorrect regex escape sequence. #### PR Summary This PR corrects the regex in the sed command within the `disable_phonehome` function to properly escape the whitespace pattern. - `functional_tests/test_setup.py`: Updated the sed command from using `\s` to `\\s` to fix the regex and stop periodic complaints during tests. Related work items: #12356 --- functional_tests/test_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functional_tests/test_setup.py b/functional_tests/test_setup.py index 352c9cedc..765e112d7 100644 --- a/functional_tests/test_setup.py +++ b/functional_tests/test_setup.py @@ -28,7 +28,7 @@ def create_vm(create_params): def disable_phonehome(ssh_node: SshNode): """Disables phonehome in the VM to allow faster rerunning of Trident.""" - ssh_node.execute("sudo sed -i 's/^\s*phonehome: .*//' /etc/trident/config.yaml") + ssh_node.execute("sudo sed -i 's/^\\s*phonehome: .*//' /etc/trident/config.yaml") def prepare_hostconfig(test_dir_path: Path, ssh_pub_key: str): From 4662dc2a915efaa62d139c91278fb942842668cd Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 30 May 2025 19:27:45 +0000 Subject: [PATCH 43/99] Merged PR 23300: engineering: update mic submodule to get CVE fix Update mic submodule Related work items: #12363 --- azure-linux-image-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-linux-image-tools b/azure-linux-image-tools index 3db1167c3..e352dc2fc 160000 --- a/azure-linux-image-tools +++ b/azure-linux-image-tools @@ -1 +1 @@ -Subproject commit 3db1167c3b59aaf02ef9ba35647c2ef7b769607d +Subproject commit e352dc2fc96199a546535b426962e88af3015823 From 9a806f14acb5c28c41928460aff03f9ad8738da1 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 2 Jun 2025 16:37:50 +0000 Subject: [PATCH 44/99] Merged PR 23329: engineering: SFI: get-tests triggered pypi notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description get-tests.yml triggered an SFI pypi issue (https://dev.azure.com/mariner-org/ECF/_build/Results?buildId=823616&view=logs&j=92337564-7ef9-5b53-b2b0-d9b9034e2956&t=b0bb38ab-73af-5396-5030-8130e6e98f73) ---- #### AI description (iteration 1) #### PR Classification CI configuration update to remove pypi.org usage during test execution. #### PR Summary This pull request updates the test pipeline by adding a new step to avoid pypi usage, directly addressing the "Remove pypi.org usage" work item. - `/.pipelines/templates/stages/testing_common/get-tests.yml`: Added a template call to `../common_tasks/avoid-pypi-usage.yml` to prevent pypi notifications. Related work items: #10524 --- .pipelines/templates/stages/testing_common/get-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pipelines/templates/stages/testing_common/get-tests.yml b/.pipelines/templates/stages/testing_common/get-tests.yml index da4efbff0..8f7bc15c5 100644 --- a/.pipelines/templates/stages/testing_common/get-tests.yml +++ b/.pipelines/templates/stages/testing_common/get-tests.yml @@ -35,6 +35,7 @@ jobs: ob_artifactBaseName: select_tests_${{ parameters.deploymentEnvironment }}_${{ parameters.runtimeEnv }}_$(System.JobAttempt) steps: + - template: ../common_tasks/avoid-pypi-usage.yml - bash: | python3 ./e2e_tests/helpers/read_target_configurations.py \ --configurations ./e2e_tests/target-configurations.yaml \ From 74a75f9788292a2f678d52c1abff48dcbec8266f Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Mon, 2 Jun 2025 21:56:24 +0000 Subject: [PATCH 45/99] Merged PR 23301: Implement pcrlock helpers and re-factor logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR adds logic to generate 2 missing .pcrlock files and also slightly re-factors the existing code. TODO: 1. Add more functional tests once static files are added, 2. Modify encryption logic to use PCR-based encryption, 3. Implement an E2E test ---- #### AI description (iteration 1) #### PR Classification This pull request refactors the PCR lock file generation logic and adds new helper methods to support PCR-based encryption. #### PR Summary The changes update the generation of .pcrlock files to use dynamic file paths and helper functions, and they rename PCR conversion methods to clarify their purpose. - `osutils/src/pcrlock.rs`: Refactors loops for locking various binaries by enumerating disks/paths, adds helper functions (`generate_pcrlock_output_path`, `generate_610_boot_loader_code_pcrlock`, `generate_720_kernel_initrd_pcrlock`), and computes hash digests for the extracted initrd. - `sysdefs/src/tpm2.rs`: Renames PCR conversion methods from `to_value`/`from_value` to `to_num`/`from_num` and updates the associated tests. - `osutils/src/encryption.rs`: Updates PCR argument generation to use the new numbering scheme. - `osutils/Cargo.toml` and `Cargo.lock`: Add the `hex` dependency required for digest encoding. Related work items: #12123 --- Cargo.lock | 39 +++++ osutils/Cargo.toml | 2 + osutils/src/encryption.rs | 2 +- osutils/src/pcrlock.rs | 325 ++++++++++++++++++++++++++++++++------ sysdefs/src/tpm2.rs | 72 +++++---- trident_api/src/error.rs | 3 + 6 files changed, 358 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26b61cf62..c62aedf48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -877,6 +877,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "goblin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "h2" version = "0.4.7" @@ -1659,6 +1670,8 @@ dependencies = [ "duct", "enumflags2", "glob", + "goblin", + "hex", "hostname", "indoc", "inventory", @@ -1874,6 +1887,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2296,6 +2315,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/osutils/Cargo.toml b/osutils/Cargo.toml index 2e441878f..a90919947 100644 --- a/osutils/Cargo.toml +++ b/osutils/Cargo.toml @@ -12,6 +12,8 @@ configparser = { version = "3.1.0", features = ["indexmap"] } const_format = "0.2.33" duct = "0.13.7" enumflags2 = { version = "0.7", features = ["serde"] } +goblin = "0.9.3" +hex = "0.4.0" hostname = "0.4.0" indoc = "2.0.5" inventory = "0.3.15" diff --git a/osutils/src/encryption.rs b/osutils/src/encryption.rs index a296ace68..251550075 100644 --- a/osutils/src/encryption.rs +++ b/osutils/src/encryption.rs @@ -169,7 +169,7 @@ fn to_tpm2_pcrs_arg(pcrs: BitFlags) -> String { format!( "--tpm2-pcrs={}", pcrs.iter() - .map(|flag| flag.to_value().to_string()) + .map(|flag| flag.to_num().to_string()) .collect::>() .join("+") ) diff --git a/osutils/src/pcrlock.rs b/osutils/src/pcrlock.rs index c00a1764c..8daf1559a 100644 --- a/osutils/src/pcrlock.rs +++ b/osutils/src/pcrlock.rs @@ -1,18 +1,26 @@ -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; use anyhow::{Context, Error, Result}; use enumflags2::{make_bitflags, BitFlags}; +use goblin::pe::PE; use log::{debug, error, trace, warn}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use tempfile::NamedTempFile; use trident_api::error::{ReportError, ServicingError, TridentError}; use sysdefs::tpm2::Pcr; -use crate::dependencies::Dependency; +use crate::dependencies::{Dependency, DependencyResultExt}; + +use crate::exe::RunAndCheck; /// Path to the pcrlock directory where .pcrlock files are stored. -#[allow(dead_code)] const PCRLOCK_DIR: &str = "/var/lib/pcrlock.d"; /// Path to the PCR policy JSON file. @@ -22,38 +30,32 @@ const PCR_POLICY_PATH: &str = "/var/lib/systemd/pcrlock.json"; /// current and updated images: /// 1. /var/lib/pcrlock.d/600-gpt.pcrlock.d, where `lock-gpt` measures the GPT partition table of /// the booted medium, as recorded to PCR 5 by the firmware, -#[allow(dead_code)] const GPT_PCRLOCK_DIR: &str = "600-gpt.pcrlock.d"; /// 2. /var/lib/pcrlock.d/610-boot-loader-code.pcrlock.d, where Trident measures the bootx64.efi /// binary, as recorded into PCR 4 following Microsoft's Authenticode hash spec, -#[allow(dead_code)] const BOOT_LOADER_CODE_PCRLOCK_DIR: &str = "610-boot-loader-code.pcrlock.d"; /// 3. /var/lib/pcrlock.d/630-boot-loader-conf.pcrlock.d, where `lock-raw` measures the boot loader /// configuration file, as recorded into PCR 5, -#[allow(dead_code)] const BOOT_LOADER_CONF_PCRLOCK_DIR: &str = "630-boot-loader-conf.pcrlock.d"; /// 4. /var/lib/pcrlock.d/650-uki.pcrlock.d, where `lock-uki` measures the UKI binary, as recorded /// into PCR 4, -#[allow(dead_code)] const UKI_PCRLOCK_DIR: &str = "650-uki.pcrlock.d"; /// 5. /var/lib/pcrlock.d/710-kernel-cmdline.pcrlock.d, where `lock-kernel-cmdline` measures the /// kernel command line, as recorded into PCR 9, -#[allow(dead_code)] const KERNEL_CMDLINE_PCRLOCK_DIR: &str = "710-kernel-cmdline.pcrlock.d"; /// 6. /var/lib/pcrlock.d/720-kernel-initrd.pcrlock.d, where Trident measures the initrd section of /// the UKI binary, as recorded into PCR 9. -#[allow(dead_code)] const KERNEL_INITRD_PCRLOCK_DIR: &str = "720-kernel-initrd.pcrlock.d"; -/// Valid PCRs for TPM2 policy generation, following the `systemd-pcrlock` spec. +/// Valid PCRs for TPM 2.0 policy generation, following the `systemd-pcrlock` spec. /// /// https://www.man7.org/linux/man-pages/man8/systemd-pcrlock.8.html. -const ALLOWED_PCRS: BitFlags = make_bitflags!(Pcr::{Pcr0 | Pcr1 | Pcr2 | Pcr3 | Pcr4 | Pcr5 | Pcr7 | Pcr11 | Pcr12 | Pcr13 | Pcr14 | Pcr15}); +const VALID_PCRLOCK_PCRS: BitFlags = make_bitflags!(Pcr::{Pcr0 | Pcr1 | Pcr2 | Pcr3 | Pcr4 | Pcr5 | Pcr7 | Pcr11 | Pcr12 | Pcr13 | Pcr14 | Pcr15}); #[derive(Debug, Deserialize)] struct PcrValue { @@ -74,11 +76,11 @@ struct PcrPolicy { pub fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), TridentError> { debug!( "Generating a new TPM 2.0 access policy for the following PCRs: {:?}", - pcrs.iter().map(|pcr| pcr.to_value()).collect::>() + pcrs.iter().map(|pcr| pcr.to_num()).collect::>() ); // Validate that all requested PCRs are allowed by systemd-pcrlock - let filtered_pcrs = pcrs & ALLOWED_PCRS; + let filtered_pcrs = pcrs & VALID_PCRLOCK_PCRS; if pcrs != filtered_pcrs { let ignored = pcrs & !filtered_pcrs; @@ -114,12 +116,13 @@ pub fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), TridentErr .filter(|pcr| !policy_pcrs.contains(pcr)) .collect(); + // If any requested PCRs are missing from the policy, return an error if !missing_pcrs.is_empty() { error!( "Some requested PCRs are missing from the generated PCR policy: '{:?}'", missing_pcrs .iter() - .map(|pcr| pcr.to_value()) + .map(|pcr| pcr.to_num()) .collect::>() ); return Err(TridentError::new(ServicingError::GenerateTpm2AccessPolicy)); @@ -157,7 +160,7 @@ fn to_pcr_arg(pcrs: BitFlags) -> String { format!( "--pcr={}", pcrs.iter() - .map(|flag| flag.to_value().to_string()) + .map(|flag| flag.to_num().to_string()) .collect::>() .join(",") ) @@ -201,7 +204,7 @@ enum LockCommand { /// measurements the firmware makes to PCR 4 ("boot-loader-code") if the specified /// binary is part of the UEFI boot process. /// - /// Used for non-UKI images only; UKI binaries must be locked with lock-uki. + /// Used for non-UKI images only; UKI binaries must be locked with `lock-uki`. #[allow(dead_code)] Pe { path: PathBuf, @@ -209,9 +212,9 @@ enum LockCommand { }, /// Generates a .pcrlock file based on the specified UKI PE binary. Useful for predicting - /// measurements the firmware makes to PCR 4 ("boot-loader-code"), and systemd-stub makes to + /// measurements the firmware makes to PCR 4 ("boot-loader-code"), and `systemd-stub` makes to /// PCR 11 ("kernel-boot"). Used for UKI images only; non-UKI binaries must be locked with - /// lock-pe. + /// `lock-pe`. Uki { path: PathBuf, pcrlock_file: PathBuf, @@ -235,7 +238,7 @@ enum LockCommand { /// Generates a .pcrlock file based on a kernel initrd cpio archive. Useful for predicting /// measurements the Linux kernel makes to PCR 9 ("kernel-initrd"). Should not be used for - /// systemd-stub UKIs, as the initrd is combined dynamically from various sources and hence + /// `systemd-stub` UKIs, as the initrd is combined dynamically from various sources and hence /// does not take a single input, like this command. #[allow(dead_code)] KernelInitrd { @@ -244,8 +247,8 @@ enum LockCommand { }, /// Generates/removes a .pcrlock file based on raw binary data. The data is either read from - /// the specified file or from STDIN. Requires that --pcrs= is specified. The generated - /// .pcrlock file is written to the file specified via --pcrlock=. + /// the specified file or from STDIN. Requires that `--pcrs=` is specified. The generated + /// .pcrlock file is written to the file specified via `--pcrlock=. Raw { path: PathBuf, pcrs: BitFlags, @@ -275,7 +278,7 @@ impl LockCommand { /// Runs a `systemd-pcrlock` command. /// /// Primarily designed for running the `lock-*` commands. - fn run(&self) -> Result<(), Error> { + fn run(&self) -> Result<(), TridentError> { let (path, pcrlock_file, pcrs) = { let mut cmd_path: Option = None; let mut cmd_pcrlock_file: Option = None; @@ -330,28 +333,34 @@ impl LockCommand { cmd.arg(format!("--pcrlock={}", pcrlock_file.display())); } - cmd.run_and_check() - .with_context(|| format!("Failed to run systemd-pcrlock {}", self.subcmd_name())) + cmd.run_and_check().message(format!( + "Failed to run systemd-pcrlock {}", + self.subcmd_name() + )) } } /// Generates dynamically defined .pcrlock files for either (1) the current boot only or (2) the /// current and the next boots. Calls the `systemd-pcrlock lock-*` commands to generate the -/// .pcrlock files, as well as native logic to generate the remaining .pcrlock files. +/// .pcrlock files, as well as helpers to generate the remaining .pcrlock files. pub fn generate_pcrlock_files( - // lock-gpt -> path of partitioned disk, pcrlock_file to write to - gpt_disks: Vec<(Option, PathBuf)>, - // lock-pe -> path of PE binary, pcrlock_file to write to - _pe_binaries: Vec<(PathBuf, PathBuf)>, - // lock-uki -> path of UKI PE binary, pcrlock_file to write to - uki_binaries: Vec<(PathBuf, PathBuf)>, - // lock-kernel-cmdline -> path of kernel cmdline, pcrlock_file to write to - kernel_cmdlines: Vec<(Option, PathBuf)>, - // lock-kernel-initrd -> path, pcrlock_file to write to + // Vector containing paths of partitioned disks to measure via lock-gpt, + gpt_disks: Vec>, + // Vector containing paths of PE binaries to measure via lock-pe, + _pe_binaries: Vec, + // Vector containing paths of UKI binaries to measure via lock-uki, + uki_binaries: Vec, + // Vector containing paths of kernel cmdlines to measure via lock-kernel-cmdline, + kernel_cmdlines: Vec>, + // Vector containing paths of kernel initrds to measure via lock-kernel-initrd; not used for + // UKI images, _kernel_initrds: Vec<(PathBuf, PathBuf)>, - // lock-raw -> path, pcrs, pcrlock_file to write to - raw_binaries: Vec<(PathBuf, BitFlags, PathBuf)>, -) -> Result<()> { + // Vector containing paths of raw binaries and PCRs they're extended to, to measure via + // lock-raw, + raw_binaries: Vec<(PathBuf, BitFlags)>, + // Vector containing paths of systemd-boot binaries to be measured by Trident, + systemd_boot_binaries: Vec, +) -> Result<(), TridentError> { let basic_cmds: Vec = vec![ LockCommand::FirmwareCode, LockCommand::FirmwareConfig, @@ -365,31 +374,237 @@ pub fn generate_pcrlock_files( cmd.run()?; } - for (path, pcrlock_file) in gpt_disks { - LockCommand::Gpt { path, pcrlock_file }.run()?; + // lock-gpt + for (id, disk_path) in gpt_disks.into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(GPT_PCRLOCK_DIR, id); + LockCommand::Gpt { + path: disk_path, + pcrlock_file: pcrlock_file.clone(), + } + .run()?; } - for (path, pcrlock_file) in uki_binaries { - LockCommand::Uki { path, pcrlock_file }.run()?; + // lock-uki + for (id, uki_path) in uki_binaries.clone().into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(UKI_PCRLOCK_DIR, id); + LockCommand::Uki { + path: uki_path, + pcrlock_file: pcrlock_file.clone(), + } + .run()?; } - for (path, pcrlock_file) in kernel_cmdlines { - LockCommand::KernelCmdline { path, pcrlock_file }.run()?; + // lock-kernel-cmdline + for (id, kernel_cmdline_path) in kernel_cmdlines.into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(KERNEL_CMDLINE_PCRLOCK_DIR, id); + LockCommand::KernelCmdline { + path: kernel_cmdline_path, + pcrlock_file: pcrlock_file.clone(), + } + .run()?; } // For now, needed to generate 630-boot-loader-conf.pcrlock.d, which measures the raw binary of // /boot/efi/loader/loader.conf into PCR 5. - for (path, pcrs, pcrlock_file) in raw_binaries { + for (id, (raw_binary_path, pcrs)) in raw_binaries.into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(BOOT_LOADER_CONF_PCRLOCK_DIR, id); LockCommand::Raw { - path, + path: raw_binary_path, pcrs, - pcrlock_file, + pcrlock_file: pcrlock_file.clone(), } .run()?; } - // TODO: Run helpers to generate remaining .pcrlock files, which cannot be generated via the - // lock-* commands. + // Run helpers to generate two remaining .pcrlock files + for (id, systemd_boot_path) in systemd_boot_binaries.into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(BOOT_LOADER_CODE_PCRLOCK_DIR, id); + generate_610_boot_loader_code_pcrlock(systemd_boot_path, pcrlock_file.clone()).structured( + ServicingError::GeneratePcrlockFile { + pcrlock_file: pcrlock_file.display().to_string(), + }, + )?; + } + + for (id, uki_path) in uki_binaries.into_iter().enumerate() { + let pcrlock_file = generate_pcrlock_output_path(KERNEL_INITRD_PCRLOCK_DIR, id); + generate_720_kernel_initrd_pcrlock(uki_path, pcrlock_file.clone()).structured( + ServicingError::GeneratePcrlockFile { + pcrlock_file: pcrlock_file.display().to_string(), + }, + )?; + } + + Ok(()) +} + +/// Represents a single digest entry in a .pcrlock file. +#[derive(Serialize)] +struct DigestEntry<'a> { + hash_alg: &'a str, + digest: String, +} + +/// Represents a single record in a .pcrlock file. +#[derive(Serialize)] +struct Record<'a> { + pcr: u8, + digests: Vec>, +} + +/// Represents a .pcrlock file. +#[derive(Serialize)] +struct PcrLock<'a> { + records: Vec>, +} + +/// Generates a full .pcrlock file path under /var/lib/pcrlock.d, given the sub-dir, e.g. 600-gpt, +/// and the index of the .pcrlock file. This is needed so that each image, current and update, gets +/// its own .pcrlock file. +fn generate_pcrlock_output_path(pcrlock_subdir: &str, index: usize) -> PathBuf { + let base = Path::new(PCRLOCK_DIR).join(pcrlock_subdir); + base.join(format!("generated-{index}.pcrlock")) +} + +/// Generates .pcrlock files under /var/lib/pcrlock.d/610-boot-loader-code.pcrlock.d, where Trident +/// measures the bootloader PE binary, i.e., the `systemd-boot` binary likely under +/// /EFI/BOOT/bootx64.efi, as recorded into PCR 4 following Microsoft's Authenticode hash spec for +/// measuring Windows PE binaries: +/// https://reversea.me/index.php/authenticode-i-understanding-windows-authenticode/. +fn generate_610_boot_loader_code_pcrlock( + systemd_boot_path: PathBuf, + pcrlock_file: PathBuf, +) -> Result<()> { + // Read the entire file into memory + let buffer = fs::read(&systemd_boot_path).with_context(|| { + format!( + "Failed to read PE binary at {}", + systemd_boot_path.display() + ) + })?; + + // Parse PE + let pe = PE::parse(&buffer).with_context(|| { + format!( + "Failed to parse PE binary at {}", + systemd_boot_path.display() + ) + })?; + + // Initialize hashers + let mut sha256 = Sha256::new(); + let mut sha384 = Sha384::new(); + let mut sha512 = Sha512::new(); + + for slice in pe.authenticode_ranges() { + sha256.update(slice); + sha384.update(slice); + sha512.update(slice); + } + + let digests = vec![ + DigestEntry { + hash_alg: "sha256", + digest: format!("{:x}", sha256.finalize()), + }, + DigestEntry { + hash_alg: "sha384", + digest: format!("{:x}", sha384.finalize()), + }, + DigestEntry { + hash_alg: "sha512", + digest: format!("{:x}", sha512.finalize()), + }, + ]; + + // Build PcrLock structure with PCR 4 + let pcrlock = PcrLock { + records: vec![Record { pcr: 4, digests }], + }; + + if let Some(parent) = pcrlock_file.parent() { + fs::create_dir_all(parent).context(format!( + "Failed to create directory for .pcrlock file at {}", + pcrlock_file.display() + ))?; + } + + let json = serde_json::to_string(&pcrlock).context(format!( + "Failed to serialize .pcrlock file {} as JSON", + pcrlock_file.display() + ))?; + fs::write(&pcrlock_file, json).context(format!( + "Failed to write .pcrlock file at {}", + pcrlock_file.display() + ))?; + + Ok(()) +} + +/// Generates .pcrlock files under /var/lib/pcrlock.d/720-kernel-initrd.pcrlock.d, where Trident +/// measures the initrd section of the UKI binary, as recorded into PCR 9. +fn generate_720_kernel_initrd_pcrlock(uki_path: PathBuf, pcrlock_file: PathBuf) -> Result<()> { + // Copy UKI to a temp file + let uki_temp = NamedTempFile::new().context("Failed to create temporary UKI file")?; + fs::copy(&uki_path, uki_temp.path()) + .with_context(|| format!("Failed to copy UKI from {}", uki_path.display()))?; + + // Extract .initrd + let initrd_temp = NamedTempFile::new().context("Failed to create temporary initrd file")?; + let initrd_path = initrd_temp.path().to_path_buf(); + Command::new("objcopy") + .arg("--dump-section") + .arg(format!(".initrd={}", initrd_path.display())) + .arg(uki_temp.path()) + .run_and_check() + .context(format!( + "Failed to execute objcopy to extract initrd section from UKI at '{}'", + uki_temp.path().display() + ))?; + + // Read extracted initrd and compute hashes + let buffer = fs::read(&initrd_path).with_context(|| { + format!( + "Failed to read extracted initrd at {}", + initrd_path.display() + ) + })?; + + let digests = vec![ + DigestEntry { + hash_alg: "sha256", + digest: hex::encode(Sha256::digest(&buffer)), + }, + DigestEntry { + hash_alg: "sha384", + digest: hex::encode(Sha384::digest(&buffer)), + }, + DigestEntry { + hash_alg: "sha512", + digest: hex::encode(Sha512::digest(&buffer)), + }, + ]; + + // Write .pcrlock file + if let Some(parent) = pcrlock_file.parent() { + fs::create_dir_all(parent).context(format!( + "Failed to create directory for .pcrlock file at {}", + pcrlock_file.display() + ))?; + } + + let pcrlock = PcrLock { + records: vec![Record { pcr: 9, digests }], + }; + + let json = serde_json::to_string(&pcrlock).context(format!( + "Failed to serialize .pcrlock file {} as JSON", + pcrlock_file.display() + ))?; + fs::write(&pcrlock_file, json).context(format!( + "Failed to write .pcrlock file at {}", + pcrlock_file.display() + ))?; Ok(()) } @@ -414,6 +629,18 @@ mod tests { "--pcr=0,1,2,3,4,5,7,9,10,11,12,13,14,15,16,23".to_string() ); } + + #[test] + fn test_generate_pcrlock_output_path() { + let index = 0; + let expected_path = Path::new(PCRLOCK_DIR) + .join(GPT_PCRLOCK_DIR) + .join(format!("generated-{index}.pcrlock")); + assert_eq!( + generate_pcrlock_output_path(GPT_PCRLOCK_DIR, index), + expected_path + ); + } } #[cfg(feature = "functional-test")] diff --git a/sysdefs/src/tpm2.rs b/sysdefs/src/tpm2.rs index 01eb97757..220f3c8f5 100644 --- a/sysdefs/src/tpm2.rs +++ b/sysdefs/src/tpm2.rs @@ -2,7 +2,12 @@ use anyhow::{bail, Error}; use enumflags2::bitflags; use serde::{self, Deserialize}; -/// Represents the Platform Configuration Registers (PCRs) in the TPM. +/// Represents the Platform Configuration Registers (PCRs) in the TPM. Each PCR is associated with +/// a digit number and a string name. +/// +/// Currently, the PCRs modified by `systemd`` are represented, e.g. as shown in the +/// `systemd-cryptenroll` documentation, but more might be added in the future: +/// https://www.man7.org/linux/man-pages/man1/systemd-cryptenroll.1.html. #[bitflags] #[repr(u32)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -42,14 +47,14 @@ pub enum Pcr { } impl Pcr { - /// Returns the digit value of the PCR. - pub fn to_value(&self) -> u32 { + /// Returns the digit representation of the PCR number. + pub fn to_num(&self) -> u32 { (*self as u32).trailing_zeros() } - /// Returns the PCR for the given digit value. Needed for deserialization. - pub fn from_value(value: u32) -> Result { - match value { + /// Returns the PCR for the given digit number. Needed for deserialization. + pub fn from_num(num: u32) -> Result { + match num { 0 => Ok(Pcr::Pcr0), 1 => Ok(Pcr::Pcr1), 2 => Ok(Pcr::Pcr2), @@ -66,10 +71,7 @@ impl Pcr { 15 => Ok(Pcr::Pcr15), 16 => Ok(Pcr::Pcr16), 23 => Ok(Pcr::Pcr23), - _ => bail!( - "Failed to convert an invalid PCR value '{}' to a Pcr", - value - ), + _ => bail!("Failed to convert an invalid PCR number '{}' to a Pcr", num), } } } @@ -80,7 +82,7 @@ impl<'de> Deserialize<'de> for Pcr { D: serde::Deserializer<'de>, { let val = u32::deserialize(deserializer)?; - Pcr::from_value(val) + Pcr::from_num(val) .map_err(|_| serde::de::Error::custom(format!("Failed to deserialize PCR: {}", val))) } } @@ -90,37 +92,37 @@ mod tests { use super::*; #[test] - fn test_to_value() { - assert_eq!(Pcr::Pcr0.to_value(), 0); - assert_eq!(Pcr::Pcr1.to_value(), 1); - assert_eq!(Pcr::Pcr2.to_value(), 2); - assert_eq!(Pcr::Pcr3.to_value(), 3); - assert_eq!(Pcr::Pcr4.to_value(), 4); - assert_eq!(Pcr::Pcr5.to_value(), 5); - assert_eq!(Pcr::Pcr7.to_value(), 7); - assert_eq!(Pcr::Pcr9.to_value(), 9); - assert_eq!(Pcr::Pcr10.to_value(), 10); - assert_eq!(Pcr::Pcr11.to_value(), 11); - assert_eq!(Pcr::Pcr12.to_value(), 12); - assert_eq!(Pcr::Pcr13.to_value(), 13); - assert_eq!(Pcr::Pcr14.to_value(), 14); - assert_eq!(Pcr::Pcr15.to_value(), 15); - assert_eq!(Pcr::Pcr16.to_value(), 16); - assert_eq!(Pcr::Pcr23.to_value(), 23); + fn test_to_num() { + assert_eq!(Pcr::Pcr0.to_num(), 0); + assert_eq!(Pcr::Pcr1.to_num(), 1); + assert_eq!(Pcr::Pcr2.to_num(), 2); + assert_eq!(Pcr::Pcr3.to_num(), 3); + assert_eq!(Pcr::Pcr4.to_num(), 4); + assert_eq!(Pcr::Pcr5.to_num(), 5); + assert_eq!(Pcr::Pcr7.to_num(), 7); + assert_eq!(Pcr::Pcr9.to_num(), 9); + assert_eq!(Pcr::Pcr10.to_num(), 10); + assert_eq!(Pcr::Pcr11.to_num(), 11); + assert_eq!(Pcr::Pcr12.to_num(), 12); + assert_eq!(Pcr::Pcr13.to_num(), 13); + assert_eq!(Pcr::Pcr14.to_num(), 14); + assert_eq!(Pcr::Pcr15.to_num(), 15); + assert_eq!(Pcr::Pcr16.to_num(), 16); + assert_eq!(Pcr::Pcr23.to_num(), 23); } #[test] - fn test_from_value() { + fn test_from_num() { // Test case #0: Convert a valid value to a PCR. - assert_eq!(Pcr::from_value(0).unwrap(), Pcr::Pcr0); - assert_eq!(Pcr::from_value(1).unwrap(), Pcr::Pcr1); - assert_eq!(Pcr::from_value(2).unwrap(), Pcr::Pcr2); - assert_eq!(Pcr::from_value(23).unwrap(), Pcr::Pcr23); + assert_eq!(Pcr::from_num(0).unwrap(), Pcr::Pcr0); + assert_eq!(Pcr::from_num(1).unwrap(), Pcr::Pcr1); + assert_eq!(Pcr::from_num(2).unwrap(), Pcr::Pcr2); + assert_eq!(Pcr::from_num(23).unwrap(), Pcr::Pcr23); // Test case #1: Convert an invalid value to a PCR. assert_eq!( - Pcr::from_value(31).unwrap_err().root_cause().to_string(), - "Failed to convert an invalid PCR value '31' to a Pcr" + Pcr::from_num(31).unwrap_err().root_cause().to_string(), + "Failed to convert an invalid PCR number '31' to a Pcr" ); } } diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index 6a4d0e84f..c5610228a 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -416,6 +416,9 @@ pub enum ServicingError { #[error("Failed to generate Netplan config")] GenerateNetplanConfig, + #[error("Failed to generate .pcrlock file at '{pcrlock_file}'")] + GeneratePcrlockFile { pcrlock_file: String }, + #[error("Failed to generate recovery key file '{key_file}'")] GenerateRecoveryKeyFile { key_file: String }, From 597d03e8dd66c04bfeb7e35ec956c8ec7d2816f2 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 2 Jun 2025 22:39:03 +0000 Subject: [PATCH 46/99] Merged PR 23336: engineering: Fixes for clippy 1.86 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Fix some stuff flagged by clippy 1.86, which apparently now runs markdown lint on doc comments. Related work items: #12390 --- osutils/src/pcrlock.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osutils/src/pcrlock.rs b/osutils/src/pcrlock.rs index 8daf1559a..d4d407989 100644 --- a/osutils/src/pcrlock.rs +++ b/osutils/src/pcrlock.rs @@ -29,15 +29,15 @@ const PCR_POLICY_PATH: &str = "/var/lib/systemd/pcrlock.json"; /// Dir-s for dynamically generated .pcrlock files that might contain 1+ .pcrlock files, for the /// current and updated images: /// 1. /var/lib/pcrlock.d/600-gpt.pcrlock.d, where `lock-gpt` measures the GPT partition table of -/// the booted medium, as recorded to PCR 5 by the firmware, +/// the booted medium, as recorded to PCR 5 by the firmware, const GPT_PCRLOCK_DIR: &str = "600-gpt.pcrlock.d"; /// 2. /var/lib/pcrlock.d/610-boot-loader-code.pcrlock.d, where Trident measures the bootx64.efi -/// binary, as recorded into PCR 4 following Microsoft's Authenticode hash spec, +/// binary, as recorded into PCR 4 following Microsoft's Authenticode hash spec, const BOOT_LOADER_CODE_PCRLOCK_DIR: &str = "610-boot-loader-code.pcrlock.d"; /// 3. /var/lib/pcrlock.d/630-boot-loader-conf.pcrlock.d, where `lock-raw` measures the boot loader -/// configuration file, as recorded into PCR 5, +/// configuration file, as recorded into PCR 5, const BOOT_LOADER_CONF_PCRLOCK_DIR: &str = "630-boot-loader-conf.pcrlock.d"; /// 4. /var/lib/pcrlock.d/650-uki.pcrlock.d, where `lock-uki` measures the UKI binary, as recorded @@ -49,7 +49,7 @@ const UKI_PCRLOCK_DIR: &str = "650-uki.pcrlock.d"; const KERNEL_CMDLINE_PCRLOCK_DIR: &str = "710-kernel-cmdline.pcrlock.d"; /// 6. /var/lib/pcrlock.d/720-kernel-initrd.pcrlock.d, where Trident measures the initrd section of -/// the UKI binary, as recorded into PCR 9. +/// the UKI binary, as recorded into PCR 9. const KERNEL_INITRD_PCRLOCK_DIR: &str = "720-kernel-initrd.pcrlock.d"; /// Valid PCRs for TPM 2.0 policy generation, following the `systemd-pcrlock` spec. From 196746feb935e144aa874c389533cd56b7c45706 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Mon, 2 Jun 2025 23:57:52 +0000 Subject: [PATCH 47/99] Merged PR 23205: feature: Support rollback with UKI #### AI description (iteration 1) #### PR Classification This pull request adds a new feature to support rollback with UKI. #### PR Summary The changes implement a dedicated UKI management module to handle boot file operations for rollback, integrating this mechanism into the clean install, update, and rollback flows. - **`src/engine/boot/uki.rs`**: Introduces a new module with functions to prepare the ESP, copy the UKI file, update the boot order, and enable oneshot boot. - **`src/engine/boot/esp.rs`**: Refactors UKI-related code by replacing inline logic with calls to the new `uki` module. - **`osutils/src/bootctl.rs`**: Adds support functions for bootctl commands to set oneshot and default boot entries. - **`src/engine/rollback.rs`, `src/engine/clean_install.rs`, and `src/engine/update.rs`**: Update their workflows to integrate the new UKI support, ensuring proper fallback and rollback behavior. Related work items: #12037 --- osutils/src/dependencies.rs | 1 + osutils/src/efivar.rs | 180 ++++++++++++++++++++++ osutils/src/lib.rs | 1 + src/engine/boot/esp.rs | 121 ++------------- src/engine/boot/mod.rs | 1 + src/engine/boot/uki.rs | 287 ++++++++++++++++++++++++++++++++++++ src/engine/bootentries.rs | 14 +- src/engine/rollback.rs | 9 +- trident_api/src/error.rs | 12 ++ 9 files changed, 513 insertions(+), 113 deletions(-) create mode 100644 osutils/src/efivar.rs create mode 100644 src/engine/boot/uki.rs diff --git a/osutils/src/dependencies.rs b/osutils/src/dependencies.rs index 97e422822..8000247ce 100644 --- a/osutils/src/dependencies.rs +++ b/osutils/src/dependencies.rs @@ -94,6 +94,7 @@ pub enum Dependency { Df, Dracut, E2fsck, + Efivar, Efibootmgr, Findmnt, Iptables, diff --git a/osutils/src/efivar.rs b/osutils/src/efivar.rs new file mode 100644 index 000000000..909008186 --- /dev/null +++ b/osutils/src/efivar.rs @@ -0,0 +1,180 @@ +use std::{fs, io::Write, path::Path}; + +use log::debug; +use tempfile::NamedTempFile; + +use trident_api::error::{ReportError, ServicingError, TridentError, TridentResultExt}; + +use crate::dependencies::{Dependency, DependencyResultExt}; + +const BOOTLOADER_INTERFACE_GUID: &str = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; + +const LOADER_ENTRY_ONESHOT: &str = "LoaderEntryOneShot"; +const LOADER_ENTRY_DEFAULT: &str = "LoaderEntryDefault"; +const LOADER_ENTRY_SELECTED: &str = "LoaderEntrySelected"; + +fn encode_utf16le(data: &str) -> Vec { + data.encode_utf16() + .flat_map(|u| u.to_le_bytes()) + .chain([0; 2]) + .collect() +} + +fn decode_utf16le(mut data: &[u8]) -> String { + if data.len() <= 2 { + return String::new(); + } + + // Remove null terminator + if data[data.len() - 2..] == [0, 0] { + data = &data[..data.len() - 2]; + } + + let utf16_data: Vec = data + .chunks(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + String::from_utf16_lossy(&utf16_data) +} + +/// Set an EFI variable using the efivar command-line tool. +/// `name` should include the GUID, e.g. "BootNext-8be4df61-93ca-11d2-aa0d-00e098032b8c" +/// `data` should be a hex string, e.g. "0100" for BootNext=0001 (little-endian) +fn set_efi_variable(name: &str, data_utf16: &[u8]) -> Result<(), TridentError> { + debug!( + "Setting EFI variable '{name}' to '{}'", + decode_utf16le(data_utf16) + ); + + // Write the UTF-16LE data to a temporary file + let mut tmpfile = NamedTempFile::new().structured(ServicingError::SetEfiVariable { + name: name.to_string(), + })?; + tmpfile + .write_all(data_utf16) + .structured(ServicingError::SetEfiVariable { + name: name.to_string(), + })?; + + Dependency::Efivar + .cmd() + .arg("--verbose") + .arg("--name") + .arg(name) + .arg("--write") + .arg("--datafile") + .arg(tmpfile.path()) + .run_and_check() + .message(format!("efivar failed to set variable '{name}'")) +} + +/// Set the LoaderEntryOneShot EFI variable for systemd-boot oneshot boot. +pub fn set_oneshot(entry: &str) -> Result<(), TridentError> { + debug!("Setting oneshot boot entry to: '{entry}'"); + set_efi_variable( + &format!("{BOOTLOADER_INTERFACE_GUID}-{LOADER_ENTRY_ONESHOT}"), + &encode_utf16le(entry), + ) +} + +/// Set the LoaderEntryDefault EFI variable for systemd-boot default boot. +pub fn set_default(entry: &str) -> Result<(), TridentError> { + debug!("Setting default boot entry to: '{entry}'"); + set_efi_variable( + &format!("{BOOTLOADER_INTERFACE_GUID}-{LOADER_ENTRY_DEFAULT}"), + &encode_utf16le(entry), + ) +} + +fn read_efi_variable(guid: &str, variable: &str) -> Result, TridentError> { + let efi_var_path = Path::new("/sys/firmware/efi/efivars/").join(format!("{variable}-{guid}")); + + // Read the LoaderEntrySelected EFI variable from efivars + let data = fs::read(efi_var_path).structured(ServicingError::ReadEfiVariable { + name: variable.to_string(), + })?; + + // The first 4 bytes are attributes, skip them + if data.len() <= 4 { + return Err(TridentError::new(ServicingError::ReadEfiVariable { + name: variable.to_string(), + })) + .message("EFI variable file is too short"); + } + Ok(data[4..].to_vec()) +} + +/// Set the LoaderEntryDefault EFI variable to the current boot entry +pub fn set_default_to_current() -> Result<(), TridentError> { + let current = read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_SELECTED)?; + debug!( + "Setting default boot entry to current: '{}'", + decode_utf16le(¤t) + ); + set_efi_variable( + &format!("{BOOTLOADER_INTERFACE_GUID}-{LOADER_ENTRY_DEFAULT}"), + ¤t, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_utf16le() { + let input = "Test"; + let expected = vec![84, 0, 101, 0, 115, 0, 116, 0, 0, 0]; + assert_eq!(encode_utf16le(input), expected); + } + + #[test] + fn test_decode_utf16le() { + let input = vec![84, 0, 101, 0, 115, 0, 116, 0, 0, 0]; + assert_eq!(decode_utf16le(&input), "Test"); + } +} + +#[cfg(feature = "functional-test")] +#[cfg_attr(not(test), allow(unused_imports, dead_code))] +mod functional_test { + use pytest_gen::functional_test; + + use super::*; + + #[functional_test(feature = "helpers")] + fn test_set_oneshot() { + let entry = "TestEntry"; + set_oneshot(entry).unwrap(); + let data = read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_ONESHOT).unwrap(); + assert_eq!(decode_utf16le(&data), entry); + + set_oneshot("").unwrap(); + } + + #[functional_test(feature = "helpers")] + fn test_set_default() { + let entry = "TestDefaultEntry"; + set_default(entry).unwrap(); + let data = read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_DEFAULT).unwrap(); + assert_eq!(decode_utf16le(&data), entry); + + set_default("").unwrap(); + } + + #[functional_test(feature = "helpers")] + fn test_set_default_to_current() { + set_efi_variable( + &format!("{BOOTLOADER_INTERFACE_GUID}-{LOADER_ENTRY_SELECTED}"), + &encode_utf16le("CurrentEntry"), + ) + .unwrap(); + + // Now set the default to the current entry + set_default_to_current().unwrap(); + let data = read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_DEFAULT).unwrap(); + assert_eq!(decode_utf16le(&data), "CurrentEntry"); + + set_default("").unwrap(); + } +} diff --git a/osutils/src/lib.rs b/osutils/src/lib.rs index 6a5fc2aa9..c164c880d 100644 --- a/osutils/src/lib.rs +++ b/osutils/src/lib.rs @@ -7,6 +7,7 @@ pub mod dependencies; pub mod df; pub mod e2fsck; pub mod efibootmgr; +pub mod efivar; pub mod encryption; pub mod exe; pub mod files; diff --git a/src/engine/boot/esp.rs b/src/engine/boot/esp.rs index 4a6f19910..48f3a9ea0 100644 --- a/src/engine/boot/esp.rs +++ b/src/engine/boot/esp.rs @@ -15,19 +15,14 @@ use osutils::{ hashing_reader::{HashingReader, HashingReader384}, image_streamer, mount::{self, MountGuard}, - path::join_relative, }; use trident_api::{ - constants::{ - internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, - ESP_MOUNT_POINT_PATH, - }, + constants::internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, error::{ReportError, TridentError, TridentResultExt, UnsupportedConfigurationError}, - status::AbVolumeSelection, }; use crate::engine::{ - boot::ESP_EXTRACTION_DIRECTORY, + boot::{uki, ESP_EXTRACTION_DIRECTORY}, constants::{ EFI_DEFAULT_BIN_RELATIVE_PATH, ESP_EFI_DIRECTORY, ESP_RELATIVE_MOUNT_POINT_PATH, GRUB2_CONFIG_FILENAME, GRUB2_CONFIG_RELATIVE_PATH, @@ -127,102 +122,11 @@ fn copy_file_artifacts( ))?; if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { - let esp_root_path = join_relative(mount_point, ESP_MOUNT_POINT_PATH); - - // The directory where systemd-boot looks for UKIs. - let esp_uki_directory = esp_root_path.join("EFI/Linux"); - - // Create the EFI/Linux directory on the ESP if it doesn't exist. - fs::create_dir_all(&esp_uki_directory) - .context("Failed to create 'EFI/Linux' on the ESP")?; - - // Find all UKIs within the image. There should only be one. - let ukis: Vec<_> = temp_mount_dir - .join("EFI/Linux") - .read_dir() - .context("Could not read UKI directory")? - .collect::, _>>() - .context("Failed while reading UKI directory")? - .into_iter() - .map(|entry| entry.path()) - .collect(); - ensure!(!ukis.is_empty(), "No UKI files found within the image"); - ensure!(ukis.len() == 1, "Multiple UKI files found within the image"); - - // Copy the UKI from the image into the AZLA/ALZB directory. Eventually the UKI will be - // placed into /EFI/Linux on the ESP. But in order to do that atomically, the file needs to - // already be located somewhere on the ESP. - const TMP_UKI_NAME: &str = "vmlinuz.efi"; - fs::copy(&ukis[0], esp_dir_path.join(TMP_UKI_NAME)) - .context("Failed to copy UKI to the ESP")?; - - // Create 'loader/entries.srel' on the ESP as required by the Boot Loader Specification. - fs::create_dir_all(esp_root_path.join("loader")) - .context("Failed to create directory loader")?; - fs::write(esp_root_path.join("loader/entries.srel"), "type1\n") - .context("Failed to write entries.srel")?; - - // Update the boot order used by systemd-boot. - // - // Every UKI placed by Trident will have a name of the form - // 'vmlinuz--azl.efi'. Due to the way systemd-boot works, the UKI with the - // highest N will be first in the boot order. The volume and install index portions of the - // name are used to map UKIs to the particular install index and A/B volume that created - // them. - // - // In the loop below, we delete any existing UKIs with the current install index and A/B - // update volume, and record the highest N of any UKI that remains. Once the loop is - // finished, this enables us to place the new UKI at the next highest N so that it will be - // first in the boot order. - // - // TODO: The rename should really happen during 'finalize' rather than when the ESP image is - // being written. - let uki_suffix = match ctx.ab_active_volume { - Some(AbVolumeSelection::VolumeA) => format!("azlb{}.efi", ctx.install_index), - None | Some(AbVolumeSelection::VolumeB) => { - format!("azla{}.efi", ctx.install_index) - } - }; - let mut max_index = 99; - let entries = fs::read_dir(&esp_uki_directory).context(format!( - "Failed to read directory '{}'", - esp_uki_directory.display() - ))?; - for entry in entries { - let entry = entry.context("Failed to read entry")?; - let filename = entry.file_name(); - - // Parse the filename according to Trident's naming scheme. Any UKIs that don't match - // the naming scheme are for some other unknown install and will be left in place. This - // means they'll either be prioritized before or after the UKI Trident is placing, but - // they won't be deleted. - let Some((index, suffix)) = filename - .to_str() - .and_then(|filename| filename.strip_prefix("vmlinuz-")) - .and_then(|f| f.split_once('-')) - .and_then(|(index, suffix)| Some((index.parse::().ok()?, suffix))) - else { - trace!( - "Ignoring existing UKI file '{}' that does not match Trident naming scheme", - entry.path().display() - ); - continue; - }; - - if suffix == uki_suffix { - fs::remove_file(entry.path()).context(format!( - "Failed to remove file '{}'", - entry.path().display() - ))?; - } else { - max_index = max_index.max(index); - } - } - fs::rename( - esp_dir_path.join(TMP_UKI_NAME), - esp_uki_directory.join(format!("vmlinuz-{}-{uki_suffix}", max_index + 1)), - ) - .context("Failed to rename UKI file")?; + // Prepare ESP directory structure for UKI boot + uki::prepare_esp_for_uki(mount_point)?; + + // Copy the UKI from the image into the ESP directory + uki::stage_uki_on_esp(temp_mount_dir, mount_point)?; } else { // In non-UKI mode, bail if grub_noprefix.efi is not found in the image. ensure!( @@ -385,14 +289,13 @@ pub fn next_install_index(mount_point: &Path) -> Result { Ok(first_available_install_index) } -/// Returns the path to the ESP directory where the boot files need to be copied -/// to. +/// Returns the path to the ESP directory where the boot files need to be copied to. /// -/// Path will be in the form of /boot/efi/EFI/, where is the install ID -/// as determined by ctx. +/// Path will be in the form of `/boot/efi/EFI/`, where `` is the install ID as determined +/// by ctx. /// -/// The function will find the next available install ID for this install and -/// update the install index in the engine context. +/// The function will find the next available install ID for this install and update the install +/// index in the engine context. pub fn generate_efi_bin_base_dir_path( ctx: &EngineContext, mount_point: &Path, diff --git a/src/engine/boot/mod.rs b/src/engine/boot/mod.rs index 30b0067d8..c7705cd70 100644 --- a/src/engine/boot/mod.rs +++ b/src/engine/boot/mod.rs @@ -18,6 +18,7 @@ use super::EngineContext; pub(super) mod esp; pub(super) mod grub; +pub(super) mod uki; pub(crate) const ESP_EXTRACTION_DIRECTORY: &str = "/tmp"; diff --git a/src/engine/boot/uki.rs b/src/engine/boot/uki.rs new file mode 100644 index 000000000..6e58d570a --- /dev/null +++ b/src/engine/boot/uki.rs @@ -0,0 +1,287 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{ensure, Context, Error}; +use const_format::formatcp; +use log::{debug, trace}; + +use osutils::efivar; +use osutils::path::join_relative; +use trident_api::error::{ + InternalError, ReportError, ServicingError, TridentError, TridentResultExt, +}; +use trident_api::{ + constants::{ESP_EFI_DIRECTORY, ESP_MOUNT_POINT_PATH}, + status::AbVolumeSelection, +}; + +use crate::engine::EngineContext; + +/// Temporary name for the UKI file before renaming. +const TMP_UKI_NAME: &str = "vmlinuz-0.efi.staged"; +const UKI_DIRECTORY: &str = formatcp!("{ESP_EFI_DIRECTORY}/Linux"); + +fn uki_suffix(ctx: &EngineContext) -> String { + match ctx.ab_active_volume { + Some(AbVolumeSelection::VolumeA) => format!("azlb{}.efi", ctx.install_index), + None | Some(AbVolumeSelection::VolumeB) => format!("azla{}.efi", ctx.install_index), + } +} + +/// Return whether there is a staged UKI file on the ESP. +pub fn is_staged(esp_dir_path: &Path) -> bool { + esp_dir_path.join(UKI_DIRECTORY).join(TMP_UKI_NAME).exists() +} + +/// Copies the UKI file from the mounted image to the ESP directory. +pub fn stage_uki_on_esp(temp_mount_dir: &Path, mount_point: &Path) -> Result<(), Error> { + let uki_source_dir = temp_mount_dir.join(UKI_DIRECTORY); + let ukis: Vec<_> = uki_source_dir + .read_dir() + .context("Could not read UKI directory")? + .collect::, _>>() + .context("Failed while reading UKI directory")? + .into_iter() + .map(|entry| entry.path()) + .collect(); + + ensure!(!ukis.is_empty(), "No UKI files found within the image"); + ensure!(ukis.len() == 1, "Multiple UKI files found within the image"); + + let dest_path = join_relative(mount_point, ESP_MOUNT_POINT_PATH) + .join(UKI_DIRECTORY) + .join(TMP_UKI_NAME); + debug!("Staging UKI file at '{}'", dest_path.display()); + fs::copy(&ukis[0], dest_path).context("Failed to copy UKI to the ESP")?; + + Ok(()) +} + +/// Prepares the ESP directory structure required for UKI boot. +pub fn prepare_esp_for_uki(root_mount_point: &Path) -> Result<(), Error> { + let esp_root_path = join_relative(root_mount_point, ESP_MOUNT_POINT_PATH); + let esp_uki_directory = esp_root_path.join(UKI_DIRECTORY); + + fs::create_dir_all(&esp_uki_directory) + .context(format!("Failed to create '{UKI_DIRECTORY}' on the ESP"))?; + + fs::create_dir_all(esp_root_path.join("loader")) + .context("Failed to create directory loader")?; + fs::write(esp_root_path.join("loader/entries.srel"), "type1\n") + .context("Failed to write entries.srel")?; + + Ok(()) +} + +/// Enumerates existing UKIs in the given directory, returning their indices and suffixes. +fn enumerate_existing_ukis( + esp_uki_directory: &Path, +) -> Result, Error> { + let mut uki_entries = Vec::new(); + + for entry in fs::read_dir(esp_uki_directory).context(format!( + "Failed to read directory '{}'", + esp_uki_directory.display() + ))? { + let entry = entry.context("Failed to read entry")?; + let filename = entry.file_name(); + + if let Some((index, suffix)) = filename + .to_str() + .and_then(|filename| filename.strip_prefix("vmlinuz-")) + .and_then(|f| f.split_once('-')) + .and_then(|(index, suffix)| Some((index.parse::().ok()?, suffix.to_string()))) + { + uki_entries.push((index, suffix, entry.path())); + } else { + trace!( + "Ignoring existing UKI file '{}' that does not match Trident naming scheme", + entry.path().display() + ); + } + } + + Ok(uki_entries) +} + +/// Updates the boot order by renaming the UKI file according to Trident's naming scheme. +pub fn update_uki_boot_order( + ctx: &EngineContext, + esp_dir_path: &Path, + oneshot: bool, +) -> Result<(), TridentError> { + let esp_uki_directory = esp_dir_path.join(UKI_DIRECTORY); + let existing_ukis = + enumerate_existing_ukis(&esp_uki_directory).structured(ServicingError::EnumerateUkis)?; + let uki_suffix = uki_suffix(ctx); + + let mut max_index = 99; + for (index, suffix, path) in existing_ukis { + if suffix == uki_suffix { + fs::remove_file(&path) + .structured(ServicingError::UpdateUki) + .message(format!("Failed to remove file '{}'", path.display()))?; + } else { + max_index = max_index.max(index); + } + } + + let dest_path = esp_uki_directory.join(format!("vmlinuz-{}-{uki_suffix}", max_index + 1)); + let entry_name = dest_path + .file_name() // TODO: should be `file_stem` but systemd-boot doesn't seem to be following the spec. + .structured(InternalError::Internal("Failed to get file stem"))? + .to_str() + .structured(InternalError::Internal("Boot entry name isn't valid UTF-8"))?; + + debug!("Renaming UKI file to '{}'", dest_path.display()); + fs::rename(esp_uki_directory.join(TMP_UKI_NAME), &dest_path) + .structured(ServicingError::UpdateUki) + .message("Failed to rename staged UKI")?; + + if oneshot { + debug!("Setting oneshot boot entry to '{entry_name}'"); + efivar::set_oneshot(entry_name)?; + } else { + debug!("Setting default boot entry to '{entry_name}'"); + efivar::set_default(entry_name)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_uki_suffix() { + use trident_api::status::AbVolumeSelection; + + let mut ctx = EngineContext { + ab_active_volume: Some(AbVolumeSelection::VolumeA), + install_index: 1, + ..Default::default() + }; + assert_eq!(uki_suffix(&ctx), "azlb1.efi"); + + ctx.ab_active_volume = Some(AbVolumeSelection::VolumeB); + ctx.install_index = 2; + assert_eq!(uki_suffix(&ctx), "azla2.efi"); + + ctx.ab_active_volume = None; + ctx.install_index = 3; + assert_eq!(uki_suffix(&ctx), "azla3.efi"); + } + + #[test] + fn test_is_staged() { + let mock_esp = tempdir().unwrap(); + let uki_dir = mock_esp.path().join(UKI_DIRECTORY); + fs::create_dir_all(&uki_dir).unwrap(); + assert!(!is_staged(mock_esp.path())); + + fs::write(uki_dir.join(TMP_UKI_NAME), b"dummy").unwrap(); + assert!(is_staged(mock_esp.path())); + } + + #[test] + fn test_copy_uki_to_esp() { + // Create source EFI/Linux directory and a dummy UKI file + let temp_mount = tempdir().unwrap(); + let src_uki_dir = temp_mount.path().join("EFI/Linux"); + fs::create_dir_all(&src_uki_dir).unwrap(); + fs::write(src_uki_dir.join("dummy-uki.efi"), b"uki-content").unwrap(); + + let mount_point = tempdir().unwrap(); + prepare_esp_for_uki(mount_point.path()).unwrap(); + + // Should succeed when exactly one UKI file is present + stage_uki_on_esp(temp_mount.path(), mount_point.path()).unwrap(); + + // Check that the file was copied to the correct destination + let dest_uki_file = join_relative(mount_point.path(), ESP_MOUNT_POINT_PATH) + .join(UKI_DIRECTORY) + .join(TMP_UKI_NAME); + assert_eq!(fs::read(&dest_uki_file).unwrap(), b"uki-content"); + + // Should fail if there are multiple UKI files + let extra_uki_file = src_uki_dir.join("another.efi"); + fs::write(&extra_uki_file, b"other").unwrap(); + stage_uki_on_esp(temp_mount.path(), mount_point.path()).unwrap_err(); + } + + #[test] + fn test_prepare_esp_for_uki() { + let root_mount = tempdir().unwrap(); + prepare_esp_for_uki(root_mount.path()).unwrap(); + + let esp_root_path = join_relative(root_mount.path(), ESP_MOUNT_POINT_PATH); + assert!(esp_root_path.join(UKI_DIRECTORY).exists()); + assert!(esp_root_path.join("loader").exists()); + assert!(esp_root_path.join("loader/entries.srel").exists()); + let content = fs::read_to_string(esp_root_path.join("loader/entries.srel")).unwrap(); + assert_eq!(content, "type1\n"); + } + + #[test] + fn test_enumerate_existing_ukis_empty_directory() { + let dir = tempdir().unwrap(); + let entries = enumerate_existing_ukis(dir.path()).unwrap(); + assert!(entries.is_empty()); + } + + #[test] + fn test_enumerate_existing_ukis_single_valid_entry() { + let dir = tempdir().unwrap(); + let uki_path = dir.path().join("vmlinuz-1-azla1.efi"); + File::create(&uki_path).unwrap(); + + let entries = enumerate_existing_ukis(dir.path()).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0], (1, "azla1.efi".to_string(), uki_path)); + } + + #[test] + fn test_enumerate_existing_ukis_multiple_valid_entries() { + let dir = tempdir().unwrap(); + let uki_path1 = dir.path().join("vmlinuz-1-azla1.efi"); + let uki_path2 = dir.path().join("vmlinuz-2-azlb2.efi"); + File::create(&uki_path1).unwrap(); + File::create(&uki_path2).unwrap(); + + let mut entries = enumerate_existing_ukis(dir.path()).unwrap(); + entries.sort_by_key(|e| e.0); + + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], (1, "azla1.efi".to_string(), uki_path1)); + assert_eq!(entries[1], (2, "azlb2.efi".to_string(), uki_path2)); + } + + #[test] + fn test_enumerate_existing_ukis_ignores_invalid_entries() { + let dir = tempdir().unwrap(); + let valid_uki = dir.path().join("vmlinuz-3-azla3.efi"); + let invalid_uki1 = dir.path().join("invalid-file.efi"); + let invalid_uki2 = dir.path().join("vmlinuz-noindex-azla.efi"); + File::create(&valid_uki).unwrap(); + File::create(&invalid_uki1).unwrap(); + File::create(&invalid_uki2).unwrap(); + + let entries = enumerate_existing_ukis(dir.path()).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0], (3, "azla3.efi".to_string(), valid_uki)); + } + + #[test] + fn test_enumerate_existing_ukis_non_numeric_index() { + let dir = tempdir().unwrap(); + let invalid_uki = dir.path().join("vmlinuz-abc-azla.efi"); + File::create(&invalid_uki).unwrap(); + + let entries = enumerate_existing_ukis(dir.path()).unwrap(); + assert!(entries.is_empty()); + } +} diff --git a/src/engine/bootentries.rs b/src/engine/bootentries.rs index 12301f84e..c29d0363f 100644 --- a/src/engine/bootentries.rs +++ b/src/engine/bootentries.rs @@ -18,7 +18,10 @@ use trident_api::{ BlockDeviceId, }; -use super::{boot, EngineContext}; +use super::{ + boot::{self, uki}, + EngineContext, +}; /// Boot EFI executable const BOOT_EFI: &str = BootloaderExecutable::Boot.current_name(); @@ -104,7 +107,14 @@ pub fn create_and_update_boot_variables( )?; // Update boot variables - set_boot_next_and_update_boot_order(ctx, added_entry_numbers) + set_boot_next_and_update_boot_order(ctx, added_entry_numbers)?; + + if uki::is_staged(esp_path) { + let oneshot = ctx.servicing_type != ServicingType::CleanInstall; + uki::update_uki_boot_order(ctx, esp_path, oneshot)?; + } + + Ok(()) } /// Update the `BootNext` and potentially also `BootOrder`. diff --git a/src/engine/rollback.rs b/src/engine/rollback.rs index 9a9ac7189..2f2bd405a 100644 --- a/src/engine/rollback.rs +++ b/src/engine/rollback.rs @@ -3,9 +3,9 @@ use std::path::PathBuf; use anyhow::{Context, Error}; use log::{debug, info, trace, warn}; -use osutils::{block_devices, lsblk, veritysetup}; +use osutils::{block_devices, efivar, lsblk, veritysetup}; use trident_api::{ - constants::internal_params::VIRTDEPLOY_BOOT_ORDER_WORKAROUND, + constants::internal_params::{ENABLE_UKI_SUPPORT, VIRTDEPLOY_BOOT_ORDER_WORKAROUND}, error::{InternalError, ReportError, ServicingError, TridentError, TridentResultExt}, status::{AbVolumeSelection, ServicingState, ServicingType}, BlockDeviceId, @@ -67,6 +67,11 @@ pub fn validate_boot(datastore: &mut DataStore) -> Result<(), TridentError> { bootentries::persist_boot_order() .message("Failed to persist boot order after reboot")?; } + + if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + efivar::set_default_to_current() + .message("Failed to set default boot entry to current")?; + } } else if datastore.host_status().servicing_state == ServicingState::CleanInstallStaged || datastore.host_status().servicing_state == ServicingState::CleanInstallFinalized { diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index c5610228a..8929e62ba 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -395,6 +395,9 @@ pub enum ServicingError { #[error("Failed to enter chroot")] EnterChroot, + #[error("Failed to enumerate UKIs")] + EnumerateUkis, + #[error("Failed to exit chroot")] ExitChroot, @@ -488,6 +491,9 @@ pub enum ServicingError { #[error("Failed to do a read operation with efibootmgr")] ReadEfibootmgr, + #[error("Failed to read EFI variable '{name}'")] + ReadEfiVariable { name: String }, + #[error("Failed to read current system hostname from {path}")] ReadHostname { path: String }, @@ -528,6 +534,9 @@ pub enum ServicingError { #[error("Failed to run post-provision script '{script_name}'")] RunPostProvisionScript { script_name: String }, + #[error("Failed to set EFI variable '{name}'")] + SetEfiVariable { name: String }, + #[error("Failed to set permissions on temporary recovery key file '{key_file}'")] SetRecoveryKeyFilePermissions { key_file: String }, @@ -537,6 +546,9 @@ pub enum ServicingError { #[error("Failed to start network")] StartNetwork, + #[error("Failed to update UKI")] + UpdateUki, + #[error( "Volume {active_volume} is active but active volume in Host Status is set to \ {hs_active_volume}" From 150a3f4873606ad10cd1a73651e08ee24ca46e44 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 3 Jun 2025 01:44:00 +0000 Subject: [PATCH 48/99] Merged PR 23335: engineering: SFI: add CODEOWNERS for github MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This will be needed for github.com. ---- #### AI description (iteration 1) #### PR Classification This pull request is a configuration enhancement that adds code ownership controls. #### PR Summary The pull request introduces a new `/.github/CODEOWNERS` file to enforce that changes receive approvals from the designated team, aligning with SFI requirements. - `/.github/CODEOWNERS`: Newly added file that assigns all changes to `@microsoft/azurelinux-trident`. Related work items: #12389 --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..3b4a67a60 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# For any changes, ensure owners approvals +* @microsoft/azurelinux-trident From 38e91cf1d9a74b8b90cfaa1dbf2213decb8ef872 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Tue, 3 Jun 2025 22:18:27 +0000 Subject: [PATCH 49/99] Merged PR 22856: engineering: Create Trident SELinux policy module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description SELinux policy domain for Trident. This PR replaces the SELinux policy used in the following VM host tests: # 🤔 Rationale Why is this PR needed? Trident's current approach creates a series of allowances in [trident-selinuxpolicies.cil](https://dev.azure.com/mariner-org/ECF/_git/trident?path=/trident-selinuxpolicies.cil). However, this means that the child processes of Trident run in an unconfined domain (`initrc_t`). This is problematic because Trident has elevated permissions on the system that are not clearly expressed to customers. In addition, some of the permissions given in trident-selinuxpolicies.cil may unintentially lead to privilege escalation. Instead, we want Trident to run in a confined setting which can be achieved by creating Trident's own domain, `trident_t`. # 📌 Follow-up Tasks * Ensure that user scripts run in unconfined domain #12388 * Create addendum for container Trident: #11257 * SELinux and verity #12022 # đŸ—’ī¸ Notes Interfaces definitions can be found in: https://github.com/SELinuxProject/refpolicy/tree/main/policy/modules Current policy succeeds so far with `combined`, `base`, `raid-small`, `verity`, and `encrypted-partition` configs with SELinux enforcing. Passing e2e test (including encryption tests): https://dev.azure.com/mariner-org/ECF/_build/results?buildId=825978&view=results Related work items: #12023, #12186 --- Dockerfile.azl3 | 6 +- Dockerfile.full | 6 +- e2e_tests/encryption_test.py | 9 + e2e_tests/target-configurations.yaml | 3 + selinux-policy-trident/trident.fc | 3 + selinux-policy-trident/trident.if | 163 +++++ selinux-policy-trident/trident.te | 782 +++++++++++++++++++++ src/lib.rs | 8 +- trident-mos/files/download-trident.service | 1 + trident-mos/iso.yaml | 12 +- trident-mos/post-install.sh | 5 + trident-selinuxpolicies.cil | 140 ---- trident.spec | 71 +- 13 files changed, 1044 insertions(+), 165 deletions(-) create mode 100644 selinux-policy-trident/trident.fc create mode 100644 selinux-policy-trident/trident.if create mode 100644 selinux-policy-trident/trident.te delete mode 100644 trident-selinuxpolicies.cil diff --git a/Dockerfile.azl3 b/Dockerfile.azl3 index ec6f476bc..a43dc894d 100644 --- a/Dockerfile.azl3 +++ b/Dockerfile.azl3 @@ -1,6 +1,6 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed selinux-policy-devel WORKDIR /work @@ -8,7 +8,9 @@ COPY trident.spec . COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te +COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc +COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if ARG TRIDENT_VERSION=dev-build ARG RPM_VER=0.1.0 diff --git a/Dockerfile.full b/Dockerfile.full index 1969c0eb1..20d4d581a 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -1,13 +1,15 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential selinux-policy-devel WORKDIR /work COPY trident.spec . COPY systemd ./systemd COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te +COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc +COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if COPY .cargo/config.toml ./.cargo/config.toml COPY build.rs . diff --git a/e2e_tests/encryption_test.py b/e2e_tests/encryption_test.py index 9df30db4a..09c56b44b 100644 --- a/e2e_tests/encryption_test.py +++ b/e2e_tests/encryption_test.py @@ -442,9 +442,18 @@ def check_crypsetup_luks_dump(conn: fabric.Connection, cryptDevPath: str) -> Non } } """ + # Running this command requires elevated privileges, so temporarily switch to Permissive mode + enforcing = conn.run("sudo getenforce").stdout.strip() == "Enforcing" + if enforcing: + sudo(conn, "setenforce 0") + stdout = sudo(conn, f"cryptsetup luksDump --dump-json-metadata {cryptDevPath}") dump = json.loads(stdout) + # Revert to Enforcing mode + if enforcing: + sudo(conn, "setenforce 1") + actual = dump["digests"]["0"]["type"] expected = "pbkdf2" assert ( diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 60a6b9cbe..d985f0106 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -126,6 +126,9 @@ virtualMachine: pullrequest: - base - combined + # TODO(10522): Can remove encrypted-partition from pipeline when combined/rerun runs in + # enforcing mode + - encrypted-partition - raid-mirrored - raid-resync-small - rerun diff --git a/selinux-policy-trident/trident.fc b/selinux-policy-trident/trident.fc new file mode 100644 index 000000000..1fe2cea99 --- /dev/null +++ b/selinux-policy-trident/trident.fc @@ -0,0 +1,3 @@ +/usr/bin/trident -- gen_context(system_u:object_r:trident_exec_t,s0) + +/var/lib/trident(/.*)? gen_context(system_u:object_r:trident_var_lib_t,s0) \ No newline at end of file diff --git a/selinux-policy-trident/trident.if b/selinux-policy-trident/trident.if new file mode 100644 index 000000000..0d5a5cf70 --- /dev/null +++ b/selinux-policy-trident/trident.if @@ -0,0 +1,163 @@ + +## policy for trident + +######################################## +## +## Execute trident_exec_t in the trident domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`trident_domtrans',` + gen_require(` + type trident_t, trident_exec_t; + ') + + corecmd_search_bin($1) + domtrans_pattern($1, trident_exec_t, trident_t) +') + +###################################### +## +## Execute trident in the caller domain. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_exec',` + gen_require(` + type trident_exec_t; + ') + + corecmd_search_bin($1) + can_exec($1, trident_exec_t) +') + +######################################## +## +## Search trident lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_search_lib',` + gen_require(` + type trident_var_lib_t; + ') + + allow $1 trident_var_lib_t:dir search_dir_perms; + files_search_var_lib($1) +') + +######################################## +## +## Read trident lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_read_lib_files',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + read_files_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + +######################################## +## +## Manage trident lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_manage_lib_files',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + manage_files_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + +######################################## +## +## Manage trident lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_manage_lib_dirs',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + manage_dirs_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + + +######################################## +## +## All of the rules required to administrate +## an trident environment +## +## +## +## Domain allowed access. +## +## +## +## +## Role allowed access. +## +## +## +# +interface(`trident_admin',` + gen_require(` + type trident_t; + type trident_var_lib_t; + ') + + allow $1 trident_t:process { signal_perms }; + ps_process_pattern($1, trident_t) + + optional_policy(` + tunable_policy(`allow_ptrace',` + allow $1 trident_t:process ptrace; + ') + ') + + optional_policy(` + tunable_policy(`deny_ptrace',` + allow $1 trident_t:process ptrace; + ') + ') + + files_search_var_lib($1) + admin_pattern($1, trident_var_lib_t) + optional_policy(` + systemd_passwd_agent_exec($1) + systemd_read_fifo_file_passwd_run($1) + ') +') diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te new file mode 100644 index 000000000..6aeaccb92 --- /dev/null +++ b/selinux-policy-trident/trident.te @@ -0,0 +1,782 @@ +policy_module(trident, 1.0.0) + +######################################## +# +# Declarations +# + +type trident_t; +type trident_exec_t; + +# Creates a domain for long running process (daemon), which is started by an init script. Domain is trident_t and entry_point is tridnet_exec_t. +init_daemon_domain(trident_t, trident_exec_t) + +# Create type to label files and directories in /var/lib that are specific to Trident, in particular the Trident datastore. +type trident_var_lib_t; +files_type(trident_var_lib_t) + +#################### +# +# Trident policy +# +require { + type admin_passwd_exec_t; + type anacron_exec_t; + type audisp_remote_exec_t; + type audit_spool_t; + type auditctl_exec_t; + type auditd_exec_t; + type auditd_unit_t; + type bluetooth_unit_t; + type boot_t; + type bootloader_t; + type cgroup_t; + type chfn_exec_t; + type chkpwd_exec_t; + type chronyc_exec_t; + type chronyd_unit_t; + type chronyd_var_lib_t; + type chronyd_var_log_t; + type cloud_init_exec_t; + type cloud_init_state_t; + type cloud_init_t; + type colord_var_lib_t; + type container_unit_t; + type crack_db_t; + type crack_exec_t; + type cron_spool_t; + type crond_unit_t; + type dbusd_unit_t; + type debugfs_t; + type default_t; + type device_t; + type devpts_t; + type dhcpc_exec_t; + type dhcpc_state_t; + type dhcpd_unit_t; + type dmesg_exec_t; + type dosfs_t; + type efivarfs_t; + type etc_runtime_t; + type fs_t; + type fsadm_exec_t; + type fsadm_t; + type getty_exec_t; + type gpg_agent_exec_t; + type gpg_pinentry_exec_t; + type gpg_secret_t; + type groupadd_exec_t; + type home_root_t; + type init_t; + type init_exec_t; + type init_runtime_t; + type init_var_lib_t; + type iptables_unit_t; + type kernel_t; + type kmod_exec_t; + type krb5kdc_exec_t; + type ld_so_t; + type ldconfig_cache_t; + type lib_t; + type loadkeys_exec_t; + type loadkeys_t; + type load_policy_t; + type locale_t; + type locate_exec_t; + type logrotate_unit_t; + type logrotate_var_lib_t; + type lost_found_t; + type lvm_metadata_t; + type lvm_t; + type lvm_unit_t; + type mail_spool_t; + type mdadm_exec_t; + type mdadm_unit_t; + type memory_pressure_t; + type mnt_t; + type modules_conf_t; + type modules_dep_t; + type modules_object_t; + type mount_t; + type mount_exec_t; + type mptctl_device_t; + type net_conf_t; + type ntpd_exec_t; + type ntpd_unit_t; + type oddjob_mkhomedir_exec_t; + type power_unit_t; + type proc_t; + type proc_kcore_t; + type proc_mdstat_t; + type proc_net_t; + type root_t; + type rpm_t; + type rpm_script_t; + type rpm_unit_t; + type security_t; + type setfiles_t; + type semanage_t; + type semanage_exec_t; + type shadow_lock_t; + type shadow_t; + type shell_exec_t; + type ssh_agent_exec_t; + type ssh_exec_t; + type ssh_home_t; + type sshd_t; + type sshd_keygen_unit_t; + type sshd_unit_t; + type sudo_exec_t; + type sulogin_exec_t; + type sysctl_fs_t; + type sysctl_kernel_t; + type sysctl_vm_overcommit_t; + type sysctl_vm_t; + type sysfs_t; + type syslog_conf_t; + type syslogd_exec_t; + type syslogd_unit_t; + type systemd_analyze_exec_t; + type systemd_backlight_exec_t; + type systemd_backlight_unit_t; + type systemd_binfmt_exec_t; + type systemd_binfmt_unit_t; + type systemd_cgroups_exec_t; + type systemd_cgtop_exec_t; + type systemd_coredump_exec_t; + type systemd_coredump_var_lib_t; + type systemd_factory_conf_t; + type systemd_generator_t; + type systemd_generator_exec_t; + type systemd_homed_exec_t; + type systemd_homework_exec_t; + type systemd_hostnamed_exec_t; + type systemd_hw_exec_t; + type systemd_hwdb_t; + type systemd_journalctl_exec_t; + type systemd_locale_exec_t; + type systemd_logind_exec_t; + type systemd_machine_id_setup_exec_t; + type systemd_modules_load_exec_t; + type systemd_networkd_exec_t; + type systemd_notify_exec_t; + type systemd_passwd_agent_exec_t; + type systemd_pcrphase_exec_t; + type systemd_pstore_exec_t; + type systemd_resolved_exec_t; + type systemd_rfkill_exec_t; + type systemd_rfkill_unit_t; + type systemd_sessions_exec_t; + type systemd_socket_proxyd_exec_t; + type systemd_stdio_bridge_exec_t; + type systemd_sysctl_exec_t; + type systemd_sysusers_exec_t; + type systemd_tmpfiles_exec_t; + type systemd_unit_t; + type systemd_update_done_exec_t; + type systemd_user_manager_unit_t; + type systemd_user_runtime_dir_exec_t; + type systemd_userdbd_exec_t; + type systemd_userdbd_unit_t; + type tmp_t; + type tmpfs_t; + type trident_t; + type udev_exec_t; + type udev_t; + type udevadm_t; + type unlabeled_t; + type unreserved_port_t; + type updpwd_exec_t; + type useradd_exec_t; + type usr_t; + type uuidd_exec_t; + type unconfined_t; + type var_run_t; + + # Define object classes that SELinux can protect + class dir { add_name create getattr open read remove_name search write }; + class file { create getattr ioctl lock open read rename setattr relabelto unlink write map execute execute_no_trans }; + class chr_file getattr; + class filesystem getattr; + class lnk_file read; + class netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; + class process { getsched noatsecure rlimitinh siginh }; + class capability sys_admin; + class security read_policy; + class tcp_socket { connect create getattr getopt name_connect read setopt shutdown write }; + class udp_socket { create ioctl }; + + attribute can_change_object_identity; + attribute domain; + + role unconfined_r; + role system_r; +} + +# Allow Trident to change (relabel) the security context of a file or directory +typeattribute trident_t can_change_object_identity; + +# Defines transition from trident_t to fsadm_t domain when Trident executes fsadm tool (i.e. mkfs) +type_transition trident_t fsadm_exec_t:process fsadm_t; + +# Allow transition between unconfined_t and trident_t domains; necessary for an interactive run +optional_policy(` + unconfined_run_to(trident_t, trident_exec_t) +') + +#============= trident_t ============== +# Gives trident_t the following elevated privileges: +# dac_override and dac_read_search - allow access files and directories without necessary DAC permissions +# sys_ptrace - allow trident_t to trace or debug other processes +# sys_rawio - allow trident_t to perform I/O operations directly on hardware devices +allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; + +allow trident_t self:alg_socket { accept bind create read write }; +allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice }; +allow trident_t self:fifo_file manage_fifo_file_perms; +allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; +allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; +allow trident_t self:netlink_route_socket { bind create getattr nlmsg_read read write }; +allow trident_t self:process { getsched setsched getcap setpgid signull getattr signal }; +allow trident_t self:tcp_socket { connect create getattr getopt read setopt shutdown write }; +allow trident_t self:unix_dgram_socket { connect create }; +allow trident_t self:key { search write }; +allow trident_t self:sem { associate create destroy read unix_read unix_write write }; + +# Ensure any new files, directories, or symbolic links created by trident_t are automatically labeled with type trident_var_lib_t +files_var_lib_filetrans(trident_t, trident_var_lib_t, { dir file lnk_file }) + +# Allow trident_t domain to interact with files and directories labeled as trident_var_lib_t +# Necessary so Trident can interact with the datastore at /var/lib/trident +allow trident_t trident_var_lib_t:dir { getattr search read write add_name create remove_name open mounton relabelto }; +allow trident_t trident_var_lib_t:file { getattr setattr create open read write unlink lock }; + +# Allow Trident to relabel its executable +allow trident_t trident_exec_t:file relabelto; + +allow trident_t audit_spool_t:dir { getattr open read }; +allow trident_t auditctl_exec_t:file getattr; +allow trident_t auditd_exec_t:file getattr; +allow trident_t auditd_unit_t:file getattr; +allow trident_t admin_passwd_exec_t:file getattr; +allow trident_t anacron_exec_t:file getattr; +allow trident_t audisp_remote_exec_t:file getattr; +allow trident_t bluetooth_unit_t:file getattr; +allow trident_t boot_t:dir mounton; +allow trident_t cgroup_t:filesystem getattr; +allow trident_t chfn_exec_t:file getattr; +allow trident_t chkpwd_exec_t:file getattr; +allow trident_t chronyc_exec_t:file getattr; +allow trident_t chronyd_unit_t:file getattr; +allow trident_t chronyd_var_lib_t:dir { getattr open read }; +allow trident_t chronyd_var_log_t:dir { getattr open read }; +allow trident_t cloud_init_exec_t:file getattr; +allow trident_t cloud_init_state_t:dir list_dir_perms; +allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; +allow trident_t cloud_init_state_t:file getattr; +allow trident_t colord_var_lib_t:dir { getattr open read }; +allow trident_t container_unit_t:file getattr; +allow trident_t crack_db_t:dir { getattr open search read }; +allow trident_t crack_db_t:file getattr; +allow trident_t crack_db_t:lnk_file getattr; +allow trident_t crack_exec_t:file getattr; +allow trident_t cron_spool_t:dir read; +allow trident_t crond_unit_t:file getattr; +allow trident_t dbusd_unit_t:file getattr; +allow trident_t debugfs_t:filesystem getattr; +allow trident_t debugfs_t:dir search; +allow trident_t default_t:dir { getattr open read relabelto search }; +allow trident_t default_t:file relabelto; +allow trident_t device_t:filesystem { getattr mount unmount }; +allow trident_t devpts_t:chr_file { read write ioctl getattr }; +allow trident_t dhcpc_exec_t:file getattr; +allow trident_t dhcpc_state_t:dir { getattr open read }; +allow trident_t dhcpd_unit_t:file getattr; +allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t dosfs_t:filesystem { getattr mount unmount }; +allow trident_t efivarfs_t:filesystem getattr; +allow trident_t etc_runtime_t:file { getattr open read relabelto setattr unlink }; +allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; +allow trident_t fs_t:filesystem { mount unmount }; +allow trident_t fsadm_t:process { siginh rlimitinh noatsecure transition }; +allow trident_t fsadm_t:fd use; +allow trident_t fsadm_t:fifo_file { read write }; +allow trident_t fsadm_exec_t:file { getattr open read execute execute_no_trans relabelto setattr unlink write }; +allow trident_t getty_exec_t:file getattr; +allow trident_t gpg_pinentry_exec_t:file getattr; +allow trident_t gpg_secret_t:file getattr; +allow trident_t groupadd_exec_t:file getattr; +allow trident_t home_root_t:dir { mounton read relabelto add_name create relabelfrom setattr write }; +allow trident_t home_root_t:file { create getattr ioctl open relabelfrom setattr write }; +allow trident_t init_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t init_t:system reboot; +allow trident_t init_t:key search; +allow trident_t init_runtime_t:dir { add_name write }; +allow trident_t init_runtime_t:file { create getattr open write }; +allow trident_t init_t:unix_stream_socket connectto; +allow trident_t init_var_lib_t:dir { getattr open read search }; +allow trident_t iptables_unit_t:file getattr; +allow trident_t kernel_t:process setsched; +allow trident_t kernel_t:system { module_request ipc_info }; +allow trident_t kmod_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t krb5kdc_exec_t:file getattr; +allow trident_t ld_so_t:file { execute_no_trans relabelto setattr unlink write }; +allow trident_t ldconfig_cache_t:dir { getattr open read search }; +allow trident_t ldconfig_cache_t:file getattr; +allow trident_t lib_t:file { create relabelto rename setattr unlink write }; +allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t locale_t:dir { add_name relabelto remove_name rmdir setattr write }; +allow trident_t locale_t:file { link relabelto rename setattr unlink write }; +allow trident_t locate_exec_t:file getattr; +allow trident_t logrotate_unit_t:file getattr; +allow trident_t logrotate_var_lib_t:dir { getattr open read }; +allow trident_t lost_found_t:dir { getattr open read relabelto }; +allow trident_t lvm_metadata_t:dir { getattr open read }; +allow trident_t lvm_unit_t:file getattr; +allow trident_t mail_spool_t:dir list_dir_perms; +allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; +allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t memory_pressure_t:file { read open getattr setattr }; +allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; +allow trident_t modules_conf_t:file { relabelto setattr unlink }; +allow trident_t modules_dep_t:file { getattr ioctl map open read relabelto setattr unlink }; +allow trident_t modules_object_t:file { getattr open read relabelto setattr unlink }; +allow trident_t mount_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t mptctl_device_t:chr_file getattr; +allow trident_t net_conf_t:file { relabelto setattr unlink }; +allow trident_t ntpd_exec_t:file { execute getattr }; +allow trident_t ntpd_unit_t:file getattr; +allow trident_t oddjob_mkhomedir_exec_t:file getattr; +allow trident_t power_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t proc_t:dir read; +allow trident_t proc_t:file { getattr open read ioctl }; +allow trident_t proc_t:filesystem { getattr mount unmount }; +allow trident_t proc_kcore_t:file getattr; +allow trident_t proc_mdstat_t:file { getattr open read }; +allow trident_t proc_net_t:file { open read }; +allow trident_t root_t:dir { add_name create mounton write }; +allow trident_t root_t:file { create getattr map open read relabelfrom write }; +allow trident_t rpm_unit_t:file getattr; +allow trident_t security_t:file { map write }; +allow trident_t security_t:filesystem getattr; +allow trident_t semanage_exec_t:file { execute execute_no_trans entrypoint getattr ioctl open read }; +allow trident_t setfiles_exec_t:file entrypoint; +allow trident_t shadow_t:file { getattr open read relabelto setattr unlink write }; +allow trident_t shadow_lock_t:file { create getattr lock open read unlink write }; +allow trident_t shell_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t ssh_agent_exec_t:file getattr; +allow trident_t ssh_exec_t:file { execute getattr }; +allow trident_t ssh_home_t:dir { setattr relabelto }; +allow trident_t ssh_home_t:file relabelto; +allow trident_t sshd_t:fd use; +allow trident_t sshd_t:fifo_file { read write getattr }; # Allow Trident to read/write stdin/stdout/stderr +allow trident_t sshd_keygen_unit_t:file getattr; +allow trident_t sshd_unit_t:file getattr; +allow trident_t sulogin_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t sysctl_fs_t:dir search; +allow trident_t sysctl_fs_t:file { getattr ioctl open read }; +allow trident_t sysctl_kernel_t:dir search; +allow trident_t sysctl_kernel_t:file { getattr ioctl open read }; +allow trident_t sysctl_vm_overcommit_t:file { open read }; +allow trident_t sysctl_vm_t:dir search; +allow trident_t sysfs_t:filesystem { mount unmount }; +allow trident_t syslog_conf_t:file { getattr open read relabelto setattr unlink }; +allow trident_t syslogd_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t syslogd_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t systemd_analyze_exec_t:file getattr; +allow trident_t systemd_backlight_exec_t:file getattr; +allow trident_t systemd_backlight_unit_t:file getattr; +allow trident_t systemd_binfmt_exec_t:file getattr; +allow trident_t systemd_binfmt_unit_t:file getattr; +allow trident_t systemd_cgroups_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_cgtop_exec_t:file getattr; +allow trident_t systemd_coredump_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_coredump_var_lib_t:dir { getattr open read }; +allow trident_t systemd_factory_conf_t:dir { getattr open read search }; +allow trident_t systemd_factory_conf_t:file getattr; +allow trident_t systemd_generator_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_homed_exec_t:file getattr; +allow trident_t systemd_homework_exec_t:file getattr; +allow trident_t systemd_hostnamed_exec_t:file { execute getattr }; +allow trident_t systemd_hw_exec_t:file getattr; +allow trident_t systemd_hwdb_t:file { getattr open read relabelto setattr unlink }; +allow trident_t systemd_journal_t:file relabelfrom_file_perms; +allow trident_t systemd_journalctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_locale_exec_t:file getattr; +allow trident_t systemd_logind_exec_t:file getattr; +allow trident_t systemd_machine_id_setup_exec_t:file getattr; +allow trident_t systemd_modules_load_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_networkd_exec_t:file { execute getattr }; +allow trident_t systemd_notify_exec_t:file getattr; +allow trident_t systemd_passwd_agent_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_pcrphase_exec_t:file { execute getattr }; +allow trident_t systemd_pstore_exec_t:file { execute getattr }; +allow trident_t systemd_resolved_exec_t:file { execute getattr }; +allow trident_t systemd_rfkill_exec_t:file getattr; +allow trident_t systemd_rfkill_unit_t:file getattr; +allow trident_t systemd_sessions_exec_t:file getattr; +allow trident_t systemd_socket_proxyd_exec_t:file getattr; +allow trident_t systemd_stdio_bridge_exec_t:file getattr; +allow trident_t systemd_sysctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_sysusers_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_tmpfiles_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_unit_t:dir read; +allow trident_t systemd_unit_t:file { getattr ioctl link open read relabelto rename setattr unlink write }; +allow trident_t systemd_unit_t:lnk_file { getattr read }; +allow trident_t systemd_update_done_exec_t:file getattr; +allow trident_t systemd_user_manager_unit_t:file getattr; +allow trident_t systemd_user_runtime_dir_exec_t:file getattr; +allow trident_t systemd_userdbd_exec_t:file getattr; +allow trident_t systemd_userdbd_unit_t:file getattr; +allow trident_t tmp_t:chr_file { create getattr unlink }; +allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write }; +allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write }; +allow trident_t tmp_t:lnk_file { create getattr read rename unlink }; +allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map }; +allow trident_t tmpfs_t:filesystem { getattr mount unmount }; +allow trident_t udev_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t udev_runtime_t:dir { read watch }; +allow trident_t unlabeled_t:dir { add_name getattr open read remove_name search write mounton relabelfrom }; +allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink write ioctl }; +allow trident_t unreserved_port_t:tcp_socket name_connect; +allow trident_t updpwd_exec_t:file getattr; +allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; +allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto }; +allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; +allow trident_t uuidd_exec_t:file getattr; +allow trident_t var_run_t:dir { add_name create remove_name write }; +allow trident_t var_run_t:file { create getattr lock open read unlink write }; + +#============= interfaces ============== +########################################### +# Authentication and User Management +########################################### +auth_create_faillog_files(trident_t) +auth_exec_pam(trident_t) +auth_login_entry_type(trident_t) +auth_read_lastlog(trident_t) +auth_read_login_records(trident_t) +domain_entry_file(trident_t, gpg_agent_exec_t) +gpg_entry_type(trident_t) +gpg_list_user_secrets(trident_t) +su_exec(trident_t) +usermanage_check_exec_passwd(trident_t) +userdom_list_user_home_dirs(trident_t) +userdom_read_user_home_content_files(trident_t) +userdom_search_user_runtime_root(trident_t) +userdom_relabel_generic_user_home_files(trident_t) +userdom_relabelto_user_home_dirs(trident_t) + +########################################### +# System Services and Daemons +########################################### +bootloader_exec(trident_t) +chronyd_exec(trident_t) +chronyd_read_config(trident_t) +chronyd_read_key_files(trident_t) +clock_exec(trident_t) +create_files_pattern(trident_t, cgroup_t, cgroup_t) +cron_exec(trident_t) +cron_exec_crontab(trident_t) +cron_read_system_spool(trident_t) +dbus_exec(trident_t) +dbus_manage_lib_files(trident_t) +dbus_read_config(trident_t) +dbus_read_lib_files(trident_t) +dbus_list_system_bus_runtime(trident_t) +dbus_system_bus_client(trident_t) +domain_read_all_domains_state(trident_t) +hostname_exec(trident_t) +init_domtrans(trident_t) +init_rw_stream_sockets(trident_t) +logrotate_exec(trident_t) +ps_process_pattern(trident_t, cloud_init_t) # equivalent to cloud_init_read_state(trident_t) +ssh_domtrans(trident_t) +ssh_domtrans_keygen(trident_t) +ssh_manage_home_files(trident_t) +systemd_list_journal_dirs(trident_t) +systemd_read_networkd_units(trident_t) +systemd_read_user_runtime_units_files(trident_t) +systemd_dbus_chat_logind(trident_t) +systemd_read_user_unit_files(trident_t) + +########################################### +# File System Operations +########################################### +files_list_kernel_modules(trident_t) +files_list_spool(trident_t) +files_list_var(trident_t) +files_manage_boot_files(trident_t) +files_manage_etc_dirs(trident_t) +files_mounton_runtime_dirs(trident_t) +files_read_etc_files(trident_t) +files_read_default_symlinks(trident_t) +files_read_kernel_symbol_table(trident_t) +files_read_usr_src_files(trident_t) +files_read_usr_symlinks(trident_t) +files_read_var_lib_files(trident_t) +files_search_etc(trident_t) +files_search_kernel_modules(trident_t) +files_search_locks(trident_t) +files_search_spool(trident_t) +files_search_var_lib(trident_t) +files_rw_etc_runtime_files(trident_t) +fstools_domtrans(trident_t) +fstools_relabelto_entry_files(trident_t) +fs_getattr_hugetlbfs(trident_t) +fs_getattr_iso9660_files(trident_t) +fs_getattr_iso9660_fs(trident_t) +fs_getattr_pstorefs(trident_t) +fs_getattr_tracefs(trident_t) +fs_getattr_xattr_fs(trident_t) +fs_list_hugetlbfs(trident_t) +fs_manage_dos_dirs(trident_t) +fs_manage_dos_files(trident_t) +fs_manage_tmpfs_dirs(trident_t) +fs_manage_tmpfs_symlinks(trident_t) +fs_read_iso9660_files(trident_t) +fs_watch_memory_pressure(trident_t) +mount_list_runtime(trident_t) + +########################################### +# Network Management +########################################### +iptables_exec(trident_t) +iptables_read_config(trident_t) +iptables_status(trident_t) +sysnet_exec_ifconfig(trident_t) +sysnet_read_config(trident_t) +sysnet_read_dhcp_config(trident_t) +sysnet_relabel_config(trident_t) +sysnet_write_config(trident_t) + +########################################### +# Storage Management +########################################### +dev_rw_loop_control(trident_t) +dev_rw_lvm_control(trident_t) +lvm_exec(trident_t) +lvm_read_config(trident_t) +manage_files_pattern(trident_t, mount_runtime_t, mount_runtime_t) +storage_getattr_fuse_dev(trident_t) +storage_getattr_scsi_generic_dev(trident_t) +storage_raw_read_removable_device(trident_t) +storage_raw_read_fixed_disk(trident_t) +storage_raw_write_fixed_disk(trident_t) + +########################################### +# SELinux Management +########################################### +corecmd_relabel_bin_files(trident_t) +files_relabel_etc_files(trident_t) +files_relabel_kernel_modules(trident_t) +files_relabelto_etc_runtime_files(trident_t) +files_relabelto_usr_files(trident_t) +libs_relabel_ld_so(trident_t) +libs_relabelto_lib_files(trident_t) +miscfiles_relabel_localization(trident_t) +relabel_files_pattern(trident_t, udev_rules_t, udev_rules_t) +selinux_load_policy(trident_t) +seutil_exec_checkpolicy(trident_t) +seutil_exec_loadpolicy(trident_t) +seutil_exec_setfiles(trident_t) +seutil_get_semanage_read_lock(trident_t) +seutil_get_semanage_trans_lock(trident_t) +seutil_manage_bin_policy(trident_t) +seutil_manage_config(trident_t) +seutil_manage_config_dirs(trident_t) +seutil_manage_file_contexts(trident_t) +seutil_manage_module_store(trident_t) +seutil_read_default_contexts(trident_t) +seutil_read_bin_policy(trident_t) +udev_relabel_rules_files(trident_t) + +########################################### +# Package Management +########################################### +rpm_delete_db(trident_t) +rpm_exec(trident_t) +rpm_read_cache(trident_t) +rpm_read_db(trident_t) + +########################################### +# Device Management +########################################### +corenet_getattr_ppp_dev(trident_t) +corenet_read_tun_tap_dev(trident_t) +dev_getattr_acpi_bios_dev(trident_t) +dev_getattr_autofs_dev(trident_t) +dev_getattr_framebuffer_dev(trident_t) +dev_getattr_generic_usb_dev(trident_t) +dev_getattr_mouse_dev(trident_t) +dev_getattr_pmqos_dev(trident_t) +dev_getattr_sysfs(trident_t) +dev_getattr_xserver_misc_dev(trident_t) +dev_list_sysfs(trident_t) +dev_manage_generic_blk_files(trident_t) +dev_manage_generic_dirs(trident_t) +dev_mounton(trident_t) +dev_mounton_sysfs_dirs(trident_t) +dev_read_input_dev(trident_t) +dev_read_kmsg(trident_t) +dev_read_rand(trident_t) +dev_read_raw_memory(trident_t) +dev_read_realtime_clock(trident_t) +dev_read_sysfs(trident_t) +dev_read_watchdog(trident_t) +dev_read_urand(trident_t) +dev_relabelfrom_generic_chr_files(trident_t) +dev_relabelfrom_vfio_dev(trident_t) +dev_rw_nvram(trident_t) +dev_rw_tpm(trident_t) +dev_rw_vhost(trident_t) +dev_search_sysfs(trident_t) +dev_write_sysfs(trident_t) +dev_write_urand(trident_t) +term_getattr_ptmx(trident_t) +term_use_virtio_console(trident_t) +term_getattr_pty_fs(trident_t) +udev_manage_rules_files(trident_t) +udev_read_runtime_files(trident_t) +udev_read_state(trident_t) + +########################################### +# System Command Execution +########################################### +can_exec(trident_t, sudo_exec_t) +corecmd_manage_bin_files(trident_t) +corecmd_bin_entry_type(trident_t) +corecmd_shell_entry_type(trident_t) +corecmd_search_bin(trident_t) +corecmd_exec_bin(trident_t) +corecmd_search_bin(trident_t) +kerberos_exec_kadmind(trident_t) +kerberos_read_config(trident_t) +libs_exec_ldconfig(trident_t) +libs_manage_lib_dirs(trident_t) +modutils_read_module_config(trident_t) +tcsd_manage_lib_dirs(trident_t) +uuidd_manage_lib_dirs(trident_t) + +########################################### +# Logging and Monitoring +########################################### +logging_read_audit_config(trident_t) +logging_read_audit_log(trident_t) +logging_search_logs(trident_t) +logging_manage_generic_log_dirs(trident_t) +logging_manage_generic_logs(trident_t) + +########################################### +# Miscellaneous +########################################### +miscfiles_read_generic_tls_privkey(trident_t) +miscfiles_read_man_pages(trident_t) +miscfiles_read_localization(trident_t) +miscfiles_read_generic_certs(trident_t) +xserver_read_xkb_libs(trident_t) + + +#################### +# +# Additional permissions given to external domains +# +#============= bootloader_t ============== +# List the contents of generic tmpfs directories; required for RAID +fs_list_tmpfs(bootloader_t) + +#============= cloud_init_t ============== +allow cloud_init_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow cloud_init_t unlabeled_t:file { create getattr ioctl open read rename write }; +allow cloud_init_t usr_t:dir { add_name create remove_name write }; + +files_exec_usr_files(cloud_init_t) +files_manage_usr_files(cloud_init_t) + +#============= fsadm_t ============== +role unconfined_r types fsadm_t; + +# Get the attributes of efivarfs filesystems +allow fsadm_t efivarfs_t:filesystem getattr; +allow fsadm_t trident_t:process { siginh rlimitinh noatsecure transition sigchld }; +allow fsadm_t fixed_disk_device_t:blk_file { open read write getattr ioctl }; + +# Create, read, write, and delete files on a efivarfs filesystem +fs_manage_efivarfs_files(fsadm_t) +fs_manage_tmpfs_dirs(fsadm_t) +fs_manage_tmpfs_files(fsadm_t) + +#============= loadkeys_t ============== +files_read_default_symlinks(loadkeys_t) +fs_search_tmpfs(loadkeys_t) + +#============= lvm_t ============== +# Allow lvm_t access to semaphores of the initrc_t type; this is necessary for Trident to create encrypted volumes +allow lvm_t trident_t:sem { associate read unix_read unix_write write }; + +#============= mount_t ============= +allow mount_t trident_var_lib_t:dir mounton; + +#============= rpm_t ============== +allow rpm_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow rpm_t unlabeled_t:file { create getattr ioctl open read rename write }; +allow rpm_t rpm_script_t:process { noatsecure rlimitinh siginh }; + +#============= rpm_script_t ============== +# Allow RPM scripts to read SELinux policy (we currently apply trident.pp as a module in the Trident spec) +allow rpm_script_t security_t:security read_policy; + +allow rpm_script_t kernel_t:fd use; +allow rpm_script_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow rpm_script_t unlabeled_t:file { create getattr ioctl open read rename write }; + +#============= semanage_t ============== +allow semanage_t proc_t:filesystem getattr; +allow semanage_t load_policy_t:process { noatsecure rlimitinh siginh }; +allow semanage_t setfiles_t:process { noatsecure rlimitinh siginh }; + +libs_manage_lib_dirs(semanage_t) +libs_manage_lib_files(semanage_t) + +#============= setfiles_t ============== +allow setfiles_t proc_t:filesystem getattr; + +#============= systemd_generator_t ============== +allow systemd_generator_t home_root_t:dir read; + +#============= udev_t ============== +allow udev_t cloud_init_t:fd use; +allow udev_t cloud_init_t:fifo_file { append write getattr }; +allow udev_t lvm_t:process { noatsecure rlimitinh siginh }; + +files_read_generic_tmp_files(udev_t) + +#============= udevadm_t ============== +allow udevadm_t cgroup_t:filesystem getattr; +allow udevadm_t self:netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; +allow udevadm_t self:udp_socket { create ioctl }; +allow udevadm_t systemd_hwdb_t:file { getattr map open read }; +allow udevadm_t kernel_t:fd use; + +# Allow udevadm to search kernel modules, read module configurations and dependencies +files_search_kernel_modules(udevadm_t) +modutils_read_module_config(udevadm_t) +modutils_read_module_deps(udevadm_t) + +# Read system network configuration files +sysnet_read_config(udevadm_t) + +# List systemd networkd runtime files +systemd_list_networkd_runtime(udevadm_t) + +# Manage udev rules, runtime directories, and files +udev_manage_rules_files(udevadm_t) +udev_manage_runtime_dirs(udevadm_t) +udev_manage_runtime_files(udevadm_t) + +# Read symbolic links in cgroup directories and search sysfs +read_lnk_files_pattern(udevadm_t, cgroup_t, cgroup_t) +dev_search_sysfs(udevadm_t) + +#============= unlabeled_t ============== +fs_associate_tmpfs(unlabeled_t) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 67722d6dc..e35e6977a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use std::{ use cli::GetKind; use engine::{bootentries, EngineContext}; -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use nix::unistd::Uid; use osutils::{block_devices, container, dependencies::Dependency}; @@ -182,6 +182,12 @@ impl Trident { info!("Running Trident in a container"); } + if let Ok(selinux_context) = fs::read_to_string("/proc/self/attr/current") { + trace!("selinux context: Trident is running in SELinux domain '{selinux_context}'"); + } else { + error!("selinux context: Failed to retrieve the SELinux context in which Trident is running"); + } + if !Uid::effective().is_root() { return Err(TridentError::new( ExecutionEnvironmentMisconfigurationError::CheckRootPrivileges, diff --git a/trident-mos/files/download-trident.service b/trident-mos/files/download-trident.service index b796e80cf..965963574 100644 --- a/trident-mos/files/download-trident.service +++ b/trident-mos/files/download-trident.service @@ -7,6 +7,7 @@ Before=trident-install.service ExecStart=/bin/bash /trident_cdrom/pre-trident-script.sh ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/trident -o /usr/bin/trident -f" ExecStart=chmod +x /usr/bin/trident +ExecStart=/sbin/restorecon -v /usr/bin/trident ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/osmodifier -o /usr/bin/osmodifier -f" ExecStart=/bin/chmod +x /usr/bin/osmodifier Type=oneshot diff --git a/trident-mos/iso.yaml b/trident-mos/iso.yaml index d936ce2b7..ba98d06ce 100644 --- a/trident-mos/iso.yaml +++ b/trident-mos/iso.yaml @@ -54,6 +54,9 @@ os: - tpm2-tools - veritysetup - vim + # Support for building SELinux module and debugging + - selinux-policy-devel + - setools-console additionalFiles: - source: files/getty@.service @@ -66,6 +69,12 @@ os: destination: /usr/lib/systemd/system/trident-install.service - source: files/download-trident.service destination: /usr/lib/systemd/system/download-trident.service + - source: ../selinux-policy-trident/trident.if + destination: /usr/share/selinux/packages/trident/trident.if + - source: ../selinux-policy-trident/trident.fc + destination: /usr/share/selinux/packages/trident/trident.fc + - source: ../selinux-policy-trident/trident.te + destination: /usr/share/selinux/packages/trident/trident.te services: enable: @@ -87,4 +96,5 @@ iso: - console=tty0 - console=ttyS0 - rd.luks=0 - - selinux=0 + - selinux=1 + - enforcing=0 diff --git a/trident-mos/post-install.sh b/trident-mos/post-install.sh index 74d7296bc..825008afb 100755 --- a/trident-mos/post-install.sh +++ b/trident-mos/post-install.sh @@ -6,3 +6,8 @@ if [ ! -d /etc/trident ]; then mkdir /etc/trident fi ln -s /trident_cdrom/trident-config.yaml /etc/trident/config.yaml + +# Compile and load Trident SELinux module (this is otherwise handled in trident.spec) +cd /usr/share/selinux/packages/trident +make -f /usr/share/selinux/devel/Makefile trident.pp +semodule -i trident.pp \ No newline at end of file diff --git a/trident-selinuxpolicies.cil b/trident-selinuxpolicies.cil deleted file mode 100644 index f15db87cd..000000000 --- a/trident-selinuxpolicies.cil +++ /dev/null @@ -1,140 +0,0 @@ -; Allow auditctl_t to manage auditd_etc_t files -(allow auditctl_t auditd_etc_t (file (map))) -(allow auditctl_t proc_t (filesystem (getattr))) - -; Allow chkpwd_t to access sysctl_kernel_t directories and files for password verification -(allow chkpwd_t proc_t (filesystem (getattr))) -(allow chkpwd_t sysctl_kernel_t (dir (search))) -(allow chkpwd_t sysctl_kernel_t (file (open read))) - -; Allow cloud_init_t to perform various operations for cloud instance initialization -; cloud-init -(allow cloud_init_t file_context_t (file (getattr open read map))) -(allow cloud_init_t gpg_secret_t (file (getattr))) -(allow cloud_init_t security_t (security (check_context))) -(allow cloud_init_t self (capability (net_admin ))) -(allow cloud_init_t selinux_config_t (file (getattr open read))) -(allow cloud_init_t sshd_key_t (file (relabelto))) -(allow cloud_init_t user_home_t (file (getattr))) - -; Allow fsadm_t to manage efivarfs_t files for filesystem administration -; efibootmgr -(allow fsadm_t efivarfs_t (dir (read))) -(allow fsadm_t efivarfs_t (file (getattr open read write))) -(allow fsadm_t efivarfs_t (filesystem (getattr))) - -; Load/Save OS Random Seed -(allow kernel_t self (capability2 (checkpoint_restore))) - -; Allow kmod_t to read iptables_runtime_t files for kernel module management -(allow kmod_t iptables_runtime_t (file (read))) - -; Allow lvm_t to perform various operations for logical volume management -; Disk Encryption -(allow lvm_t bpf_t (dir (search))) -(allow lvm_t cgroup_t (dir (search))) -(allow lvm_t cgroup_t (filesystem (getattr))) -(allow lvm_t init_t (key (search))) -(allow lvm_t initrc_runtime_t (dir (add_name open read write search))) -(allow lvm_t initrc_runtime_t (file (create open read write lock getattr))) -(allow lvm_t initrc_t (sem (associate read unix_read unix_write write))) -(allow lvm_t kernel_t (key (search))) -(allow lvm_t proc_t (filesystem (getattr))) -(allow lvm_t pstore_t (dir (search))) -(allow lvm_t self (capability (dac_read_search))) -(allow lvm_t systemd_passwd_runtime_t (dir (getattr search))) -(allow lvm_t tmpfs_t (filesystem (getattr))) -(allow lvm_t tpm_device_t (chr_file (read write open ioctl))) -(allow lvm_t user_home_dir_t (dir (search))) -(allow lvm_t var_run_t (dir (create))) - -; Allow mdadm_t to perform RAID operations using the mdadm utility -(allow mdadm_t debugfs_t (dir (search read write add_name remove_name getattr open))) -(allow mdadm_t device_t (lnk_file (create read write getattr open link rename setattr unlink))) -(allow mdadm_t event_device_t (chr_file (getattr))) -(allow mdadm_t lvm_control_t (chr_file (getattr))) -(allow mdadm_t nvram_device_t (chr_file (getattr))) -(allow mdadm_t udev_runtime_t (dir (search read write add_name remove_name getattr open))) -(allow mdadm_t vfio_device_t (chr_file (getattr))) -(allow mdadm_t vhost_device_t (chr_file (getattr))) - -(allow ntpd_t proc_t (file (write))) - -; Allow passwd_t to manage proc files for password updates -(allow passwd_t proc_t (filesystem (getattr))) - -; Allow semanage_t and setfiles_t to get attributes of the proc_t filesystem -(allow semanage_t proc_t (filesystem (getattr))) -(allow setfiles_t proc_t (filesystem (getattr))) - -; Allow ssh_keygen_t to manage files and directories for SSH key generation -(allow ssh_keygen_t security_t (filesystem (getattr))) -(allow ssh_keygen_t selinux_config_t (dir (search))) - -; Allow sshd_t to get attributes of the proc_t filesystem for SSH daemon -(allow sshd_t proc_t (filesystem (getattr))) -(allow sshd_t self (capability (net_admin))) - -; Allow syslogd_t to manage logging files and directories -(allow syslogd_t cgroup_t (dir (read))) -(allow syslogd_t proc_t (file (write read append getattr lock open))) -(allow syslogd_t systemd_journal_t (file (relabelfrom relabelto))) - -; Allow systemd_cgroups_t to manage proc_t filesystem -(allow systemd_cgroups_t proc_t (filesystem (getattr))) - -; Allow systemd_generator_t to manage home_root_t directories and selinux_config_t directories -(allow systemd_generator_t home_root_t (dir (read search write add_name remove_name getattr open))) -(allow systemd_generator_t self (capability (sys_rawio))) -(allow systemd_generator_t selinux_config_t (dir (search))) - -; Allow systemd_hw_t to manage capabilities for systemd hardware management -(allow systemd_hw_t self (capability (dac_override))) - -; Allow systemd_locale_t to manage selinux_config_t files for locale settings -(allow systemd_locale_t selinux_config_t (file (getattr open read))) - -; Allow systemd_logind_t to manage various directories and files for login daemon -(allow systemd_logind_t proc_t (file (write ))) -(allow systemd_logind_t proc_t (filesystem (getattr))) - -; Allow systemd_networkd_t to manage various directories and files for network management -(allow systemd_networkd_t proc_t (file (write))) -(allow systemd_networkd_t selinux_config_t (dir (search))) - -; Allow systemd_resolved_t to perform name binding on howl_port_t, write to proc_t files, and search tmpfs_t directories for DNS resolution -(allow systemd_resolved_t howl_port_t (udp_socket (name_bind))) -(allow systemd_resolved_t proc_t (file (write))) -(allow systemd_resolved_t tmpfs_t (dir (search))) - -(allow systemd_sessions_t self (capability (net_admin))) - -; Allow systemd_sysctl_t to manage selinux_config_t directories and perform various operations on tmpfs_t directories for system configuration -(allow systemd_sysctl_t selinux_config_t (dir (search))) -(allow systemd_sysctl_t tmpfs_t (dir (search read write add_name remove_name getattr open))) - -; Allow systemd_tmpfiles_t to manage etc_t symbolic links and various directories and files for temporary file management -(allow systemd_tmpfiles_t etc_t (lnk_file (relabelto relabelfrom))) -(allow systemd_tmpfiles_t init_var_lib_t (dir (create))) -(allow systemd_tmpfiles_t user_home_dir_t (dir (write relabelto relabelfrom))) - -(allow systemd_update_done_t self (capability (net_admin))) - -; Allow systemd_user_runtime_dir_t to get attributes of the proc_t filesystem for user runtime directory management -(allow systemd_user_runtime_dir_t proc_t (filesystem (getattr))) -(allow systemd_user_runtime_dir_t self (capability (net_admin))) - -; Allow systemd_userdbd_t to connect to kernel_t Unix stream sockets, write to proc_t files for user database operations -(allow systemd_userdbd_t kernel_t (unix_stream_socket (connectto))) -(allow systemd_userdbd_t proc_t (file (write))) -(allow systemd_userdbd_t self (capability (sys_resource))) -(allow systemd_userdbd_t self (process (getcap))) - -; Allow udev_t to manage init_runtime_t directories, write to proc_t files for device management -; udev -(allow udev_t init_runtime_t (dir (read))) -(allow udev_t proc_t (file (write))) - -; Allow useradd_t to manage shadow_t files for user addition -; OSModifier -(allow useradd_t shadow_t (file (open read))) diff --git a/trident.spec b/trident.spec index 9038c6310..1c8c540b3 100644 --- a/trident.spec +++ b/trident.spec @@ -1,3 +1,5 @@ +%global selinuxtype targeted + Summary: Agent for bare metal platform Name: trident Version: %{rpm_ver} @@ -5,7 +7,9 @@ Release: %{rpm_rel}%{?dist} Vendor: Microsoft Corporation License: Proprietary Source1: osmodifier -Source2: trident-selinuxpolicies.cil +Source2: selinux-policy-trident/trident.fc +Source3: selinux-policy-trident/trident.if +Source4: selinux-policy-trident/trident.te BuildRequires: openssl-devel BuildRequires: rust BuildRequires: systemd-units @@ -17,6 +21,7 @@ Requires: efibootmgr Requires: lsof Requires: systemd >= 255 Requires: systemd-udev +Requires: (%{name}-selinux if selinux-policy-%{selinuxtype}) # Optional dependencies for various optional features @@ -42,20 +47,6 @@ Agent for bare metal platform %{_bindir}/%{name} %dir /etc/%{name} %{_bindir}/osmodifier -%{_datadir}/selinux/packages/trident-selinuxpolicies.cil - -%post -#!/bin/sh -# Apply required selinux policies only if selinux-policy is present -if rpm -q selinux-policy &> /dev/null; then - semodule -i %{_datadir}/selinux/packages/trident-selinuxpolicies.cil -fi - -%postun -# If selinux-policy is present, remove the trident-selinuxpolicies module -if rpm -q selinux-policy &> /dev/null; then - semodule -r trident-selinuxpolicies -fi # ------------------------------------------------------------------------------ @@ -123,10 +114,52 @@ SystemD timer for update polling with Harpoon. # ------------------------------------------------------------------------------ +%package selinux +Summary: Trident SELinux policy +BuildArch: noarch +Requires: selinux-policy-%{selinuxtype} +Requires(post): selinux-policy-%{selinuxtype} +BuildRequires: selinux-policy-devel +%{?selinux_requires} + +%description selinux +Custom SELinux policy module + +%files selinux +%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.* +%{_datadir}/selinux/devel/include/distributed/%{name}.if +%ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{name} + +# SELinux contexts are saved so that only affected files can be +# relabeled after the policy module installation +%pre selinux +%selinux_relabel_pre -s %{selinuxtype} + +%post selinux +%selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 + +%postun selinux +if [ $1 -eq 0 ]; then + %selinux_modules_uninstall -s %{selinuxtype} %{name} +fi + +%posttrans selinux +%selinux_relabel_post -s %{selinuxtype} + +# ------------------------------------------------------------------------------ + %build export TRIDENT_VERSION="%{trident_version}" cargo build --release +mkdir selinux +cp -p %{SOURCE2} selinux/ +cp -p %{SOURCE3} selinux/ +cp -p %{SOURCE4} selinux/ + +make -f %{_datadir}/selinux/devel/Makefile %{name}.pp +bzip2 -9 %{name}.pp + %check test "$(./target/release/trident --version)" = "trident %{trident_version}" @@ -135,13 +168,13 @@ install -D -m 755 %{SOURCE1} %{buildroot}%{_bindir}/osmodifier install -D -m 755 target/release/%{name} %{buildroot}/%{_bindir}/%{name} +# Copy Trident SELinux policy module to /usr/share/selinux/packages +install -D -m 0644 %{name}.pp.bz2 %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 +install -D -p -m 0644 selinux/%{name}.if %{buildroot}%{_datadir}/selinux/devel/include/distributed/%{name}.if + mkdir -p %{buildroot}%{_unitdir} install -D -m 644 systemd/%{name}.service %{buildroot}%{_unitdir}/%{name}.service install -D -m 644 systemd/%{name}-network.service %{buildroot}%{_unitdir}/%{name}-network.service install -D -m 644 systemd/%{name}.timer %{buildroot}%{_unitdir}/%{name}.timer mkdir -p %{buildroot}/etc/%{name} - -# Copy the trident-selinuxpolicies file to /usr/share/selinux/packages/ -mkdir -p %{buildroot}%{_datadir}/selinux/packages/ -install -m 755 %{SOURCE2} %{buildroot}%{_datadir}/selinux/packages/ \ No newline at end of file From e7a6ae24add46149956825ac7d58bedb16162b3a Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 4 Jun 2025 04:48:32 +0000 Subject: [PATCH 50/99] Merged PR 23363: engineering: Merge Unit Tests and Coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Merge unit testing with coverage to get rid of one job ---- #### AI description (iteration 1) #### PR Classification This pull request enhances the CI pipeline by integrating unit testing with code coverage analysis. #### PR Summary The changes update the CI configuration to upgrade testing tools, generate code coverage reports, and enforce a minimum coverage threshold. - `/.pipelines/templates/stages/trident_rpms/unit-test.yml`: Upgraded cargo tool versions, added installation and execution of cargo-llvm-cov to generate lcov reports, and introduced tasks to publish code coverage results and assert that coverage meets the baseline. - `/.pipelines/templates/stages/trident_rpms/build-source.yml`: Removed the separate Coverage job and migrated code coverage parameters into the unit-test job template. Related work items: #12426 --- .../stages/trident_rpms/build-source.yml | 18 +------- .../stages/trident_rpms/unit-test.yml | 41 +++++++++++++++++-- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/.pipelines/templates/stages/trident_rpms/build-source.yml b/.pipelines/templates/stages/trident_rpms/build-source.yml index 49f14614a..8d5873f81 100644 --- a/.pipelines/templates/stages/trident_rpms/build-source.yml +++ b/.pipelines/templates/stages/trident_rpms/build-source.yml @@ -56,23 +56,9 @@ stages: - template: ../common_tasks/rustup.yml - template: ../common_tasks/cargo-auth.yml - template: unit-test.yml - - - job: Coverage - displayName: Evaluate Unit Test Code Coverage - condition: eq(${{ parameters.codeCoverage }}, true) - pool: - type: linux - - variables: - ob_outputDirectory: "$(Build.SourcesDirectory)/trident/out" - - steps: - - template: ../common_tasks/os-info.yml - - template: ../common_tasks/avoid-pypi-usage.yml - - template: ../common_tasks/cargo-auth.yml - - template: ../common_tasks/coverage.yml parameters: - codeCoverageBaseline: 70 # Unit tests + codeCoverage: ${{ parameters.codeCoverage }} + unitTestCoverageBaseline: 70 # Unit tests - job: BuildTrident displayName: Build Trident 3.0 RPMs diff --git a/.pipelines/templates/stages/trident_rpms/unit-test.yml b/.pipelines/templates/stages/trident_rpms/unit-test.yml index 3e546949a..917133c45 100644 --- a/.pipelines/templates/stages/trident_rpms/unit-test.yml +++ b/.pipelines/templates/stages/trident_rpms/unit-test.yml @@ -1,3 +1,11 @@ +parameters: + - name: codeCoverage + type: boolean + default: true + + - name: unitTestCoverageBaseline + type: number + steps: - script: sudo tdnf install -y protobuf protobuf-c openssl-devel clang-devel rust displayName: Install native dependencies @@ -5,14 +13,41 @@ steps: - script: | set -eux - cargo install cargo-nextest --locked --version 0.9.85 + cargo install cargo-nextest --locked --version 0.9.97 + cargo install cargo-llvm-cov --locked --version 0.6.16 + # Exclude pytest_gen as Mariner Rust is currently failing to build it. See # for more details: https://dev.azure.com/mariner-org/ECF/_workitems/edit/6517 - cargo nextest run --workspace --exclude pytest_gen --profile ci - displayName: Test Debug + cargo llvm-cov nextest --remap-path-prefix --lcov --output-path target/lcov.info --workspace --exclude pytest_gen --profile ci + displayName: Run Unit Tests - task: PublishTestResults@2 condition: succeededOrFailed() inputs: testResultsFormat: "JUnit" testResultsFiles: "./target/nextest/ci/trident_unit_tests.xml" + + - task: PublishCodeCoverageResults@2 + condition: and(succeededOrFailed(), eq(${{ parameters.codeCoverage }}, true)) + inputs: + summaryFileLocation: "./target/lcov.info" + + - bash: | + set -eux + cargo llvm-cov report \ + --ignore-filename-regex 'docbuilder' \ + --summary-only --json > ./target/coverage.json + + MEASURED_COVERAGE="$(jq '.data[0].totals.lines.percent' ./target/coverage.json)" + BASELINE="${{ parameters.unitTestCoverageBaseline }}" + + if (( $(echo "$MEASURED_COVERAGE < $BASELINE" | bc -l) )); then + set +x + echo "##vso[task.logissue type=error]Code coverage ($MEASURED_COVERAGE) is below baseline ($BASELINE)" + set -x + exit 1 + else + echo "Code coverage ($MEASURED_COVERAGE) meets or exceeds baseline ($BASELINE)" + fi + displayName: Assert code coverage is above baseline + condition: and(succeededOrFailed(), eq(${{ parameters.codeCoverage }}, true)) From 60ccda3e47804cfedf04d9efcbd7899161f239d9 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 4 Jun 2025 20:38:29 +0000 Subject: [PATCH 51/99] Merged PR 23337: engineering: Stage ESP in block device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Stage ESP in `/var/tmp` to redude memory usage when a large ESP is used. Re-enable memory constrained test. RESULTS: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=827077&view=results (running e2e in the PR will not run the memory constrained test!) The call to `fs::copy()` for the UKI still requires some more memory than before, so I switched it to `MemoryHigh` instead of `MemoryMax`, which allows for short and small spikes over the limit instead of just killing the proces immediately. We could do a proper investigation to get resource usage under control, but that is not my priority right now. https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#MemoryHigh=bytes ---- #### AI description (iteration 1) #### PR Classification This pull request implements engineering improvements that refine ESP image validation and extraction by integrating new EngineContext utilities and updating disk space checks. #### PR Summary The changes enhance the ESP handling code by refactoring validation tests and methods to use EngineContext and dynamic extraction paths, ensuring accurate block device sizing and better error messaging. - **`src/subsystems/storage/osimage.rs`**: Refactored ESP validation tests to build EngineContext with storage specifications and updated the `validate_esp` API to take a context parameter, along with improved error messages. - **`src/engine/context/mod.rs`**: Added a new helper method `filesystem_block_device_size` to estimate block device sizes with corresponding tests. - **`src/engine/boot/esp.rs`**: Modified `load_raw_image` and `deploy_esp` to use a dynamic ESP extraction directory sourced from EngineContext. - **Configuration Files**: Adjusted several end-to-end test YAML files to update disk sizes and RAID configurations to align with the refined storage handling logic. Related work items: #12418 --- e2e_tests/target-configurations.yaml | 8 +- .../combined/trident-config.yaml | 34 +++---- .../trident-config.yaml | 19 ++-- src/engine/boot/esp.rs | 36 +++++-- src/engine/boot/mod.rs | 4 +- src/engine/context/mod.rs | 48 +++++++++- src/subsystems/storage/osimage.rs | 94 +++++++++++-------- 7 files changed, 165 insertions(+), 78 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index d985f0106..a53f07e73 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -95,7 +95,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12349): fix #- memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -112,7 +112,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12349): fix #- memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -142,7 +142,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12349): fix #- memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small @@ -159,7 +159,7 @@ virtualMachine: - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12349): fix #- memory-constraint-combined + - memory-constraint-combined - misc - raid-mirrored - raid-resync-small diff --git a/e2e_tests/trident_configurations/combined/trident-config.yaml b/e2e_tests/trident_configurations/combined/trident-config.yaml index 83780b5ae..71e75d14d 100644 --- a/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -20,9 +20,9 @@ storage: type: xbootldr size: 200M - id: root-a-1 - size: 4G + size: 2G - id: root-b-1 - size: 4G + size: 2G - id: usr-data-a-1 size: 2G - id: usr-data-b-1 @@ -42,9 +42,9 @@ storage: partitionTableType: gpt partitions: - id: root-a-2 - size: 4G + size: 2G - id: root-b-2 - size: 4G + size: 2G - id: usr-data-a-2 size: 2G - id: usr-data-b-2 @@ -79,18 +79,7 @@ storage: devices: - root-b-1 - root-b-2 - - id: web-a-e - name: web-a-e - level: raid1 - devices: - - web-a-e-1 - - web-a-e-2 - - id: web-b-e - name: web-b-e - level: raid1 - devices: - - web-b-e-1 - - web-b-e-2 + - id: usr-data-a name: usr-data-a level: raid1 @@ -115,6 +104,19 @@ storage: devices: - usr-hash-b-1 - usr-hash-b-2 + + - id: web-a-e + name: web-a-e + level: raid1 + devices: + - web-a-e-1 + - web-a-e-2 + - id: web-b-e + name: web-b-e + level: raid1 + devices: + - web-b-e-1 + - web-b-e-2 abUpdate: volumePairs: - id: boot diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index 4edd2d828..283f36581 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -28,15 +28,15 @@ storage: - id: usr-data-b-1 size: 2G - id: usr-hash-a-1 - size: 500M + size: 256M - id: usr-hash-b-1 - size: 500M + size: 256M - id: web-a-e-1 size: 1G - id: web-b-e-1 size: 1G - id: trident - size: 100M + size: 500M - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt @@ -50,9 +50,9 @@ storage: - id: usr-data-b-2 size: 2G - id: usr-hash-a-2 - size: 500M + size: 256M - id: usr-hash-b-2 - size: 500M + size: 256M - id: web-a-e-2 size: 1G - id: web-b-e-2 @@ -160,21 +160,22 @@ storage: mountPoint: /var/lib/trident scripts: preServicing: + # Script to restart trident with a memory limit. The last recorded peak on a clean install on June 2025 was: + # trident-install.service: Consumed 34.583s CPU time, 64.7M memory peak, 0B memory swap peak. - name: rerun-trident-with-memory-limit runOn: - clean-install content: | - systemctl status trident | grep Memory + set -eux + systemctl status trident-install.service | grep Memory if [ ! -f "/run/already-run" ]; then echo "Setting memory limit for trident-install.service" - systemctl set-property trident-install.service MemoryMax=64M + systemctl set-property trident-install.service MemoryMax=100M systemd-run --property=After=trident-install.service --no-block systemctl start trident-install.service touch "/run/already-run" exit 1 fi os: - selinux: - mode: permissive network: version: 2 ethernets: diff --git a/src/engine/boot/esp.rs b/src/engine/boot/esp.rs index 48f3a9ea0..75a565d09 100644 --- a/src/engine/boot/esp.rs +++ b/src/engine/boot/esp.rs @@ -15,6 +15,7 @@ use osutils::{ hashing_reader::{HashingReader, HashingReader384}, image_streamer, mount::{self, MountGuard}, + path, }; use trident_api::{ constants::internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, @@ -36,17 +37,27 @@ const GRUB_EFI: &str = BootloaderExecutable::Grub.current_name(); const GRUB_NOPREFIX_EFI: &str = BootloaderExecutable::GrubNoPrefix.current_name(); /// Takes in a reader to the raw zstd-compressed ESP image and decompresses it -/// into a temporary file. Returns a tuple containing the temporary file and the -/// computed hash (SHA256 or SHA384) of the image. +/// into a temporary file under `//`. +/// Returns a tuple containing the temporary file and the computed hash (SHA256 +/// or SHA384) of the image. /// /// It also takes in the URL of the image to be shown in case of errors. -fn load_raw_image(source: &Url, reader: R) -> Result<(NamedTempFile, String), Error> +fn load_raw_image( + esp_extraction_dir: &Path, + source: &Url, + reader: R, +) -> Result<(NamedTempFile, String), Error> where R: Read + HashingReader, { // Create a temporary file to download ESP image - let temp_image = NamedTempFile::new_in(ESP_EXTRACTION_DIRECTORY) - .context("Failed to create a temporary file")?; + trace!( + "Creating temporary file for ESP image extraction at {}", + esp_extraction_dir.display() + ); + + let temp_image = + NamedTempFile::new_in(esp_extraction_dir).context("Failed to create a temporary file")?; let temp_image_path = temp_image.path().to_path_buf(); debug!("Extracting ESP image to {}", temp_image_path.display()); @@ -352,9 +363,18 @@ pub(super) fn deploy_esp(ctx: &EngineContext, mount_point: &Path) -> Result<(), .reader() .context("Failed to get reader for ESP image from OS image")?; - let (temp_file, computed_sha384) = - load_raw_image(os_image.source(), HashingReader384::new(stream)) - .context("Failed to load raw image")?; + // Extract the ESP image to a temporary file in + // `/ESP_EXTRACTION_DIRECTORY`. This location is generally + // guaranteed to be writable and backed by a real block device, so we don't + // have to store a potentially large ESP image in memory. + let esp_extraction_dir = path::join_relative(mount_point, ESP_EXTRACTION_DIRECTORY); + + let (temp_file, computed_sha384) = load_raw_image( + &esp_extraction_dir, + os_image.source(), + HashingReader384::new(stream), + ) + .context("Failed to load raw image")?; if esp_img.image_file.sha384 != computed_sha384 { bail!( diff --git a/src/engine/boot/mod.rs b/src/engine/boot/mod.rs index c7705cd70..946805744 100644 --- a/src/engine/boot/mod.rs +++ b/src/engine/boot/mod.rs @@ -6,7 +6,7 @@ use strum::IntoEnumIterator; use trident_api::{ constants::{ internal_params::ENABLE_UKI_SUPPORT, AB_VOLUME_A_NAME, AB_VOLUME_B_NAME, - AZURE_LINUX_INSTALL_ID_PREFIX, + AZURE_LINUX_INSTALL_ID_PREFIX, VAR_TMP_PATH, }, error::{ReportError, ServicingError, TridentError}, status::AbVolumeSelection, @@ -20,7 +20,7 @@ pub(super) mod esp; pub(super) mod grub; pub(super) mod uki; -pub(crate) const ESP_EXTRACTION_DIRECTORY: &str = "/tmp"; +pub(crate) const ESP_EXTRACTION_DIRECTORY: &str = VAR_TMP_PATH; #[derive(Default, Debug)] pub(super) struct BootSubsystem; diff --git a/src/engine/context/mod.rs b/src/engine/context/mod.rs index db4f08d6b..4ff3b20c2 100644 --- a/src/engine/context/mod.rs +++ b/src/engine/context/mod.rs @@ -1,6 +1,6 @@ use std::{ collections::{BTreeMap, HashMap}, - path::PathBuf, + path::{Path, PathBuf}, }; use anyhow::{Context, Error}; @@ -242,6 +242,19 @@ impl EngineContext { Ok(verity_device_config) } + + /// Returns the estimated size of the block device holding the filesystem that contains the + /// given path. If the path is not mounted anywhere, or if the block device size cannot be + /// estimated, returns None. + pub(crate) fn filesystem_block_device_size(&self, path: impl AsRef) -> Option { + let device = self + .spec + .storage + .path_to_mount_point_info(path) + .and_then(|mp| mp.device_id)?; + + self.storage_graph.block_device_size(device) + } } #[cfg(test)] @@ -255,7 +268,7 @@ mod tests { use trident_api::config::{ self, AbUpdate, AbVolumePair, Disk, FileSystem, FileSystemSource, MountOptions, MountPoint, - Partition, PartitionType, + Partition, PartitionType, Storage, }; #[test] @@ -554,4 +567,35 @@ mod tests { "Failed to find configuration for verity device 'non-existent'" ); } + + #[test] + fn test_filesystem_block_device_size() { + let ctx = EngineContext::default().with_spec(HostConfiguration { + storage: Storage { + disks: vec![Disk { + id: "disk1".to_owned(), + device: PathBuf::from("/dev/sda"), + partitions: vec![Partition { + id: "part1".to_owned(), + size: 4096.into(), + partition_type: PartitionType::Root, + }], + ..Default::default() + }], + filesystems: vec![FileSystem { + device_id: Some("part1".to_owned()), + mount_point: Some("/data".into()), + source: FileSystemSource::Image, + }], + ..Default::default() + }, + ..Default::default() + }); + + assert_eq!(ctx.filesystem_block_device_size("/data"), Some(4096)); + + assert_eq!(ctx.filesystem_block_device_size("/data/subdir"), Some(4096)); + + assert_eq!(ctx.filesystem_block_device_size("/nonexistent"), None); + } } diff --git a/src/subsystems/storage/osimage.rs b/src/subsystems/storage/osimage.rs index fb21a29b4..5bc587c85 100644 --- a/src/subsystems/storage/osimage.rs +++ b/src/subsystems/storage/osimage.rs @@ -6,7 +6,7 @@ use std::{ use const_format::formatcp; use log::{debug, error, trace, warn}; -use osutils::{df, lsblk}; +use osutils::lsblk; use trident_api::{ config::FileSystemSource, constants::{ @@ -53,7 +53,7 @@ pub fn validate_host_config(ctx: &EngineContext) -> Result<(), TridentError> { validate_verity_match(os_image, &ctx.storage_graph)?; debug!("Validating ESP image in OS image"); - validate_esp(os_image)?; + validate_esp(os_image, ctx)?; debug!("Validating filesystem mounted at or containing /boot"); ensure_boot_is_ext4(os_image)?; @@ -297,7 +297,7 @@ fn validate_verity_match( /// Checks that the ESP filesystem never has its verity entry populated. In addition, checks that /// there is enough space in /tmp to perform file-based copy of ESP image, and warns the user if not /// (this will not produce a fatal error). -fn validate_esp(os_image: &OsImage) -> Result<(), TridentError> { +fn validate_esp(os_image: &OsImage, ctx: &EngineContext) -> Result<(), TridentError> { let Ok(esp_img) = os_image.esp_filesystem() else { trace!("Unable to access ESP filesystem."); return Ok(()); @@ -308,11 +308,11 @@ fn validate_esp(os_image: &OsImage) -> Result<(), TridentError> { return Err(TridentError::new(InvalidInputError::UnexpectedVerityOnEsp)); } - // Ensure there is enough space in /tmp to perform file-based copy of ESP image - let Ok(available_space) = df::available_space_in_fs(ESP_EXTRACTION_DIRECTORY) else { + let Some(available_space) = ctx.filesystem_block_device_size(ESP_EXTRACTION_DIRECTORY) else { warn!("Failed to check if there is enough space available on '{ESP_EXTRACTION_DIRECTORY}' to copy ESP image."); return Ok(()); }; + trace!("Found {available_space} bytes of available space in {ESP_EXTRACTION_DIRECTORY}."); let esp_img_size = esp_img.image_file.uncompressed_size; @@ -320,15 +320,15 @@ fn validate_esp(os_image: &OsImage) -> Result<(), TridentError> { if esp_img_size >= available_space { error!( - "There is not enough space to copy the ESP image into {ESP_EXTRACTION_DIRECTORY}. The \ - uncompressed size of the ESP image is {}, while {ESP_EXTRACTION_DIRECTORY} has {} available.", + "There is not enough space to copy the ESP image into '{ESP_EXTRACTION_DIRECTORY}'. The \ + uncompressed size of the ESP image is {}, while '{ESP_EXTRACTION_DIRECTORY}' has {} available.", ByteCount::from(esp_img_size).to_human_readable_approx(), ByteCount::from(available_space).to_human_readable_approx() ); } else if esp_img_size >= available_space / 2 { warn!( - "There may not be enough space to copy the ESP image into {ESP_EXTRACTION_DIRECTORY}. \ - The uncompressed size of the ESP image is {}, while {ESP_EXTRACTION_DIRECTORY} has {} available.", + "There may not be enough space to copy the ESP image into '{ESP_EXTRACTION_DIRECTORY}'. \ + The uncompressed size of the ESP image is {}, while '{ESP_EXTRACTION_DIRECTORY}' has {} available.", ByteCount::from(esp_img_size).to_human_readable_approx(), ByteCount::from(available_space).to_human_readable_approx() ); @@ -620,39 +620,59 @@ mod tests { #[test] fn test_validate_esp_success() { - // Generate mock OS image - let mock_image = MockOsImage { - source: Url::parse(OSIMAGE_DUMMY_SOURCE).unwrap(), - os_arch: SystemArchitecture::Amd64, - os_release: OsRelease::default(), - images: vec![ - MockImage { - mount_point: PathBuf::from(ESP_MOUNT_POINT_PATH), - fs_type: OsImageFileSystemType::Vfat, - fs_uuid: OsUuid::Uuid(Uuid::new_v4()), - part_type: DiscoverablePartitionType::Esp, - verity: None, - }, - MockImage { - mount_point: PathBuf::from(ROOT_MOUNT_POINT_PATH), - fs_type: OsImageFileSystemType::Ext4, - fs_uuid: OsUuid::Uuid(Uuid::new_v4()), - part_type: DiscoverablePartitionType::Root, - verity: Some(MockVerity { - roothash: "mock-roothash".to_string(), - }), + let ctx = EngineContext::default() + .with_image(MockOsImage { + source: Url::parse(OSIMAGE_DUMMY_SOURCE).unwrap(), + os_arch: SystemArchitecture::Amd64, + os_release: OsRelease::default(), + images: vec![ + MockImage { + mount_point: PathBuf::from(ESP_MOUNT_POINT_PATH), + fs_type: OsImageFileSystemType::Vfat, + fs_uuid: OsUuid::Uuid(Uuid::new_v4()), + part_type: DiscoverablePartitionType::Esp, + verity: None, + }, + MockImage { + mount_point: PathBuf::from(ROOT_MOUNT_POINT_PATH), + fs_type: OsImageFileSystemType::Ext4, + fs_uuid: OsUuid::Uuid(Uuid::new_v4()), + part_type: DiscoverablePartitionType::Root, + verity: Some(MockVerity { + roothash: "mock-roothash".to_string(), + }), + }, + ], + }) + .with_spec(HostConfiguration { + storage: Storage { + disks: vec![Disk { + id: "disk1".to_owned(), + device: PathBuf::from("/dev/sda"), + partitions: vec![Partition { + id: "part1".to_owned(), + size: 4096.into(), + partition_type: Default::default(), + }], + ..Default::default() + }], + filesystems: vec![FileSystem { + device_id: Some("part1".to_owned()), + mount_point: Some("/data".into()), + source: FileSystemSource::Image, + }], + ..Default::default() }, - ], - }; + ..Default::default() + }); // Expect validation to succeed - validate_esp(&OsImage::mock(mock_image)).unwrap(); + validate_esp(ctx.image.as_ref().unwrap(), &ctx).unwrap(); } #[test] fn test_validate_esp_failure() { - // Generate mock OS image - let mock_image = MockOsImage { + let ctx = EngineContext::default().with_image(MockOsImage { source: Url::parse(OSIMAGE_DUMMY_SOURCE).unwrap(), os_arch: SystemArchitecture::Amd64, os_release: OsRelease::default(), @@ -676,10 +696,10 @@ mod tests { }), }, ], - }; + }); // Expect validation to fail - let err = validate_esp(&OsImage::mock(mock_image)).unwrap_err(); + let err = validate_esp(ctx.image.as_ref().unwrap(), &ctx).unwrap_err(); assert_eq!( err.kind(), &ErrorKind::InvalidInput(InvalidInputError::UnexpectedVerityOnEsp) From 6fca9fe64ff4fcf4d19cf6425c27fa1c0d023774 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 4 Jun 2025 21:18:05 +0000 Subject: [PATCH 52/99] Merged PR 23280: engineering: Mock OB Template for PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description OB template mock to avoid all the extra steps we don't really need for validation builds. About 10-15 minutes less than before in pr-e2e: ![image.png](https://dev.azure.com/mariner-org/2311650c-e79e-4301-b4d2-96543fdd84ff/_apis/git/repositories/895b6b3d-5077-488a-8001-ab6b5a14c1a3/pullRequests/23280/attachments/image.png) ![image (2).png](https://dev.azure.com/mariner-org/2311650c-e79e-4301-b4d2-96543fdd84ff/_apis/git/repositories/895b6b3d-5077-488a-8001-ab6b5a14c1a3/pullRequests/23280/attachments/image%20%282%29.png) ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new mock OB pipeline template and updates related pipeline configurations to include additional test image entries and refine validation conditions. #### PR Summary The pull request adds a new pipeline template and adjusts configuration files to align with updated testing and validation requirements. - `/.pipelines/templates/MockOB.yml`: New template file that defines customizable stages, jobs, and error handling for undefined variables. - `/.pipelines/trident-pr.yml`: Updated to extend the new mock OB template and removed/replaced references to the old template. - `/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml` and `/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml`: Added new usrverity test image entries for container and non-container scenarios. - `/.pipelines/templates/e2e-template.yml`: Revised the condition for Makefile validation to execute only for CI stages. Related work items: #12425 --- .pipelines/templates/MockOB.yml | 113 ++++++++++++++++++ .../templates/stages/common_tasks/os-info.yml | 9 ++ .pipelines/trident-pr-e2e.yml | 44 +++---- 3 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 .pipelines/templates/MockOB.yml diff --git a/.pipelines/templates/MockOB.yml b/.pipelines/templates/MockOB.yml new file mode 100644 index 000000000..4912a434e --- /dev/null +++ b/.pipelines/templates/MockOB.yml @@ -0,0 +1,113 @@ +parameters: + - name: stages + type: stageList + default: [] + +stages: + - ${{ each stage in parameters.stages }}: + - stage: ${{ stage.stage }} + ${{ if ne(stage.displayName, '') }}: + displayName: ${{ stage.displayName }} + + ${{ if ne(join(stage.dependsOn, ','), '') }}: + dependsOn: ${{ stage.dependsOn }} + + ${{ if ne(stage.condition, '') }}: + condition: ${{ stage.condition }} + + ${{ if ne(stage.variables, '') }}: + variables: ${{ stage.variables }} + + ${{ if ne(stage.isSkippable, '') }}: + isSkippable: ${{ stage.isSkippable }} + + ${{ if ne(stage.lockBehavior, '') }}: + lockBehavior: ${{ stage.lockBehavior }} + + jobs: + - ${{ each job in stage.jobs }}: + - job: ${{ job.job }} + ${{ if ne(job.displayName, '') }}: + displayName: ${{ job.displayName }} + + ${{ if and(ne(job.timeoutInMinutes, ''), gt(2880, job.timeoutInMinutes)) }}: + timeoutInMinutes: ${{ job.timeoutInMinutes }} + ${{ else }}: + timeoutInMinutes: 1440 + + ${{ if ne(job.condition, '') }}: + condition: ${{ job.condition }} + + ${{ if ne(job.strategy, '') }}: + strategy: ${{ job.strategy }} + + ${{ if ne(join(job.dependsOn, ','), '') }}: + dependsOn: ${{ job.dependsOn }} + + variables: + - ${{ if ne(job.variables, '') }}: + - ${{ each v in job.variables }}: + - ${{ if ne(v.key, '') }}: + - name: ${{ v.key }} + value: ${{ v.value }} + - ${{ else }}: + - ${{ insert }}: ${{ v }} + - name: ob_customArtifactName + value: ${{ coalesce(variables.ob_artifactBaseName, format('drop_{0}_{1}', stage.stage, job.job)) }}${{ coalesce(variables.ob_artifactSuffix, '') }} + + pool: + name: ${{ coalesce(job.pool.name, 'trident-azl3-1es-pool-westus2') }} + + ${{ if ne(job.pool.hostArchitecture, '') }}: + HostArchitecture: ${{ job.pool.hostArchitecture }} + + LinuxHostVersion: + vmBuild: true + distribution: mariner + Host: linux + + steps: + - ${{ if eq(variables.ob_outputDirectory, '' ) }}: + - ${{ job.job }} + - "Variable ob_outputDirectory is not defined. Please check https://aka.ms/obpipelines/artifacts": error + + - checkout: self + fetchDepth: 1 + submodules: recursive + path: s + + - bash: | + set -eux + mkdir -p "${{ variables.ob_outputDirectory }}" + displayName: "Create output directory" + + - ${{ each step in job.steps }}: + - ${{ if ne(step.task, '') }}: + task: ${{ step.task }} + ${{ if ne(step.displayName, '') }}: + displayName: ${{ step.displayName }} + ${{ if ne(step.inputs, '') }}: + inputs: ${{ step.inputs }} + ${{ if ne(step.condition, '') }}: + condition: ${{ step.condition }} + ${{ if ne(step.env, '') }}: + env: ${{ step.env }} + ${{ if ne(step.retryCountOnTaskFailure, '') }}: + retryCountOnTaskFailure: ${{ step.retryCountOnTaskFailure }} + ${{ if ne(step.continueOnError, '') }}: + continueOnError: ${{ step.continueOnError }} + ${{ if ne(step.target, '') }}: + target: ${{ step.target }} + ${{ if ne(step.enabled, '') }}: + enabled: ${{ step.enabled }} + ${{ if ne(step.name, '') }}: + name: ${{ step.name }} + ${{ if ne(step.timeoutInMinutes, '') }}: + timeoutInMinutes: ${{ step.timeoutInMinutes }} + + # - bash: | + # # ACTUAL: + # # ${{ convertToJson(step) }} + + - publish: ${{ variables.ob_outputDirectory }} + artifact: ${{ variables.ob_customArtifactName }} diff --git a/.pipelines/templates/stages/common_tasks/os-info.yml b/.pipelines/templates/stages/common_tasks/os-info.yml index ff97d8521..629770f61 100644 --- a/.pipelines/templates/stages/common_tasks/os-info.yml +++ b/.pipelines/templates/stages/common_tasks/os-info.yml @@ -19,3 +19,12 @@ steps: fi displayName: os-release + - script: | + echo "CPU Info:" + echo "N proc: $(nproc)" + lscpu + echo "Memory:" + free -h + echo "Disk space:" + df -h + displayName: system-info diff --git a/.pipelines/trident-pr-e2e.yml b/.pipelines/trident-pr-e2e.yml index a1aeb09c6..a781a9a90 100644 --- a/.pipelines/trident-pr-e2e.yml +++ b/.pipelines/trident-pr-e2e.yml @@ -12,10 +12,10 @@ trigger: none resources: repositories: - - repository: templates - type: git - name: OneBranch.Pipelines/GovernedTemplates - ref: refs/heads/main + # - repository: templates + # type: git + # name: OneBranch.Pipelines/GovernedTemplates + # ref: refs/heads/main - repository: argus-toolkit type: git @@ -44,24 +44,28 @@ resources: variables: - group: bot # added for access to dom0 RPMs +# Preserving old OneBranch Pipelines template for reference. +# extends: +# template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates +# parameters: +# globalSdl: # https://aka.ms/obpipelines/sdl +# # tsa: +# # enabled: true # SDL results of non-official builds aren't uploaded to TSA by default. +# credscan: +# suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json +# policheck: +# break: true # always break the build on policheck issues. You can disable it by setting to 'false' +# # suppression: +# # suppressionFile: $(Build.SourcesDirectory)\.gdn\global.gdnsuppress +# cg: +# alertWarningLevel: Medium +# featureFlags: +# runOnHost: true +# EnableCDPxPAT: false + extends: - template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates + template: templates/MockOB.yml parameters: - globalSdl: # https://aka.ms/obpipelines/sdl - # tsa: - # enabled: true # SDL results of non-official builds aren't uploaded to TSA by default. - credscan: - suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json - policheck: - break: true # always break the build on policheck issues. You can disable it by setting to 'false' - # suppression: - # suppressionFile: $(Build.SourcesDirectory)\.gdn\global.gdnsuppress - cg: - alertWarningLevel: Medium - featureFlags: - runOnHost: true - EnableCDPxPAT: false - stages: - template: templates/pipeline-selector.yml parameters: From cde8c4b63fb02708ddece78feaae380afcbd42b3 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Wed, 4 Jun 2025 21:31:05 +0000 Subject: [PATCH 53/99] Merged PR 23375: Revert 'engineering: Create Trident SELinux policy module' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Reverting this PR until I can debug baremetal tests. ---------------------- # 🔍 Description SELinux policy domain for Trident. This PR replaces the SELinux policy used in the following VM host tests: # 🤔 Rationale Why is this PR needed? Trident's current approach creates a series of allowances in [trident-selinuxpolicies.cil](https://dev.azure.com/mariner-org/ECF/_git/trident?path=/trident-selinuxpolicies.cil). However, this means that the child processes of Trident run in an unconfined domain (`initrc_t`). This is problematic because Trident has elevated permissions on the system that are not clearly expressed to customers. In addition, some of the permissions given in trident-selinuxpolicies.cil may unintentially lead to privilege escalation. Instead, we want Trident to run in a confined setting which can be achieved by creating Trident's own domain, `trident_t`. # 📌 Follow-up Tasks * Ensure that user scripts run in unconfined domain #12388 * Create addendum for container Trident: #11257 * SELinux and verity #12022 * Installer ISOs should run in enforcing mode #10522 # đŸ—’ī¸ Notes Interfaces definitions can be found in: https://github.com/SELinuxProject/refpolicy/tree/main/policy/modules Current policy succeeds so far with `combined`, `base`, `raid-small`, `verity`, and `encrypted-partition` configs with SELinux enforcing. Passing e2e test (including encryption tests): https://dev.azure.com/mariner-org/ECF/_build/results?buildId=825978&view=results Reverts !22856 Related work items: #12429 --- Dockerfile.azl3 | 6 +- Dockerfile.full | 6 +- e2e_tests/encryption_test.py | 9 - e2e_tests/target-configurations.yaml | 3 - selinux-policy-trident/trident.fc | 3 - selinux-policy-trident/trident.if | 163 ----- selinux-policy-trident/trident.te | 782 --------------------- src/lib.rs | 8 +- trident-mos/files/download-trident.service | 1 - trident-mos/iso.yaml | 12 +- trident-mos/post-install.sh | 5 - trident-selinuxpolicies.cil | 140 ++++ trident.spec | 71 +- 13 files changed, 165 insertions(+), 1044 deletions(-) delete mode 100644 selinux-policy-trident/trident.fc delete mode 100644 selinux-policy-trident/trident.if delete mode 100644 selinux-policy-trident/trident.te create mode 100644 trident-selinuxpolicies.cil diff --git a/Dockerfile.azl3 b/Dockerfile.azl3 index a43dc894d..ec6f476bc 100644 --- a/Dockerfile.azl3 +++ b/Dockerfile.azl3 @@ -1,6 +1,6 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed selinux-policy-devel +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed WORKDIR /work @@ -8,9 +8,7 @@ COPY trident.spec . COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te -COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc -COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if +COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil ARG TRIDENT_VERSION=dev-build ARG RPM_VER=0.1.0 diff --git a/Dockerfile.full b/Dockerfile.full index 20d4d581a..1969c0eb1 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -1,15 +1,13 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential selinux-policy-devel +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential WORKDIR /work COPY trident.spec . COPY systemd ./systemd COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te -COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc -COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if +COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil COPY .cargo/config.toml ./.cargo/config.toml COPY build.rs . diff --git a/e2e_tests/encryption_test.py b/e2e_tests/encryption_test.py index 09c56b44b..9df30db4a 100644 --- a/e2e_tests/encryption_test.py +++ b/e2e_tests/encryption_test.py @@ -442,18 +442,9 @@ def check_crypsetup_luks_dump(conn: fabric.Connection, cryptDevPath: str) -> Non } } """ - # Running this command requires elevated privileges, so temporarily switch to Permissive mode - enforcing = conn.run("sudo getenforce").stdout.strip() == "Enforcing" - if enforcing: - sudo(conn, "setenforce 0") - stdout = sudo(conn, f"cryptsetup luksDump --dump-json-metadata {cryptDevPath}") dump = json.loads(stdout) - # Revert to Enforcing mode - if enforcing: - sudo(conn, "setenforce 1") - actual = dump["digests"]["0"]["type"] expected = "pbkdf2" assert ( diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index a53f07e73..1f8f68468 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -126,9 +126,6 @@ virtualMachine: pullrequest: - base - combined - # TODO(10522): Can remove encrypted-partition from pipeline when combined/rerun runs in - # enforcing mode - - encrypted-partition - raid-mirrored - raid-resync-small - rerun diff --git a/selinux-policy-trident/trident.fc b/selinux-policy-trident/trident.fc deleted file mode 100644 index 1fe2cea99..000000000 --- a/selinux-policy-trident/trident.fc +++ /dev/null @@ -1,3 +0,0 @@ -/usr/bin/trident -- gen_context(system_u:object_r:trident_exec_t,s0) - -/var/lib/trident(/.*)? gen_context(system_u:object_r:trident_var_lib_t,s0) \ No newline at end of file diff --git a/selinux-policy-trident/trident.if b/selinux-policy-trident/trident.if deleted file mode 100644 index 0d5a5cf70..000000000 --- a/selinux-policy-trident/trident.if +++ /dev/null @@ -1,163 +0,0 @@ - -## policy for trident - -######################################## -## -## Execute trident_exec_t in the trident domain. -## -## -## -## Domain allowed to transition. -## -## -# -interface(`trident_domtrans',` - gen_require(` - type trident_t, trident_exec_t; - ') - - corecmd_search_bin($1) - domtrans_pattern($1, trident_exec_t, trident_t) -') - -###################################### -## -## Execute trident in the caller domain. -## -## -## -## Domain allowed access. -## -## -# -interface(`trident_exec',` - gen_require(` - type trident_exec_t; - ') - - corecmd_search_bin($1) - can_exec($1, trident_exec_t) -') - -######################################## -## -## Search trident lib directories. -## -## -## -## Domain allowed access. -## -## -# -interface(`trident_search_lib',` - gen_require(` - type trident_var_lib_t; - ') - - allow $1 trident_var_lib_t:dir search_dir_perms; - files_search_var_lib($1) -') - -######################################## -## -## Read trident lib files. -## -## -## -## Domain allowed access. -## -## -# -interface(`trident_read_lib_files',` - gen_require(` - type trident_var_lib_t; - ') - - files_search_var_lib($1) - read_files_pattern($1, trident_var_lib_t, trident_var_lib_t) -') - -######################################## -## -## Manage trident lib files. -## -## -## -## Domain allowed access. -## -## -# -interface(`trident_manage_lib_files',` - gen_require(` - type trident_var_lib_t; - ') - - files_search_var_lib($1) - manage_files_pattern($1, trident_var_lib_t, trident_var_lib_t) -') - -######################################## -## -## Manage trident lib directories. -## -## -## -## Domain allowed access. -## -## -# -interface(`trident_manage_lib_dirs',` - gen_require(` - type trident_var_lib_t; - ') - - files_search_var_lib($1) - manage_dirs_pattern($1, trident_var_lib_t, trident_var_lib_t) -') - - -######################################## -## -## All of the rules required to administrate -## an trident environment -## -## -## -## Domain allowed access. -## -## -## -## -## Role allowed access. -## -## -## -# -interface(`trident_admin',` - gen_require(` - type trident_t; - type trident_var_lib_t; - ') - - allow $1 trident_t:process { signal_perms }; - ps_process_pattern($1, trident_t) - - optional_policy(` - tunable_policy(`allow_ptrace',` - allow $1 trident_t:process ptrace; - ') - ') - - optional_policy(` - tunable_policy(`deny_ptrace',` - allow $1 trident_t:process ptrace; - ') - ') - - files_search_var_lib($1) - admin_pattern($1, trident_var_lib_t) - optional_policy(` - systemd_passwd_agent_exec($1) - systemd_read_fifo_file_passwd_run($1) - ') -') diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te deleted file mode 100644 index 6aeaccb92..000000000 --- a/selinux-policy-trident/trident.te +++ /dev/null @@ -1,782 +0,0 @@ -policy_module(trident, 1.0.0) - -######################################## -# -# Declarations -# - -type trident_t; -type trident_exec_t; - -# Creates a domain for long running process (daemon), which is started by an init script. Domain is trident_t and entry_point is tridnet_exec_t. -init_daemon_domain(trident_t, trident_exec_t) - -# Create type to label files and directories in /var/lib that are specific to Trident, in particular the Trident datastore. -type trident_var_lib_t; -files_type(trident_var_lib_t) - -#################### -# -# Trident policy -# -require { - type admin_passwd_exec_t; - type anacron_exec_t; - type audisp_remote_exec_t; - type audit_spool_t; - type auditctl_exec_t; - type auditd_exec_t; - type auditd_unit_t; - type bluetooth_unit_t; - type boot_t; - type bootloader_t; - type cgroup_t; - type chfn_exec_t; - type chkpwd_exec_t; - type chronyc_exec_t; - type chronyd_unit_t; - type chronyd_var_lib_t; - type chronyd_var_log_t; - type cloud_init_exec_t; - type cloud_init_state_t; - type cloud_init_t; - type colord_var_lib_t; - type container_unit_t; - type crack_db_t; - type crack_exec_t; - type cron_spool_t; - type crond_unit_t; - type dbusd_unit_t; - type debugfs_t; - type default_t; - type device_t; - type devpts_t; - type dhcpc_exec_t; - type dhcpc_state_t; - type dhcpd_unit_t; - type dmesg_exec_t; - type dosfs_t; - type efivarfs_t; - type etc_runtime_t; - type fs_t; - type fsadm_exec_t; - type fsadm_t; - type getty_exec_t; - type gpg_agent_exec_t; - type gpg_pinentry_exec_t; - type gpg_secret_t; - type groupadd_exec_t; - type home_root_t; - type init_t; - type init_exec_t; - type init_runtime_t; - type init_var_lib_t; - type iptables_unit_t; - type kernel_t; - type kmod_exec_t; - type krb5kdc_exec_t; - type ld_so_t; - type ldconfig_cache_t; - type lib_t; - type loadkeys_exec_t; - type loadkeys_t; - type load_policy_t; - type locale_t; - type locate_exec_t; - type logrotate_unit_t; - type logrotate_var_lib_t; - type lost_found_t; - type lvm_metadata_t; - type lvm_t; - type lvm_unit_t; - type mail_spool_t; - type mdadm_exec_t; - type mdadm_unit_t; - type memory_pressure_t; - type mnt_t; - type modules_conf_t; - type modules_dep_t; - type modules_object_t; - type mount_t; - type mount_exec_t; - type mptctl_device_t; - type net_conf_t; - type ntpd_exec_t; - type ntpd_unit_t; - type oddjob_mkhomedir_exec_t; - type power_unit_t; - type proc_t; - type proc_kcore_t; - type proc_mdstat_t; - type proc_net_t; - type root_t; - type rpm_t; - type rpm_script_t; - type rpm_unit_t; - type security_t; - type setfiles_t; - type semanage_t; - type semanage_exec_t; - type shadow_lock_t; - type shadow_t; - type shell_exec_t; - type ssh_agent_exec_t; - type ssh_exec_t; - type ssh_home_t; - type sshd_t; - type sshd_keygen_unit_t; - type sshd_unit_t; - type sudo_exec_t; - type sulogin_exec_t; - type sysctl_fs_t; - type sysctl_kernel_t; - type sysctl_vm_overcommit_t; - type sysctl_vm_t; - type sysfs_t; - type syslog_conf_t; - type syslogd_exec_t; - type syslogd_unit_t; - type systemd_analyze_exec_t; - type systemd_backlight_exec_t; - type systemd_backlight_unit_t; - type systemd_binfmt_exec_t; - type systemd_binfmt_unit_t; - type systemd_cgroups_exec_t; - type systemd_cgtop_exec_t; - type systemd_coredump_exec_t; - type systemd_coredump_var_lib_t; - type systemd_factory_conf_t; - type systemd_generator_t; - type systemd_generator_exec_t; - type systemd_homed_exec_t; - type systemd_homework_exec_t; - type systemd_hostnamed_exec_t; - type systemd_hw_exec_t; - type systemd_hwdb_t; - type systemd_journalctl_exec_t; - type systemd_locale_exec_t; - type systemd_logind_exec_t; - type systemd_machine_id_setup_exec_t; - type systemd_modules_load_exec_t; - type systemd_networkd_exec_t; - type systemd_notify_exec_t; - type systemd_passwd_agent_exec_t; - type systemd_pcrphase_exec_t; - type systemd_pstore_exec_t; - type systemd_resolved_exec_t; - type systemd_rfkill_exec_t; - type systemd_rfkill_unit_t; - type systemd_sessions_exec_t; - type systemd_socket_proxyd_exec_t; - type systemd_stdio_bridge_exec_t; - type systemd_sysctl_exec_t; - type systemd_sysusers_exec_t; - type systemd_tmpfiles_exec_t; - type systemd_unit_t; - type systemd_update_done_exec_t; - type systemd_user_manager_unit_t; - type systemd_user_runtime_dir_exec_t; - type systemd_userdbd_exec_t; - type systemd_userdbd_unit_t; - type tmp_t; - type tmpfs_t; - type trident_t; - type udev_exec_t; - type udev_t; - type udevadm_t; - type unlabeled_t; - type unreserved_port_t; - type updpwd_exec_t; - type useradd_exec_t; - type usr_t; - type uuidd_exec_t; - type unconfined_t; - type var_run_t; - - # Define object classes that SELinux can protect - class dir { add_name create getattr open read remove_name search write }; - class file { create getattr ioctl lock open read rename setattr relabelto unlink write map execute execute_no_trans }; - class chr_file getattr; - class filesystem getattr; - class lnk_file read; - class netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; - class process { getsched noatsecure rlimitinh siginh }; - class capability sys_admin; - class security read_policy; - class tcp_socket { connect create getattr getopt name_connect read setopt shutdown write }; - class udp_socket { create ioctl }; - - attribute can_change_object_identity; - attribute domain; - - role unconfined_r; - role system_r; -} - -# Allow Trident to change (relabel) the security context of a file or directory -typeattribute trident_t can_change_object_identity; - -# Defines transition from trident_t to fsadm_t domain when Trident executes fsadm tool (i.e. mkfs) -type_transition trident_t fsadm_exec_t:process fsadm_t; - -# Allow transition between unconfined_t and trident_t domains; necessary for an interactive run -optional_policy(` - unconfined_run_to(trident_t, trident_exec_t) -') - -#============= trident_t ============== -# Gives trident_t the following elevated privileges: -# dac_override and dac_read_search - allow access files and directories without necessary DAC permissions -# sys_ptrace - allow trident_t to trace or debug other processes -# sys_rawio - allow trident_t to perform I/O operations directly on hardware devices -allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; - -allow trident_t self:alg_socket { accept bind create read write }; -allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice }; -allow trident_t self:fifo_file manage_fifo_file_perms; -allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; -allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; -allow trident_t self:netlink_route_socket { bind create getattr nlmsg_read read write }; -allow trident_t self:process { getsched setsched getcap setpgid signull getattr signal }; -allow trident_t self:tcp_socket { connect create getattr getopt read setopt shutdown write }; -allow trident_t self:unix_dgram_socket { connect create }; -allow trident_t self:key { search write }; -allow trident_t self:sem { associate create destroy read unix_read unix_write write }; - -# Ensure any new files, directories, or symbolic links created by trident_t are automatically labeled with type trident_var_lib_t -files_var_lib_filetrans(trident_t, trident_var_lib_t, { dir file lnk_file }) - -# Allow trident_t domain to interact with files and directories labeled as trident_var_lib_t -# Necessary so Trident can interact with the datastore at /var/lib/trident -allow trident_t trident_var_lib_t:dir { getattr search read write add_name create remove_name open mounton relabelto }; -allow trident_t trident_var_lib_t:file { getattr setattr create open read write unlink lock }; - -# Allow Trident to relabel its executable -allow trident_t trident_exec_t:file relabelto; - -allow trident_t audit_spool_t:dir { getattr open read }; -allow trident_t auditctl_exec_t:file getattr; -allow trident_t auditd_exec_t:file getattr; -allow trident_t auditd_unit_t:file getattr; -allow trident_t admin_passwd_exec_t:file getattr; -allow trident_t anacron_exec_t:file getattr; -allow trident_t audisp_remote_exec_t:file getattr; -allow trident_t bluetooth_unit_t:file getattr; -allow trident_t boot_t:dir mounton; -allow trident_t cgroup_t:filesystem getattr; -allow trident_t chfn_exec_t:file getattr; -allow trident_t chkpwd_exec_t:file getattr; -allow trident_t chronyc_exec_t:file getattr; -allow trident_t chronyd_unit_t:file getattr; -allow trident_t chronyd_var_lib_t:dir { getattr open read }; -allow trident_t chronyd_var_log_t:dir { getattr open read }; -allow trident_t cloud_init_exec_t:file getattr; -allow trident_t cloud_init_state_t:dir list_dir_perms; -allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; -allow trident_t cloud_init_state_t:file getattr; -allow trident_t colord_var_lib_t:dir { getattr open read }; -allow trident_t container_unit_t:file getattr; -allow trident_t crack_db_t:dir { getattr open search read }; -allow trident_t crack_db_t:file getattr; -allow trident_t crack_db_t:lnk_file getattr; -allow trident_t crack_exec_t:file getattr; -allow trident_t cron_spool_t:dir read; -allow trident_t crond_unit_t:file getattr; -allow trident_t dbusd_unit_t:file getattr; -allow trident_t debugfs_t:filesystem getattr; -allow trident_t debugfs_t:dir search; -allow trident_t default_t:dir { getattr open read relabelto search }; -allow trident_t default_t:file relabelto; -allow trident_t device_t:filesystem { getattr mount unmount }; -allow trident_t devpts_t:chr_file { read write ioctl getattr }; -allow trident_t dhcpc_exec_t:file getattr; -allow trident_t dhcpc_state_t:dir { getattr open read }; -allow trident_t dhcpd_unit_t:file getattr; -allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t dosfs_t:filesystem { getattr mount unmount }; -allow trident_t efivarfs_t:filesystem getattr; -allow trident_t etc_runtime_t:file { getattr open read relabelto setattr unlink }; -allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; -allow trident_t fs_t:filesystem { mount unmount }; -allow trident_t fsadm_t:process { siginh rlimitinh noatsecure transition }; -allow trident_t fsadm_t:fd use; -allow trident_t fsadm_t:fifo_file { read write }; -allow trident_t fsadm_exec_t:file { getattr open read execute execute_no_trans relabelto setattr unlink write }; -allow trident_t getty_exec_t:file getattr; -allow trident_t gpg_pinentry_exec_t:file getattr; -allow trident_t gpg_secret_t:file getattr; -allow trident_t groupadd_exec_t:file getattr; -allow trident_t home_root_t:dir { mounton read relabelto add_name create relabelfrom setattr write }; -allow trident_t home_root_t:file { create getattr ioctl open relabelfrom setattr write }; -allow trident_t init_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t init_t:system reboot; -allow trident_t init_t:key search; -allow trident_t init_runtime_t:dir { add_name write }; -allow trident_t init_runtime_t:file { create getattr open write }; -allow trident_t init_t:unix_stream_socket connectto; -allow trident_t init_var_lib_t:dir { getattr open read search }; -allow trident_t iptables_unit_t:file getattr; -allow trident_t kernel_t:process setsched; -allow trident_t kernel_t:system { module_request ipc_info }; -allow trident_t kmod_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; -allow trident_t krb5kdc_exec_t:file getattr; -allow trident_t ld_so_t:file { execute_no_trans relabelto setattr unlink write }; -allow trident_t ldconfig_cache_t:dir { getattr open read search }; -allow trident_t ldconfig_cache_t:file getattr; -allow trident_t lib_t:file { create relabelto rename setattr unlink write }; -allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t locale_t:dir { add_name relabelto remove_name rmdir setattr write }; -allow trident_t locale_t:file { link relabelto rename setattr unlink write }; -allow trident_t locate_exec_t:file getattr; -allow trident_t logrotate_unit_t:file getattr; -allow trident_t logrotate_var_lib_t:dir { getattr open read }; -allow trident_t lost_found_t:dir { getattr open read relabelto }; -allow trident_t lvm_metadata_t:dir { getattr open read }; -allow trident_t lvm_unit_t:file getattr; -allow trident_t mail_spool_t:dir list_dir_perms; -allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; -allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; -allow trident_t memory_pressure_t:file { read open getattr setattr }; -allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; -allow trident_t modules_conf_t:file { relabelto setattr unlink }; -allow trident_t modules_dep_t:file { getattr ioctl map open read relabelto setattr unlink }; -allow trident_t modules_object_t:file { getattr open read relabelto setattr unlink }; -allow trident_t mount_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; -allow trident_t mptctl_device_t:chr_file getattr; -allow trident_t net_conf_t:file { relabelto setattr unlink }; -allow trident_t ntpd_exec_t:file { execute getattr }; -allow trident_t ntpd_unit_t:file getattr; -allow trident_t oddjob_mkhomedir_exec_t:file getattr; -allow trident_t power_unit_t:file { getattr open read relabelto setattr unlink }; -allow trident_t proc_t:dir read; -allow trident_t proc_t:file { getattr open read ioctl }; -allow trident_t proc_t:filesystem { getattr mount unmount }; -allow trident_t proc_kcore_t:file getattr; -allow trident_t proc_mdstat_t:file { getattr open read }; -allow trident_t proc_net_t:file { open read }; -allow trident_t root_t:dir { add_name create mounton write }; -allow trident_t root_t:file { create getattr map open read relabelfrom write }; -allow trident_t rpm_unit_t:file getattr; -allow trident_t security_t:file { map write }; -allow trident_t security_t:filesystem getattr; -allow trident_t semanage_exec_t:file { execute execute_no_trans entrypoint getattr ioctl open read }; -allow trident_t setfiles_exec_t:file entrypoint; -allow trident_t shadow_t:file { getattr open read relabelto setattr unlink write }; -allow trident_t shadow_lock_t:file { create getattr lock open read unlink write }; -allow trident_t shell_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; -allow trident_t ssh_agent_exec_t:file getattr; -allow trident_t ssh_exec_t:file { execute getattr }; -allow trident_t ssh_home_t:dir { setattr relabelto }; -allow trident_t ssh_home_t:file relabelto; -allow trident_t sshd_t:fd use; -allow trident_t sshd_t:fifo_file { read write getattr }; # Allow Trident to read/write stdin/stdout/stderr -allow trident_t sshd_keygen_unit_t:file getattr; -allow trident_t sshd_unit_t:file getattr; -allow trident_t sulogin_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t sysctl_fs_t:dir search; -allow trident_t sysctl_fs_t:file { getattr ioctl open read }; -allow trident_t sysctl_kernel_t:dir search; -allow trident_t sysctl_kernel_t:file { getattr ioctl open read }; -allow trident_t sysctl_vm_overcommit_t:file { open read }; -allow trident_t sysctl_vm_t:dir search; -allow trident_t sysfs_t:filesystem { mount unmount }; -allow trident_t syslog_conf_t:file { getattr open read relabelto setattr unlink }; -allow trident_t syslogd_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t syslogd_unit_t:file { getattr open read relabelto setattr unlink }; -allow trident_t systemd_analyze_exec_t:file getattr; -allow trident_t systemd_backlight_exec_t:file getattr; -allow trident_t systemd_backlight_unit_t:file getattr; -allow trident_t systemd_binfmt_exec_t:file getattr; -allow trident_t systemd_binfmt_unit_t:file getattr; -allow trident_t systemd_cgroups_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_cgtop_exec_t:file getattr; -allow trident_t systemd_coredump_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_coredump_var_lib_t:dir { getattr open read }; -allow trident_t systemd_factory_conf_t:dir { getattr open read search }; -allow trident_t systemd_factory_conf_t:file getattr; -allow trident_t systemd_generator_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_homed_exec_t:file getattr; -allow trident_t systemd_homework_exec_t:file getattr; -allow trident_t systemd_hostnamed_exec_t:file { execute getattr }; -allow trident_t systemd_hw_exec_t:file getattr; -allow trident_t systemd_hwdb_t:file { getattr open read relabelto setattr unlink }; -allow trident_t systemd_journal_t:file relabelfrom_file_perms; -allow trident_t systemd_journalctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_locale_exec_t:file getattr; -allow trident_t systemd_logind_exec_t:file getattr; -allow trident_t systemd_machine_id_setup_exec_t:file getattr; -allow trident_t systemd_modules_load_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_networkd_exec_t:file { execute getattr }; -allow trident_t systemd_notify_exec_t:file getattr; -allow trident_t systemd_passwd_agent_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_pcrphase_exec_t:file { execute getattr }; -allow trident_t systemd_pstore_exec_t:file { execute getattr }; -allow trident_t systemd_resolved_exec_t:file { execute getattr }; -allow trident_t systemd_rfkill_exec_t:file getattr; -allow trident_t systemd_rfkill_unit_t:file getattr; -allow trident_t systemd_sessions_exec_t:file getattr; -allow trident_t systemd_socket_proxyd_exec_t:file getattr; -allow trident_t systemd_stdio_bridge_exec_t:file getattr; -allow trident_t systemd_sysctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_sysusers_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_tmpfiles_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_unit_t:dir read; -allow trident_t systemd_unit_t:file { getattr ioctl link open read relabelto rename setattr unlink write }; -allow trident_t systemd_unit_t:lnk_file { getattr read }; -allow trident_t systemd_update_done_exec_t:file getattr; -allow trident_t systemd_user_manager_unit_t:file getattr; -allow trident_t systemd_user_runtime_dir_exec_t:file getattr; -allow trident_t systemd_userdbd_exec_t:file getattr; -allow trident_t systemd_userdbd_unit_t:file getattr; -allow trident_t tmp_t:chr_file { create getattr unlink }; -allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write }; -allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write }; -allow trident_t tmp_t:lnk_file { create getattr read rename unlink }; -allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map }; -allow trident_t tmpfs_t:filesystem { getattr mount unmount }; -allow trident_t udev_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; -allow trident_t udev_runtime_t:dir { read watch }; -allow trident_t unlabeled_t:dir { add_name getattr open read remove_name search write mounton relabelfrom }; -allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink write ioctl }; -allow trident_t unreserved_port_t:tcp_socket name_connect; -allow trident_t updpwd_exec_t:file getattr; -allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; -allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto }; -allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; -allow trident_t uuidd_exec_t:file getattr; -allow trident_t var_run_t:dir { add_name create remove_name write }; -allow trident_t var_run_t:file { create getattr lock open read unlink write }; - -#============= interfaces ============== -########################################### -# Authentication and User Management -########################################### -auth_create_faillog_files(trident_t) -auth_exec_pam(trident_t) -auth_login_entry_type(trident_t) -auth_read_lastlog(trident_t) -auth_read_login_records(trident_t) -domain_entry_file(trident_t, gpg_agent_exec_t) -gpg_entry_type(trident_t) -gpg_list_user_secrets(trident_t) -su_exec(trident_t) -usermanage_check_exec_passwd(trident_t) -userdom_list_user_home_dirs(trident_t) -userdom_read_user_home_content_files(trident_t) -userdom_search_user_runtime_root(trident_t) -userdom_relabel_generic_user_home_files(trident_t) -userdom_relabelto_user_home_dirs(trident_t) - -########################################### -# System Services and Daemons -########################################### -bootloader_exec(trident_t) -chronyd_exec(trident_t) -chronyd_read_config(trident_t) -chronyd_read_key_files(trident_t) -clock_exec(trident_t) -create_files_pattern(trident_t, cgroup_t, cgroup_t) -cron_exec(trident_t) -cron_exec_crontab(trident_t) -cron_read_system_spool(trident_t) -dbus_exec(trident_t) -dbus_manage_lib_files(trident_t) -dbus_read_config(trident_t) -dbus_read_lib_files(trident_t) -dbus_list_system_bus_runtime(trident_t) -dbus_system_bus_client(trident_t) -domain_read_all_domains_state(trident_t) -hostname_exec(trident_t) -init_domtrans(trident_t) -init_rw_stream_sockets(trident_t) -logrotate_exec(trident_t) -ps_process_pattern(trident_t, cloud_init_t) # equivalent to cloud_init_read_state(trident_t) -ssh_domtrans(trident_t) -ssh_domtrans_keygen(trident_t) -ssh_manage_home_files(trident_t) -systemd_list_journal_dirs(trident_t) -systemd_read_networkd_units(trident_t) -systemd_read_user_runtime_units_files(trident_t) -systemd_dbus_chat_logind(trident_t) -systemd_read_user_unit_files(trident_t) - -########################################### -# File System Operations -########################################### -files_list_kernel_modules(trident_t) -files_list_spool(trident_t) -files_list_var(trident_t) -files_manage_boot_files(trident_t) -files_manage_etc_dirs(trident_t) -files_mounton_runtime_dirs(trident_t) -files_read_etc_files(trident_t) -files_read_default_symlinks(trident_t) -files_read_kernel_symbol_table(trident_t) -files_read_usr_src_files(trident_t) -files_read_usr_symlinks(trident_t) -files_read_var_lib_files(trident_t) -files_search_etc(trident_t) -files_search_kernel_modules(trident_t) -files_search_locks(trident_t) -files_search_spool(trident_t) -files_search_var_lib(trident_t) -files_rw_etc_runtime_files(trident_t) -fstools_domtrans(trident_t) -fstools_relabelto_entry_files(trident_t) -fs_getattr_hugetlbfs(trident_t) -fs_getattr_iso9660_files(trident_t) -fs_getattr_iso9660_fs(trident_t) -fs_getattr_pstorefs(trident_t) -fs_getattr_tracefs(trident_t) -fs_getattr_xattr_fs(trident_t) -fs_list_hugetlbfs(trident_t) -fs_manage_dos_dirs(trident_t) -fs_manage_dos_files(trident_t) -fs_manage_tmpfs_dirs(trident_t) -fs_manage_tmpfs_symlinks(trident_t) -fs_read_iso9660_files(trident_t) -fs_watch_memory_pressure(trident_t) -mount_list_runtime(trident_t) - -########################################### -# Network Management -########################################### -iptables_exec(trident_t) -iptables_read_config(trident_t) -iptables_status(trident_t) -sysnet_exec_ifconfig(trident_t) -sysnet_read_config(trident_t) -sysnet_read_dhcp_config(trident_t) -sysnet_relabel_config(trident_t) -sysnet_write_config(trident_t) - -########################################### -# Storage Management -########################################### -dev_rw_loop_control(trident_t) -dev_rw_lvm_control(trident_t) -lvm_exec(trident_t) -lvm_read_config(trident_t) -manage_files_pattern(trident_t, mount_runtime_t, mount_runtime_t) -storage_getattr_fuse_dev(trident_t) -storage_getattr_scsi_generic_dev(trident_t) -storage_raw_read_removable_device(trident_t) -storage_raw_read_fixed_disk(trident_t) -storage_raw_write_fixed_disk(trident_t) - -########################################### -# SELinux Management -########################################### -corecmd_relabel_bin_files(trident_t) -files_relabel_etc_files(trident_t) -files_relabel_kernel_modules(trident_t) -files_relabelto_etc_runtime_files(trident_t) -files_relabelto_usr_files(trident_t) -libs_relabel_ld_so(trident_t) -libs_relabelto_lib_files(trident_t) -miscfiles_relabel_localization(trident_t) -relabel_files_pattern(trident_t, udev_rules_t, udev_rules_t) -selinux_load_policy(trident_t) -seutil_exec_checkpolicy(trident_t) -seutil_exec_loadpolicy(trident_t) -seutil_exec_setfiles(trident_t) -seutil_get_semanage_read_lock(trident_t) -seutil_get_semanage_trans_lock(trident_t) -seutil_manage_bin_policy(trident_t) -seutil_manage_config(trident_t) -seutil_manage_config_dirs(trident_t) -seutil_manage_file_contexts(trident_t) -seutil_manage_module_store(trident_t) -seutil_read_default_contexts(trident_t) -seutil_read_bin_policy(trident_t) -udev_relabel_rules_files(trident_t) - -########################################### -# Package Management -########################################### -rpm_delete_db(trident_t) -rpm_exec(trident_t) -rpm_read_cache(trident_t) -rpm_read_db(trident_t) - -########################################### -# Device Management -########################################### -corenet_getattr_ppp_dev(trident_t) -corenet_read_tun_tap_dev(trident_t) -dev_getattr_acpi_bios_dev(trident_t) -dev_getattr_autofs_dev(trident_t) -dev_getattr_framebuffer_dev(trident_t) -dev_getattr_generic_usb_dev(trident_t) -dev_getattr_mouse_dev(trident_t) -dev_getattr_pmqos_dev(trident_t) -dev_getattr_sysfs(trident_t) -dev_getattr_xserver_misc_dev(trident_t) -dev_list_sysfs(trident_t) -dev_manage_generic_blk_files(trident_t) -dev_manage_generic_dirs(trident_t) -dev_mounton(trident_t) -dev_mounton_sysfs_dirs(trident_t) -dev_read_input_dev(trident_t) -dev_read_kmsg(trident_t) -dev_read_rand(trident_t) -dev_read_raw_memory(trident_t) -dev_read_realtime_clock(trident_t) -dev_read_sysfs(trident_t) -dev_read_watchdog(trident_t) -dev_read_urand(trident_t) -dev_relabelfrom_generic_chr_files(trident_t) -dev_relabelfrom_vfio_dev(trident_t) -dev_rw_nvram(trident_t) -dev_rw_tpm(trident_t) -dev_rw_vhost(trident_t) -dev_search_sysfs(trident_t) -dev_write_sysfs(trident_t) -dev_write_urand(trident_t) -term_getattr_ptmx(trident_t) -term_use_virtio_console(trident_t) -term_getattr_pty_fs(trident_t) -udev_manage_rules_files(trident_t) -udev_read_runtime_files(trident_t) -udev_read_state(trident_t) - -########################################### -# System Command Execution -########################################### -can_exec(trident_t, sudo_exec_t) -corecmd_manage_bin_files(trident_t) -corecmd_bin_entry_type(trident_t) -corecmd_shell_entry_type(trident_t) -corecmd_search_bin(trident_t) -corecmd_exec_bin(trident_t) -corecmd_search_bin(trident_t) -kerberos_exec_kadmind(trident_t) -kerberos_read_config(trident_t) -libs_exec_ldconfig(trident_t) -libs_manage_lib_dirs(trident_t) -modutils_read_module_config(trident_t) -tcsd_manage_lib_dirs(trident_t) -uuidd_manage_lib_dirs(trident_t) - -########################################### -# Logging and Monitoring -########################################### -logging_read_audit_config(trident_t) -logging_read_audit_log(trident_t) -logging_search_logs(trident_t) -logging_manage_generic_log_dirs(trident_t) -logging_manage_generic_logs(trident_t) - -########################################### -# Miscellaneous -########################################### -miscfiles_read_generic_tls_privkey(trident_t) -miscfiles_read_man_pages(trident_t) -miscfiles_read_localization(trident_t) -miscfiles_read_generic_certs(trident_t) -xserver_read_xkb_libs(trident_t) - - -#################### -# -# Additional permissions given to external domains -# -#============= bootloader_t ============== -# List the contents of generic tmpfs directories; required for RAID -fs_list_tmpfs(bootloader_t) - -#============= cloud_init_t ============== -allow cloud_init_t unlabeled_t:dir { add_name getattr remove_name search write }; -allow cloud_init_t unlabeled_t:file { create getattr ioctl open read rename write }; -allow cloud_init_t usr_t:dir { add_name create remove_name write }; - -files_exec_usr_files(cloud_init_t) -files_manage_usr_files(cloud_init_t) - -#============= fsadm_t ============== -role unconfined_r types fsadm_t; - -# Get the attributes of efivarfs filesystems -allow fsadm_t efivarfs_t:filesystem getattr; -allow fsadm_t trident_t:process { siginh rlimitinh noatsecure transition sigchld }; -allow fsadm_t fixed_disk_device_t:blk_file { open read write getattr ioctl }; - -# Create, read, write, and delete files on a efivarfs filesystem -fs_manage_efivarfs_files(fsadm_t) -fs_manage_tmpfs_dirs(fsadm_t) -fs_manage_tmpfs_files(fsadm_t) - -#============= loadkeys_t ============== -files_read_default_symlinks(loadkeys_t) -fs_search_tmpfs(loadkeys_t) - -#============= lvm_t ============== -# Allow lvm_t access to semaphores of the initrc_t type; this is necessary for Trident to create encrypted volumes -allow lvm_t trident_t:sem { associate read unix_read unix_write write }; - -#============= mount_t ============= -allow mount_t trident_var_lib_t:dir mounton; - -#============= rpm_t ============== -allow rpm_t unlabeled_t:dir { add_name getattr remove_name search write }; -allow rpm_t unlabeled_t:file { create getattr ioctl open read rename write }; -allow rpm_t rpm_script_t:process { noatsecure rlimitinh siginh }; - -#============= rpm_script_t ============== -# Allow RPM scripts to read SELinux policy (we currently apply trident.pp as a module in the Trident spec) -allow rpm_script_t security_t:security read_policy; - -allow rpm_script_t kernel_t:fd use; -allow rpm_script_t unlabeled_t:dir { add_name getattr remove_name search write }; -allow rpm_script_t unlabeled_t:file { create getattr ioctl open read rename write }; - -#============= semanage_t ============== -allow semanage_t proc_t:filesystem getattr; -allow semanage_t load_policy_t:process { noatsecure rlimitinh siginh }; -allow semanage_t setfiles_t:process { noatsecure rlimitinh siginh }; - -libs_manage_lib_dirs(semanage_t) -libs_manage_lib_files(semanage_t) - -#============= setfiles_t ============== -allow setfiles_t proc_t:filesystem getattr; - -#============= systemd_generator_t ============== -allow systemd_generator_t home_root_t:dir read; - -#============= udev_t ============== -allow udev_t cloud_init_t:fd use; -allow udev_t cloud_init_t:fifo_file { append write getattr }; -allow udev_t lvm_t:process { noatsecure rlimitinh siginh }; - -files_read_generic_tmp_files(udev_t) - -#============= udevadm_t ============== -allow udevadm_t cgroup_t:filesystem getattr; -allow udevadm_t self:netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; -allow udevadm_t self:udp_socket { create ioctl }; -allow udevadm_t systemd_hwdb_t:file { getattr map open read }; -allow udevadm_t kernel_t:fd use; - -# Allow udevadm to search kernel modules, read module configurations and dependencies -files_search_kernel_modules(udevadm_t) -modutils_read_module_config(udevadm_t) -modutils_read_module_deps(udevadm_t) - -# Read system network configuration files -sysnet_read_config(udevadm_t) - -# List systemd networkd runtime files -systemd_list_networkd_runtime(udevadm_t) - -# Manage udev rules, runtime directories, and files -udev_manage_rules_files(udevadm_t) -udev_manage_runtime_dirs(udevadm_t) -udev_manage_runtime_files(udevadm_t) - -# Read symbolic links in cgroup directories and search sysfs -read_lnk_files_pattern(udevadm_t, cgroup_t, cgroup_t) -dev_search_sysfs(udevadm_t) - -#============= unlabeled_t ============== -fs_associate_tmpfs(unlabeled_t) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e35e6977a..67722d6dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use std::{ use cli::GetKind; use engine::{bootentries, EngineContext}; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info, warn}; use nix::unistd::Uid; use osutils::{block_devices, container, dependencies::Dependency}; @@ -182,12 +182,6 @@ impl Trident { info!("Running Trident in a container"); } - if let Ok(selinux_context) = fs::read_to_string("/proc/self/attr/current") { - trace!("selinux context: Trident is running in SELinux domain '{selinux_context}'"); - } else { - error!("selinux context: Failed to retrieve the SELinux context in which Trident is running"); - } - if !Uid::effective().is_root() { return Err(TridentError::new( ExecutionEnvironmentMisconfigurationError::CheckRootPrivileges, diff --git a/trident-mos/files/download-trident.service b/trident-mos/files/download-trident.service index 965963574..b796e80cf 100644 --- a/trident-mos/files/download-trident.service +++ b/trident-mos/files/download-trident.service @@ -7,7 +7,6 @@ Before=trident-install.service ExecStart=/bin/bash /trident_cdrom/pre-trident-script.sh ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/trident -o /usr/bin/trident -f" ExecStart=chmod +x /usr/bin/trident -ExecStart=/sbin/restorecon -v /usr/bin/trident ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/osmodifier -o /usr/bin/osmodifier -f" ExecStart=/bin/chmod +x /usr/bin/osmodifier Type=oneshot diff --git a/trident-mos/iso.yaml b/trident-mos/iso.yaml index ba98d06ce..d936ce2b7 100644 --- a/trident-mos/iso.yaml +++ b/trident-mos/iso.yaml @@ -54,9 +54,6 @@ os: - tpm2-tools - veritysetup - vim - # Support for building SELinux module and debugging - - selinux-policy-devel - - setools-console additionalFiles: - source: files/getty@.service @@ -69,12 +66,6 @@ os: destination: /usr/lib/systemd/system/trident-install.service - source: files/download-trident.service destination: /usr/lib/systemd/system/download-trident.service - - source: ../selinux-policy-trident/trident.if - destination: /usr/share/selinux/packages/trident/trident.if - - source: ../selinux-policy-trident/trident.fc - destination: /usr/share/selinux/packages/trident/trident.fc - - source: ../selinux-policy-trident/trident.te - destination: /usr/share/selinux/packages/trident/trident.te services: enable: @@ -96,5 +87,4 @@ iso: - console=tty0 - console=ttyS0 - rd.luks=0 - - selinux=1 - - enforcing=0 + - selinux=0 diff --git a/trident-mos/post-install.sh b/trident-mos/post-install.sh index 825008afb..74d7296bc 100755 --- a/trident-mos/post-install.sh +++ b/trident-mos/post-install.sh @@ -6,8 +6,3 @@ if [ ! -d /etc/trident ]; then mkdir /etc/trident fi ln -s /trident_cdrom/trident-config.yaml /etc/trident/config.yaml - -# Compile and load Trident SELinux module (this is otherwise handled in trident.spec) -cd /usr/share/selinux/packages/trident -make -f /usr/share/selinux/devel/Makefile trident.pp -semodule -i trident.pp \ No newline at end of file diff --git a/trident-selinuxpolicies.cil b/trident-selinuxpolicies.cil new file mode 100644 index 000000000..f15db87cd --- /dev/null +++ b/trident-selinuxpolicies.cil @@ -0,0 +1,140 @@ +; Allow auditctl_t to manage auditd_etc_t files +(allow auditctl_t auditd_etc_t (file (map))) +(allow auditctl_t proc_t (filesystem (getattr))) + +; Allow chkpwd_t to access sysctl_kernel_t directories and files for password verification +(allow chkpwd_t proc_t (filesystem (getattr))) +(allow chkpwd_t sysctl_kernel_t (dir (search))) +(allow chkpwd_t sysctl_kernel_t (file (open read))) + +; Allow cloud_init_t to perform various operations for cloud instance initialization +; cloud-init +(allow cloud_init_t file_context_t (file (getattr open read map))) +(allow cloud_init_t gpg_secret_t (file (getattr))) +(allow cloud_init_t security_t (security (check_context))) +(allow cloud_init_t self (capability (net_admin ))) +(allow cloud_init_t selinux_config_t (file (getattr open read))) +(allow cloud_init_t sshd_key_t (file (relabelto))) +(allow cloud_init_t user_home_t (file (getattr))) + +; Allow fsadm_t to manage efivarfs_t files for filesystem administration +; efibootmgr +(allow fsadm_t efivarfs_t (dir (read))) +(allow fsadm_t efivarfs_t (file (getattr open read write))) +(allow fsadm_t efivarfs_t (filesystem (getattr))) + +; Load/Save OS Random Seed +(allow kernel_t self (capability2 (checkpoint_restore))) + +; Allow kmod_t to read iptables_runtime_t files for kernel module management +(allow kmod_t iptables_runtime_t (file (read))) + +; Allow lvm_t to perform various operations for logical volume management +; Disk Encryption +(allow lvm_t bpf_t (dir (search))) +(allow lvm_t cgroup_t (dir (search))) +(allow lvm_t cgroup_t (filesystem (getattr))) +(allow lvm_t init_t (key (search))) +(allow lvm_t initrc_runtime_t (dir (add_name open read write search))) +(allow lvm_t initrc_runtime_t (file (create open read write lock getattr))) +(allow lvm_t initrc_t (sem (associate read unix_read unix_write write))) +(allow lvm_t kernel_t (key (search))) +(allow lvm_t proc_t (filesystem (getattr))) +(allow lvm_t pstore_t (dir (search))) +(allow lvm_t self (capability (dac_read_search))) +(allow lvm_t systemd_passwd_runtime_t (dir (getattr search))) +(allow lvm_t tmpfs_t (filesystem (getattr))) +(allow lvm_t tpm_device_t (chr_file (read write open ioctl))) +(allow lvm_t user_home_dir_t (dir (search))) +(allow lvm_t var_run_t (dir (create))) + +; Allow mdadm_t to perform RAID operations using the mdadm utility +(allow mdadm_t debugfs_t (dir (search read write add_name remove_name getattr open))) +(allow mdadm_t device_t (lnk_file (create read write getattr open link rename setattr unlink))) +(allow mdadm_t event_device_t (chr_file (getattr))) +(allow mdadm_t lvm_control_t (chr_file (getattr))) +(allow mdadm_t nvram_device_t (chr_file (getattr))) +(allow mdadm_t udev_runtime_t (dir (search read write add_name remove_name getattr open))) +(allow mdadm_t vfio_device_t (chr_file (getattr))) +(allow mdadm_t vhost_device_t (chr_file (getattr))) + +(allow ntpd_t proc_t (file (write))) + +; Allow passwd_t to manage proc files for password updates +(allow passwd_t proc_t (filesystem (getattr))) + +; Allow semanage_t and setfiles_t to get attributes of the proc_t filesystem +(allow semanage_t proc_t (filesystem (getattr))) +(allow setfiles_t proc_t (filesystem (getattr))) + +; Allow ssh_keygen_t to manage files and directories for SSH key generation +(allow ssh_keygen_t security_t (filesystem (getattr))) +(allow ssh_keygen_t selinux_config_t (dir (search))) + +; Allow sshd_t to get attributes of the proc_t filesystem for SSH daemon +(allow sshd_t proc_t (filesystem (getattr))) +(allow sshd_t self (capability (net_admin))) + +; Allow syslogd_t to manage logging files and directories +(allow syslogd_t cgroup_t (dir (read))) +(allow syslogd_t proc_t (file (write read append getattr lock open))) +(allow syslogd_t systemd_journal_t (file (relabelfrom relabelto))) + +; Allow systemd_cgroups_t to manage proc_t filesystem +(allow systemd_cgroups_t proc_t (filesystem (getattr))) + +; Allow systemd_generator_t to manage home_root_t directories and selinux_config_t directories +(allow systemd_generator_t home_root_t (dir (read search write add_name remove_name getattr open))) +(allow systemd_generator_t self (capability (sys_rawio))) +(allow systemd_generator_t selinux_config_t (dir (search))) + +; Allow systemd_hw_t to manage capabilities for systemd hardware management +(allow systemd_hw_t self (capability (dac_override))) + +; Allow systemd_locale_t to manage selinux_config_t files for locale settings +(allow systemd_locale_t selinux_config_t (file (getattr open read))) + +; Allow systemd_logind_t to manage various directories and files for login daemon +(allow systemd_logind_t proc_t (file (write ))) +(allow systemd_logind_t proc_t (filesystem (getattr))) + +; Allow systemd_networkd_t to manage various directories and files for network management +(allow systemd_networkd_t proc_t (file (write))) +(allow systemd_networkd_t selinux_config_t (dir (search))) + +; Allow systemd_resolved_t to perform name binding on howl_port_t, write to proc_t files, and search tmpfs_t directories for DNS resolution +(allow systemd_resolved_t howl_port_t (udp_socket (name_bind))) +(allow systemd_resolved_t proc_t (file (write))) +(allow systemd_resolved_t tmpfs_t (dir (search))) + +(allow systemd_sessions_t self (capability (net_admin))) + +; Allow systemd_sysctl_t to manage selinux_config_t directories and perform various operations on tmpfs_t directories for system configuration +(allow systemd_sysctl_t selinux_config_t (dir (search))) +(allow systemd_sysctl_t tmpfs_t (dir (search read write add_name remove_name getattr open))) + +; Allow systemd_tmpfiles_t to manage etc_t symbolic links and various directories and files for temporary file management +(allow systemd_tmpfiles_t etc_t (lnk_file (relabelto relabelfrom))) +(allow systemd_tmpfiles_t init_var_lib_t (dir (create))) +(allow systemd_tmpfiles_t user_home_dir_t (dir (write relabelto relabelfrom))) + +(allow systemd_update_done_t self (capability (net_admin))) + +; Allow systemd_user_runtime_dir_t to get attributes of the proc_t filesystem for user runtime directory management +(allow systemd_user_runtime_dir_t proc_t (filesystem (getattr))) +(allow systemd_user_runtime_dir_t self (capability (net_admin))) + +; Allow systemd_userdbd_t to connect to kernel_t Unix stream sockets, write to proc_t files for user database operations +(allow systemd_userdbd_t kernel_t (unix_stream_socket (connectto))) +(allow systemd_userdbd_t proc_t (file (write))) +(allow systemd_userdbd_t self (capability (sys_resource))) +(allow systemd_userdbd_t self (process (getcap))) + +; Allow udev_t to manage init_runtime_t directories, write to proc_t files for device management +; udev +(allow udev_t init_runtime_t (dir (read))) +(allow udev_t proc_t (file (write))) + +; Allow useradd_t to manage shadow_t files for user addition +; OSModifier +(allow useradd_t shadow_t (file (open read))) diff --git a/trident.spec b/trident.spec index 1c8c540b3..9038c6310 100644 --- a/trident.spec +++ b/trident.spec @@ -1,5 +1,3 @@ -%global selinuxtype targeted - Summary: Agent for bare metal platform Name: trident Version: %{rpm_ver} @@ -7,9 +5,7 @@ Release: %{rpm_rel}%{?dist} Vendor: Microsoft Corporation License: Proprietary Source1: osmodifier -Source2: selinux-policy-trident/trident.fc -Source3: selinux-policy-trident/trident.if -Source4: selinux-policy-trident/trident.te +Source2: trident-selinuxpolicies.cil BuildRequires: openssl-devel BuildRequires: rust BuildRequires: systemd-units @@ -21,7 +17,6 @@ Requires: efibootmgr Requires: lsof Requires: systemd >= 255 Requires: systemd-udev -Requires: (%{name}-selinux if selinux-policy-%{selinuxtype}) # Optional dependencies for various optional features @@ -47,6 +42,20 @@ Agent for bare metal platform %{_bindir}/%{name} %dir /etc/%{name} %{_bindir}/osmodifier +%{_datadir}/selinux/packages/trident-selinuxpolicies.cil + +%post +#!/bin/sh +# Apply required selinux policies only if selinux-policy is present +if rpm -q selinux-policy &> /dev/null; then + semodule -i %{_datadir}/selinux/packages/trident-selinuxpolicies.cil +fi + +%postun +# If selinux-policy is present, remove the trident-selinuxpolicies module +if rpm -q selinux-policy &> /dev/null; then + semodule -r trident-selinuxpolicies +fi # ------------------------------------------------------------------------------ @@ -114,52 +123,10 @@ SystemD timer for update polling with Harpoon. # ------------------------------------------------------------------------------ -%package selinux -Summary: Trident SELinux policy -BuildArch: noarch -Requires: selinux-policy-%{selinuxtype} -Requires(post): selinux-policy-%{selinuxtype} -BuildRequires: selinux-policy-devel -%{?selinux_requires} - -%description selinux -Custom SELinux policy module - -%files selinux -%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.* -%{_datadir}/selinux/devel/include/distributed/%{name}.if -%ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{name} - -# SELinux contexts are saved so that only affected files can be -# relabeled after the policy module installation -%pre selinux -%selinux_relabel_pre -s %{selinuxtype} - -%post selinux -%selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 - -%postun selinux -if [ $1 -eq 0 ]; then - %selinux_modules_uninstall -s %{selinuxtype} %{name} -fi - -%posttrans selinux -%selinux_relabel_post -s %{selinuxtype} - -# ------------------------------------------------------------------------------ - %build export TRIDENT_VERSION="%{trident_version}" cargo build --release -mkdir selinux -cp -p %{SOURCE2} selinux/ -cp -p %{SOURCE3} selinux/ -cp -p %{SOURCE4} selinux/ - -make -f %{_datadir}/selinux/devel/Makefile %{name}.pp -bzip2 -9 %{name}.pp - %check test "$(./target/release/trident --version)" = "trident %{trident_version}" @@ -168,13 +135,13 @@ install -D -m 755 %{SOURCE1} %{buildroot}%{_bindir}/osmodifier install -D -m 755 target/release/%{name} %{buildroot}/%{_bindir}/%{name} -# Copy Trident SELinux policy module to /usr/share/selinux/packages -install -D -m 0644 %{name}.pp.bz2 %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 -install -D -p -m 0644 selinux/%{name}.if %{buildroot}%{_datadir}/selinux/devel/include/distributed/%{name}.if - mkdir -p %{buildroot}%{_unitdir} install -D -m 644 systemd/%{name}.service %{buildroot}%{_unitdir}/%{name}.service install -D -m 644 systemd/%{name}-network.service %{buildroot}%{_unitdir}/%{name}-network.service install -D -m 644 systemd/%{name}.timer %{buildroot}%{_unitdir}/%{name}.timer mkdir -p %{buildroot}/etc/%{name} + +# Copy the trident-selinuxpolicies file to /usr/share/selinux/packages/ +mkdir -p %{buildroot}%{_datadir}/selinux/packages/ +install -m 755 %{SOURCE2} %{buildroot}%{_datadir}/selinux/packages/ \ No newline at end of file From ce4b2d9ceb310ab50106aa4c0224dafca1f0a105 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 5 Jun 2025 00:03:15 +0000 Subject: [PATCH 54/99] Merged PR 23372: Update naming of test image Aligns with: https://dev.azure.com/mariner-org/ECF/_git/test-images/pullrequest/23361 ---- #### AI description (iteration 1) #### PR Classification This pull request implements a configuration update by renaming the test image build targets. #### PR Summary The changes update the naming convention in the CI pipeline configuration to replace "verity-uki" with "root-verity" in the build targets. - In `/.pipelines/templates/stages/testing_servicing/vm-testing.yml`, the `makeTarget` parameter is updated in three sections for both QCOW2 and COSI image builds. Related work items: #12441 --- .../stages/testing_servicing/vm-testing.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.pipelines/templates/stages/testing_servicing/vm-testing.yml b/.pipelines/templates/stages/testing_servicing/vm-testing.yml index 510493456..922529458 100644 --- a/.pipelines/templates/stages/testing_servicing/vm-testing.yml +++ b/.pipelines/templates/stages/testing_servicing/vm-testing.yml @@ -96,7 +96,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "qemu-base" - makeTarget: "build/trident-vm-verity-testimage.qcow2" + makeTarget: "build/trident-vm-grub-verity-testimage.qcow2" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -108,7 +108,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "qemu-update-a" - makeTarget: "build/trident-vm-verity-testimage.cosi" + makeTarget: "build/trident-vm-grub-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -120,7 +120,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "qemu-update-b" - makeTarget: "build/trident-vm-verity-testimage.cosi" + makeTarget: "build/trident-vm-grub-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -140,7 +140,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "azure-base" - makeTarget: "build/trident-vm-verity-azure-testimage.vhd" + makeTarget: "build/trident-vm-grub-verity-azure-testimage.vhd" baseimgType: core_selinux baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -151,7 +151,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "azure-update-a" - makeTarget: "build/trident-vm-verity-azure-testimage.cosi" + makeTarget: "build/trident-vm-grub-verity-azure-testimage.cosi" baseimgType: core_selinux baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -162,7 +162,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "azure-update-b" - makeTarget: "build/trident-vm-verity-azure-testimage.cosi" + makeTarget: "build/trident-vm-grub-verity-azure-testimage.cosi" baseimgType: core_selinux baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -181,7 +181,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-base" - makeTarget: "build/trident-vm-verity-uki-testimage.qcow2" + makeTarget: "build/trident-vm-root-verity-testimage.qcow2" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -193,7 +193,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-update-a" - makeTarget: "build/trident-vm-verity-uki-testimage.cosi" + makeTarget: "build/trident-vm-root-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -205,7 +205,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-update-b" - makeTarget: "build/trident-vm-verity-uki-testimage.cosi" + makeTarget: "build/trident-vm-root-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} From e634fd5e46369cbd9138a4cde3ff971e85896fd7 Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Thu, 5 Jun 2025 17:36:29 +0000 Subject: [PATCH 55/99] Merged PR 23391: Fix renaming for Servicing Test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description PR to fix this failure following the renaming: https://dev.azure.com/mariner-org/ECF/_workitems/edit/12451 ---- #### AI description (iteration 1) #### PR Classification Bug fix to correct an incorrect naming in the Servicing Testing pipeline. #### PR Summary This pull request fixes the image definition name in the testing pipeline configuration to address failing servicing tests. - `/.pipelines/templates/stages/testing_servicing/testing-template.yml`: Updated the `IMAGE_DEFINITION` value from "trident-vm-verity-testimage-$(System.DefinitionId)" to "trident-vm-grub-verity-testimage-$(System.DefinitionId)" in two job definitions. Related work items: #12451 --- .../templates/stages/testing_servicing/testing-template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/templates/stages/testing_servicing/testing-template.yml b/.pipelines/templates/stages/testing_servicing/testing-template.yml index d0d137c65..63c656e5b 100644 --- a/.pipelines/templates/stages/testing_servicing/testing-template.yml +++ b/.pipelines/templates/stages/testing_servicing/testing-template.yml @@ -69,7 +69,7 @@ jobs: displayName: Publish Base Image env: SUBSCRIPTION: 04cdc145-a4f9-42d4-9868-c46d23d0c63f # CoreOS_Mariner_BMP_Staging - IMAGE_DEFINITION: "trident-vm-verity-testimage-$(System.DefinitionId)" + IMAGE_DEFINITION: "trident-vm-grub-verity-testimage-$(System.DefinitionId)" ARTIFACTS: $(Build.ArtifactStagingDirectory) STORAGE_ACCOUNT: "azlinuxbmpstagingeastus2" RESOURCE_GROUP: "azlinux_bmp_staging_eastus2" @@ -92,7 +92,7 @@ jobs: tridentSourceDirectory: $(Build.SourcesDirectory) ob_outputDirectory: $(tridentSourceDirectory)/deployment_logs_${{ parameters.flavor }} ob_artifactBaseName: "update-testing-${{ parameters.flavor }}-$(System.JobPositionInPhase)" - IMAGE_DEFINITION: "trident-vm-verity-testimage-$(System.DefinitionId)" + IMAGE_DEFINITION: "trident-vm-grub-verity-testimage-$(System.DefinitionId)" TEST_RESOURCE_GROUP: trident-vm-servicing-validation-$(Build.BuildId)-$(System.JobPositionInPhase) SUBSCRIPTION: 04cdc145-a4f9-42d4-9868-c46d23d0c63f # CoreOS_Mariner_BMP_Staging From bdc31391269bde3c3d010f0a5ecf42dc7bb6543f Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Thu, 5 Jun 2025 22:01:02 +0000 Subject: [PATCH 56/99] Merged PR 23396: Rename image in loop-update scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR renames the image following the re-factoring, to fix the failure in `loop-update` scripts: https://dev.azure.com/mariner-org/ECF/_workitems/edit/12452 ---- #### AI description (iteration 1) #### PR Classification This pull request delivers a bug fix by renaming image references in loop-update scripts to resolve issues in servicing testing. #### PR Summary The pull request standardizes the image naming by updating configuration values in the loop-update scripts to ensure consistency and proper image publication. - `scripts/loop-update/common.sh`: Renamed the IMAGE_DEFINITION and OFFER variables to use a new image name. - `scripts/loop-update/publish-sig-image.sh`: Updated IMAGE_PATH to reference the newly named image file. Related work items: #12452 --- scripts/loop-update/common.sh | 4 ++-- scripts/loop-update/publish-sig-image.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/loop-update/common.sh b/scripts/loop-update/common.sh index d95899671..be12540c0 100755 --- a/scripts/loop-update/common.sh +++ b/scripts/loop-update/common.sh @@ -10,14 +10,14 @@ OUTPUT=${OUTPUT:-} ALIAS=${ALIAS:-`whoami`} SUBSCRIPTION=${SUBSCRIPTION:-b8a0db63-c5fa-4198-8e2a-f9d6ff52465e} # CoreOS_AzureLinux_BMP_dev -IMAGE_DEFINITION=${IMAGE_DEFINITION:-trident-vm-verity-azure-testimage} +IMAGE_DEFINITION=${IMAGE_DEFINITION:-trident-vm-grub-verity-azure-testimage} RESOURCE_GROUP=${RESOURCE_GROUP:-azlinux_bmp_dev} PUBLISH_LOCATION=${PUBLISH_LOCATION:-eastus2} GALLERY_RESOURCE_GROUP=${GALLERY_RESOURCE_GROUP:-$ALIAS-trident-rg} STORAGE_ACCOUNT=${STORAGE_ACCOUNT:-azlinuxbmpdev} GALLERY_NAME=${GALLERY_NAME:-${ALIAS}_trident_gallery} PUBLISHER=${PUBLISHER:-$ALIAS} -OFFER=${OFFER:-trident-vm-verity-azure-offer} +OFFER=${OFFER:-trident-vm-grub-verity-azure-offer} export AZCOPY_AUTO_LOGIN_TYPE=${AZCOPY_AUTO_LOGIN_TYPE:-AZCLI} TEST_RESOURCE_GROUP=${TEST_RESOURCE_GROUP:-$GALLERY_RESOURCE_GROUP-test} TEST_VM_SIZE=${TEST_VM_SIZE:-Standard_D2ds_v5} diff --git a/scripts/loop-update/publish-sig-image.sh b/scripts/loop-update/publish-sig-image.sh index e6105827b..585c4163f 100755 --- a/scripts/loop-update/publish-sig-image.sh +++ b/scripts/loop-update/publish-sig-image.sh @@ -15,7 +15,7 @@ STORAGE_ACCOUNT_RESOURCE_ID="/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOUR export STORAGE_CONTAINER_NAME="${STORAGE_CONTAINER_NAME:-$ALIAS-test}" "$SCRIPTS_DIR/publish-sig-image-prepare.sh" -export IMAGE_PATH="${IMAGE_PATH:-$ARTIFACTS/trident-vm-verity-azure-testimage.vhd}" +export IMAGE_PATH="${IMAGE_PATH:-$ARTIFACTS/trident-vm-grub-verity-azure-testimage.vhd}" IMAGE_VERSION="`getImageVersion increment`" echo using image version $IMAGE_VERSION From 41817691c87bdb578301c9eef8bee69748dab47d Mon Sep 17 00:00:00 2001 From: Brian Telfer Date: Fri, 6 Jun 2025 16:49:31 -0700 Subject: [PATCH 57/99] Create codeql.yml --- .github/workflows/codeql.yml | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..b86ceb8ba --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '43 9 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: go + build-mode: autobuild + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From db9b5cc8d2b0a166402158d9eb7d00cee095f512 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Mon, 9 Jun 2025 20:38:35 +0000 Subject: [PATCH 58/99] Merged PR 23381: Test usr-verity This changes the PR tests to check usr verity instead of root verity, and adds testing of UKI rollback #### AI description (iteration 1) #### PR Classification This pull request updates the testing pipeline configurations to validate usr-verity functionality. #### PR Summary The changes adjust pipeline templates to use usr-verity test images and modify the rollback testing step. - `/.pipelines/templates/stages/testing_servicing/testing-template.yml`: Removed the conditional check for the rollback test step, ensuring the loop-update script is executed when rollback testing is enabled. - `/.pipelines/templates/stages/testing_servicing/vm-testing.yml`: Replaced makeTarget values from "root-verity" to "usr-verity" for both qcow2 and cosi image builds. Related work items: #12332 --- .../testing_servicing/testing-template.yml | 26 +++++++++---------- .../stages/testing_servicing/vm-testing.yml | 6 ++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.pipelines/templates/stages/testing_servicing/testing-template.yml b/.pipelines/templates/stages/testing_servicing/testing-template.yml index 63c656e5b..202a334d3 100644 --- a/.pipelines/templates/stages/testing_servicing/testing-template.yml +++ b/.pipelines/templates/stages/testing_servicing/testing-template.yml @@ -193,20 +193,18 @@ jobs: # Configuration, and validate that they succeed. Rollback testing will only be run when # the rollbackTesting parameter is true. The scaling test logic will set it to false. - # TODO: reenable as part of https://dev.azure.com/mariner-org/ECF/_workitems/edit/10624 - - ${{ if ne(parameters.flavor, 'uki') }}: - - bash: ./scripts/loop-update/loop-update.sh - env: - ARTIFACTS: $(Build.ArtifactStagingDirectory) - OUTPUT: $(ob_outputDirectory) - VERBOSE: ${{ parameters.verboseLogging }} - RETRY_COUNT: 3 - EXPECTED_VOLUME: "volume-b" - ROLLBACK: "true" - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - TEST_PLATFORM: ${{ parameters.platform }} - displayName: "Check that Trident can roll back and perform A/B update after" - condition: and(succeeded(), eq(${{ parameters.rollbackTesting }}, true)) + - bash: ./scripts/loop-update/loop-update.sh + env: + ARTIFACTS: $(Build.ArtifactStagingDirectory) + OUTPUT: $(ob_outputDirectory) + VERBOSE: ${{ parameters.verboseLogging }} + RETRY_COUNT: 3 + EXPECTED_VOLUME: "volume-b" + ROLLBACK: "true" + TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) + TEST_PLATFORM: ${{ parameters.platform }} + displayName: "Check that Trident can roll back and perform A/B update after" + condition: and(succeeded(), eq(${{ parameters.rollbackTesting }}, true)) # TODO add more e2e tests here (Task 8813) diff --git a/.pipelines/templates/stages/testing_servicing/vm-testing.yml b/.pipelines/templates/stages/testing_servicing/vm-testing.yml index 922529458..6b2c2d954 100644 --- a/.pipelines/templates/stages/testing_servicing/vm-testing.yml +++ b/.pipelines/templates/stages/testing_servicing/vm-testing.yml @@ -181,7 +181,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-base" - makeTarget: "build/trident-vm-root-verity-testimage.qcow2" + makeTarget: "build/trident-vm-usr-verity-testimage.qcow2" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -193,7 +193,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-update-a" - makeTarget: "build/trident-vm-root-verity-testimage.cosi" + makeTarget: "build/trident-vm-usr-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} @@ -205,7 +205,7 @@ stages: parameters: baseimgBuildType: ${{ parameters.baseimgBuildType }} label: "uki-update-b" - makeTarget: "build/trident-vm-root-verity-testimage.cosi" + makeTarget: "build/trident-vm-usr-verity-testimage.cosi" baseimgType: qemu_guest baseimgAzlVersion: ${{ parameters.baseimgAzlVersion }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} From 4f070ef569537f544a03a45470b84b36a10d4312 Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Mon, 9 Jun 2025 21:02:43 +0000 Subject: [PATCH 59/99] Merged PR 23359: Add statically defined .pcrlock files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description PCR-based encryption with `systemd-pcrlock` requires that the host contains a range of .pcrlock files, which match measurements for the boot components with the PCRs where they're being measured. Statically defined .pcrlock files have been provided by `systemd-pcrlock` in [this commit](https://github.com/systemd/systemd/commit/8e35338d098e9fd78fd4611b36690ea4110ce526#diff-2ac30702a3066ca580e538f92165be3494c2f5bba878d87f4a606f96d2f4155a) since they cover some pre-defined measurements, such as the separator strings. This PR adds these static .pcrlock files as a package in the container image, where they can be accessed by `systemd-pcrlock`: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=831552&view=logs&j=44eda718-2c60-57ae-3c3c-350bf1eeee47&t=f75aab8d-83ce-5b79-4feb-f393899a59ad Pre-defined .pcrlock files, which are a part of `systemd` v255, are currently provided by `systemd-devel` likely due to [this pattern match](https://github.com/microsoft/azurelinux/blob/3.0/SPECS/systemd/split-files.py#L125) during packaging. This change updates the pattern so that the .pcrlock files are explicitly provided by `systemd-udev`, which is the same package that provides the `systemd-pcrlock` binary. Fedora has just merged [their fix](https://src.fedoraproject.org/rpms/systemd/pull-request/207). There is a future fix for AZL published. However, because there is a simple workaround for this issue, i.e. installing `systemd-devel`, the fix will only be merged for AZL 4.0. Hence, this PR is currently needed for PCR-based encryption to work. ---- #### AI description (iteration 1) #### PR Classification This pull request implements a new feature. #### PR Summary The changes add a new RPM package to provide statically defined .pcrlock files for PCR-based encryption. - `trident.spec`: Introduced a `%package static-pcrlock-files` section with summary, description, file list, and installation commands to extract the files, along with a new `Source3` entry. - `Dockerfile.azl3` and `Dockerfile.full`: Updated to copy the `static-pcrlock-files.tar.gz` into the build sources. Related work items: #12344 --- Dockerfile.azl2 | 1 + Dockerfile.azl3 | 1 + Dockerfile.full | 1 + Dockerfile.runtime | 2 ++ .../350-action-efi-application.pcrlock | 1 + .../300-0x00000000.pcrlock | 1 + .../600-0xffffffff.pcrlock | 1 + .../300-0x00000000.pcrlock | 1 + .../600-0xffffffff.pcrlock | 1 + .../300-present.pcrlock | 1 + .../600-absent.pcrlock | 1 + .../750-enter-initrd.pcrlock | 1 + .../800-leave-initrd.pcrlock | 1 + .../static-pcrlock-files/850-sysinit.pcrlock | 1 + .../static-pcrlock-files/900-ready.pcrlock | 1 + .../static-pcrlock-files/950-shutdown.pcrlock | 1 + .../static-pcrlock-files/990-final.pcrlock | 1 + trident.spec | 28 ++++++++++++++++++- 18 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 packaging/static-pcrlock-files/350-action-efi-application.pcrlock create mode 100644 packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock create mode 100644 packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock create mode 100644 packaging/static-pcrlock-files/500-separator.pcrlock.d/300-0x00000000.pcrlock create mode 100644 packaging/static-pcrlock-files/500-separator.pcrlock.d/600-0xffffffff.pcrlock create mode 100644 packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock create mode 100644 packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock create mode 100644 packaging/static-pcrlock-files/750-enter-initrd.pcrlock create mode 100644 packaging/static-pcrlock-files/800-leave-initrd.pcrlock create mode 100644 packaging/static-pcrlock-files/850-sysinit.pcrlock create mode 100644 packaging/static-pcrlock-files/900-ready.pcrlock create mode 100644 packaging/static-pcrlock-files/950-shutdown.pcrlock create mode 100644 packaging/static-pcrlock-files/990-final.pcrlock diff --git a/Dockerfile.azl2 b/Dockerfile.azl2 index a5955355f..9610f6661 100644 --- a/Dockerfile.azl2 +++ b/Dockerfile.azl2 @@ -9,6 +9,7 @@ COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/mariner/SOURCES/osmodifier COPY trident-selinuxpolicies.cil /usr/src/mariner/SOURCES/trident-selinuxpolicies.cil +COPY packaging/static-pcrlock-files/ /usr/src/mariner/SOURCES/static-pcrlock-files/ ARG TRIDENT_VERSION=dev-build ARG RPM_VER=0.1.0 diff --git a/Dockerfile.azl3 b/Dockerfile.azl3 index ec6f476bc..c6ede965e 100644 --- a/Dockerfile.azl3 +++ b/Dockerfile.azl3 @@ -9,6 +9,7 @@ COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY packaging/static-pcrlock-files/ /usr/src/azl/SOURCES/static-pcrlock-files/ ARG TRIDENT_VERSION=dev-build ARG RPM_VER=0.1.0 diff --git a/Dockerfile.full b/Dockerfile.full index 1969c0eb1..9245f4c4e 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -8,6 +8,7 @@ COPY trident.spec . COPY systemd ./systemd COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY packaging/static-pcrlock-files/ /usr/src/azl/SOURCES/static-pcrlock-files/ COPY .cargo/config.toml ./.cargo/config.toml COPY build.rs . diff --git a/Dockerfile.runtime b/Dockerfile.runtime index 6c39e20ae..af56a6f0c 100644 --- a/Dockerfile.runtime +++ b/Dockerfile.runtime @@ -21,6 +21,8 @@ RUN \ --mount=type=bind,source=./bin/RPMS,target=/trident \ tdnf install -y \ /trident/x86_64/trident-0*.rpm && \ + tdnf install -y \ + /trident/x86_64/trident-static-pcrlock-files-0*.rpm && \ tdnf clean all ENV DOCKER_ENVIRONMENT=true diff --git a/packaging/static-pcrlock-files/350-action-efi-application.pcrlock b/packaging/static-pcrlock-files/350-action-efi-application.pcrlock new file mode 100644 index 000000000..2baaa9ceb --- /dev/null +++ b/packaging/static-pcrlock-files/350-action-efi-application.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"cd0fdb4531a6ec41be2753ba042637d6e5f7f256"},{"hashAlg":"sha256","digest":"3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba"},{"hashAlg":"sha384","digest":"77a0dab2312b4e1e57a84d865a21e5b2ee8d677a21012ada819d0a98988078d3d740f6346bfe0abaa938ca20439a8d71"},{"hashAlg":"sha512","digest":"03020279c5ea3676d6630c82a9931343225e8eab81529b65c786aeb6a445d3852a34dd193178f938b6b47345a72d4b647df309c971f7c02f0ede296a136a1086"}]}]} diff --git a/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock b/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock new file mode 100644 index 000000000..c577c9874 --- /dev/null +++ b/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":7,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]}]} diff --git a/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock b/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock new file mode 100644 index 000000000..2e86898c9 --- /dev/null +++ b/packaging/static-pcrlock-files/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":7,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]}]} diff --git a/packaging/static-pcrlock-files/500-separator.pcrlock.d/300-0x00000000.pcrlock b/packaging/static-pcrlock-files/500-separator.pcrlock.d/300-0x00000000.pcrlock new file mode 100644 index 000000000..f1e473f1a --- /dev/null +++ b/packaging/static-pcrlock-files/500-separator.pcrlock.d/300-0x00000000.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":0,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":1,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":2,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":3,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":6,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]}]} diff --git a/packaging/static-pcrlock-files/500-separator.pcrlock.d/600-0xffffffff.pcrlock b/packaging/static-pcrlock-files/500-separator.pcrlock.d/600-0xffffffff.pcrlock new file mode 100644 index 000000000..0b8d20b9c --- /dev/null +++ b/packaging/static-pcrlock-files/500-separator.pcrlock.d/600-0xffffffff.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":0,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":1,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":2,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":3,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":6,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]}]} diff --git a/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock b/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock new file mode 100644 index 000000000..d7012df2e --- /dev/null +++ b/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"443a6b7b82b7af564f2e393cd9d5a388b7fa4a98"},{"hashAlg":"sha256","digest":"d8043d6b7b85ad358eb3b6ae6a873ab7ef23a26352c5dc4faa5aeedacf5eb41b"},{"hashAlg":"sha384","digest":"214b0bef1379756011344877743fdc2a5382bac6e70362d624ccf3f654407c1b4badf7d8f9295dd3dabdef65b27677e0"},{"hashAlg":"sha512","digest":"0fed3a4c9552021436534d27f3adb481e22b50b29e4b37a63f518540a651a174f149b69f500b0bdb2cb3bf4e0e21e0781451090af33e88f6bee4cbebd15c1668"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"475545ddc978d7bfd036facc7e2e987f48189f0d"},{"hashAlg":"sha256","digest":"b54f7542cbd872a81a9d9dea839b2b8d747c7ebd5ea6615c40f42f44a6dbeba0"},{"hashAlg":"sha384","digest":"0a2e01c85deae718a530ad8c6d20a84009babe6c8989269e950d8cf440c6e997695e64d455c4174a652cd080f6230b74"},{"hashAlg":"sha512","digest":"1bb30cdbd6da78fe2a8a161ef51176e22d64dce305b40b47243673af64a2b16fca6182116433e3891be94773f6d7d411275721d5bf7d40ea51a274d5c891637c"}]}]} diff --git a/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock b/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock new file mode 100644 index 000000000..a16142b6e --- /dev/null +++ b/packaging/static-pcrlock-files/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock @@ -0,0 +1 @@ +{"records":[]} diff --git a/packaging/static-pcrlock-files/750-enter-initrd.pcrlock b/packaging/static-pcrlock-files/750-enter-initrd.pcrlock new file mode 100644 index 000000000..a2332dc32 --- /dev/null +++ b/packaging/static-pcrlock-files/750-enter-initrd.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"b1b01d5f73f321eb70e76f8a0e241ac0a3fa4a6e"},{"hashAlg":"sha256","digest":"51e6b92f405d1f98d96e3de343d61d420ad6923b25de21d766f9298192f14fed"},{"hashAlg":"sha384","digest":"687eef3a3a8c716439b5ed583657e8668401630c321f2f35d19b953ddf20b68a96474d0c2e5f0e1757bfa5ba70b9fc32"},{"hashAlg":"sha512","digest":"ab0ddfdabe43f1d06b3e58fbe17439a0f7f552e9e228d85665d485ececf7e733bae4cd7e0a17e5456e2ee7e412f5a0f37de05a782cce781e173ee26958de7f30"}]}]} diff --git a/packaging/static-pcrlock-files/800-leave-initrd.pcrlock b/packaging/static-pcrlock-files/800-leave-initrd.pcrlock new file mode 100644 index 000000000..bd8f436a7 --- /dev/null +++ b/packaging/static-pcrlock-files/800-leave-initrd.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"865e1ff2cc5b8db815313b23fe3d8b561212f5d1"},{"hashAlg":"sha256","digest":"3be261aff7db92bf507eae947f4003ffa2bcad0bffe3524601d62d0bc8be7135"},{"hashAlg":"sha384","digest":"9c0743b7a2e1ee06c70b7137b763cd2205c26ced274149959b05bd5a51bfa96b4fedaa4f87398b5c88986d1ff0879910"},{"hashAlg":"sha512","digest":"01b8ca86b9f8fac967f383380aff7cdffd2ef0c496574517c25398f7c74aa611821dd469ba021b2aa9b9a7232865708ca45c79368f2e7fffda3dd6b308264008"}]}]} diff --git a/packaging/static-pcrlock-files/850-sysinit.pcrlock b/packaging/static-pcrlock-files/850-sysinit.pcrlock new file mode 100644 index 000000000..3bae4452f --- /dev/null +++ b/packaging/static-pcrlock-files/850-sysinit.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"aeabcf402223916e804cce79778a55d5a9276983"},{"hashAlg":"sha256","digest":"730bb5a583ba880c277e656d2dc8aba1a314a11b14d25b05153d2bab82567a48"},{"hashAlg":"sha384","digest":"955cc8939f81d862b3119aabe612fd36bf91668bb62397f5e4126085d79ba6d7cbfa4e3a2345747f0b476ce4b1cbc2c9"},{"hashAlg":"sha512","digest":"a9eb62cdd1cd8292b6325a8ee3770d6f1b613426a749e17ffba8f90bdd6c41806468fb79d01276de7cc791877dfebae165d4ed07585154acf96652c6db92acc1"}]}]} diff --git a/packaging/static-pcrlock-files/900-ready.pcrlock b/packaging/static-pcrlock-files/900-ready.pcrlock new file mode 100644 index 000000000..9a0e82f6e --- /dev/null +++ b/packaging/static-pcrlock-files/900-ready.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"75c0533730caf1f78561c0883fb87bc8d98ef04b"},{"hashAlg":"sha256","digest":"b24d6d33736ecd5604a4b17bc9c6481039fac362bb7df044ef1c10a2bfd21db6"},{"hashAlg":"sha384","digest":"23ed5781da39fe6dc17f79478aeeb9eb2bca1d776061da188e10f9c85f7933fb39cfdba50f39af8aed24e5b45b80d006"},{"hashAlg":"sha512","digest":"ca6616f94a209e53f6fdc526b473172eb4b2157cf4809c31e36ad52db614ed352e68407be53c238ba17a561c4fde43f4a859aa8711f9781a0c934296d4d7571b"}]}]} diff --git a/packaging/static-pcrlock-files/950-shutdown.pcrlock b/packaging/static-pcrlock-files/950-shutdown.pcrlock new file mode 100644 index 000000000..1bc3f76e0 --- /dev/null +++ b/packaging/static-pcrlock-files/950-shutdown.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"53669f193b2174641c72654b5c3e5b67950334ae"},{"hashAlg":"sha256","digest":"08434ba9cdf55a02284e2913400586cd289878e0f055f7bb0b07ce392caeb989"},{"hashAlg":"sha384","digest":"186e2d6603b9755221b7ef894dd52b1154b48ef4786aec06ab6f7709e639715e89bd59fa80736bb45f0ca88583c212c1"},{"hashAlg":"sha512","digest":"9e5549deb36fc48768cb80e03bc91c36cf549ff5921e05bab5b68faefda7fac8c8a0755db783cbf1c1b98c80dc22ef06ff3f4a0a16704749f5cd4acf40e42a94"}]}]} diff --git a/packaging/static-pcrlock-files/990-final.pcrlock b/packaging/static-pcrlock-files/990-final.pcrlock new file mode 100644 index 000000000..77081ae75 --- /dev/null +++ b/packaging/static-pcrlock-files/990-final.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"d594c2cc0a53025004791399d80e20852af4c988"},{"hashAlg":"sha256","digest":"2443630b4620165c8b173e7265e17526fe2787ae594364dd6d839ad58f2fc007"},{"hashAlg":"sha384","digest":"90697eec39ed47f2b7ed278aa6fe6a1c073fcc7f3af54299fb95ac8a18c771acbac71e25b5a5639554943bfdfab76737"},{"hashAlg":"sha512","digest":"b3d9598ca0aa5da28be1c97a45d53cc5c72a80e61c439c8bf3e89c5c0661f49df8fa34019a21cd5e31261ae3a3a87ef4592d8010aad6a5ecdc9dbaae38cd1470"}]}]} diff --git a/trident.spec b/trident.spec index 9038c6310..615a8ac44 100644 --- a/trident.spec +++ b/trident.spec @@ -123,6 +123,21 @@ SystemD timer for update polling with Harpoon. # ------------------------------------------------------------------------------ +%package static-pcrlock-files +Summary: Statically defined .pcrlock files +Requires: %{name} + +%description static-pcrlock-files +Statically defined .pcrlock files for PCR-based encryption. This is a workaround needed because AZL +3.0 fails to provide these files inside the same package as the systemd-pcrlock binary; this should +be removed once the fix is merged in AZL 4.0. + +%files static-pcrlock-files +%dir %{_sharedstatedir}/pcrlock.d +%{_sharedstatedir}/pcrlock.d/ + +# ------------------------------------------------------------------------------ + %build export TRIDENT_VERSION="%{trident_version}" cargo build --release @@ -144,4 +159,15 @@ mkdir -p %{buildroot}/etc/%{name} # Copy the trident-selinuxpolicies file to /usr/share/selinux/packages/ mkdir -p %{buildroot}%{_datadir}/selinux/packages/ -install -m 755 %{SOURCE2} %{buildroot}%{_datadir}/selinux/packages/ \ No newline at end of file +install -m 755 %{SOURCE2} %{buildroot}%{_datadir}/selinux/packages/ + +# Copy statically defined .pcrlock files into /var/lib/pcrlock.d +pcrlockroot="%{buildroot}%{_sharedstatedir}/pcrlock.d" +mkdir -p "$pcrlockroot" +( + cd %{_sourcedir}/static-pcrlock-files + find . -type f -print0 | while IFS= read -r -d '' f; do + mkdir -p "$pcrlockroot/$(dirname "$f")" + install -m 644 "$f" "$pcrlockroot/$f" + done +) \ No newline at end of file From dc5301644f8cee7f375d015379fe8d5d2e055efe Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 9 Jun 2025 22:05:17 +0000 Subject: [PATCH 60/99] Merged PR 23443: engineering: Build FT Image in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Build the FT image in CI builds so pr-e2e and local tests can easily download it. Validated stage runs fine here: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=831769&view=results ---- #### AI description (iteration 1) #### PR Classification New feature – this PR adds a dedicated CI pipeline step to build the FT image. #### PR Summary This pull request introduces a new CI stage for building the FT base image, enabling the creation of a "trident-functest" image with configurable build parameters. - Modified `/.pipelines/templates/e2e-template.yml` to add a conditional stage that triggers the build-image template for FT in CI and PR-e2e stages. Related work items: #12483 --- .pipelines/templates/e2e-template.yml | 13 +++++++++++++ .../templates/stages/build_image/build-image.yml | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index f38cfdc26..a30e1c3c2 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -81,6 +81,19 @@ stages: - ${{ if eq(parameters.stageType, 'ci') }}: - template: stages/validate_makefile/dev-build.yml + # Build FT base Image, only in CI + - ${{ if eq(parameters.stageType, 'ci') }}: + # Build Trident installer ISO (host) + - template: stages/build_image/build-image.yml + parameters: + imageName: trident-functest + dependsOnTrident: false + baseimgBuildType: ${{ parameters.baseimgBuildType }} + baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} + micBuildType: ${{ parameters.micBuildType }} + micVersion: ${{ parameters.micVersion }} + clones: 1 + # Build Trident container image - template: stages/build_docker_image/trident-container.yml parameters: diff --git a/.pipelines/templates/stages/build_image/build-image.yml b/.pipelines/templates/stages/build_image/build-image.yml index 2c021cb7d..205474fa9 100644 --- a/.pipelines/templates/stages/build_image/build-image.yml +++ b/.pipelines/templates/stages/build_image/build-image.yml @@ -43,11 +43,15 @@ parameters: type: number default: 2 + - name: dependsOnTrident + type: boolean + default: true + stages: - stage: TridentTestImg_${{ replace(parameters.imageName, '-', '_') }} displayName: Build ${{ parameters.imageName }} dependsOn: - - ${{ if eq(parameters.runtimeEnv, 'host') }}: + - ${{ if and(eq(parameters.runtimeEnv, 'host'), parameters.dependsOnTrident) }}: - GetTridentBinaries_rpms - ${{ else }}: [] @@ -72,7 +76,7 @@ stages: artifactName: trident-binaries targetPath: "$(Build.ArtifactStagingDirectory)/trident" displayName: Download Trident RPMs - condition: eq('${{ parameters.runtimeEnv }}', 'host') + condition: and(eq('${{ parameters.runtimeEnv }}', 'host'), eq('${{ parameters.dependsOnTrident }}', true)) - template: ../common_tasks/find-base-image-version.yml parameters: From 2472b0821e19ad8935b5c9b445523dc0f6ecd834 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Tue, 10 Jun 2025 00:03:07 +0000 Subject: [PATCH 61/99] Merged PR 23437: Populate sha384 in HC if user sets it to ignored This eliminates the need to have a new URL during updates (or to force re-staging of a clean install) by having Trident query the image hash at the start of servicing before deciding whether the HC has changed. ---- #### AI description (iteration 1) #### PR Classification This pull request is a feature enhancement that populates the sha384 checksum in the Host Configuration when it is set to ignored. #### PR Summary The changes introduce a new OS image loading process that calculates and assigns the sha384 checksum during image load while propagating the updated image object throughout the update and clean install flows. Key modifications include: - **`src/osimage/mod.rs`**: Adds the `load` function to load an OS image, compute its metadata sha384 when set to ignored, and validate system architecture; also introduces the `metadata_sha384` accessor. - **`src/osimage/cosi/mod.rs`**: Updates the COSI metadata reader to return a tuple (metadata and computed sha384), stores the new `metadata_sha384` field in the Cosi structure, and refines hash validation. - **`src/lib.rs`, `src/engine/update.rs`, and `src/engine/clean_install.rs`**: Adapt update and clean install routines to use the new `OsImage::load` output by passing the computed image object. - **`src/osimage/mock.rs`**: Implements a mock `metadata_sha384` method to ensure test consistency. - **Module reorganization**: Removes the deprecated `src/engine/osimage.rs` file, consolidating OS image logic in the new location. Related work items: #12479 --- src/engine/clean_install.rs | 8 +++-- src/engine/mod.rs | 1 - src/engine/osimage.rs | 53 --------------------------- src/engine/update.rs | 6 ++-- src/lib.rs | 14 ++++++-- src/osimage/cosi/mod.rs | 31 +++++++++++----- src/osimage/mock.rs | 4 +++ src/osimage/mod.rs | 62 ++++++++++++++++++++++++++++++-- tools/storm/helpers/ab_update.go | 1 + 9 files changed, 109 insertions(+), 71 deletions(-) delete mode 100644 src/engine/osimage.rs diff --git a/src/engine/clean_install.rs b/src/engine/clean_install.rs index 1c39ca9c9..ed2fb5f38 100644 --- a/src/engine/clean_install.rs +++ b/src/engine/clean_install.rs @@ -25,8 +25,9 @@ use trident_api::{ use crate::{ datastore::DataStore, - engine::{self, boot::esp, bootentries, osimage, storage, EngineContext, SUBSYSTEMS}, + engine::{self, boot::esp, bootentries, storage, EngineContext, SUBSYSTEMS}, monitor_metrics, + osimage::OsImage, subsystems::hooks::HooksSubsystem, ExitKind, SAFETY_OVERRIDE_CHECK_PATH, }; @@ -41,6 +42,7 @@ pub(crate) fn clean_install( state: &mut DataStore, allowed_operations: &Operations, multiboot: bool, + image: OsImage, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, ) -> Result { info!("Starting clean install"); @@ -73,6 +75,7 @@ pub(crate) fn clean_install( &mut subsystems, state, host_config, + image, #[cfg(feature = "grpc-dangerous")] sender, )?; @@ -169,6 +172,7 @@ fn stage_clean_install( subsystems: &mut MutexGuard>>, state: &mut DataStore, host_config: &HostConfiguration, + image: OsImage, #[cfg(feature = "grpc-dangerous")] sender: &mut Option< mpsc::UnboundedSender>, >, @@ -193,7 +197,7 @@ fn stage_clean_install( partition_paths: Default::default(), // Will be initialized later disk_uuids: Default::default(), // Will be initialized later install_index: 0, // Will be initialized later - image: osimage::load_os_image(host_config)?, + image: Some(image), storage_graph: engine::build_storage_graph(&host_config.storage)?, // Build storage graph filesystems: Vec::new(), // Will be populated after dynamic validation }; diff --git a/src/engine/mod.rs b/src/engine/mod.rs index f30efb0e1..7a7281b62 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -38,7 +38,6 @@ mod clean_install; mod context; mod kexec; mod newroot; -mod osimage; pub mod provisioning_network; pub mod rollback; mod update; diff --git a/src/engine/osimage.rs b/src/engine/osimage.rs deleted file mode 100644 index 0f7ced71c..000000000 --- a/src/engine/osimage.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::fmt::Write; - -use log::{debug, info}; - -use sysdefs::arch::SystemArchitecture; -use trident_api::{ - config::HostConfiguration, - error::{InvalidInputError, ReportError, TridentError}, -}; - -use crate::osimage::OsImage; - -/// Attempts to load an OS image based on the provided host configuration. -pub(super) fn load_os_image( - host_config: &HostConfiguration, -) -> Result, TridentError> { - let Some(os_image_source) = &host_config.image else { - return Err(TridentError::new(InvalidInputError::MissingOsImage)); - }; - - debug!("Loading COSI file '{}'", os_image_source.url); - let os_image = OsImage::cosi(os_image_source).structured(InvalidInputError::LoadCosi { - url: os_image_source.url.clone(), - })?; - - info!( - "Successfully loaded OS image of type '{}' from '{}'", - os_image.name(), - os_image.source() - ); - - // Ensure the OS image architecture matches the current system architecture - if SystemArchitecture::current() != os_image.architecture() { - return Err(TridentError::new( - InvalidInputError::MismatchedArchitecture { - expected: SystemArchitecture::current().into(), - actual: os_image.architecture().into(), - }, - )); - } - - debug!( - "OS image provides the following mount points:\n{}", - os_image - .available_mount_points() - .fold(String::new(), |mut acc, p| { - let _ = writeln!(acc, " - {}", p.display()); - acc - }) - ); - - Ok(Some(os_image)) -} diff --git a/src/engine/update.rs b/src/engine/update.rs index 30b8d5f30..b2fdaf7e1 100644 --- a/src/engine/update.rs +++ b/src/engine/update.rs @@ -18,11 +18,12 @@ use trident_api::{ use crate::{ datastore::DataStore, engine::{ - self, bootentries, osimage, rollback, + self, bootentries, rollback, storage::{self, verity}, EngineContext, NewrootMount, SUBSYSTEMS, }, monitor_metrics, + osimage::OsImage, subsystems::hooks::HooksSubsystem, ExitKind, }; @@ -36,6 +37,7 @@ pub(crate) fn update( host_config: &HostConfiguration, state: &mut DataStore, allowed_operations: &Operations, + image: OsImage, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, ) -> Result { info!("Starting update"); @@ -59,7 +61,7 @@ pub(crate) fn update( ab_active_volume: state.host_status().ab_active_volume, disk_uuids: state.host_status().disk_uuids.clone(), install_index: state.host_status().install_index, - image: osimage::load_os_image(host_config)?, + image: Some(image), storage_graph: engine::build_storage_graph(&host_config.storage)?, // Build storage graph filesystems: Vec::new(), // Will be populated after dynamic validation }; diff --git a/src/lib.rs b/src/lib.rs index 67722d6dc..808069709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,8 @@ pub use logging::{ }; pub use orchestrate::OrchestratorConnection; +use crate::osimage::OsImage; + /// Trident version as provided by environment variables at build time pub const TRIDENT_VERSION: &str = match option_env!("TRIDENT_VERSION") { Some(v) => v, @@ -452,7 +454,7 @@ impl Trident { multiboot: bool, #[cfg(feature = "grpc-dangerous")] sender: &mut Option, ) -> Result { - let host_config = self + let mut host_config = self .host_config .clone() .structured(InternalError::Internal( @@ -502,6 +504,8 @@ impl Trident { } } + let image = OsImage::load(&mut host_config.image)?; + if datastore.host_status().spec != host_config { debug!("Host Configuration has been updated"); @@ -511,6 +515,7 @@ impl Trident { datastore, &allowed_operations, multiboot, + image, #[cfg(feature = "grpc-dangerous")] sender, ) @@ -558,6 +563,7 @@ impl Trident { datastore, &allowed_operations, multiboot, + image, #[cfg(feature = "grpc-dangerous")] sender, ) @@ -606,13 +612,15 @@ impl Trident { .map_err(Into::into) .message("Invalid Host Configuration provided")?; + let image = OsImage::load(&mut host_config.image)?; + // If HS.spec in the datastore is different from the new HC, need to both stage and // finalize the update, regardless of state if datastore.host_status().spec != host_config { debug!("Host Configuration has been updated"); // If allowed operations include 'stage', start update if allowed_operations.has_stage() { - engine::update(&host_config, datastore, &allowed_operations, #[cfg(feature = "grpc-dangerous")] sender).message("Failed to execute an update") + engine::update(&host_config, datastore, &allowed_operations, image, #[cfg(feature = "grpc-dangerous")] sender).message("Failed to execute an update") } else { warn!("Host Configuration has been updated but allowed operations do not include 'stage'. Add 'stage' and re-run to stage the update"); Ok(ExitKind::Done) @@ -641,7 +649,7 @@ impl Trident { ServicingState::AbUpdateFinalized | ServicingState::Provisioned => { // Need to either re-execute the failed update OR inform the user that no update // is needed. - engine::update(&host_config, datastore, &allowed_operations,#[cfg(feature = "grpc-dangerous")] sender).message("Failed to update host") + engine::update(&host_config, datastore, &allowed_operations, image, #[cfg(feature = "grpc-dangerous")] sender).message("Failed to update host") } servicing_state => { Err(TridentError::new(InternalError::UnexpectedServicingState { diff --git a/src/osimage/cosi/mod.rs b/src/osimage/cosi/mod.rs index fe5dc7b04..9e75e9671 100644 --- a/src/osimage/cosi/mod.rs +++ b/src/osimage/cosi/mod.rs @@ -8,7 +8,10 @@ use anyhow::{bail, ensure, Context, Error}; use log::{debug, trace}; use osutils::hashing_reader::{HashingReader, HashingReader384}; use tar::Archive; -use trident_api::config::{ImageSha384, OsImage}; +use trident_api::{ + config::{ImageSha384, OsImage}, + primitives::hash::Sha384Hash, +}; use url::Url; use sysdefs::arch::SystemArchitecture; @@ -34,6 +37,7 @@ pub(super) struct Cosi { source: Url, entries: HashMap, metadata: CosiMetadata, + metadata_sha384: Sha384Hash, reader: CosiReader, } @@ -57,13 +61,16 @@ impl Cosi { let entries = read_entries_from_tar_archive(cosi_reader.reader()?)?; trace!("Collected {} COSI entries", entries.len()); + let (metadata, sha384) = read_cosi_metadata(&cosi_reader, &entries, source.sha384.clone()) + .context("Failed to read COSI file metadata.")?; + // Create a new COSI instance. Ok(Cosi { - metadata: read_cosi_metadata(&cosi_reader, &entries, source.sha384.clone()) - .context("Failed to read COSI file metadata.")?, + metadata, entries, source: source.url.clone(), reader: cosi_reader, + metadata_sha384: sha384, }) } @@ -97,6 +104,10 @@ impl Cosi { pub(super) fn architecture(&self) -> SystemArchitecture { self.metadata.os_arch } + + pub(super) fn metadata_sha384(&self) -> Sha384Hash { + self.metadata_sha384.clone() + } } /// Converts a COSI metadata Image to an OsImageFileSystem. @@ -202,7 +213,7 @@ fn read_cosi_metadata( cosi_reader: &CosiReader, entries: &HashMap, expected_sha384: ImageSha384, -) -> Result { +) -> Result<(CosiMetadata, Sha384Hash), Error> { trace!( "Retrieving metadata from COSI file from '{}'", COSI_METADATA_PATH @@ -228,9 +239,10 @@ fn read_cosi_metadata( .read_to_string(&mut raw_metadata) .context("Failed to read COSI metadata")?; + let actual_sha384 = Sha384Hash::from(metadata_reader.hash()); if let ImageSha384::Checksum(ref sha384) = expected_sha384 { - if metadata_reader.hash() != sha384.as_str() { - bail!("COSI metadata hash does not match expected hash"); + if actual_sha384 != *sha384 { + bail!("COSI metadata hash '{actual_sha384}' does not match expected hash '{sha384}'"); } } trace!("Raw COSI metadata:\n{}", raw_metadata); @@ -257,7 +269,7 @@ fn read_cosi_metadata( metadata.version.major, metadata.version.minor ); - Ok(metadata) + Ok((metadata, actual_sha384)) } /// Validates the COSI metadata version. @@ -564,7 +576,8 @@ mod tests { &entries, ImageSha384::Checksum(metadata_sha384.into()), ) - .unwrap(); + .unwrap() + .0; // Now check that the images in the metadata have the correct entries. for (image, (path, offset, size)) in metadata.images.iter().zip(image_paths.iter()) { @@ -793,6 +806,7 @@ mod tests { images, }, reader: CosiReader::Mock(data), + metadata_sha384: Sha384Hash::from("0".repeat(96)), } } @@ -811,6 +825,7 @@ mod tests { os_packages: None, }, reader: CosiReader::Mock(Cursor::new(Vec::::new())), + metadata_sha384: Sha384Hash::from("0".repeat(96)), }; // Weird behavior with none/multiple ESPs is primarily tested by the diff --git a/src/osimage/mock.rs b/src/osimage/mock.rs index f44c1259e..16bbe6339 100644 --- a/src/osimage/mock.rs +++ b/src/osimage/mock.rs @@ -144,6 +144,10 @@ impl MockOsImage { pub fn architecture(&self) -> SystemArchitecture { self.os_arch } + + pub fn metadata_sha384(&self) -> Sha384Hash { + Sha384Hash::from("0".repeat(96)) + } } impl MockImage { diff --git a/src/osimage/mod.rs b/src/osimage/mod.rs index d3b855a3b..a3be7fc8b 100644 --- a/src/osimage/mod.rs +++ b/src/osimage/mod.rs @@ -1,10 +1,11 @@ use std::{ - fmt::{Display, Formatter}, + fmt::{Display, Formatter, Write}, io::{Error as IoError, Read}, path::{Path, PathBuf}, }; use anyhow::Error; +use log::{debug, info}; use serde::{Deserialize, Serialize}; use url::Url; @@ -12,7 +13,12 @@ use sysdefs::{ arch::SystemArchitecture, filesystems::RealFilesystemType, osuuid::OsUuid, partition_types::DiscoverablePartitionType, }; -use trident_api::{config, constants::ROOT_MOUNT_POINT_PATH, primitives::hash::Sha384Hash}; +use trident_api::{ + config::{self, ImageSha384}, + constants::ROOT_MOUNT_POINT_PATH, + error::{InvalidInputError, ReportError, TridentError}, + primitives::hash::Sha384Hash, +}; mod cosi; @@ -55,6 +61,50 @@ impl OsImage { Self(OsImageInner::Mock(Box::new(mock_os_image))) } + /// Load the OS given the image source from the Host Configuration and either validate or + /// populate the associated metadata sha384 checksum. + pub(crate) fn load(image_source: &mut Option) -> Result { + let Some(ref mut image_source) = image_source else { + return Err(TridentError::new(InvalidInputError::MissingOsImage)); + }; + + debug!("Loading COSI file '{}'", image_source.url); + let os_image = OsImage::cosi(image_source).structured(InvalidInputError::LoadCosi { + url: image_source.url.clone(), + })?; + if image_source.sha384 == ImageSha384::Ignored { + image_source.sha384 = ImageSha384::Checksum(os_image.metadata_sha384()); + } + + info!( + "Successfully loaded OS image of type '{}' from '{}'", + os_image.name(), + os_image.source() + ); + + // Ensure the OS image architecture matches the current system architecture + if SystemArchitecture::current() != os_image.architecture() { + return Err(TridentError::new( + InvalidInputError::MismatchedArchitecture { + expected: SystemArchitecture::current().into(), + actual: os_image.architecture().into(), + }, + )); + } + + debug!( + "OS image provides the following mount points:\n{}", + os_image + .available_mount_points() + .fold(String::new(), |mut acc, p| { + let _ = writeln!(acc, " - {}", p.display()); + acc + }) + ); + + Ok(os_image) + } + /// Returns the name of the OS image type. pub(crate) fn name(&self) -> &'static str { match &self.0 { @@ -122,6 +172,14 @@ impl OsImage { self.filesystems() .find(|fs| fs.mount_point == Path::new(ROOT_MOUNT_POINT_PATH)) } + + pub(crate) fn metadata_sha384(&self) -> Sha384Hash { + match &self.0 { + OsImageInner::Cosi(cosi) => cosi.metadata_sha384(), + #[cfg(test)] + OsImageInner::Mock(mock) => mock.metadata_sha384(), + } + } } #[derive(Debug)] diff --git a/tools/storm/helpers/ab_update.go b/tools/storm/helpers/ab_update.go index 06f0abf64..a873e10db 100644 --- a/tools/storm/helpers/ab_update.go +++ b/tools/storm/helpers/ab_update.go @@ -135,6 +135,7 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { // Update the image URL in the configuration h.config["image"].(map[string]any)["url"] = newUrl + h.config["image"].(map[string]any)["sha384"] = "ignored" // Set the config to NOT self-upgrade trident, ok := h.config["trident"].(map[string]any) From ec8452fa3afb99b15a26b684518fe393f488f04a Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 10 Jun 2025 00:20:36 +0000 Subject: [PATCH 62/99] Merged PR 23379: engineering: UKI on Baremetal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description - Improvements to bare metal debug script & miniproxy. - Disable cloud-init networking module when trident is configuring the network via cfg file. - Re-enable UKI tests on baremetal. ---- #### AI description (iteration 1) #### PR Classification Enhancement aimed at improving connection handling and updating baremetal testing configurations. #### PR Summary This pull request refactors the miniproxy connection handling for more robust concurrency management and updates various testing and deployment configurations to support baremetal environments. - **`/tools/cmd/miniproxy/main.go`**: Replaced channel-based synchronization with a sync.WaitGroup for bidirectional data copying, improved logging with humanize for byte counts, and renamed variables for clarity. - **`/.pipelines/templates/e2e-template.yml`**: Updated the testing pipeline by deactivating legacy VM testing templates and activating the baremetal testing template. - **`/scripts/lab-netlaunch`**: Modified jumpbox configuration (updating name and IP) and changed miniproxy command-line flags from `-s/-d` to `-l/-f`. - **`/e2e_tests/target-configurations.yaml`**: Added new pullrequest configurations for baremetal targets. - **`/tools/go.mod` & `/tools/go.sum`**: Added dependency for the go-humanize library to support enhanced logging. Related work items: #12291 --- .../testing_baremetal/update_host_config.py | 68 ++-- e2e_tests/target-configurations.yaml | 22 +- .../usr-verity/trident-config.yaml | 6 +- scripts/lab-netlaunch | 339 ++++++++++++------ src/subsystems/network.rs | 23 ++ systemd/trident.service | 2 +- tools/cmd/miniproxy/main.go | 45 +-- tools/go.mod | 1 + tools/go.sum | 2 + trident_api/src/error.rs | 3 + 10 files changed, 351 insertions(+), 160 deletions(-) diff --git a/.pipelines/templates/stages/testing_baremetal/update_host_config.py b/.pipelines/templates/stages/testing_baremetal/update_host_config.py index 7dbf00be5..9c14ac38d 100755 --- a/.pipelines/templates/stages/testing_baremetal/update_host_config.py +++ b/.pipelines/templates/stages/testing_baremetal/update_host_config.py @@ -8,39 +8,58 @@ import logging +def wait_online_script(interface_name: str) -> str: + """ + Generates a script to add a wait for the given network interface to be + online before starting the Trident service. + """ + return "\n".join( + [ + "set -eux", + f"systemctl enable systemd-networkd-wait-online@{interface_name}.service", + "mkdir -p /etc/systemd/system/trident.service.d", + "cat << EOF > /etc/systemd/system/trident.service.d/override.conf", + "[Unit]", + f"Requires=systemd-networkd-wait-online@{interface_name}.service", + "EOF", + ] + ) + + def update_trident_host_config( host_configuration: str, oam_ip: str, interface_name: str, - oam_gateway: Optional[str] = None, - oam_mac: Optional[str] = None, + interface_mac: Optional[str] = None, + network_gateway: Optional[str] = None, use_dhcp: bool = False, ): logging.info("Updating host config section of trident.yaml") - logging.info("oam_ip: %s", oam_ip) - logging.info("oam_gateway: %s", oam_gateway) os = host_configuration.setdefault("os", {}) - network = os.setdefault("network", {}) - ethernets = network.setdefault("ethernets", {}) - - # Ensure that all interface dhcp4 settings are consistent - for ethernet in ethernets: - if "dhcp4" in ethernets[ethernet]: - ethernets[ethernet]["dhcp4"] = use_dhcp - eno_interface = ethernets.setdefault(interface_name, {}) + main_interface = { + "addresses": [f"{oam_ip}/23"], + "dhcp4": use_dhcp, + "set-name": interface_name, + } # Temporary fix for #8837. - if oam_mac: - eno_interface["match"] = {"macaddress": oam_mac} - - eno_interface.setdefault("addresses", []).append(oam_ip + "/23") - eno_interface["dhcp4"] = use_dhcp - if oam_gateway: - eno_interface.setdefault("routes", []).append( - {"to": "0.0.0.0/0", "via": oam_gateway} + if interface_mac: + main_interface["match"] = {"macaddress": interface_mac} + + if network_gateway: + main_interface.setdefault("routes", []).append( + {"to": "0.0.0.0/0", "via": network_gateway} ) + # Override network to only preserve the eno interface. + os["network"] = { + "version": 2, + "ethernets": { + interface_name: main_interface, + }, + } + logging.info("Updating os disks device in trident.yaml") disks = host_configuration.get("storage", {}).get("disks", []) for disk in disks: @@ -49,8 +68,13 @@ def update_trident_host_config( elif disk["id"] == "disk2": disk["device"] = "/dev/sdb" - internal_params = host_configuration.setdefault("internalParams", {}) - internal_params["waitForSystemdNetworkd"] = True + host_configuration.setdefault("scripts", {}).setdefault("postConfigure", []).append( + { + "content": wait_online_script(interface_name), + "name": "wait-for-network", + "runOn": ["all"], + } + ) logging.info( "Final trident_yaml content post all the updates: %s", host_configuration diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 1f8f68468..495ccb3d1 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -5,7 +5,7 @@ bareMetal: # TODO(12276) re-enable: #- combined - encrypted-partition - split - # TODO(12291) re-enable: #- usr-verity + - usr-verity validation: - base # TODO(12276) re-enable: #- combined @@ -21,8 +21,8 @@ bareMetal: - root-verity - simple - split - # TODO(12291) re-enable: #- usr-verity - # TODO(12291) re-enable: #- usr-verity-raid + - usr-verity + - usr-verity-raid weekly: - base # TODO(12276) re-enable: #- combined @@ -39,8 +39,8 @@ bareMetal: - root-verity - simple - split - # TODO(12291) re-enable: #- usr-verity - # TODO(12291) re-enable: #- usr-verity-raid + - usr-verity + - usr-verity-raid container: daily: - base @@ -55,8 +55,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - # TODO(12291) re-enable: #- usr-verity - # TODO(12291) re-enable: #- usr-verity-raid + - usr-verity + - usr-verity-raid validation: - base # TODO(12276) re-enable: #- combined @@ -70,8 +70,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - # TODO(12291) re-enable: #- usr-verity - # TODO(12291) re-enable: #- usr-verity-raid + - usr-verity + - usr-verity-raid weekly: - base # TODO(12276) re-enable: #- combined @@ -85,8 +85,8 @@ bareMetal: # TODO(12276) re-enable: #- rerun - root-verity - simple - # TODO(12291) re-enable: #- usr-verity - # TODO(12291) re-enable: #- usr-verity-raid + - usr-verity + - usr-verity-raid virtualMachine: host: daily: diff --git a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml index 8889990c8..082dbe506 100644 --- a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml +++ b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml @@ -8,7 +8,7 @@ trident: storage: disks: - id: os - device: /dev/sda + device: /dev/disk/by-path/pci-0000:00:1f.2-ata-2 partitionTableType: gpt partitions: - id: esp @@ -39,6 +39,10 @@ storage: - 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: boot diff --git a/scripts/lab-netlaunch b/scripts/lab-netlaunch index 727eab23a..fafc05986 100755 --- a/scripts/lab-netlaunch +++ b/scripts/lab-netlaunch @@ -21,6 +21,10 @@ # This is necessary because the SSHD is configured with `GatewayPorts no`, which means that the # forwarded port is only accessible from the jumpbox itself. Miniproxy is used to forward the # connection to a port that is accessible from the outside. +# +# +# More info about lab access: +# https://dev.azure.com/msazuredev/AzureForOperatorsIndustry/_wiki/wikis/AzureForOperatorsIndustry.wiki/23997/AEP-Labs-on-prem-access def delete_me_to_run(): @@ -36,13 +40,18 @@ def delete_me_to_run(): # Prevent accidental execution of this script. delete_me_to_run() -import subprocess +import inspect import json -import time -from typing import List +import logging import psutil -from pathlib import Path +import signal +import subprocess +import time +import threading + from contextlib import contextmanager +from pathlib import Path +from typing import List # This is an arbitrary port that we will use for netlaunch to listen on. LISTEN_HTTP_PORT_LOCAL = 32365 @@ -68,36 +77,63 @@ BMC_SHH_PORT_LOCAL = 32556 # These are the details of the jumpbox that we will use to establish the SSH tunnel. JUMPBOX_SUBSCRIPTION = "f18e9c89-62a2-4c08-95cf-12d482634db4" JUMPBOX_RESOURCE_GROUP = "b37-westus3-labs-rg" -JUMPBOX_NAME = "b37-westus3-dev-azl3-arc-vm" -JUMPBOX_IP = "10.249.132.133" +JUMPBOX_NAME = "b37-westus3-aep-azl3-vm" +JUMPBOX_IP = "10.249.132.138" BAREMETAL_MACHINE_IP = "10.8.4.50/23" BAREMETAL_MACHINE_GATEWAY = "10.8.4.1" BAREMETAL_MACHINE_DEFAULT_INTERFACE = "eth0" -def recursive_kill(proc: int): +logging.basicConfig(level=logging.INFO) +log = logging.getLogger("lab-netlaunch") + + +def recursive_kill(proc: int, signal_type: signal.Signals = signal.SIGINT): """Given a process ID, kill the process and all its children.""" psutil_proc = psutil.Process(proc) - print("") - print(f"Terminating process {psutil_proc.name()}[{proc}]") + log.info( + f"Sending signal {signal_type.name} to process {psutil_proc.name()}[{proc}]" + ) for child in psutil_proc.children(recursive=True): - print(f"Terminating child process {child.name()}[{child.pid}]") - child.kill() - psutil_proc.kill() + log.info( + f"Sending signal {signal_type.name} to child process {child.name()}[{child.pid}]" + ) + child.send_signal(signal_type) + psutil_proc.send_signal(signal_type) @contextmanager -def background_process(cmd: List[str]): +def background_process(cmd: List[str], **kwargs): """Run a command in the background and yield the process object. On exit, kill the process and all its children and wait for it to finish.""" - print(f"Running command: {cmd[0]}") - proc = subprocess.Popen(cmd) - print(f"Started process {cmd[0]}[{proc.pid}]") + log.info(f"Running command: {cmd[0]}") + proc = subprocess.Popen(cmd, text=True, **kwargs) + log.info(f"Started process {cmd[0]}[{proc.pid}]") try: yield proc finally: + if proc.poll() is not None: + log.info( + f"Process {cmd[0]}[{proc.pid}] already exited with code {proc.returncode}." + ) + return + + log.info(f"Killing process '{cmd[0]}' and all its children...") recursive_kill(proc.pid) + log.info(f"Waiting for process {cmd[0]}[{proc.pid}] to finish...") + start_time = time.time() + while proc.poll() is None and time.time() - start_time < 10: + time.sleep(0.2) + if proc.poll() is None: + log.warning( + f"Process {cmd[0]}[{proc.pid}] did not exit after SIGINT, sending SIGKILL..." + ) + recursive_kill(proc.pid, signal.SIGKILL) + else: + log.info( + f"Process {cmd[0]}[{proc.pid}] exited graciously with code {proc.returncode}" + ) proc.wait() @@ -137,93 +173,188 @@ def get_secret_value(keyvault: str, secret: str) -> str: return json.loads(res.stdout)["value"] -bmc_username = get_secret_value("kvAfoStaging", "j25-bmc-username") -bmc_password = get_secret_value("kvAfoStaging", "j25-bmc-password") - -netlaunch_config_file = Path("./input/baremetal-netlaunch.yaml") - -contents = f"""# This file is generated by {__file__} -netlaunch: - bmc: - # Connect to localhost because we will establish an SSH tunnel to the BMC. - ip: "localhost" - # Connect to the local port that we will forward to the BMC's Redfish service. - port: {BMC_REDFISH_PORT_LOCAL} - username: "{bmc_username}" - password: "{bmc_password}" - # Set up the serial-over-SSH connection to the BMC. - serialOverSsh: - # Connect to forwarded port. - sshPort: {BMC_SHH_PORT_LOCAL} - comPort: {BMC_SERIAL_PORT} - output: "baremetal-serial.log" - # Instruct netlaunch to tell Trident and the BMC to connect to this address. (The jumpbox) - announceIp: "{JUMPBOX_IP}" - announcePort: {LISTEN_HTTP_PORT_REMOTE} - -# Set up the initial network on the baremetal machine. -iso: - preTridentScript: | - ip addr add {BAREMETAL_MACHINE_IP} dev {BAREMETAL_MACHINE_DEFAULT_INTERFACE} - ip route add default via {BAREMETAL_MACHINE_GATEWAY} dev {BAREMETAL_MACHINE_DEFAULT_INTERFACE} - while true; do - curl -s -o /dev/null "http://{JUMPBOX_IP}:{LISTEN_HTTP_PORT_REMOTE}" && break - sleep 1 - done -""" - -netlaunch_config_file.write_text(contents) -print(f"Generated {netlaunch_config_file}:\n{contents}") - -print("Copying miniproxy to the jumpbox...") -with open("bin/miniproxy", "rb") as miniproxy: - copy_cmd = get_ssh_command_base() - # Write the miniproxy binary to the jumpbox via stdin and make it - # executable. - copy_cmd.extend( - [ - "cat - > ~/miniproxy && chmod +x ~/miniproxy", - ] - ) - # Feed in the miniproxy binary to the stdin of the SSH command. - subprocess.run( - copy_cmd, - stdin=miniproxy, - check=True, - stdout=subprocess.DEVNULL, - ) +def generate_netlaunch_config() -> Path: + bmc_username = get_secret_value("kvAfoStaging", "j25-bmc-username") + bmc_password = get_secret_value("kvAfoStaging", "j25-bmc-password") + + netlaunch_config_file = Path("./input/baremetal-netlaunch.yaml") + + contents = f""" + # This file is generated by {__file__} + netlaunch: + bmc: + # Connect to localhost because we will establish an SSH tunnel to the BMC. + ip: "localhost" + # Connect to the local port that we will forward to the BMC's Redfish service. + port: {BMC_REDFISH_PORT_LOCAL} + username: "{bmc_username}" + password: "{bmc_password}" + # Set up the serial-over-SSH connection to the BMC. + serialOverSsh: + # Connect to forwarded port. + sshPort: {BMC_SHH_PORT_LOCAL} + comPort: {BMC_SERIAL_PORT} + output: "baremetal-serial.log" + # Instruct netlaunch to tell Trident and the BMC to connect to this address. (The jumpbox) + announceIp: "{JUMPBOX_IP}" + announcePort: {LISTEN_HTTP_PORT_REMOTE} + + # Set up the initial network on the baremetal machine. + iso: + preTridentScript: | + ip addr add {BAREMETAL_MACHINE_IP} dev {BAREMETAL_MACHINE_DEFAULT_INTERFACE} + ip route add default via {BAREMETAL_MACHINE_GATEWAY} dev {BAREMETAL_MACHINE_DEFAULT_INTERFACE} + while true; do + curl -s -o /dev/null "http://{JUMPBOX_IP}:{LISTEN_HTTP_PORT_REMOTE}" && break + sleep 1 + done + """ + contents = inspect.cleandoc(contents) + log.info(f"Generated {netlaunch_config_file}:\n{contents}") + netlaunch_config_file.write_text(contents) + return netlaunch_config_file + + +def copy_miniproxy_to_jumpbox(): + log.info("Copying miniproxy to the jumpbox...") + with open("bin/miniproxy", "rb") as miniproxy: + copy_cmd = get_ssh_command_base() + # Write the miniproxy binary to the jumpbox via stdin and make it + # executable. + copy_cmd.extend( + [ + "killall miniproxy && cat - > ~/miniproxy && chmod +x ~/miniproxy", + ] + ) + # Feed in the miniproxy binary to the stdin of the SSH command. + subprocess.run( + copy_cmd, + stdin=miniproxy, + check=True, + text=True, + ) + + +def make_tunnel_command() -> List[str]: + tunnel_command = get_ssh_command_base() + forwarding_params = [ + "-R", + f"{LISTEN_HTTP_PORT_LOCAL}:localhost:{LISTEN_HTTP_PORT_LOCAL}", + "-L", + f"{BMC_REDFISH_PORT_LOCAL}:{BMC_IP}:{BMC_REDFISH_PORT}", + "-L", + f"{BMC_SHH_PORT_LOCAL}:{BMC_IP}:{BMC_SSH_PORT}", + f"./miniproxy -l {LISTEN_HTTP_PORT_REMOTE} -f {LISTEN_HTTP_PORT_LOCAL}", + ] + + log.info(f"SSH Tunnel settings: {' '.join(forwarding_params)}") + + tunnel_command.extend(forwarding_params) + return tunnel_command + + +def wait_for_ssh_tunnel(tunnel: subprocess.Popen, timeout: float = 15) -> None: + """Wait for the SSH tunnel to be established.""" + log.info("Waiting for SSH tunnel to be established...") + start_time = time.time() + while tunnel.poll() is None: + if time.time() - start_time > timeout: + log.error("SSH tunnel did not establish within the timeout period.") + raise TimeoutError( + "SSH tunnel did not establish within the timeout period." + ) + + line = tunnel.stderr.readline() + if not line: + continue + + if "Failed to listen" in line: + log.error(f"SSH tunnel failed to establish: {line.strip()}") + raise RuntimeError(f"SSH tunnel failed to establish: {line.strip()}") + + if "Listening..." in line: + log.info(f"SSH tunnel established: {line.strip()}") + return + + +sigint_received = False + + +def handle_sigint(signum, frame): + global sigint_received + sigint_received = True + raise KeyboardInterrupt("Received SIGINT, shutting down gracefully...") + + +signal.signal(signal.SIGINT, handle_sigint) + + +class OutputReader: + def __init__(self, proc: subprocess.Popen, name: str = "OutputReader"): + self.log = logging.getLogger(name) + self.proc = proc + self.stderr_thread = threading.Thread(target=self._read_error, daemon=True) + self.stdout_thread = threading.Thread(target=self._read_output, daemon=True) + self.stderr_thread.start() + self.stdout_thread.start() + + def _read_output(self): + try: + while self.proc.poll() is None: + line = self.proc.stdout.readline() + if not line or sigint_received: + break + self.log.info(line.strip()) + except Exception as e: + log.error(f"Error reading output: {e}") + + def _read_error(self): + try: + while self.proc.poll() is None: + line = self.proc.stderr.readline() + if not line or sigint_received: + break + self.log.info(line.strip()) + except Exception as e: + log.error(f"Error reading error output: {e}") + + +def deploy(netlaunch_config_file: Path): + log.info("Establishing SSH tunnel to BMC...") + tunnel_command = make_tunnel_command() + + with background_process( + tunnel_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as ssh_tunnel: + wait_for_ssh_tunnel(ssh_tunnel) + log.info("SSH tunnel established successfully.") + reader = OutputReader(ssh_tunnel, "SSH-TUNNEL") + + # Start netlaunch + subprocess.run( + [ + "make", + "run-netlaunch", + f"NETLAUNCH_CONFIG={netlaunch_config_file}", + f"NETLAUNCH_PORT={LISTEN_HTTP_PORT_LOCAL}", + ], + check=True, + ) + + while True: + time.sleep(60) + + +def main(): + try: + nl_cfg = generate_netlaunch_config() + copy_miniproxy_to_jumpbox() + deploy(nl_cfg) + except KeyboardInterrupt: + log.warning("Received KeyboardInterrupt, shutting down...") -print("Establishing SSH tunnel to BMC...") -tunnel_command = get_ssh_command_base() -forwarding_params = [ - "-R", - f"{LISTEN_HTTP_PORT_LOCAL}:localhost:{LISTEN_HTTP_PORT_LOCAL}", - "-L", - f"{BMC_REDFISH_PORT_LOCAL}:{BMC_IP}:{BMC_REDFISH_PORT}", - "-L", - f"{BMC_SHH_PORT_LOCAL}:{BMC_IP}:{BMC_SSH_PORT}", - f"./miniproxy -s {LISTEN_HTTP_PORT_REMOTE} -d {LISTEN_HTTP_PORT_LOCAL}", -] - -print(f"SSH Tunnel settings: {' '.join(forwarding_params)}") - -tunnel_command.extend(forwarding_params) - -with background_process(tunnel_command) as ssh_tunnel: - # Wait for the tunnel to be established. We *should* do this by reading the - # output of the SSH command instead, but that's a bit more complicated. - time.sleep(5) - - # Start netlaunch - subprocess.run( - [ - "make", - "run-netlaunch", - f"NETLAUNCH_CONFIG={netlaunch_config_file}", - f"NETLAUNCH_PORT={LISTEN_HTTP_PORT_LOCAL}", - ], - check=True, - ) - while True: - time.sleep(60) +if __name__ == "__main__": + main() diff --git a/src/subsystems/network.rs b/src/subsystems/network.rs index 72d094a21..6faea67aa 100644 --- a/src/subsystems/network.rs +++ b/src/subsystems/network.rs @@ -1,3 +1,6 @@ +use std::fs; + +use anyhow::Context; use log::debug; use osutils::netplan; @@ -5,6 +8,9 @@ use trident_api::error::{ReportError, ServicingError, TridentError}; use crate::engine::{EngineContext, Subsystem}; +const CLOUD_INIT_DISABLE_FILE: &str = "/etc/cloud/cloud.cfg.d/99-use-trident-networking.cfg"; +const CLOUD_INIT_DISABLE_CONTENT: &str = "network: {config: disabled}"; + #[derive(Default, Debug)] pub struct NetworkSubsystem; impl Subsystem for NetworkSubsystem { @@ -19,6 +25,12 @@ impl Subsystem for NetworkSubsystem { debug!("Configuring network"); netplan::write(config).structured(ServicingError::WriteNetplanConfig)?; netplan::generate().structured(ServicingError::GenerateNetplanConfig)?; + + // We need to disable cloud-init's network configuration when + // Trident is configuring the network, otherwise cloud-init may + // deploy additional configurations that are undesired and may + // conflict with or otherwise affect Trident's network setup. + disable_cloud_init_networking()?; } None => { debug!("Network config not provided"); @@ -27,3 +39,14 @@ impl Subsystem for NetworkSubsystem { Ok(()) } } + +fn disable_cloud_init_networking() -> Result<(), TridentError> { + fs::write(CLOUD_INIT_DISABLE_FILE, CLOUD_INIT_DISABLE_CONTENT) + .with_context(|| { + format!( + "Failed to write to cloud-init disable file at {}", + CLOUD_INIT_DISABLE_FILE + ) + }) + .structured(ServicingError::DisableCloudInitNetworking) +} diff --git a/systemd/trident.service b/systemd/trident.service index c4f1216d4..7797523ae 100644 --- a/systemd/trident.service +++ b/systemd/trident.service @@ -7,4 +7,4 @@ ExecStart=trident commit Type=oneshot [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/tools/cmd/miniproxy/main.go b/tools/cmd/miniproxy/main.go index 1ffa7ab1a..1adf106e4 100644 --- a/tools/cmd/miniproxy/main.go +++ b/tools/cmd/miniproxy/main.go @@ -8,7 +8,9 @@ import ( "net" "os" "strconv" + "sync" + "github.com/dustin/go-humanize" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -29,8 +31,8 @@ func init() { rootCmd.PersistentFlags().Uint16VarP(&listen_port, "listen-port", "l", 0, "Port to listen on.") rootCmd.PersistentFlags().Uint16VarP(&dest_port, "forward-port", "f", 0, "Port to forward to.") - rootCmd.MarkFlagRequired("src-port") - rootCmd.MarkFlagRequired("dst-port") + rootCmd.MarkFlagRequired("listen-port") + rootCmd.MarkFlagRequired("forward-port") log.SetLevel(log.DebugLevel) } @@ -60,35 +62,36 @@ func run(cmd *cobra.Command, args []string) { } } -func handleConnection(source net.Conn, id uint64) { - log.WithField("id", id).WithField("address", source.RemoteAddr().String()).Info("Accepted connection") - defer source.Close() - dest, err := net.Dial("tcp", "localhost:"+strconv.Itoa(int(dest_port))) +func handleConnection(client net.Conn, id uint64) { + log.WithField("id", id).WithField("address", client.RemoteAddr().String()).Info("Accepted connection") + defer client.Close() + server, err := net.Dial("tcp", "localhost:"+strconv.Itoa(int(dest_port))) if err != nil { log.WithField("id", id).WithError(err).Fatalf("Failed to connect to destination") } - defer dest.Close() + defer server.Close() - log.WithField("id", id).WithField("address", dest.RemoteAddr().String()).Info("Connected to destination") + log.WithField("id", id).WithField("address", server.RemoteAddr().String()).Info("Connected to destination") - done := make(chan bool) + var wg sync.WaitGroup + + wg.Add(2) go func() { - defer source.Close() - defer dest.Close() - copied, _ := io.Copy(dest, source) - log.WithField("id", id).WithField("bytes", copied).Info("Done copying from src to dst") - done <- true + defer wg.Done() + defer client.Close() + defer server.Close() + copied, _ := io.Copy(server, client) + log.WithField("id", id).Infof("Done copying %s from client to server", humanize.IBytes(uint64(copied))) }() go func() { - defer source.Close() - defer dest.Close() - copied, _ := io.Copy(source, dest) - log.WithField("id", id).WithField("bytes", copied).Info("Done copying from dst to src") - done <- true + defer wg.Done() + defer client.Close() + defer server.Close() + copied, _ := io.Copy(client, server) + log.WithField("id", id).Infof("Done copying %s from server to client", humanize.IBytes(uint64(copied))) }() - <-done - <-done + wg.Wait() log.WithField("id", id).Info("Connection closed") } diff --git a/tools/go.mod b/tools/go.mod index 50e45ea61..0de70f2ef 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -10,6 +10,7 @@ replace storm => ../storm replace golang.org/x/net => golang.org/x/net v0.39.0 require ( + github.com/dustin/go-humanize v1.0.1 github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230530141715-da28e42c453f github.com/fatih/color v1.18.0 github.com/google/uuid v1.6.0 diff --git a/tools/go.sum b/tools/go.sum index 369ff8cec..bad425d31 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -21,6 +21,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitalocean/go-libvirt v0.0.0-20250512231903-57024326652b h1:o/RoLbHmKtibc3lMpuPcYGUjnboEORpLFnqtC89tfqY= github.com/digitalocean/go-libvirt v0.0.0-20250512231903-57024326652b/go.mod h1:B2R8mtJc0BNx0NvvfOajL5no+MaFDumyD5sHsxll62g= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index 8929e62ba..f6a1f7a1b 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -381,6 +381,9 @@ pub enum ServicingError { #[error("Failed to deploy images")] DeployImages, + #[error("Failed to disable cloud-init networking")] + DisableCloudInitNetworking, + #[error( "Failed to encrypt and open block device '{device_path}' with id '{device_id}' as \ '{encrypted_volume_device_name}' for encrypted volume '{encrypted_volume}'" From 73c531f2be09d776c8cf848129425f2c0a7fc6db Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Tue, 10 Jun 2025 18:48:47 +0000 Subject: [PATCH 63/99] Merged PR 23449: engineering: SELinux domain policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Add SELinux policy domain, replacing old policy. Since !23379 removes use of internal param for BM HCs, this PR should not fail BM tests. pre2e: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=831911&view=results ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new SELinux domain policy module to enhance Trident’s security. #### PR Summary The changes add a comprehensive SELinux policy with new definitions, transitions, and interfaces for Trident, and update packaging, build, and runtime components to support it. - **`selinux-policy-trident/`**: Introduces new policy files (`trident.te`, `trident.if`, and `trident.fc`) that define security types, file contexts, domain transitions, and interface rules for Trident. - **`trident.spec`**: Updates the RPM build specification to package, install, and manage the SELinux policy module, including pre/post installation scripts. - **Dockerfiles (`Dockerfile.full`, `Dockerfile.azl2`, `Dockerfile.azl3`)**: Modified to add SELinux dependencies and copy the new policy files for proper build and container setup. - **`src/lib.rs`**: Adds logging to capture and report the current SELinux context during Trident startup. - **`e2e_tests/encryption_test.py` and trident-mos scripts**: Adjusted to switch SELinux modes as needed and apply correct file labels, ensuring policy enforcement during testing and post-installation. Related work items: #12023, #12429 --- Dockerfile.azl2 | 4 +- Dockerfile.azl3 | 6 +- Dockerfile.full | 6 +- e2e_tests/encryption_test.py | 10 + e2e_tests/target-configurations.yaml | 3 + selinux-policy-trident/trident.fc | 3 + selinux-policy-trident/trident.if | 163 +++++ selinux-policy-trident/trident.te | 786 +++++++++++++++++++++ src/lib.rs | 8 +- trident-mos/files/download-trident.service | 1 + trident-mos/iso.yaml | 3 +- trident-mos/post-install.sh | 5 + trident-selinuxpolicies.cil | 140 ---- trident.spec | 73 +- 14 files changed, 1044 insertions(+), 167 deletions(-) create mode 100644 selinux-policy-trident/trident.fc create mode 100644 selinux-policy-trident/trident.if create mode 100644 selinux-policy-trident/trident.te delete mode 100644 trident-selinuxpolicies.cil diff --git a/Dockerfile.azl2 b/Dockerfile.azl2 index 9610f6661..0f45f9ede 100644 --- a/Dockerfile.azl2 +++ b/Dockerfile.azl2 @@ -8,7 +8,9 @@ COPY trident.spec . COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/mariner/SOURCES/osmodifier -COPY trident-selinuxpolicies.cil /usr/src/mariner/SOURCES/trident-selinuxpolicies.cil +COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te +COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc +COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if COPY packaging/static-pcrlock-files/ /usr/src/mariner/SOURCES/static-pcrlock-files/ ARG TRIDENT_VERSION=dev-build diff --git a/Dockerfile.azl3 b/Dockerfile.azl3 index c6ede965e..42fb692c9 100644 --- a/Dockerfile.azl3 +++ b/Dockerfile.azl3 @@ -1,6 +1,6 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed selinux-policy-devel WORKDIR /work @@ -8,7 +8,9 @@ COPY trident.spec . COPY systemd ./systemd COPY bin/trident ./target/release/trident COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te +COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc +COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if COPY packaging/static-pcrlock-files/ /usr/src/azl/SOURCES/static-pcrlock-files/ ARG TRIDENT_VERSION=dev-build diff --git a/Dockerfile.full b/Dockerfile.full index 9245f4c4e..e600166d2 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -1,13 +1,15 @@ FROM mcr.microsoft.com/azurelinux/base/core:3.0 -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential +RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential selinux-policy-devel WORKDIR /work COPY trident.spec . COPY systemd ./systemd COPY artifacts/osmodifier /usr/src/azl/SOURCES/osmodifier -COPY trident-selinuxpolicies.cil /usr/src/azl/SOURCES/trident-selinuxpolicies.cil +COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te +COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc +COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if COPY packaging/static-pcrlock-files/ /usr/src/azl/SOURCES/static-pcrlock-files/ COPY .cargo/config.toml ./.cargo/config.toml diff --git a/e2e_tests/encryption_test.py b/e2e_tests/encryption_test.py index 9df30db4a..b846fc0a4 100644 --- a/e2e_tests/encryption_test.py +++ b/e2e_tests/encryption_test.py @@ -442,9 +442,19 @@ def check_crypsetup_luks_dump(conn: fabric.Connection, cryptDevPath: str) -> Non } } """ + # Running this command requires additional SELinux permission for lvm_t, so temporarily switch to Permissive mode + # Missing permission: allow lvm_t initrc_runtime_t:dir { read } + enforcing = sudo(conn, "getenforce").strip() == "Enforcing" + if enforcing: + sudo(conn, "setenforce 0") + stdout = sudo(conn, f"cryptsetup luksDump --dump-json-metadata {cryptDevPath}") dump = json.loads(stdout) + # Revert to Enforcing mode + if enforcing: + sudo(conn, "setenforce 1") + actual = dump["digests"]["0"]["type"] expected = "pbkdf2" assert ( diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 495ccb3d1..147d4b887 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -126,6 +126,9 @@ virtualMachine: pullrequest: - base - combined + # TODO(10522): Can remove encrypted-partition from pipeline when combined/rerun runs in + # enforcing mode + - encrypted-partition - raid-mirrored - raid-resync-small - rerun diff --git a/selinux-policy-trident/trident.fc b/selinux-policy-trident/trident.fc new file mode 100644 index 000000000..1fe2cea99 --- /dev/null +++ b/selinux-policy-trident/trident.fc @@ -0,0 +1,3 @@ +/usr/bin/trident -- gen_context(system_u:object_r:trident_exec_t,s0) + +/var/lib/trident(/.*)? gen_context(system_u:object_r:trident_var_lib_t,s0) \ No newline at end of file diff --git a/selinux-policy-trident/trident.if b/selinux-policy-trident/trident.if new file mode 100644 index 000000000..0d5a5cf70 --- /dev/null +++ b/selinux-policy-trident/trident.if @@ -0,0 +1,163 @@ + +## policy for trident + +######################################## +## +## Execute trident_exec_t in the trident domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`trident_domtrans',` + gen_require(` + type trident_t, trident_exec_t; + ') + + corecmd_search_bin($1) + domtrans_pattern($1, trident_exec_t, trident_t) +') + +###################################### +## +## Execute trident in the caller domain. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_exec',` + gen_require(` + type trident_exec_t; + ') + + corecmd_search_bin($1) + can_exec($1, trident_exec_t) +') + +######################################## +## +## Search trident lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_search_lib',` + gen_require(` + type trident_var_lib_t; + ') + + allow $1 trident_var_lib_t:dir search_dir_perms; + files_search_var_lib($1) +') + +######################################## +## +## Read trident lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_read_lib_files',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + read_files_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + +######################################## +## +## Manage trident lib files. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_manage_lib_files',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + manage_files_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + +######################################## +## +## Manage trident lib directories. +## +## +## +## Domain allowed access. +## +## +# +interface(`trident_manage_lib_dirs',` + gen_require(` + type trident_var_lib_t; + ') + + files_search_var_lib($1) + manage_dirs_pattern($1, trident_var_lib_t, trident_var_lib_t) +') + + +######################################## +## +## All of the rules required to administrate +## an trident environment +## +## +## +## Domain allowed access. +## +## +## +## +## Role allowed access. +## +## +## +# +interface(`trident_admin',` + gen_require(` + type trident_t; + type trident_var_lib_t; + ') + + allow $1 trident_t:process { signal_perms }; + ps_process_pattern($1, trident_t) + + optional_policy(` + tunable_policy(`allow_ptrace',` + allow $1 trident_t:process ptrace; + ') + ') + + optional_policy(` + tunable_policy(`deny_ptrace',` + allow $1 trident_t:process ptrace; + ') + ') + + files_search_var_lib($1) + admin_pattern($1, trident_var_lib_t) + optional_policy(` + systemd_passwd_agent_exec($1) + systemd_read_fifo_file_passwd_run($1) + ') +') diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te new file mode 100644 index 000000000..1e59e5a78 --- /dev/null +++ b/selinux-policy-trident/trident.te @@ -0,0 +1,786 @@ +policy_module(trident, 1.0.0) + +######################################## +# +# Declarations +# + +type trident_t; +type trident_exec_t; + +# Creates a domain for long running process (daemon), which is started by an init script. Domain is trident_t and entry_point is tridnet_exec_t. +init_daemon_domain(trident_t, trident_exec_t) + +# Create type to label files and directories in /var/lib that are specific to Trident, in particular the Trident datastore. +type trident_var_lib_t; +files_type(trident_var_lib_t) + +#################### +# +# Trident policy +# +require { + type admin_passwd_exec_t; + type anacron_exec_t; + type audisp_remote_exec_t; + type audit_spool_t; + type auditctl_exec_t; + type auditd_exec_t; + type auditd_unit_t; + type bluetooth_unit_t; + type boot_t; + type bootloader_t; + type cgroup_t; + type chfn_exec_t; + type chkpwd_exec_t; + type chronyc_exec_t; + type chronyd_unit_t; + type chronyd_var_lib_t; + type chronyd_var_log_t; + type cloud_init_exec_t; + type cloud_init_state_t; + type cloud_init_t; + type colord_var_lib_t; + type container_unit_t; + type crack_db_t; + type crack_exec_t; + type cron_spool_t; + type crond_unit_t; + type dbusd_unit_t; + type debugfs_t; + type default_t; + type device_t; + type devpts_t; + type dhcpc_exec_t; + type dhcpc_state_t; + type dhcpd_unit_t; + type dmesg_exec_t; + type dosfs_t; + type efivarfs_t; + type etc_runtime_t; + type fs_t; + type fsadm_exec_t; + type fsadm_t; + type getty_exec_t; + type gpg_agent_exec_t; + type gpg_pinentry_exec_t; + type gpg_secret_t; + type groupadd_exec_t; + type home_root_t; + type init_t; + type init_exec_t; + type init_runtime_t; + type init_var_lib_t; + type iptables_unit_t; + type kernel_t; + type kmod_exec_t; + type krb5kdc_exec_t; + type ld_so_t; + type ldconfig_cache_t; + type lib_t; + type loadkeys_exec_t; + type loadkeys_t; + type load_policy_t; + type locale_t; + type locate_exec_t; + type logrotate_unit_t; + type logrotate_var_lib_t; + type lost_found_t; + type lvm_metadata_t; + type lvm_t; + type lvm_unit_t; + type mail_spool_t; + type mdadm_exec_t; + type mdadm_unit_t; + type memory_pressure_t; + type mnt_t; + type modules_conf_t; + type modules_dep_t; + type modules_object_t; + type mount_t; + type mount_exec_t; + type mptctl_device_t; + type net_conf_t; + type ntpd_exec_t; + type ntpd_unit_t; + type oddjob_mkhomedir_exec_t; + type power_unit_t; + type proc_t; + type proc_kcore_t; + type proc_mdstat_t; + type proc_net_t; + type root_t; + type rpm_t; + type rpm_script_t; + type rpm_unit_t; + type security_t; + type setfiles_t; + type semanage_t; + type semanage_exec_t; + type shadow_lock_t; + type shadow_t; + type shell_exec_t; + type ssh_agent_exec_t; + type ssh_exec_t; + type ssh_home_t; + type sshd_t; + type sshd_keygen_unit_t; + type sshd_unit_t; + type sudo_exec_t; + type sulogin_exec_t; + type sysctl_fs_t; + type sysctl_kernel_t; + type sysctl_vm_overcommit_t; + type sysctl_vm_t; + type sysfs_t; + type syslog_conf_t; + type syslogd_exec_t; + type syslogd_unit_t; + type systemd_analyze_exec_t; + type systemd_backlight_exec_t; + type systemd_backlight_unit_t; + type systemd_binfmt_exec_t; + type systemd_binfmt_unit_t; + type systemd_cgroups_exec_t; + type systemd_cgtop_exec_t; + type systemd_coredump_exec_t; + type systemd_coredump_var_lib_t; + type systemd_factory_conf_t; + type systemd_generator_t; + type systemd_generator_exec_t; + type systemd_homed_exec_t; + type systemd_homework_exec_t; + type systemd_hostnamed_exec_t; + type systemd_hw_exec_t; + type systemd_hwdb_t; + type systemd_journalctl_exec_t; + type systemd_locale_exec_t; + type systemd_logind_exec_t; + type systemd_machine_id_setup_exec_t; + type systemd_modules_load_exec_t; + type systemd_networkd_t; + type systemd_networkd_unit_t; + type systemd_networkd_exec_t; + type systemd_notify_exec_t; + type systemd_passwd_agent_exec_t; + type systemd_pcrphase_exec_t; + type systemd_pstore_exec_t; + type systemd_resolved_exec_t; + type systemd_rfkill_exec_t; + type systemd_rfkill_unit_t; + type systemd_sessions_exec_t; + type systemd_socket_proxyd_exec_t; + type systemd_stdio_bridge_exec_t; + type systemd_sysctl_exec_t; + type systemd_sysusers_exec_t; + type systemd_tmpfiles_exec_t; + type systemd_unit_t; + type systemd_update_done_exec_t; + type systemd_user_manager_unit_t; + type systemd_user_runtime_dir_exec_t; + type systemd_userdbd_exec_t; + type systemd_userdbd_unit_t; + type tmp_t; + type tmpfs_t; + type trident_t; + type udev_exec_t; + type udev_t; + type udevadm_t; + type unlabeled_t; + type unreserved_port_t; + type updpwd_exec_t; + type useradd_exec_t; + type usr_t; + type uuidd_exec_t; + type unconfined_t; + type var_run_t; + + # Define object classes that SELinux can protect + class dir { add_name create getattr open read remove_name search write }; + class file { create getattr ioctl lock open read rename setattr relabelto unlink write map execute execute_no_trans }; + class chr_file getattr; + class filesystem getattr; + class lnk_file read; + class netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; + class process { getsched noatsecure rlimitinh siginh }; + class capability sys_admin; + class security read_policy; + class service start; + class tcp_socket { connect create getattr getopt name_connect read setopt shutdown write }; + class udp_socket { create ioctl }; + + attribute can_change_object_identity; + attribute domain; + + role unconfined_r; + role system_r; +} + +# Allow Trident to change (relabel) the security context of a file or directory +typeattribute trident_t can_change_object_identity; + +# Defines transition from trident_t to fsadm_t domain when Trident executes fsadm tool (i.e. mkfs) +type_transition trident_t fsadm_exec_t:process fsadm_t; + +# Allow transition between unconfined_t and trident_t domains; necessary for an interactive run +optional_policy(` + unconfined_run_to(trident_t, trident_exec_t) +') + +#============= trident_t ============== +# Gives trident_t the following elevated privileges: +# dac_override and dac_read_search - allow access files and directories without necessary DAC permissions +# sys_ptrace - allow trident_t to trace or debug other processes +# sys_rawio - allow trident_t to perform I/O operations directly on hardware devices +allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; + +allow trident_t self:alg_socket { accept bind create read write }; +allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice }; +allow trident_t self:fifo_file manage_fifo_file_perms; +allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; +allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; +allow trident_t self:netlink_route_socket { bind create getattr nlmsg_read read write }; +allow trident_t self:process { getsched setsched getcap setpgid signull getattr signal }; +allow trident_t self:tcp_socket { connect create getattr getopt read setopt shutdown write }; +allow trident_t self:unix_dgram_socket { connect create }; +allow trident_t self:key { search write }; +allow trident_t self:sem { associate create destroy read unix_read unix_write write }; + +# Ensure any new files, directories, or symbolic links created by trident_t are automatically labeled with type trident_var_lib_t +files_var_lib_filetrans(trident_t, trident_var_lib_t, { dir file lnk_file }) + +# Allow trident_t domain to interact with files and directories labeled as trident_var_lib_t +# Necessary so Trident can interact with the datastore at /var/lib/trident +allow trident_t trident_var_lib_t:dir { getattr search read write add_name create remove_name open mounton relabelto }; +allow trident_t trident_var_lib_t:file { getattr setattr create open read write unlink lock }; + +# Allow Trident to relabel its executable +allow trident_t trident_exec_t:file relabelto; + +allow trident_t audit_spool_t:dir { getattr open read }; +allow trident_t auditctl_exec_t:file getattr; +allow trident_t auditd_exec_t:file getattr; +allow trident_t auditd_unit_t:file getattr; +allow trident_t admin_passwd_exec_t:file getattr; +allow trident_t anacron_exec_t:file getattr; +allow trident_t audisp_remote_exec_t:file getattr; +allow trident_t bluetooth_unit_t:file getattr; +allow trident_t boot_t:dir mounton; +allow trident_t cgroup_t:filesystem getattr; +allow trident_t chfn_exec_t:file getattr; +allow trident_t chkpwd_exec_t:file getattr; +allow trident_t chronyc_exec_t:file getattr; +allow trident_t chronyd_unit_t:file getattr; +allow trident_t chronyd_var_lib_t:dir { getattr open read }; +allow trident_t chronyd_var_log_t:dir { getattr open read }; +allow trident_t cloud_init_exec_t:file getattr; +allow trident_t cloud_init_state_t:dir list_dir_perms; +allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; +allow trident_t cloud_init_state_t:file getattr; +allow trident_t colord_var_lib_t:dir { getattr open read }; +allow trident_t container_unit_t:file getattr; +allow trident_t crack_db_t:dir { getattr open search read }; +allow trident_t crack_db_t:file getattr; +allow trident_t crack_db_t:lnk_file getattr; +allow trident_t crack_exec_t:file getattr; +allow trident_t cron_spool_t:dir read; +allow trident_t crond_unit_t:file getattr; +allow trident_t dbusd_unit_t:file getattr; +allow trident_t debugfs_t:filesystem getattr; +allow trident_t debugfs_t:dir search; +allow trident_t default_t:dir { getattr open read relabelto search }; +allow trident_t default_t:file relabelto; +allow trident_t device_t:filesystem { getattr mount unmount }; +allow trident_t devpts_t:chr_file { read write ioctl getattr }; +allow trident_t dhcpc_exec_t:file getattr; +allow trident_t dhcpc_state_t:dir { getattr open read }; +allow trident_t dhcpd_unit_t:file getattr; +allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t dosfs_t:filesystem { getattr mount unmount }; +allow trident_t efivarfs_t:filesystem getattr; +allow trident_t etc_runtime_t:file { getattr open read relabelto relabelfrom setattr unlink }; +allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; +allow trident_t fs_t:filesystem { mount unmount }; +allow trident_t fsadm_t:process { siginh rlimitinh noatsecure transition }; +allow trident_t fsadm_t:fd use; +allow trident_t fsadm_t:fifo_file { read write }; +allow trident_t fsadm_exec_t:file { getattr open read execute execute_no_trans relabelto setattr unlink write }; +allow trident_t getty_exec_t:file getattr; +allow trident_t gpg_pinentry_exec_t:file getattr; +allow trident_t gpg_secret_t:file getattr; +allow trident_t groupadd_exec_t:file getattr; +allow trident_t home_root_t:dir { mounton read relabelto add_name create relabelfrom setattr write }; +allow trident_t home_root_t:file { create getattr ioctl open relabelfrom setattr write }; +allow trident_t init_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t init_t:system reboot; +allow trident_t init_t:key search; +allow trident_t init_runtime_t:dir { add_name write }; +allow trident_t init_runtime_t:file { create getattr open write }; +allow trident_t init_t:unix_stream_socket connectto; +allow trident_t init_var_lib_t:dir { getattr open read search }; +allow trident_t iptables_unit_t:file getattr; +allow trident_t kernel_t:process setsched; +allow trident_t kernel_t:system { module_request ipc_info }; +allow trident_t kmod_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t krb5kdc_exec_t:file getattr; +allow trident_t ld_so_t:file { execute_no_trans relabelto setattr unlink write }; +allow trident_t ldconfig_cache_t:dir { getattr open read search }; +allow trident_t ldconfig_cache_t:file getattr; +allow trident_t lib_t:file { create relabelto rename setattr unlink write }; +allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t locale_t:dir { add_name relabelto remove_name rmdir setattr write }; +allow trident_t locale_t:file { link relabelto rename setattr unlink write }; +allow trident_t locate_exec_t:file getattr; +allow trident_t logrotate_unit_t:file getattr; +allow trident_t logrotate_var_lib_t:dir { getattr open read }; +allow trident_t lost_found_t:dir { getattr open read relabelto }; +allow trident_t lvm_metadata_t:dir { getattr open read }; +allow trident_t lvm_unit_t:file getattr; +allow trident_t mail_spool_t:dir list_dir_perms; +allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; +allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t memory_pressure_t:file { read open getattr setattr }; +allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; +allow trident_t modules_conf_t:file { relabelto setattr unlink }; +allow trident_t modules_dep_t:file { getattr ioctl map open read relabelto setattr unlink }; +allow trident_t modules_object_t:file { getattr open read relabelto setattr unlink }; +allow trident_t mount_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t mptctl_device_t:chr_file getattr; +allow trident_t net_conf_t:file { relabelto setattr unlink }; +allow trident_t ntpd_exec_t:file { execute getattr }; +allow trident_t ntpd_unit_t:file getattr; +allow trident_t oddjob_mkhomedir_exec_t:file getattr; +allow trident_t power_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t proc_t:dir read; +allow trident_t proc_t:file { getattr open read ioctl }; +allow trident_t proc_t:filesystem { getattr mount unmount }; +allow trident_t proc_kcore_t:file getattr; +allow trident_t proc_mdstat_t:file { getattr open read }; +allow trident_t proc_net_t:file { open read }; +allow trident_t root_t:dir { add_name create mounton write }; +allow trident_t root_t:file { create getattr map open read relabelfrom write }; +allow trident_t rpm_unit_t:file getattr; +allow trident_t security_t:file { map write }; +allow trident_t security_t:filesystem getattr; +allow trident_t semanage_exec_t:file { execute execute_no_trans entrypoint getattr ioctl open read map }; +allow trident_t setfiles_exec_t:file entrypoint; +allow trident_t shadow_t:file { getattr open read relabelto setattr unlink write }; +allow trident_t shadow_lock_t:file { create getattr lock open read unlink write }; +allow trident_t shell_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t ssh_agent_exec_t:file getattr; +allow trident_t ssh_exec_t:file { execute getattr }; +allow trident_t ssh_home_t:dir { setattr relabelto }; +allow trident_t ssh_home_t:file relabelto; +allow trident_t sshd_t:fd use; +allow trident_t sshd_t:fifo_file { read write getattr }; # Allow Trident to read/write stdin/stdout/stderr +allow trident_t sshd_keygen_unit_t:file getattr; +allow trident_t sshd_unit_t:file getattr; +allow trident_t sulogin_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t sysctl_fs_t:dir search; +allow trident_t sysctl_fs_t:file { getattr ioctl open read }; +allow trident_t sysctl_kernel_t:dir search; +allow trident_t sysctl_kernel_t:file { getattr ioctl open read }; +allow trident_t sysctl_vm_overcommit_t:file { open read }; +allow trident_t sysctl_vm_t:dir search; +allow trident_t sysfs_t:filesystem { mount unmount }; +allow trident_t syslog_conf_t:file { getattr open read relabelto setattr unlink }; +allow trident_t syslogd_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t syslogd_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t systemd_analyze_exec_t:file getattr; +allow trident_t systemd_backlight_exec_t:file getattr; +allow trident_t systemd_backlight_unit_t:file getattr; +allow trident_t systemd_binfmt_exec_t:file getattr; +allow trident_t systemd_binfmt_unit_t:file getattr; +allow trident_t systemd_cgroups_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_cgtop_exec_t:file getattr; +allow trident_t systemd_coredump_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_coredump_var_lib_t:dir { getattr open read }; +allow trident_t systemd_factory_conf_t:dir { getattr open read search }; +allow trident_t systemd_factory_conf_t:file getattr; +allow trident_t systemd_generator_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_homed_exec_t:file getattr; +allow trident_t systemd_homework_exec_t:file getattr; +allow trident_t systemd_hostnamed_exec_t:file { execute getattr }; +allow trident_t systemd_hw_exec_t:file getattr; +allow trident_t systemd_hwdb_t:file { getattr open read relabelto setattr unlink }; +allow trident_t systemd_journal_t:file relabelfrom_file_perms; +allow trident_t systemd_journalctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_locale_exec_t:file getattr; +allow trident_t systemd_logind_exec_t:file getattr; +allow trident_t systemd_machine_id_setup_exec_t:file getattr; +allow trident_t systemd_modules_load_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_networkd_unit_t:service { start status }; +allow trident_t systemd_networkd_exec_t:file { execute getattr }; +allow trident_t systemd_notify_exec_t:file getattr; +allow trident_t systemd_passwd_agent_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_pcrphase_exec_t:file { execute getattr }; +allow trident_t systemd_pstore_exec_t:file { execute getattr }; +allow trident_t systemd_resolved_exec_t:file { execute getattr }; +allow trident_t systemd_rfkill_exec_t:file getattr; +allow trident_t systemd_rfkill_unit_t:file getattr; +allow trident_t systemd_sessions_exec_t:file getattr; +allow trident_t systemd_socket_proxyd_exec_t:file getattr; +allow trident_t systemd_stdio_bridge_exec_t:file getattr; +allow trident_t systemd_sysctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_sysusers_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_tmpfiles_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t systemd_unit_t:dir { read add_name create write }; +allow trident_t systemd_unit_t:file { getattr ioctl link open read relabelto rename setattr unlink write create }; +allow trident_t systemd_unit_t:lnk_file { getattr read }; +allow trident_t systemd_update_done_exec_t:file getattr; +allow trident_t systemd_user_manager_unit_t:file getattr; +allow trident_t systemd_user_runtime_dir_exec_t:file getattr; +allow trident_t systemd_userdbd_exec_t:file getattr; +allow trident_t systemd_userdbd_unit_t:file getattr; +allow trident_t tmp_t:chr_file { create getattr unlink }; +allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write }; +allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write }; +allow trident_t tmp_t:lnk_file { create getattr read rename unlink }; +allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map }; +allow trident_t tmpfs_t:filesystem { getattr mount unmount }; +allow trident_t udev_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; +allow trident_t udev_runtime_t:dir { read watch }; +allow trident_t unlabeled_t:dir { create add_name getattr setattr open read remove_name search write mounton relabelfrom }; +allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink write ioctl relabelfrom }; +allow trident_t unreserved_port_t:tcp_socket name_connect; +allow trident_t updpwd_exec_t:file getattr; +allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; +allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto }; +allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; +allow trident_t uuidd_exec_t:file getattr; +allow trident_t var_run_t:dir { add_name create remove_name write }; +allow trident_t var_run_t:file { create getattr lock open read unlink write }; + +#============= interfaces ============== +########################################### +# Authentication and User Management +########################################### +auth_create_faillog_files(trident_t) +auth_exec_pam(trident_t) +auth_login_entry_type(trident_t) +auth_read_lastlog(trident_t) +auth_read_login_records(trident_t) +domain_entry_file(trident_t, gpg_agent_exec_t) +gpg_entry_type(trident_t) +gpg_list_user_secrets(trident_t) +su_exec(trident_t) +usermanage_check_exec_passwd(trident_t) +userdom_list_user_home_dirs(trident_t) +userdom_read_user_home_content_files(trident_t) +userdom_search_user_runtime_root(trident_t) +userdom_relabel_generic_user_home_files(trident_t) +userdom_relabelto_user_home_dirs(trident_t) + +########################################### +# System Services and Daemons +########################################### +bootloader_exec(trident_t) +chronyd_exec(trident_t) +chronyd_read_config(trident_t) +chronyd_read_key_files(trident_t) +clock_exec(trident_t) +create_files_pattern(trident_t, cgroup_t, cgroup_t) +cron_exec(trident_t) +cron_exec_crontab(trident_t) +cron_read_system_spool(trident_t) +dbus_exec(trident_t) +dbus_manage_lib_files(trident_t) +dbus_read_config(trident_t) +dbus_read_lib_files(trident_t) +dbus_list_system_bus_runtime(trident_t) +dbus_system_bus_client(trident_t) +domain_read_all_domains_state(trident_t) +hostname_exec(trident_t) +init_domtrans(trident_t) +init_rw_stream_sockets(trident_t) +logrotate_exec(trident_t) +ps_process_pattern(trident_t, cloud_init_t) # equivalent to cloud_init_read_state(trident_t) +ssh_domtrans(trident_t) +ssh_domtrans_keygen(trident_t) +ssh_manage_home_files(trident_t) +systemd_list_journal_dirs(trident_t) +systemd_read_networkd_units(trident_t) +systemd_read_user_runtime_units_files(trident_t) +systemd_dbus_chat_logind(trident_t) +systemd_read_user_unit_files(trident_t) + +########################################### +# File System Operations +########################################### +files_list_kernel_modules(trident_t) +files_list_spool(trident_t) +files_list_var(trident_t) +files_manage_boot_files(trident_t) +files_manage_etc_dirs(trident_t) +files_mounton_runtime_dirs(trident_t) +files_read_etc_files(trident_t) +files_read_default_symlinks(trident_t) +files_read_kernel_symbol_table(trident_t) +files_read_usr_src_files(trident_t) +files_read_usr_symlinks(trident_t) +files_read_var_lib_files(trident_t) +files_search_etc(trident_t) +files_search_kernel_modules(trident_t) +files_search_locks(trident_t) +files_search_spool(trident_t) +files_search_var_lib(trident_t) +files_rw_etc_runtime_files(trident_t) +fstools_domtrans(trident_t) +fstools_relabelto_entry_files(trident_t) +fs_getattr_hugetlbfs(trident_t) +fs_getattr_iso9660_files(trident_t) +fs_getattr_iso9660_fs(trident_t) +fs_getattr_pstorefs(trident_t) +fs_getattr_tracefs(trident_t) +fs_getattr_xattr_fs(trident_t) +fs_list_hugetlbfs(trident_t) +fs_manage_dos_dirs(trident_t) +fs_manage_dos_files(trident_t) +fs_manage_tmpfs_dirs(trident_t) +fs_manage_tmpfs_symlinks(trident_t) +fs_read_iso9660_files(trident_t) +fs_watch_memory_pressure(trident_t) +mount_list_runtime(trident_t) + +########################################### +# Network Management +########################################### +iptables_exec(trident_t) +iptables_read_config(trident_t) +iptables_status(trident_t) +sysnet_exec_ifconfig(trident_t) +sysnet_read_config(trident_t) +sysnet_read_dhcp_config(trident_t) +sysnet_relabel_config(trident_t) +sysnet_write_config(trident_t) + +########################################### +# Storage Management +########################################### +dev_rw_loop_control(trident_t) +dev_rw_lvm_control(trident_t) +lvm_exec(trident_t) +lvm_read_config(trident_t) +manage_files_pattern(trident_t, mount_runtime_t, mount_runtime_t) +storage_getattr_fuse_dev(trident_t) +storage_getattr_scsi_generic_dev(trident_t) +storage_raw_read_removable_device(trident_t) +storage_raw_read_fixed_disk(trident_t) +storage_raw_write_fixed_disk(trident_t) + +########################################### +# SELinux Management +########################################### +corecmd_relabel_bin_files(trident_t) +files_relabel_etc_files(trident_t) +files_relabel_kernel_modules(trident_t) +files_relabelto_etc_runtime_files(trident_t) +files_relabelto_usr_files(trident_t) +libs_relabel_ld_so(trident_t) +libs_relabelto_lib_files(trident_t) +miscfiles_relabel_localization(trident_t) +relabel_files_pattern(trident_t, udev_rules_t, udev_rules_t) +selinux_load_policy(trident_t) +seutil_exec_checkpolicy(trident_t) +seutil_exec_loadpolicy(trident_t) +seutil_exec_setfiles(trident_t) +seutil_get_semanage_read_lock(trident_t) +seutil_get_semanage_trans_lock(trident_t) +seutil_manage_bin_policy(trident_t) +seutil_manage_config(trident_t) +seutil_manage_config_dirs(trident_t) +seutil_manage_file_contexts(trident_t) +seutil_manage_module_store(trident_t) +seutil_read_default_contexts(trident_t) +seutil_read_bin_policy(trident_t) +udev_relabel_rules_files(trident_t) + +########################################### +# Package Management +########################################### +rpm_delete_db(trident_t) +rpm_exec(trident_t) +rpm_read_cache(trident_t) +rpm_read_db(trident_t) + +########################################### +# Device Management +########################################### +corenet_getattr_ppp_dev(trident_t) +corenet_read_tun_tap_dev(trident_t) +dev_getattr_acpi_bios_dev(trident_t) +dev_getattr_autofs_dev(trident_t) +dev_getattr_framebuffer_dev(trident_t) +dev_getattr_generic_usb_dev(trident_t) +dev_getattr_mouse_dev(trident_t) +dev_getattr_pmqos_dev(trident_t) +dev_getattr_sysfs(trident_t) +dev_getattr_xserver_misc_dev(trident_t) +dev_list_sysfs(trident_t) +dev_manage_generic_blk_files(trident_t) +dev_manage_generic_dirs(trident_t) +dev_mounton(trident_t) +dev_mounton_sysfs_dirs(trident_t) +dev_read_input_dev(trident_t) +dev_read_kmsg(trident_t) +dev_read_rand(trident_t) +dev_read_raw_memory(trident_t) +dev_read_realtime_clock(trident_t) +dev_read_sysfs(trident_t) +dev_read_watchdog(trident_t) +dev_read_urand(trident_t) +dev_relabelfrom_generic_chr_files(trident_t) +dev_relabelfrom_vfio_dev(trident_t) +dev_rw_nvram(trident_t) +dev_rw_tpm(trident_t) +dev_rw_vhost(trident_t) +dev_search_sysfs(trident_t) +dev_write_sysfs(trident_t) +dev_write_urand(trident_t) +term_getattr_ptmx(trident_t) +term_use_virtio_console(trident_t) +term_getattr_pty_fs(trident_t) +udev_manage_rules_files(trident_t) +udev_read_runtime_files(trident_t) +udev_read_state(trident_t) + +########################################### +# System Command Execution +########################################### +can_exec(trident_t, sudo_exec_t) +corecmd_manage_bin_files(trident_t) +corecmd_bin_entry_type(trident_t) +corecmd_shell_entry_type(trident_t) +corecmd_search_bin(trident_t) +corecmd_exec_bin(trident_t) +corecmd_search_bin(trident_t) +kerberos_exec_kadmind(trident_t) +kerberos_read_config(trident_t) +libs_exec_ldconfig(trident_t) +libs_manage_lib_dirs(trident_t) +modutils_read_module_config(trident_t) +tcsd_manage_lib_dirs(trident_t) +uuidd_manage_lib_dirs(trident_t) + +########################################### +# Logging and Monitoring +########################################### +logging_read_audit_config(trident_t) +logging_read_audit_log(trident_t) +logging_search_logs(trident_t) +logging_manage_generic_log_dirs(trident_t) +logging_manage_generic_logs(trident_t) + +########################################### +# Miscellaneous +########################################### +miscfiles_read_generic_tls_privkey(trident_t) +miscfiles_read_man_pages(trident_t) +miscfiles_read_localization(trident_t) +miscfiles_read_generic_certs(trident_t) +xserver_read_xkb_libs(trident_t) + + +#################### +# +# Additional permissions given to external domains +# +#============= bootloader_t ============== +# List the contents of generic tmpfs directories; required for RAID +fs_list_tmpfs(bootloader_t) + +#============= cloud_init_t ============== +allow cloud_init_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow cloud_init_t unlabeled_t:file { create getattr ioctl open read rename write }; +allow cloud_init_t usr_t:dir { add_name create remove_name write }; + +files_exec_usr_files(cloud_init_t) +files_manage_usr_files(cloud_init_t) + +#============= fsadm_t ============== +role unconfined_r types fsadm_t; + +# Get the attributes of efivarfs filesystems +allow fsadm_t efivarfs_t:filesystem getattr; +allow fsadm_t trident_t:process { siginh rlimitinh noatsecure transition sigchld }; +allow fsadm_t fixed_disk_device_t:blk_file { open read write getattr ioctl }; + +# Create, read, write, and delete files on a efivarfs filesystem +fs_manage_efivarfs_files(fsadm_t) +fs_manage_tmpfs_dirs(fsadm_t) +fs_manage_tmpfs_files(fsadm_t) + +#============= loadkeys_t ============== +files_read_default_symlinks(loadkeys_t) +fs_search_tmpfs(loadkeys_t) + +#============= lvm_t ============== +# Allow lvm_t access to semaphores of the initrc_t type; this is necessary for Trident to create encrypted volumes +allow lvm_t trident_t:sem { associate read unix_read unix_write write }; + +#============= mount_t ============= +allow mount_t trident_var_lib_t:dir mounton; + +#============= rpm_t ============== +allow rpm_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow rpm_t unlabeled_t:file { create getattr ioctl open read rename write }; +allow rpm_t rpm_script_t:process { noatsecure rlimitinh siginh }; + +#============= rpm_script_t ============== +# Allow RPM scripts to read SELinux policy (we currently apply trident.pp as a module in the Trident spec) +allow rpm_script_t security_t:security read_policy; + +allow rpm_script_t kernel_t:fd use; +allow rpm_script_t unlabeled_t:dir { add_name getattr remove_name search write }; +allow rpm_script_t unlabeled_t:file { create getattr ioctl open read rename write }; + +#============= semanage_t ============== +allow semanage_t proc_t:filesystem getattr; +allow semanage_t load_policy_t:process { noatsecure rlimitinh siginh }; +allow semanage_t setfiles_t:process { noatsecure rlimitinh siginh }; + +libs_manage_lib_dirs(semanage_t) +libs_manage_lib_files(semanage_t) + +#============= setfiles_t ============== +allow setfiles_t proc_t:filesystem getattr; + +#============= systemd_generator_t ============== +allow systemd_generator_t home_root_t:dir read; + +#============= udev_t ============== +allow udev_t cloud_init_t:fd use; +allow udev_t cloud_init_t:fifo_file { append write getattr }; +allow udev_t lvm_t:process { noatsecure rlimitinh siginh }; + +files_read_generic_tmp_files(udev_t) + +#============= udevadm_t ============== +allow udevadm_t cgroup_t:filesystem getattr; +allow udevadm_t self:netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; +allow udevadm_t self:udp_socket { create ioctl }; +allow udevadm_t systemd_hwdb_t:file { getattr map open read }; +allow udevadm_t kernel_t:fd use; + +# Allow udevadm to search kernel modules, read module configurations and dependencies +files_search_kernel_modules(udevadm_t) +modutils_read_module_config(udevadm_t) +modutils_read_module_deps(udevadm_t) + +# Read system network configuration files +sysnet_read_config(udevadm_t) + +# List systemd networkd runtime files +systemd_list_networkd_runtime(udevadm_t) + +# Manage udev rules, runtime directories, and files +udev_manage_rules_files(udevadm_t) +udev_manage_runtime_dirs(udevadm_t) +udev_manage_runtime_files(udevadm_t) + +# Read symbolic links in cgroup directories and search sysfs +read_lnk_files_pattern(udevadm_t, cgroup_t, cgroup_t) +dev_search_sysfs(udevadm_t) + +#============= unlabeled_t ============== +fs_associate_tmpfs(unlabeled_t) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 808069709..c444d42fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use std::{ use cli::GetKind; use engine::{bootentries, EngineContext}; -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use nix::unistd::Uid; use osutils::{block_devices, container, dependencies::Dependency}; @@ -184,6 +184,12 @@ impl Trident { info!("Running Trident in a container"); } + if let Ok(selinux_context) = fs::read_to_string("/proc/self/attr/current") { + trace!("selinux context: Trident is running in SELinux domain '{selinux_context}'"); + } else { + error!("selinux context: Failed to retrieve the SELinux context in which Trident is running"); + } + if !Uid::effective().is_root() { return Err(TridentError::new( ExecutionEnvironmentMisconfigurationError::CheckRootPrivileges, diff --git a/trident-mos/files/download-trident.service b/trident-mos/files/download-trident.service index b796e80cf..965963574 100644 --- a/trident-mos/files/download-trident.service +++ b/trident-mos/files/download-trident.service @@ -7,6 +7,7 @@ Before=trident-install.service ExecStart=/bin/bash /trident_cdrom/pre-trident-script.sh ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/trident -o /usr/bin/trident -f" ExecStart=chmod +x /usr/bin/trident +ExecStart=/sbin/restorecon -v /usr/bin/trident ExecStart=/bin/bash -c "curl $(grep -oP '^\s*phonehome: \\K.*(?=phonehome)' /etc/trident/config.yaml)files/osmodifier -o /usr/bin/osmodifier -f" ExecStart=/bin/chmod +x /usr/bin/osmodifier Type=oneshot diff --git a/trident-mos/iso.yaml b/trident-mos/iso.yaml index d936ce2b7..241b53ee8 100644 --- a/trident-mos/iso.yaml +++ b/trident-mos/iso.yaml @@ -87,4 +87,5 @@ iso: - console=tty0 - console=ttyS0 - rd.luks=0 - - selinux=0 + - selinux=1 + - enforcing=0 diff --git a/trident-mos/post-install.sh b/trident-mos/post-install.sh index 74d7296bc..825008afb 100755 --- a/trident-mos/post-install.sh +++ b/trident-mos/post-install.sh @@ -6,3 +6,8 @@ if [ ! -d /etc/trident ]; then mkdir /etc/trident fi ln -s /trident_cdrom/trident-config.yaml /etc/trident/config.yaml + +# Compile and load Trident SELinux module (this is otherwise handled in trident.spec) +cd /usr/share/selinux/packages/trident +make -f /usr/share/selinux/devel/Makefile trident.pp +semodule -i trident.pp \ No newline at end of file diff --git a/trident-selinuxpolicies.cil b/trident-selinuxpolicies.cil deleted file mode 100644 index f15db87cd..000000000 --- a/trident-selinuxpolicies.cil +++ /dev/null @@ -1,140 +0,0 @@ -; Allow auditctl_t to manage auditd_etc_t files -(allow auditctl_t auditd_etc_t (file (map))) -(allow auditctl_t proc_t (filesystem (getattr))) - -; Allow chkpwd_t to access sysctl_kernel_t directories and files for password verification -(allow chkpwd_t proc_t (filesystem (getattr))) -(allow chkpwd_t sysctl_kernel_t (dir (search))) -(allow chkpwd_t sysctl_kernel_t (file (open read))) - -; Allow cloud_init_t to perform various operations for cloud instance initialization -; cloud-init -(allow cloud_init_t file_context_t (file (getattr open read map))) -(allow cloud_init_t gpg_secret_t (file (getattr))) -(allow cloud_init_t security_t (security (check_context))) -(allow cloud_init_t self (capability (net_admin ))) -(allow cloud_init_t selinux_config_t (file (getattr open read))) -(allow cloud_init_t sshd_key_t (file (relabelto))) -(allow cloud_init_t user_home_t (file (getattr))) - -; Allow fsadm_t to manage efivarfs_t files for filesystem administration -; efibootmgr -(allow fsadm_t efivarfs_t (dir (read))) -(allow fsadm_t efivarfs_t (file (getattr open read write))) -(allow fsadm_t efivarfs_t (filesystem (getattr))) - -; Load/Save OS Random Seed -(allow kernel_t self (capability2 (checkpoint_restore))) - -; Allow kmod_t to read iptables_runtime_t files for kernel module management -(allow kmod_t iptables_runtime_t (file (read))) - -; Allow lvm_t to perform various operations for logical volume management -; Disk Encryption -(allow lvm_t bpf_t (dir (search))) -(allow lvm_t cgroup_t (dir (search))) -(allow lvm_t cgroup_t (filesystem (getattr))) -(allow lvm_t init_t (key (search))) -(allow lvm_t initrc_runtime_t (dir (add_name open read write search))) -(allow lvm_t initrc_runtime_t (file (create open read write lock getattr))) -(allow lvm_t initrc_t (sem (associate read unix_read unix_write write))) -(allow lvm_t kernel_t (key (search))) -(allow lvm_t proc_t (filesystem (getattr))) -(allow lvm_t pstore_t (dir (search))) -(allow lvm_t self (capability (dac_read_search))) -(allow lvm_t systemd_passwd_runtime_t (dir (getattr search))) -(allow lvm_t tmpfs_t (filesystem (getattr))) -(allow lvm_t tpm_device_t (chr_file (read write open ioctl))) -(allow lvm_t user_home_dir_t (dir (search))) -(allow lvm_t var_run_t (dir (create))) - -; Allow mdadm_t to perform RAID operations using the mdadm utility -(allow mdadm_t debugfs_t (dir (search read write add_name remove_name getattr open))) -(allow mdadm_t device_t (lnk_file (create read write getattr open link rename setattr unlink))) -(allow mdadm_t event_device_t (chr_file (getattr))) -(allow mdadm_t lvm_control_t (chr_file (getattr))) -(allow mdadm_t nvram_device_t (chr_file (getattr))) -(allow mdadm_t udev_runtime_t (dir (search read write add_name remove_name getattr open))) -(allow mdadm_t vfio_device_t (chr_file (getattr))) -(allow mdadm_t vhost_device_t (chr_file (getattr))) - -(allow ntpd_t proc_t (file (write))) - -; Allow passwd_t to manage proc files for password updates -(allow passwd_t proc_t (filesystem (getattr))) - -; Allow semanage_t and setfiles_t to get attributes of the proc_t filesystem -(allow semanage_t proc_t (filesystem (getattr))) -(allow setfiles_t proc_t (filesystem (getattr))) - -; Allow ssh_keygen_t to manage files and directories for SSH key generation -(allow ssh_keygen_t security_t (filesystem (getattr))) -(allow ssh_keygen_t selinux_config_t (dir (search))) - -; Allow sshd_t to get attributes of the proc_t filesystem for SSH daemon -(allow sshd_t proc_t (filesystem (getattr))) -(allow sshd_t self (capability (net_admin))) - -; Allow syslogd_t to manage logging files and directories -(allow syslogd_t cgroup_t (dir (read))) -(allow syslogd_t proc_t (file (write read append getattr lock open))) -(allow syslogd_t systemd_journal_t (file (relabelfrom relabelto))) - -; Allow systemd_cgroups_t to manage proc_t filesystem -(allow systemd_cgroups_t proc_t (filesystem (getattr))) - -; Allow systemd_generator_t to manage home_root_t directories and selinux_config_t directories -(allow systemd_generator_t home_root_t (dir (read search write add_name remove_name getattr open))) -(allow systemd_generator_t self (capability (sys_rawio))) -(allow systemd_generator_t selinux_config_t (dir (search))) - -; Allow systemd_hw_t to manage capabilities for systemd hardware management -(allow systemd_hw_t self (capability (dac_override))) - -; Allow systemd_locale_t to manage selinux_config_t files for locale settings -(allow systemd_locale_t selinux_config_t (file (getattr open read))) - -; Allow systemd_logind_t to manage various directories and files for login daemon -(allow systemd_logind_t proc_t (file (write ))) -(allow systemd_logind_t proc_t (filesystem (getattr))) - -; Allow systemd_networkd_t to manage various directories and files for network management -(allow systemd_networkd_t proc_t (file (write))) -(allow systemd_networkd_t selinux_config_t (dir (search))) - -; Allow systemd_resolved_t to perform name binding on howl_port_t, write to proc_t files, and search tmpfs_t directories for DNS resolution -(allow systemd_resolved_t howl_port_t (udp_socket (name_bind))) -(allow systemd_resolved_t proc_t (file (write))) -(allow systemd_resolved_t tmpfs_t (dir (search))) - -(allow systemd_sessions_t self (capability (net_admin))) - -; Allow systemd_sysctl_t to manage selinux_config_t directories and perform various operations on tmpfs_t directories for system configuration -(allow systemd_sysctl_t selinux_config_t (dir (search))) -(allow systemd_sysctl_t tmpfs_t (dir (search read write add_name remove_name getattr open))) - -; Allow systemd_tmpfiles_t to manage etc_t symbolic links and various directories and files for temporary file management -(allow systemd_tmpfiles_t etc_t (lnk_file (relabelto relabelfrom))) -(allow systemd_tmpfiles_t init_var_lib_t (dir (create))) -(allow systemd_tmpfiles_t user_home_dir_t (dir (write relabelto relabelfrom))) - -(allow systemd_update_done_t self (capability (net_admin))) - -; Allow systemd_user_runtime_dir_t to get attributes of the proc_t filesystem for user runtime directory management -(allow systemd_user_runtime_dir_t proc_t (filesystem (getattr))) -(allow systemd_user_runtime_dir_t self (capability (net_admin))) - -; Allow systemd_userdbd_t to connect to kernel_t Unix stream sockets, write to proc_t files for user database operations -(allow systemd_userdbd_t kernel_t (unix_stream_socket (connectto))) -(allow systemd_userdbd_t proc_t (file (write))) -(allow systemd_userdbd_t self (capability (sys_resource))) -(allow systemd_userdbd_t self (process (getcap))) - -; Allow udev_t to manage init_runtime_t directories, write to proc_t files for device management -; udev -(allow udev_t init_runtime_t (dir (read))) -(allow udev_t proc_t (file (write))) - -; Allow useradd_t to manage shadow_t files for user addition -; OSModifier -(allow useradd_t shadow_t (file (open read))) diff --git a/trident.spec b/trident.spec index 615a8ac44..51d430bf5 100644 --- a/trident.spec +++ b/trident.spec @@ -1,3 +1,5 @@ +%global selinuxtype targeted + Summary: Agent for bare metal platform Name: trident Version: %{rpm_ver} @@ -5,7 +7,9 @@ Release: %{rpm_rel}%{?dist} Vendor: Microsoft Corporation License: Proprietary Source1: osmodifier -Source2: trident-selinuxpolicies.cil +Source2: trident.fc +Source3: trident.if +Source4: trident.te BuildRequires: openssl-devel BuildRequires: rust BuildRequires: systemd-units @@ -17,6 +21,7 @@ Requires: efibootmgr Requires: lsof Requires: systemd >= 255 Requires: systemd-udev +Requires: (%{name}-selinux if selinux-policy-%{selinuxtype}) # Optional dependencies for various optional features @@ -42,20 +47,6 @@ Agent for bare metal platform %{_bindir}/%{name} %dir /etc/%{name} %{_bindir}/osmodifier -%{_datadir}/selinux/packages/trident-selinuxpolicies.cil - -%post -#!/bin/sh -# Apply required selinux policies only if selinux-policy is present -if rpm -q selinux-policy &> /dev/null; then - semodule -i %{_datadir}/selinux/packages/trident-selinuxpolicies.cil -fi - -%postun -# If selinux-policy is present, remove the trident-selinuxpolicies module -if rpm -q selinux-policy &> /dev/null; then - semodule -r trident-selinuxpolicies -fi # ------------------------------------------------------------------------------ @@ -123,6 +114,40 @@ SystemD timer for update polling with Harpoon. # ------------------------------------------------------------------------------ +%package selinux +Summary: Trident SELinux policy +BuildArch: noarch +Requires: selinux-policy-%{selinuxtype} +Requires(post): selinux-policy-%{selinuxtype} +BuildRequires: selinux-policy-devel +%{?selinux_requires} + +%description selinux +Custom SELinux policy module + +%files selinux +%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.* +%{_datadir}/selinux/devel/include/distributed/%{name}.if +%ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{name} + +# SELinux contexts are saved so that only affected files can be +# relabeled after the policy module installation +%pre selinux +%selinux_relabel_pre -s %{selinuxtype} + +%post selinux +%selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 + +%postun selinux +if [ $1 -eq 0 ]; then + %selinux_modules_uninstall -s %{selinuxtype} %{name} +fi + +%posttrans selinux +%selinux_relabel_post -s %{selinuxtype} + +# ------------------------------------------------------------------------------ + %package static-pcrlock-files Summary: Statically defined .pcrlock files Requires: %{name} @@ -142,6 +167,14 @@ be removed once the fix is merged in AZL 4.0. export TRIDENT_VERSION="%{trident_version}" cargo build --release +mkdir selinux +cp -p %{SOURCE2} selinux/ +cp -p %{SOURCE3} selinux/ +cp -p %{SOURCE4} selinux/ + +make -f %{_datadir}/selinux/devel/Makefile %{name}.pp +bzip2 -9 %{name}.pp + %check test "$(./target/release/trident --version)" = "trident %{trident_version}" @@ -150,6 +183,10 @@ install -D -m 755 %{SOURCE1} %{buildroot}%{_bindir}/osmodifier install -D -m 755 target/release/%{name} %{buildroot}/%{_bindir}/%{name} +# Copy Trident SELinux policy module to /usr/share/selinux/packages +install -D -m 0644 %{name}.pp.bz2 %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2 +install -D -p -m 0644 selinux/%{name}.if %{buildroot}%{_datadir}/selinux/devel/include/distributed/%{name}.if + mkdir -p %{buildroot}%{_unitdir} install -D -m 644 systemd/%{name}.service %{buildroot}%{_unitdir}/%{name}.service install -D -m 644 systemd/%{name}-network.service %{buildroot}%{_unitdir}/%{name}-network.service @@ -157,10 +194,6 @@ install -D -m 644 systemd/%{name}.timer %{buildroot}%{_unitdir}/%{name}.timer mkdir -p %{buildroot}/etc/%{name} -# Copy the trident-selinuxpolicies file to /usr/share/selinux/packages/ -mkdir -p %{buildroot}%{_datadir}/selinux/packages/ -install -m 755 %{SOURCE2} %{buildroot}%{_datadir}/selinux/packages/ - # Copy statically defined .pcrlock files into /var/lib/pcrlock.d pcrlockroot="%{buildroot}%{_sharedstatedir}/pcrlock.d" mkdir -p "$pcrlockroot" @@ -170,4 +203,4 @@ mkdir -p "$pcrlockroot" mkdir -p "$pcrlockroot/$(dirname "$f")" install -m 644 "$f" "$pcrlockroot/$f" done -) \ No newline at end of file +) From 08ce716404a324cb6cb4c98791b9171e2acbb0d7 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 10 Jun 2025 19:04:45 +0000 Subject: [PATCH 64/99] Merged PR 23457: engineering: A couple fixes to BM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description In a last hour update to the last PR I changed some variable names and messed up the script. Also fixing an issue with the ab update helper. ---- #### AI description (iteration 1) #### PR Classification Bug fix. #### PR Summary This pull request resolves parameter naming mismatches and corrects URL formatting in the bare metal configuration and update helper modules. - In `/.pipelines/templates/stages/testing_baremetal/update_host_config.py`, positional parameters are replaced by keyword arguments with updated names (e.g., changing oam_ip to interface_ip) for clarity and consistency. - In `/tools/storm/helpers/ab_update.go`, the URL generation logic is fixed by removing an extra slash during image URL construction. Related work items: #12291 --- .../testing_baremetal/update_host_config.py | 17 +++++++++-------- tools/storm/helpers/ab_update.go | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.pipelines/templates/stages/testing_baremetal/update_host_config.py b/.pipelines/templates/stages/testing_baremetal/update_host_config.py index 9c14ac38d..1e64bb4e9 100755 --- a/.pipelines/templates/stages/testing_baremetal/update_host_config.py +++ b/.pipelines/templates/stages/testing_baremetal/update_host_config.py @@ -27,9 +27,10 @@ def wait_online_script(interface_name: str) -> str: def update_trident_host_config( + *, host_configuration: str, - oam_ip: str, interface_name: str, + interface_ip: str, interface_mac: Optional[str] = None, network_gateway: Optional[str] = None, use_dhcp: bool = False, @@ -38,7 +39,7 @@ def update_trident_host_config( os = host_configuration.setdefault("os", {}) main_interface = { - "addresses": [f"{oam_ip}/23"], + "addresses": [f"{interface_ip}/23"], "dhcp4": use_dhcp, "set-name": interface_name, } @@ -114,12 +115,12 @@ def main(): trident_yaml_content = yaml.safe_load(f) update_trident_host_config( - trident_yaml_content, - args.oam_ip, - args.interface_name, - args.oam_gateway, - args.oam_mac, - args.use_dhcp, + host_configuration=trident_yaml_content, + interface_name=args.interface_name, + interface_ip=args.oam_ip, + interface_mac=args.oam_mac, + network_gateway=args.oam_gateway, + use_dhcp=args.use_dhcp, ) with open(args.trident_yaml, "w") as f: yaml.dump(trident_yaml_content, f, default_flow_style=False) diff --git a/tools/storm/helpers/ab_update.go b/tools/storm/helpers/ab_update.go index a873e10db..c4f7ddd17 100644 --- a/tools/storm/helpers/ab_update.go +++ b/tools/storm/helpers/ab_update.go @@ -122,7 +122,7 @@ func (h *AbUpdateHelper) updateHostConfig(tc storm.TestCase) error { ext := matches[3] newCosiName := fmt.Sprintf("%s_v%s.%s", name, h.args.Version, ext) - newUrl := fmt.Sprintf("%s/%s", urlPath, newCosiName) + newUrl := fmt.Sprintf("%s%s", urlPath, newCosiName) logrus.Infof("New image URL: %s", newUrl) logrus.Infof("Checking if new image URL is accessible...") From 1df0b6376f1ddc75921f95ab083747e4d3937fb3 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Tue, 10 Jun 2025 20:59:01 +0000 Subject: [PATCH 65/99] Merged PR 23459: bug: Serve COSI file for finalize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description We now need the cosi file during finalize. ---- #### AI description (iteration 1) #### PR Classification Bug fix to ensure the COSI file is correctly served during the finalize step in the A/B update process. #### PR Summary This pull request fixes an issue in the A/B update pipeline by updating the netlisten command in the finalize stage. The changes add the artifacts directory parameter and standardize the command formatting. - `/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml`: Updated the netlisten invocation in the finalize step to include `-s "${{ parameters.artifactsDirectory }}"` and reformatted the command. - `/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml`: Adjusted the netlisten call in the stage update section for consistency. Related work items: #12488 --- .../e2e-ab-update-stage-finalize-test-run.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml index 712e04cbc..9a16a1788 100644 --- a/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml +++ b/.pipelines/templates/stages/testing_common/e2e-ab-update-stage-finalize-test-run.yml @@ -59,8 +59,8 @@ steps: if pgrep netlisten > /dev/null; then pkill netlisten; fi ./bin/netlisten -m $(Build.SourcesDirectory)/trident-stage-update-metrics.jsonl \ - -p ${{ parameters.netlistenPort }} \ - -s "${{ parameters.artifactsDirectory }}" > ./stage-ab-update-deployment.log 2>&1 & + -p ${{ parameters.netlistenPort }} \ + -s "${{ parameters.artifactsDirectory }}" > ./stage-ab-update-deployment.log 2>&1 & echo "Running script to stage A/B update..." ./bin/storm-trident helper ab-update \ @@ -113,7 +113,10 @@ steps: set -eux # If there is a netlisten process, kill it so there is no port clash in the instance if pgrep netlisten > /dev/null; then pkill netlisten; fi - ./bin/netlisten -m $(Build.SourcesDirectory)/trident-finalize-update-metrics.jsonl -p ${{ parameters.netlistenPort }} > ./finalize-ab-update.log 2>&1 & + ./bin/netlisten -m $(Build.SourcesDirectory)/trident-finalize-update-metrics.jsonl \ + -p ${{ parameters.netlistenPort }} \ + -s "${{ parameters.artifactsDirectory }}" > ./finalize-ab-update.log 2>&1 & + echo "Running script to finalize A/B update..." ./bin/storm-trident helper ab-update \ "${{ parameters.sshKeyPath }}" \ From 4ef4de00f14160ca90f52e7f397094ec7fe016c7 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Tue, 10 Jun 2025 22:06:30 +0000 Subject: [PATCH 66/99] Merged PR 23462: engineering: Pass SELinux policy files so ISO builds policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Pass SELinux policy files so we can build in `post-install.sh` script. ---- #### AI description (iteration 1) #### PR Classification Configuration update enhancing ISO builds by including necessary SELinux policy files and development packages. #### PR Summary This pull request modifies the ISO build configuration to pass SELinux policy files and add essential packages for building and debugging SELinux modules. - In `trident-mos/iso.yaml`, added three SELinux policy files (`trident.if`, `trident.fc`, and `trident.te`) with appropriate destination paths. - In `trident-mos/iso.yaml`, appended `selinux-policy-devel` and `setools-console` to the package list to support SELinux module building and debugging. Related work items: #12023 --- Makefile | 2 +- trident-mos/iso.yaml | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e017da6e1..196969387 100644 --- a/Makefile +++ b/Makefile @@ -622,7 +622,7 @@ artifacts/imagecustomizer: @chmod +x artifacts/imagecustomizer @touch artifacts/imagecustomizer -bin/trident-mos.iso: artifacts/baremetal.vhdx artifacts/imagecustomizer systemd/trident-install.service trident-mos/iso.yaml trident-mos/files/* trident-mos/post-install.sh +bin/trident-mos.iso: artifacts/baremetal.vhdx artifacts/imagecustomizer systemd/trident-install.service trident-mos/iso.yaml trident-mos/files/* trident-mos/post-install.sh selinux-policy-trident/* @mkdir -p bin BUILD_DIR=`mktemp -d` && \ trap 'sudo rm -rf $$BUILD_DIR' EXIT; \ diff --git a/trident-mos/iso.yaml b/trident-mos/iso.yaml index 241b53ee8..ba98d06ce 100644 --- a/trident-mos/iso.yaml +++ b/trident-mos/iso.yaml @@ -54,6 +54,9 @@ os: - tpm2-tools - veritysetup - vim + # Support for building SELinux module and debugging + - selinux-policy-devel + - setools-console additionalFiles: - source: files/getty@.service @@ -66,6 +69,12 @@ os: destination: /usr/lib/systemd/system/trident-install.service - source: files/download-trident.service destination: /usr/lib/systemd/system/download-trident.service + - source: ../selinux-policy-trident/trident.if + destination: /usr/share/selinux/packages/trident/trident.if + - source: ../selinux-policy-trident/trident.fc + destination: /usr/share/selinux/packages/trident/trident.fc + - source: ../selinux-policy-trident/trident.te + destination: /usr/share/selinux/packages/trident/trident.te services: enable: From 80bc05961c57c108b7d6491d8c89f12ef8768e25 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 11 Jun 2025 19:02:54 +0000 Subject: [PATCH 67/99] Merged PR 23475: engineering: Fix storm scenario setup/cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Fixing an issue where the real runnable was being obscured by an interim object so an interface assertion to run setup/cleanup was never successful. ---- #### AI description (iteration 1) #### PR Classification Bug fix addressing incorrect storm scenario setup/cleanup implementation. #### PR Summary This pull request resolves issues with the storm scenario lifecycle by implementing missing core.Scenario methods in the HelloWorldScenario and correcting the invocation of setup and cleanup procedures in the test runner. - `storm/helloworld/scenario.go` now implements the Setup, Cleanup, Args, RequiredFiles, StagePaths, and Tags methods. - `storm/internal/runner/runner.go` updates type handling to call Setup and Cleanup on the embedded TestRegistrant. Related work items: #12515 --- storm/helloworld/scenario.go | 39 ++++++++++++++++++++++++++++++++- storm/internal/runner/runner.go | 9 +++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/storm/helloworld/scenario.go b/storm/helloworld/scenario.go index 2af87c2b0..6b2219b2e 100644 --- a/storm/helloworld/scenario.go +++ b/storm/helloworld/scenario.go @@ -3,18 +3,55 @@ package helloworld import ( "storm" + "storm/pkg/storm/core" "github.com/sirupsen/logrus" ) type HelloWorldScenario struct { - storm.BaseScenario + // You can embed storm.BaseScenario to get the default implementation of the Scenario interface. + // This is useful if you are not using most methods. + // storm.BaseScenario } +// Args implements core.Scenario. +func (s *HelloWorldScenario) Args() any { + return nil +} + +// Setup implements core.Scenario. +func (s *HelloWorldScenario) Setup(core.SetupCleanupContext) error { + logrus.Info("Setup called for HelloWorldScenario") + return nil +} + +// Cleanup implements core.Scenario. +func (s *HelloWorldScenario) Cleanup(core.SetupCleanupContext) error { + logrus.Info("Cleanup called for HelloWorldScenario") + return nil +} + +// RequiredFiles implements core.Scenario. +func (s *HelloWorldScenario) RequiredFiles() []string { + return nil +} + +// StagePaths implements core.Scenario. +func (s *HelloWorldScenario) StagePaths() []string { + return nil +} + +// Tags implements core.Scenario. +func (s *HelloWorldScenario) Tags() []string { + return nil +} + +// Type implements core.Scenario. func (s *HelloWorldScenario) Name() string { return "hello-world" } +// Description implements core.Scenario. func (h *HelloWorldScenario) RegisterTestCases(r storm.TestRegistrar) error { r.RegisterTestCase("myPassingTestCase", func(tc storm.TestCase) error { logrus.Info("This message will be logged in the test case!") diff --git a/storm/internal/runner/runner.go b/storm/internal/runner/runner.go index dd53085bc..895c3486a 100644 --- a/storm/internal/runner/runner.go +++ b/storm/internal/runner/runner.go @@ -68,10 +68,7 @@ func RegisterAndRunTests(suite core.SuiteContext, } func executeTestCases(suite core.SuiteContext, - runnable interface { - core.TestRegistrantMetadata - core.TestRegistrant - }, + runnable *runnableInstance, testManager *testmgr.StormTestManager, watch bool, ) error { @@ -83,7 +80,7 @@ func executeTestCases(suite core.SuiteContext, // If the runnable implements the SetupCleanup interface, we call // the setup method before running the tests. - if r, ok := runnable.(core.SetupCleanup); ok { + if r, ok := runnable.TestRegistrant.(core.SetupCleanup); ok { err := runCatchPanic(func() error { return r.Setup(ctx) }) if err != nil { return newSetupError(runnable, err) @@ -139,7 +136,7 @@ func executeTestCases(suite core.SuiteContext, // If the runnable implements the SetupCleanup interface, we call // the Cleanup method after running the tests. - if r, ok := runnable.(core.SetupCleanup); ok { + if r, ok := runnable.TestRegistrant.(core.SetupCleanup); ok { err := runCatchPanic(func() error { return r.Cleanup(ctx) }) if err != nil { return newCleanupError(runnable, err) From fafe6630aec5103a7ae02169cc88ffc0a4bf7174 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Wed, 11 Jun 2025 22:07:13 +0000 Subject: [PATCH 68/99] Merged PR 23474: bug: Allow Trident to enable services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description BM testing pipeline: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=833700&view=results ---- #### AI description (iteration 1) #### PR Classification Bug fix updating SELinux policies to enable Trident services. #### PR Summary This PR updates the SELinux policy to grant Trident the necessary permissions for service operations, addressing issues in baremetal deployment testing. - `selinux-policy-trident/trident.te`: Added the `create` permission to `systemd_unit_t:lnk_file`. - `selinux-policy-trident/trident.te`: Granted the `sys_admin` capability to `udevadm_t`. Related work items: #12510 --- selinux-policy-trident/trident.te | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index 1e59e5a78..273428ec7 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -426,7 +426,7 @@ allow trident_t systemd_sysusers_exec_t:file { execute getattr map open read rel allow trident_t systemd_tmpfiles_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t systemd_unit_t:dir { read add_name create write }; allow trident_t systemd_unit_t:file { getattr ioctl link open read relabelto rename setattr unlink write create }; -allow trident_t systemd_unit_t:lnk_file { getattr read }; +allow trident_t systemd_unit_t:lnk_file { getattr read create }; allow trident_t systemd_update_done_exec_t:file getattr; allow trident_t systemd_user_manager_unit_t:file getattr; allow trident_t systemd_user_runtime_dir_exec_t:file getattr; @@ -759,6 +759,7 @@ files_read_generic_tmp_files(udev_t) allow udevadm_t cgroup_t:filesystem getattr; allow udevadm_t self:netlink_route_socket { bind create getattr getopt nlmsg_read read setopt write }; allow udevadm_t self:udp_socket { create ioctl }; +allow udevadm_t self:capability { sys_admin }; allow udevadm_t systemd_hwdb_t:file { getattr map open read }; allow udevadm_t kernel_t:fd use; From 850d564aec0b5799f064943aae108c4d386f4e97 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 11 Jun 2025 22:13:19 +0000 Subject: [PATCH 69/99] Merged PR 23483: engineering: bm-script: Use trident's capabilities instead of scripts. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Use `os.services.enable` and `os.additionalFiles` instead of a script for baremetal tests. ---- #### AI description (iteration 1) #### PR Classification This PR implements an engineering refactor by replacing a custom wait script with Trident’s native systemd service configuration for ensuring network readiness. #### PR Summary The changes remove the legacy wait script and update the host configuration to directly enable the systemd-networkd-wait-online service and add an override file so that the Trident service waits for the network interface to be online. This update supports the "Enable UKI+RAID+BM" work item. - In `/.pipelines/templates/stages/testing_baremetal/update_host_config.py`, the custom `wait_online_script` function and its usage in the postConfigure script are removed. - In the same file, the configuration now appends the systemd service enablement and override file to ensure proper network wait behavior for the Trident service. Related work items: #12291 --- .../testing_baremetal/update_host_config.py | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/.pipelines/templates/stages/testing_baremetal/update_host_config.py b/.pipelines/templates/stages/testing_baremetal/update_host_config.py index 1e64bb4e9..fe8af435c 100755 --- a/.pipelines/templates/stages/testing_baremetal/update_host_config.py +++ b/.pipelines/templates/stages/testing_baremetal/update_host_config.py @@ -8,24 +8,6 @@ import logging -def wait_online_script(interface_name: str) -> str: - """ - Generates a script to add a wait for the given network interface to be - online before starting the Trident service. - """ - return "\n".join( - [ - "set -eux", - f"systemctl enable systemd-networkd-wait-online@{interface_name}.service", - "mkdir -p /etc/systemd/system/trident.service.d", - "cat << EOF > /etc/systemd/system/trident.service.d/override.conf", - "[Unit]", - f"Requires=systemd-networkd-wait-online@{interface_name}.service", - "EOF", - ] - ) - - def update_trident_host_config( *, host_configuration: str, @@ -61,6 +43,25 @@ def update_trident_host_config( }, } + # Name of the wait online service for this interface + wait_online_service = f"systemd-networkd-wait-online@{interface_name}.service" + + # Enable systemd-networkd-wait-online service for the interface. + enable_services = os.setdefault("services", {}).setdefault("enable", []) + if wait_online_service not in enable_services: + enable_services.append(wait_online_service) + + # Add an override for the trident service to wait for the network + # interface to be online before starting. + os.setdefault("additionalFiles", []).append( + { + "destination": "/etc/systemd/system/trident.service.d/override.conf", + "content": "[Unit]\n" + f"Requires={wait_online_service}\n" + f"After={wait_online_service}\n", + } + ) + logging.info("Updating os disks device in trident.yaml") disks = host_configuration.get("storage", {}).get("disks", []) for disk in disks: @@ -69,14 +70,6 @@ def update_trident_host_config( elif disk["id"] == "disk2": disk["device"] = "/dev/sdb" - host_configuration.setdefault("scripts", {}).setdefault("postConfigure", []).append( - { - "content": wait_online_script(interface_name), - "name": "wait-for-network", - "runOn": ["all"], - } - ) - logging.info( "Final trident_yaml content post all the updates: %s", host_configuration ) From 68beca5a214fca73f0133236f881dee40ff3dd4b Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 12 Jun 2025 19:05:56 +0000 Subject: [PATCH 70/99] Merged PR 23497: engineering: Re-enable all tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Re enable missing BM tests Related work items: #12232, #12276 --- e2e_tests/target-configurations.yaml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index 147d4b887..d481bdba8 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -2,22 +2,22 @@ bareMetal: host: daily: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - split - usr-verity validation: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12276) re-enable: #- memory-constraint-combined + - memory-constraint-combined - misc - raid-big - raid-mirrored - raid-resync-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -25,17 +25,16 @@ bareMetal: - usr-verity-raid weekly: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap - # TODO(12232): enable - # - memory-constraint-combined + - memory-constraint-combined - misc - raid-big - raid-mirrored - raid-resync-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - split @@ -44,7 +43,7 @@ bareMetal: container: daily: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -52,14 +51,14 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity - usr-verity-raid validation: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -67,14 +66,14 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity - usr-verity-raid weekly: - base - # TODO(12276) re-enable: #- combined + - combined - encrypted-partition - encrypted-raid - encrypted-swap @@ -82,7 +81,7 @@ bareMetal: - raid-mirrored - raid-resync-small - raid-small - # TODO(12276) re-enable: #- rerun + - rerun - root-verity - simple - usr-verity From a7732d031fc80004ad54277c92111f436e6e6cb6 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Thu, 12 Jun 2025 19:19:09 +0000 Subject: [PATCH 71/99] Merged PR 23496: bug: Fix root-verity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Fix for #12522 Related work items: #12522 --- .../testing_baremetal/update_host_config.py | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/.pipelines/templates/stages/testing_baremetal/update_host_config.py b/.pipelines/templates/stages/testing_baremetal/update_host_config.py index fe8af435c..8e2ce5f0b 100755 --- a/.pipelines/templates/stages/testing_baremetal/update_host_config.py +++ b/.pipelines/templates/stages/testing_baremetal/update_host_config.py @@ -10,7 +10,7 @@ def update_trident_host_config( *, - host_configuration: str, + host_configuration: dict, interface_name: str, interface_ip: str, interface_mac: Optional[str] = None, @@ -63,18 +63,64 @@ def update_trident_host_config( ) logging.info("Updating os disks device in trident.yaml") - disks = host_configuration.get("storage", {}).get("disks", []) + disks = host_configuration.get("storage").get("disks") for disk in disks: if disk["id"] == "os": disk["device"] = "/dev/sda" elif disk["id"] == "disk2": disk["device"] = "/dev/sdb" + # If this is root verity, we need to set an internal param to be able to + # configure the network. + if is_root_verity(host_configuration): + logging.info( + "Detected root verity configuration, setting 'writableEtcOverlayHooks' internal param." + ) + host_configuration.setdefault("internalParams", {})[ + "writableEtcOverlayHooks" + ] = True + logging.info( "Final trident_yaml content post all the updates: %s", host_configuration ) +def is_root_verity(host_configuration: dict) -> bool: + """ + Check if the host configuration is using root verity. + """ + + verity_config = host_configuration.get("storage", {}).get("verity", []) + if len(verity_config) == 0: + return False + + if len(verity_config) > 1: + raise ValueError("Multiple verity configurations found, expected only one.") + + verity = verity_config[0] + verity_id = verity.get("id") + + filesystems = host_configuration.get("storage", {}).get("filesystems", []) + verity_filesystem = None + for fs in filesystems: + if fs.get("deviceId") == verity_id: + verity_filesystem = fs + break + + if verity_filesystem is None: + return False + + mount_point = verity_filesystem.get("mountPoint") + if mount_point is None: + return False + if isinstance(mount_point, str): + return mount_point == "/" + if isinstance(mount_point, dict): + return mount_point.get("path") == "/" + + return False + + def main(): logging.basicConfig( level=logging.INFO, @@ -102,6 +148,12 @@ def main(): "--oam-mac", default=None, help="MAC address of the OAM interface." ) parser.add_argument("--use-dhcp", default=False, help="Configure DHCP.") + parser.add_argument( + "-o", + "--output", + default=None, + help="Output file path. Defaults to editing the input file.", + ) args = parser.parse_args() with open(args.trident_yaml) as f: @@ -115,7 +167,9 @@ def main(): network_gateway=args.oam_gateway, use_dhcp=args.use_dhcp, ) - with open(args.trident_yaml, "w") as f: + + output_path = args.output or args.trident_yaml + with open(output_path, "w") as f: yaml.dump(trident_yaml_content, f, default_flow_style=False) From ea78c49880e5afd5ef9c1ee966c994665b18de5e Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Thu, 12 Jun 2025 20:10:40 +0000 Subject: [PATCH 72/99] Merged PR 23499: engineering: A couple small fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR includes a couple small fixes, including removing `Dockerfile.azl2` and adding some explanatory comments to the other Docker files for the different uses. ---- #### AI description (iteration 1) #### PR Classification Engineering fix focused on build configuration cleanup and documentation improvements. #### PR Summary This pull request cleans up build configurations by deleting an outdated Dockerfile and updating related Makefile targets, while also adding descriptive headers to Dockerfiles. - `Dockerfile.azl2` was deleted, removing the associated build target. - `Makefile` was updated to eliminate the `bin/trident-rpms-azl2.tar.gz` target from both its recipe and the overall dependency list, and the `bin/trident-rpms-azl3.tar.gz` target now includes additional dependencies. - `Dockerfile.azl3`, `Dockerfile.full`, and `Dockerfile.runtime` received header comments that clarify their specific roles in local testing, pipeline builds, and runtime image construction. Related work items: #12533 --- Dockerfile.azl2 | 26 -------------------------- Dockerfile.azl3 | 2 ++ Dockerfile.full | 2 ++ Dockerfile.runtime | 2 ++ Makefile | 17 ++--------------- 5 files changed, 8 insertions(+), 41 deletions(-) delete mode 100644 Dockerfile.azl2 diff --git a/Dockerfile.azl2 b/Dockerfile.azl2 deleted file mode 100644 index 0f45f9ede..000000000 --- a/Dockerfile.azl2 +++ /dev/null @@ -1,26 +0,0 @@ -FROM mcr.microsoft.com/cbl-mariner/base/rust:1 - -RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel - -WORKDIR /work - -COPY trident.spec . -COPY systemd ./systemd -COPY bin/trident ./target/release/trident -COPY artifacts/osmodifier /usr/src/mariner/SOURCES/osmodifier -COPY selinux-policy-trident/trident.te /usr/src/azl/SOURCES/trident.te -COPY selinux-policy-trident/trident.fc /usr/src/azl/SOURCES/trident.fc -COPY selinux-policy-trident/trident.if /usr/src/azl/SOURCES/trident.if -COPY packaging/static-pcrlock-files/ /usr/src/mariner/SOURCES/static-pcrlock-files/ - -ARG TRIDENT_VERSION=dev-build -ARG RPM_VER=0.1.0 -ARG RPM_REL=1 - -RUN \ - sed -i "s/cargo build/#cargo build/g" trident.spec && \ - rpmbuild -bb --build-in-place trident.spec \ - --define="trident_version $TRIDENT_VERSION" \ - --define="rpm_ver $RPM_VER" \ - --define="rpm_rel $RPM_REL" && \ - tar -czvf trident-rpms.tar.gz -C /usr/src/mariner ./RPMS diff --git a/Dockerfile.azl3 b/Dockerfile.azl3 index 42fb692c9..1ca3dd430 100644 --- a/Dockerfile.azl3 +++ b/Dockerfile.azl3 @@ -1,3 +1,5 @@ +### This file is primarily used to build Trident RPMs for local testing. + FROM mcr.microsoft.com/azurelinux/base/core:3.0 RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed selinux-policy-devel diff --git a/Dockerfile.full b/Dockerfile.full index e600166d2..a7fcd3a96 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -1,3 +1,5 @@ +### This file is used in pipelines to build Trident RPMs. + FROM mcr.microsoft.com/azurelinux/base/core:3.0 RUN tdnf install -y rpmdevtools openssl-devel clang-devel protobuf-devel rust sed ca-certificates perl build-essential selinux-policy-devel diff --git a/Dockerfile.runtime b/Dockerfile.runtime index af56a6f0c..f385cdb38 100644 --- a/Dockerfile.runtime +++ b/Dockerfile.runtime @@ -1,3 +1,5 @@ +### This file is used to build a container image with Trident inside. + FROM mcr.microsoft.com/azurelinux/base/core:3.0 RUN tdnf -y install \ diff --git a/Makefile b/Makefile index 196969387..11192a3fe 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ NETLAUNCH_CONFIG ?= input/netlaunch.yaml OVERRIDE_RUST_FEED ?= true .PHONY: all -all: format check test build-api-docs bin/trident-rpms-azl2.tar.gz bin/trident-rpms-azl3.tar.gz docker-build build-functional-test coverage validate-configs generate-mermaid-diagrams +all: format check test build-api-docs bin/trident-rpms-azl3.tar.gz docker-build build-functional-test coverage validate-configs generate-mermaid-diagrams .PHONY: check check: @@ -117,20 +117,7 @@ bin/trident: build @mkdir -p bin @cp -u target/release/trident bin/ -bin/trident-rpms-azl2.tar.gz: Dockerfile.azl2 systemd/*.service trident.spec artifacts/osmodifier bin/trident - @docker build --quiet -t trident/trident-build:latest \ - --build-arg TRIDENT_VERSION="$(TRIDENT_CARGO_VERSION)-dev.$(GIT_COMMIT)" \ - --build-arg RPM_VER="$(TRIDENT_CARGO_VERSION)" \ - --build-arg RPM_REL="dev.$(GIT_COMMIT)" \ - -f Dockerfile.azl2 \ - . - @id=$$(docker create trident/trident-build:latest) && \ - docker cp -q $$id:/work/trident-rpms.tar.gz $@ && \ - docker rm -v $$id - @rm -rf bin/RPMS/x86_64 - @tar xf $@ -C bin/ - -bin/trident-rpms-azl3.tar.gz: Dockerfile.azl3 systemd/*.service trident.spec artifacts/osmodifier bin/trident +bin/trident-rpms-azl3.tar.gz: Dockerfile.azl3 systemd/*.service trident.spec artifacts/osmodifier bin/trident selinux-policy-trident/* @docker build -t trident/trident-build:latest \ --build-arg TRIDENT_VERSION="$(TRIDENT_CARGO_VERSION)-dev.$(GIT_COMMIT)" \ --build-arg RPM_VER="$(TRIDENT_CARGO_VERSION)" \ From 8ec82d31d9af0eef9e882e77d1393eaf269c3511 Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Fri, 13 Jun 2025 03:27:44 +0000 Subject: [PATCH 73/99] Merged PR 23498: Add logic to parse and validate systemd-pcrlock log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR adds logic to parse and validate systemd-pcrlock log. ---- #### AI description (iteration 1) #### PR Classification This pull request implements a new feature by adding logic to parse and validate systemd-pcrlock logs for improved PCR-based encryption integration. #### PR Summary The changes introduce a function to run the systemd-pcrlock log command with JSON output, parse the log entries, and validate that each entry corresponds to a recognized component. - `osutils/src/pcrlock.rs`: Added the `parse_and_validate_log` function to run the command with a JSON flag, deserialize the log into structured entries, and error out if any entry lacks a recognized component. - `osutils/src/pcrlock.rs`: Integrated the new log validation call into the PCR lock file generation process and implemented a related functional test. - `trident_api/src/error.rs`: Added new error variants (`ValidatePcrlockLog`, `GetPcrlockLog`, and `ParsePcrlockLog`) to handle failures in receiving or parsing log output. Related work items: #12123 --- osutils/src/pcrlock.rs | 90 ++++++++++++++++++++++++++++++++++++---- trident_api/src/error.rs | 3 ++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/osutils/src/pcrlock.rs b/osutils/src/pcrlock.rs index d4d407989..e795fa370 100644 --- a/osutils/src/pcrlock.rs +++ b/osutils/src/pcrlock.rs @@ -4,7 +4,7 @@ use std::{ process::Command, }; -use anyhow::{Context, Error, Result}; +use anyhow::{bail, Context, Error, Result}; use enumflags2::{make_bitflags, BitFlags}; use goblin::pe::PE; use log::{debug, error, trace, warn}; @@ -12,7 +12,10 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256, Sha384, Sha512}; use tempfile::NamedTempFile; -use trident_api::error::{ReportError, ServicingError, TridentError}; +use trident_api::{ + error::{ReportError, ServicingError, TridentError}, + primitives::hash::Sha256Hash, +}; use sysdefs::tpm2::Pcr; @@ -131,15 +134,73 @@ pub fn generate_tpm2_access_policy(pcrs: BitFlags) -> Result<(), TridentErr Ok(()) } -/// Runs `systemd-pcrlock log` command to view the combined TPM 2.0 event log matched against the -/// current PCR values, output in a tabular format. -#[allow(dead_code)] -fn log() -> Result { - Dependency::SystemdPcrlock +#[derive(Debug, Deserialize)] +struct LogEntry { + pcr: Pcr, + pcrname: Option, + event: Option, + sha256: Option, + component: Option, + description: Option, +} + +#[derive(Debug, Deserialize)] +struct LogOutput { + log: Vec, +} + +/// Runs `systemd-pcrlock log` to get the combined TPM 2.0 event log, output as a "pretty" JSON. +/// Parses the output and validates that every log entry has been matched to a recognized boot +/// component. +/// +/// If a log entry has a null component, it means that there is no .pcrlock file that records that +/// specific measurement extended into the given PCR, for any boot process component. For that +/// reason, .pcrlock files are known as boot component definition files. If a log entry for a PCR +/// has its component missing, then the value of that PCR cannot be predicted and so the PCR cannot +/// be included in a pcrlock policy. Thus, this validation ensures that all .pcrlock files have +/// been added & generated, so that a valid TPM 2.0 access policy can be generated. +/// Please refer to `systemd-pcrlock` doc for additional info: +/// https://www.man7.org/linux/man-pages/man8/systemd-pcrlock.8.html. +fn validate_log() -> Result<(), Error> { + let output = Dependency::SystemdPcrlock .cmd() .arg("log") + .arg("--json=pretty") .output_and_check() - .context("Failed to run systemd-pcrlock log") + .context("Failed to run systemd-pcrlock log")?; + + let parsed: LogOutput = + serde_json::from_str(&output).context("Failed to parse systemd-pcrlock log output")?; + + let unrecognized: Vec<_> = parsed + .log + .iter() + .filter(|entry| entry.component.is_none()) + .collect(); + + if unrecognized.is_empty() { + return Ok(()); + } + + let entries: Vec = unrecognized + .into_iter() + .map(|entry| { + format!( + "pcr='{}', pcrname='{}', event='{}', sha256='{}', description='{}'", + entry.pcr.to_num(), + entry.pcrname.as_deref().unwrap_or("null"), + entry.event.as_deref().unwrap_or("null"), + entry.sha256.as_ref().map(|h| h.as_str()).unwrap_or("null"), + entry.description.as_deref().unwrap_or("null"), + ) + }) + .collect(); + + bail!( + "Failed to validate systemd-pcrlock log output as some log entries cannot be matched \ + to recognized components:\n{}", + entries.join("\n") + ); } /// Runs `systemd-pcrlock make-policy` command to predict the PCR state for future boots and then @@ -435,6 +496,11 @@ pub fn generate_pcrlock_files( )?; } + // Parse the systemd-pcrlock log output to validate that every log entry has been matched to a + // recognized boot component, and thus that all necessary .pcrlock files have been added or + // generated + validate_log().structured(ServicingError::ValidatePcrlockLog)?; + Ok(()) } @@ -670,4 +736,12 @@ mod functional_test { // TODO: Add other/more test cases once helpers are implemented and statically defined // pcrlock files have been added. } + + #[functional_test(feature = "helpers")] + fn test_validate_log() { + // TODO: This test will fail for now since .pcrlock files have not been generated/added + // yet. Once static .pcrlock files are added and dynamic files are generated, the test + // should pass. + validate_log().unwrap_err(); + } } diff --git a/trident_api/src/error.rs b/trident_api/src/error.rs index f6a1f7a1b..88fc90f59 100644 --- a/trident_api/src/error.rs +++ b/trident_api/src/error.rs @@ -561,6 +561,9 @@ pub enum ServicingError { hs_active_volume: String, }, + #[error("Failed to validate systemd-pcrlock log output")] + ValidatePcrlockLog, + #[error("Trident rebuild-raid validation failed")] ValidateRebuildRaid, From e05500566e6669536e3763e7287f1f0c8b0a8fe1 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Fri, 13 Jun 2025 16:01:45 +0000 Subject: [PATCH 74/99] Merged PR 23486: engineering: Enable enforcing mode on usrverity tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Enable `enforcing` mode on all e2e tests that use usrverity. Passing e2e tests: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=835091&view=results ---- #### AI description (iteration 1) #### PR Classification This PR implements a configuration update to enable enforcing SELinux mode for usrverity tests. #### PR Summary The pull request introduces SELinux configuration support and updates associated policy rules to enforce the new mode during usrverity tests. - `src/subsystems/osconfig/mod.rs`: Added a code block to update SELinux settings when UKI support is enabled. - `osutils/src/osmodifier.rs`: Extended the OSModifierConfig struct to include a SELinux configuration field. - `selinux-policy-trident/trident.te`: Modified multiple SELinux policy rules (including new capabilities, file permissions, and type definitions) to support enforcing mode. - `e2e_tests/trident_configurations/*/trident-config.yaml`: Updated test configuration files to set SELinux mode to enforcing. Related work items: #12482 --- azure-linux-image-tools | 2 +- .../combined/trident-config.yaml | 2 + .../rerun/trident-config.yaml | 2 +- .../usr-verity-raid/trident-config.yaml | 2 + .../usr-verity/trident-config.yaml | 2 + osutils/src/osmodifier.rs | 3 ++ selinux-policy-trident/trident.te | 38 +++++++++++++++---- src/lib.rs | 8 ++-- src/subsystems/osconfig/mod.rs | 8 ++++ 9 files changed, 55 insertions(+), 12 deletions(-) diff --git a/azure-linux-image-tools b/azure-linux-image-tools index e352dc2fc..e52c4572c 160000 --- a/azure-linux-image-tools +++ b/azure-linux-image-tools @@ -1 +1 @@ -Subproject commit e352dc2fc96199a546535b426962e88af3015823 +Subproject commit e52c4572cf1b8d98738a2ba40d17006752b7d0e4 diff --git a/e2e_tests/trident_configurations/combined/trident-config.yaml b/e2e_tests/trident_configurations/combined/trident-config.yaml index 71e75d14d..81c96b0e8 100644 --- a/e2e_tests/trident_configurations/combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/combined/trident-config.yaml @@ -159,6 +159,8 @@ storage: source: new mountPoint: /var/lib/trident os: + selinux: + mode: enforcing network: version: 2 ethernets: diff --git a/e2e_tests/trident_configurations/rerun/trident-config.yaml b/e2e_tests/trident_configurations/rerun/trident-config.yaml index 5f132b635..917d01798 100644 --- a/e2e_tests/trident_configurations/rerun/trident-config.yaml +++ b/e2e_tests/trident_configurations/rerun/trident-config.yaml @@ -188,7 +188,7 @@ scripts: exit 1 os: selinux: - mode: permissive + mode: enforcing network: version: 2 ethernets: diff --git a/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml index 329b35ba4..bc8493996 100644 --- a/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml +++ b/e2e_tests/trident_configurations/usr-verity-raid/trident-config.yaml @@ -82,6 +82,8 @@ storage: source: new mountPoint: /var/lib/trident os: + selinux: + mode: enforcing network: version: 2 ethernets: diff --git a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml index 082dbe506..0ba061068 100644 --- a/e2e_tests/trident_configurations/usr-verity/trident-config.yaml +++ b/e2e_tests/trident_configurations/usr-verity/trident-config.yaml @@ -79,6 +79,8 @@ storage: source: new mountPoint: /var/lib/trident os: + selinux: + mode: enforcing network: version: 2 ethernets: diff --git a/osutils/src/osmodifier.rs b/osutils/src/osmodifier.rs index 42063359f..79f9fcdf4 100644 --- a/osutils/src/osmodifier.rs +++ b/osutils/src/osmodifier.rs @@ -26,6 +26,9 @@ pub struct OSModifierConfig { #[serde(skip_serializing_if = "Option::is_none")] pub kernel_command_line: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub selinux: Option, } impl OSModifierConfig { diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index 273428ec7..b2eb7da77 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -29,6 +29,7 @@ require { type auditd_unit_t; type bluetooth_unit_t; type boot_t; + type bpf_t; type bootloader_t; type cgroup_t; type chfn_exec_t; @@ -71,6 +72,7 @@ require { type init_exec_t; type init_runtime_t; type init_var_lib_t; + type initctl_t; type iptables_unit_t; type kernel_t; type kmod_exec_t; @@ -163,6 +165,7 @@ require { type systemd_networkd_exec_t; type systemd_notify_exec_t; type systemd_passwd_agent_exec_t; + type systemd_pcrphase_t; type systemd_pcrphase_exec_t; type systemd_pstore_exec_t; type systemd_resolved_exec_t; @@ -235,7 +238,7 @@ optional_policy(` allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; allow trident_t self:alg_socket { accept bind create read write }; -allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice }; +allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice linux_immutable }; allow trident_t self:fifo_file manage_fifo_file_perms; allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; @@ -265,7 +268,8 @@ allow trident_t admin_passwd_exec_t:file getattr; allow trident_t anacron_exec_t:file getattr; allow trident_t audisp_remote_exec_t:file getattr; allow trident_t bluetooth_unit_t:file getattr; -allow trident_t boot_t:dir mounton; +allow trident_t boot_t:dir { mounton create }; +allow trident_t bpf_t:dir search; allow trident_t cgroup_t:filesystem getattr; allow trident_t chfn_exec_t:file getattr; allow trident_t chkpwd_exec_t:file getattr; @@ -292,12 +296,14 @@ allow trident_t default_t:dir { getattr open read relabelto search }; allow trident_t default_t:file relabelto; allow trident_t device_t:filesystem { getattr mount unmount }; allow trident_t devpts_t:chr_file { read write ioctl getattr }; +allow trident_t devlog_t:sock_file getattr; allow trident_t dhcpc_exec_t:file getattr; allow trident_t dhcpc_state_t:dir { getattr open read }; allow trident_t dhcpd_unit_t:file getattr; allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t dosfs_t:filesystem { getattr mount unmount }; allow trident_t efivarfs_t:filesystem getattr; +allow trident_t efivarfs_t:dir search; allow trident_t etc_runtime_t:file { getattr open read relabelto relabelfrom setattr unlink }; allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; allow trident_t fs_t:filesystem { mount unmount }; @@ -312,11 +318,12 @@ allow trident_t groupadd_exec_t:file getattr; allow trident_t home_root_t:dir { mounton read relabelto add_name create relabelfrom setattr write }; allow trident_t home_root_t:file { create getattr ioctl open relabelfrom setattr write }; allow trident_t init_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t init_t:system reboot; +allow trident_t init_t:system { reboot start status }; allow trident_t init_t:key search; +allow trident_t init_t:unix_stream_socket connectto; +allow trident_t initctl_t:fifo_file getattr; allow trident_t init_runtime_t:dir { add_name write }; allow trident_t init_runtime_t:file { create getattr open write }; -allow trident_t init_t:unix_stream_socket connectto; allow trident_t init_var_lib_t:dir { getattr open read search }; allow trident_t iptables_unit_t:file getattr; allow trident_t kernel_t:process setsched; @@ -339,6 +346,7 @@ allow trident_t lvm_unit_t:file getattr; allow trident_t mail_spool_t:dir list_dir_perms; allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t mdadm_runtime_t:dir { add_name remove_name search write }; allow trident_t memory_pressure_t:file { read open getattr setattr }; allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; allow trident_t modules_conf_t:file { relabelto setattr unlink }; @@ -385,6 +393,7 @@ allow trident_t sysctl_vm_t:dir search; allow trident_t sysfs_t:filesystem { mount unmount }; allow trident_t syslog_conf_t:file { getattr open read relabelto setattr unlink }; allow trident_t syslogd_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +allow trident_t syslogd_runtime_t:dir search; allow trident_t syslogd_unit_t:file { getattr open read relabelto setattr unlink }; allow trident_t systemd_analyze_exec_t:file getattr; allow trident_t systemd_backlight_exec_t:file getattr; @@ -414,6 +423,7 @@ allow trident_t systemd_networkd_exec_t:file { execute getattr }; allow trident_t systemd_notify_exec_t:file getattr; allow trident_t systemd_passwd_agent_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t systemd_pcrphase_exec_t:file { execute getattr }; +allow trident_t pstore_t:dir search; allow trident_t systemd_pstore_exec_t:file { execute getattr }; allow trident_t systemd_resolved_exec_t:file { execute getattr }; allow trident_t systemd_rfkill_exec_t:file getattr; @@ -434,7 +444,7 @@ allow trident_t systemd_userdbd_exec_t:file getattr; allow trident_t systemd_userdbd_unit_t:file getattr; allow trident_t tmp_t:chr_file { create getattr unlink }; allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write }; -allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write }; +allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write map }; allow trident_t tmp_t:lnk_file { create getattr read rename unlink }; allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map }; allow trident_t tmpfs_t:filesystem { getattr mount unmount }; @@ -445,9 +455,10 @@ allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink allow trident_t unreserved_port_t:tcp_socket name_connect; allow trident_t updpwd_exec_t:file getattr; allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; -allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto }; +allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto mounton }; allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; allow trident_t uuidd_exec_t:file getattr; +allow trident_t var_t:dir mounton; allow trident_t var_run_t:dir { add_name create remove_name write }; allow trident_t var_run_t:file { create getattr lock open read unlink write }; @@ -507,11 +518,13 @@ systemd_read_user_unit_files(trident_t) ########################################### # File System Operations ########################################### +files_create_boot_dirs(trident_t) files_list_kernel_modules(trident_t) files_list_spool(trident_t) files_list_var(trident_t) files_manage_boot_files(trident_t) files_manage_etc_dirs(trident_t) +files_manage_etc_symlinks(trident_t) files_mounton_runtime_dirs(trident_t) files_read_etc_files(trident_t) files_read_default_symlinks(trident_t) @@ -533,11 +546,14 @@ fs_getattr_iso9660_fs(trident_t) fs_getattr_pstorefs(trident_t) fs_getattr_tracefs(trident_t) fs_getattr_xattr_fs(trident_t) +fs_list_efivars(trident_t) fs_list_hugetlbfs(trident_t) fs_manage_dos_dirs(trident_t) fs_manage_dos_files(trident_t) +fs_manage_efivarfs_files(trident_t) fs_manage_tmpfs_dirs(trident_t) fs_manage_tmpfs_symlinks(trident_t) +fs_mounton_tmpfs(trident_t) fs_read_iso9660_files(trident_t) fs_watch_memory_pressure(trident_t) mount_list_runtime(trident_t) @@ -637,6 +653,7 @@ dev_rw_vhost(trident_t) dev_search_sysfs(trident_t) dev_write_sysfs(trident_t) dev_write_urand(trident_t) +raid_manage_mdadm_runtime_files(trident_t) term_getattr_ptmx(trident_t) term_use_virtio_console(trident_t) term_getattr_pty_fs(trident_t) @@ -667,7 +684,9 @@ uuidd_manage_lib_dirs(trident_t) ########################################### logging_read_audit_config(trident_t) logging_read_audit_log(trident_t) +logging_relabelto_devlog_sock_files(trident_t) logging_search_logs(trident_t) +logging_stream_connect_journald_varlink(trident_t) logging_manage_generic_log_dirs(trident_t) logging_manage_generic_logs(trident_t) @@ -715,12 +734,17 @@ files_read_default_symlinks(loadkeys_t) fs_search_tmpfs(loadkeys_t) #============= lvm_t ============== -# Allow lvm_t access to semaphores of the initrc_t type; this is necessary for Trident to create encrypted volumes +# This is necessary for Trident to create encrypted volumes allow lvm_t trident_t:sem { associate read unix_read unix_write write }; +allow lvm_t initrc_t:sem { associate read unix_read unix_write write }; #============= mount_t ============= allow mount_t trident_var_lib_t:dir mounton; +#============= systemd_pcrphase_t ============== +allow systemd_pcrphase_t tmpfs_t:dir { getattr open read search }; +allow systemd_pcrphase_t tmpfs_t:file { getattr lock open setattr write }; + #============= rpm_t ============== allow rpm_t unlabeled_t:dir { add_name getattr remove_name search write }; allow rpm_t unlabeled_t:file { create getattr ioctl open read rename write }; diff --git a/src/lib.rs b/src/lib.rs index c444d42fd..51ddadb3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use std::{ use cli::GetKind; use engine::{bootentries, EngineContext}; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info, warn}; use nix::unistd::Uid; use osutils::{block_devices, container, dependencies::Dependency}; @@ -185,9 +185,11 @@ impl Trident { } if let Ok(selinux_context) = fs::read_to_string("/proc/self/attr/current") { - trace!("selinux context: Trident is running in SELinux domain '{selinux_context}'"); + debug!("selinux debug: Trident is running in SELinux domain '{selinux_context}'"); } else { - error!("selinux context: Failed to retrieve the SELinux context in which Trident is running"); + error!( + "selinux debug: Failed to retrieve the SELinux context in which Trident is running" + ); } if !Uid::effective().is_root() { diff --git a/src/subsystems/osconfig/mod.rs b/src/subsystems/osconfig/mod.rs index bc029225e..1ef85271c 100644 --- a/src/subsystems/osconfig/mod.rs +++ b/src/subsystems/osconfig/mod.rs @@ -176,6 +176,14 @@ impl Subsystem for OsConfigSubsystem { os_modifier_config.kernel_command_line = Some(ctx.spec.os.kernel_command_line.clone()); } + // If UKI support is enabled, update SELinux mode here + if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) + && ctx.spec.os.selinux.mode.is_some() + { + debug!("Updating SELinux config"); + os_modifier_config.selinux = Some(ctx.spec.os.selinux.clone()); + } + os_modifier_config .call_os_modifier(Path::new(OS_MODIFIER_NEWROOT_PATH)) .structured(ServicingError::RunOsModifier)?; From 93aa672b283783307087cca28d4ac2a8a99855d4 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Fri, 13 Jun 2025 17:04:03 +0000 Subject: [PATCH 75/99] Merged PR 23510: engineering: Explicitly set memory-constraint-combined to enforcing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description e2e test (currently running): https://dev.azure.com/mariner-org/ECF/_build/results?buildId=835447&view=results ---- #### AI description (iteration 1) #### PR Classification Configuration update to enforce SELinux mode for the memory-constraint-combined test environment. #### PR Summary This PR updates configuration files to explicitly set SELinux to enforcing mode for memory-constraint-combined and includes it in the list of target configurations. - `e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml`: Added SELinux with `mode: enforcing`. - `e2e_tests/target-configurations.yaml`: Appended `memory-constraint-combined` to the pullrequest configuration list. Related work items: #12482 --- e2e_tests/target-configurations.yaml | 3 --- .../memory-constraint-combined/trident-config.yaml | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e_tests/target-configurations.yaml b/e2e_tests/target-configurations.yaml index d481bdba8..bf35e8283 100644 --- a/e2e_tests/target-configurations.yaml +++ b/e2e_tests/target-configurations.yaml @@ -125,9 +125,6 @@ virtualMachine: pullrequest: - base - combined - # TODO(10522): Can remove encrypted-partition from pipeline when combined/rerun runs in - # enforcing mode - - encrypted-partition - raid-mirrored - raid-resync-small - rerun diff --git a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml index 283f36581..a1e980af4 100644 --- a/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml +++ b/e2e_tests/trident_configurations/memory-constraint-combined/trident-config.yaml @@ -176,6 +176,8 @@ scripts: exit 1 fi os: + selinux: + mode: enforcing network: version: 2 ethernets: From ca15f066fcbb2b8c724cf82766c903c29279afd7 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Fri, 13 Jun 2025 20:55:09 +0000 Subject: [PATCH 76/99] Merged PR 23465: engineering: Faster FTs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Faster FTs :) ---- #### AI description (iteration 1) #### PR Classification This pull request introduces performance improvements to the functional test framework by refactoring VM setup and pipeline configurations. #### PR Summary This PR streamlines functional tests to run faster by overhauling the VM creation process and updating related build and pipeline settings. - **`functional_tests/test_setup.py`**: Refactored VM creation and online check logic with cloud-init configuration, added logging and timeout handling, and removed deprecated functions. - **`functional_tests/custom/test_trident_e2e.py`**: Commented out several Trident tests to bypass unnecessary execution during functional test runs. - **Pipeline templates (.pipelines/templates)**: Updated parameter names and artifact download steps to use a prebuilt FT image and adjust rerun logic. - **`Makefile` and `functional_tests/conftest.py`**: Modified build targets to generate a new FT QCOW2 image and updated fixture types for improved configuration. Related work items: #12544 --- .pipelines/templates/e2e-template.yml | 4 +- .../testing_functional/functional-testing.yml | 65 ++++-- Makefile | 43 ++-- dev-docs/testing.md | 75 ++++--- functional_tests/conftest.py | 86 ++++---- functional_tests/custom/test_trident_e2e.py | 198 +++++++++-------- functional_tests/test_setup.py | 207 ++++++++---------- osutils/src/encryption.rs | 14 +- osutils/src/sfdisk.rs | 95 ++++---- src/engine/boot/grub.rs | 48 +++- src/engine/boot/test_files/grub_boot.cfg | 137 ++++++++++++ src/engine/boot/test_files/grub_esp.cfg | 13 ++ src/engine/mod.rs | 36 +-- 13 files changed, 624 insertions(+), 397 deletions(-) create mode 100644 src/engine/boot/test_files/grub_boot.cfg create mode 100644 src/engine/boot/test_files/grub_esp.cfg diff --git a/.pipelines/templates/e2e-template.yml b/.pipelines/templates/e2e-template.yml index a30e1c3c2..0b9faa364 100644 --- a/.pipelines/templates/e2e-template.yml +++ b/.pipelines/templates/e2e-template.yml @@ -287,6 +287,8 @@ stages: - ${{ if eq(parameters.stageType, 'ci') }}: # Functional Testing - template: stages/testing_functional/functional-testing.yml + parameters: + downloadPrebuiltImage: false # VM Testing (host, post_merge) - template: stages/testing_vm/netlaunch-testing.yml @@ -305,7 +307,7 @@ stages: # Functional Testing (short version) - template: stages/testing_functional/functional-testing.yml parameters: - buildPurpose: "validation" + rerunTests: false # VM Testing (host, pullrequest) - template: stages/testing_vm/netlaunch-testing.yml diff --git a/.pipelines/templates/stages/testing_functional/functional-testing.yml b/.pipelines/templates/stages/testing_functional/functional-testing.yml index ff792d8fe..fa3b95858 100644 --- a/.pipelines/templates/stages/testing_functional/functional-testing.yml +++ b/.pipelines/templates/stages/testing_functional/functional-testing.yml @@ -1,27 +1,30 @@ parameters: - - name: testingRun - displayName: "Download prebuilt test artifacts" + - name: downloadPrebuiltImage + displayName: "Download prebuilt base image" type: boolean - default: false + default: true - - name: buildPurpose + - name: rerunTests + displayName: "Rerun functional tests on the same VM" + type: boolean + default: true + + - name: functestImageArtifact type: string - default: "post_merge" - values: - - post_merge - - validation + default: "trident-functest" + + - name: functestImageArtifactPipeline + type: number + default: 3371 # trident-ci + displayName: Where to download prebuilt image from stages: - stage: FunctionalTesting displayName: Functional Testing dependsOn: - - ${{ if eq(parameters.testingRun, true) }}: - - DownloadTestingElements - - ${{ else }}: - - BuildingTools - - TridentTestImg_trident_installer - - TridentTestImg_trident_testimage - - TridentTestImg_trident_verity_testimage + - ${{ if eq(parameters.downloadPrebuiltImage, false) }}: + - TridentTestImg_trident_functest + - ${{ else }}: [] jobs: - job: FunctionalTests @@ -34,7 +37,7 @@ stages: variables: ob_outputDirectory: $(Build.SourcesDirectory)/build - argusToolkitSourceDirectory: $(Build.SourcesDirectory)/argus-toolkit + baseImageDirectory: "$(Build.SourcesDirectory)/artifacts" steps: - checkout: argus-toolkit @@ -44,11 +47,27 @@ stages: - template: ../common_tasks/cargo-auth.yml - # Download all test images for host - - template: ../testing_common/download-test-images.yml - - template: ../testing_vm/netlaunch-prep.yml + - ${{ if eq(parameters.downloadPrebuiltImage, false) }}: + - task: DownloadPipelineArtifact@2 + displayName: "Download FT Image from current build" + inputs: + buildType: current + artifactName: "${{ parameters.functestImageArtifact }}" + targetPath: "${{ variables.baseImageDirectory }}" + - ${{ else }}: + - task: DownloadPipelineArtifact@2 + displayName: "Download FT Image from latest build of ${{ parameters.functestImageArtifactPipeline}}" + inputs: + buildType: specific + project: "ECF" + definition: ${{ parameters.functestImageArtifactPipeline}} + buildVersionToDownload: latestFromBranch + branchName: "refs/heads/main" + artifactName: "${{ parameters.functestImageArtifact }}" + targetPath: "${{ variables.baseImageDirectory }}" + - bash: | set -eux @@ -58,6 +77,8 @@ stages: displayName: Install dependencies retryCountOnTaskFailure: 3 + - template: ../common_tasks/build-osmodifier.yml + - bash: | set -eux @@ -78,7 +99,7 @@ stages: # rebuilding the test binaries and invoking pytest. The regular # target is meant for local use and does extra setup not required # here. - sg libvirt "make functional-test-core INSTALLER_ISO_PATH=./artifacts/iso/trident-installer.iso ARGUS_TOOLKIT_PATH=argus-toolkit" + sg libvirt "make functional-test-core ARGUS_TOOLKIT_PATH=argus-toolkit" displayName: Execute Functional Tests - template: ../common_tasks/coverage.yml @@ -97,6 +118,6 @@ stages: - bash: | set -eux - sg libvirt "make patch-functional-test INSTALLER_ISO_PATH=$(System.ArtifactsDirectory)/trident-installer.iso ARGUS_TOOLKIT_PATH=argus-toolkit" - condition: and(succeeded(), eq('${{ parameters.buildPurpose }}', 'post_merge')) + sg libvirt "make patch-functional-test ARGUS_TOOLKIT_PATH=argus-toolkit" + condition: and(succeeded(), eq('${{ parameters.rerunTests }}', 'true')) displayName: Rerun Functional Tests diff --git a/Makefile b/Makefile index 11192a3fe..1eed1d99c 100644 --- a/Makefile +++ b/Makefile @@ -260,28 +260,45 @@ build-functional-test-cc: .cargo/config cargo build --target-dir $(TRIDENT_COVERAGE_TARGET) --lib --tests --features functional-test --all .PHONY: functional-test -functional-test: bin/trident-mos.iso bin/trident artifacts/osmodifier artifacts/test-image/regular.cosi bin/netlaunch +functional-test: artifacts/trident-functest.qcow2 cp $(PLATFORM_TESTS_PATH)/tools/marinerhci_test_tools/node_interface.py functional_tests/ cp $(PLATFORM_TESTS_PATH)/tools/marinerhci_test_tools/ssh_node.py functional_tests/ - cp bin/trident artifacts/test-image/ - cp artifacts/osmodifier artifacts/test-image/ $(MAKE) functional-test-core # A target for pipelines that skips all setup and building steps that are not # required in the pipeline environment. .PHONY: functional-test-core -functional-test-core: build-functional-test-cc generate-functional-test-manifest -# Check if INSTALLER_ISO_PATH is set, if not, check if the installer iso is present in the bin directory -ifndef INSTALLER_ISO_PATH -ifeq ($(wildcard bin/trident-mos.iso),) - $(error INSTALLER_ISO_PATH is not set and bin/trident-mos.iso is not present in the bin directory) -endif -endif - python3 -u -m pytest functional_tests/test_setup.py functional_tests/$(FILTER) --keep-duplicates -v -o junit_logging=all --junitxml $(FUNCTIONAL_TEST_JUNIT_XML) ${FUNCTIONAL_TEST_EXTRA_PARAMS} --keep-environment --test-dir $(FUNCTIONAL_TEST_DIR) --build-output $(BUILD_OUTPUT) --force-upload +functional-test-core: artifacts/osmodifier build-functional-test-cc generate-functional-test-manifest artifacts/trident-functest.qcow2 + python3 -u -m \ + pytest --color=yes \ + --log-level=INFO \ + --force-upload \ + functional_tests/test_setup.py \ + functional_tests/$(FILTER) \ + --keep-duplicates \ + -v \ + -o junit_logging=all \ + --junitxml $(FUNCTIONAL_TEST_JUNIT_XML) \ + ${FUNCTIONAL_TEST_EXTRA_PARAMS} \ + --keep-environment \ + --test-dir $(FUNCTIONAL_TEST_DIR) \ + --build-output $(BUILD_OUTPUT) .PHONY: patch-functional-test -patch-functional-test: build-functional-test-cc generate-functional-test-manifest - ARGUS_TOOLKIT_PATH=$(ARGUS_TOOLKIT_PATH) python3 -u -m pytest functional_tests/$(FILTER) -v -o junit_logging=all --junitxml $(FUNCTIONAL_TEST_JUNIT_XML) ${FUNCTIONAL_TEST_EXTRA_PARAMS} --keep-environment --test-dir $(FUNCTIONAL_TEST_DIR) --build-output $(BUILD_OUTPUT) --reuse-environment +patch-functional-test: artifacts/osmodifier build-functional-test-cc generate-functional-test-manifest + python3 -u -m \ + pytest --color=yes \ + --log-level=INFO \ + --force-upload \ + functional_tests/$(FILTER) \ + -v \ + -o junit_logging=all \ + --junitxml $(FUNCTIONAL_TEST_JUNIT_XML) \ + ${FUNCTIONAL_TEST_EXTRA_PARAMS} \ + --keep-environment \ + --test-dir $(FUNCTIONAL_TEST_DIR) \ + --build-output $(BUILD_OUTPUT) \ + --reuse-environment .PHONY: generate-functional-test-manifest generate-functional-test-manifest: .cargo/config diff --git a/dev-docs/testing.md b/dev-docs/testing.md index d3122132d..2a0140422 100644 --- a/dev-docs/testing.md +++ b/dev-docs/testing.md @@ -22,7 +22,7 @@ more details in the following sections. - [Deploy Baremetal Environment - baremetal-deploy.yml](#deploy-baremetal-environment---baremetal-deployyml) - [Run End-to-End Tests on the BM Host - e2e-test-run.yml](#run-end-to-end-tests-on-the-bm-host---e2e-test-runyml) - [Update trident.yaml to reflect the OAM IP, HTTP server and SSH Key Details - baremetal-update-trident-host-config.yml](#update-tridentyaml-to-reflect-the-oam-ip-http-server-and-ssh-key-details---baremetal-update-trident-host-configyml) - - [Boot baremetal lab machine - .pipelines/stages/testing\_baremetal/deploy\_on\_bm.py](#boot-baremetal-lab-machine---pipelinesstagestesting_baremetaldeploy_on_bmpy) + - [Boot baremetal lab machine - .pipelines/templates/stages/testing\_baremetal/deploy\_on\_bm.py](#boot-baremetal-lab-machine---pipelinestemplatesstagestesting_baremetaldeploy_on_bmpy) ## Unit Tests @@ -89,11 +89,11 @@ Functional tests should: Functional tests are structured as follows: - `/functional_tests`: Contains the functional test code, leveraging `pytest` - and common SSH interface from `platform-tests` repo. `pytest` creates the test VM - using is Fixtures concept and while currently only a single VM is created to - run all the tests, this could be easily extended to support seperate VMs for - different tests. Most of the time, no changes will be required to this layer - while developing functional tests. + and common SSH interface from `platform-tests` repo. `pytest` creates the test + VM using is Fixtures concept and while currently only a single VM is created + to run all the tests, this could be easily extended to support seperate VMs + for different tests. Most of the time, no changes will be required to this + layer while developing functional tests. - `/functional_tests/trident-setup.yaml`: Contains the initial host configuration for the VM that will be used to execute the functional tests. - `/functional_tests/custom/../*.py`: Manually authored Pytest modules for more @@ -139,10 +139,9 @@ deployment. The tests are started on the deployed OS through SSH connection. In the functional test environment, tests are run on top of block device /dev/sda. As a result, any changes made by the testing logic should **not** -modify this block device. E.g., this block device should not be reformatted to -a clean filesystem, be mounted/unmounted, etc. On the other hand, **/dev/sdb** -is available for any modifications that are needed for functional testing -purposes. +modify this block device. E.g., this block device should not be reformatted to a +clean filesystem, be mounted/unmounted, etc. On the other hand, **/dev/sdb** is +available for any modifications that are needed for functional testing purposes. ### Functional Test Building and Execution @@ -156,12 +155,9 @@ targets: - `make functional-test`: This will build the tests locally with code coverage profile (using internal `build-functional-test-cc` target), a new - `virt-deploy` VM will be created and deployed using `netlaunch`. Afterwards, + `virt-deploy` VM will be created using a prebuilt qcow image. Afterwards, tests will be uploaded into the VM, executed and code coverage will be - downloaded for later viewing. To note, this will also execute all UTs. If you - want to iterate on the tests without recreating the VM, but do want to - redeploy the OS, you can: `make functional-test - FUNCTIONAL_TEST_EXTRA_PARAMS="--reuse-environment --redeploy"`. + downloaded for later viewing. - `make patch-functional-test`: This will build the tests locally with code coverage profile (using internal `build-functional-test-cc` target), upload @@ -169,17 +165,16 @@ targets: code coverage for later viewing. This is useful when you want to iterate on the tests and don't want to wait for the VM to be deployed again. It is important to note that only tests that have changed will be re-uploaded. This - is determined based on `cargo build` output. To note, this will also execute - all UTs. - -To execute the functional tests, ensure that `platform-tests` and `argus-toolkit` of -recent version are checked out side by side with the `trident` repo. -Additionally, the following dependencies are required for the Ubuntu based -pipelines, so you might need to install them on your development machine as well -(note that this set is different per Ubuntu version and is provided just as an -illustration of what works for [pipelines](.pipelines/netlaunch-testing.yml), so -if you are on 22.04 or newer, you might not need to for example reinstall -`python3-openssl`): + is determined based on `cargo build` output. + +To execute the functional tests, ensure that `platform-tests` and +`argus-toolkit` of recent version are checked out side by side with the +`trident` repo. Additionally, the following dependencies are required for the +Ubuntu based pipelines, so you might need to install them on your development +machine as well (note that this set is different per Ubuntu version and is +provided just as an illustration of what works for +[pipelines](.pipelines/netlaunch-testing.yml), so if you are on 22.04 or newer, +you might not need to for example reinstall `python3-openssl`): ```bash sudo apt install -y protobuf-compiler clang-7 bc @@ -215,11 +210,11 @@ aggregated with the locally produced coverage results. ### Additional Notes -`functional-test` target depends on `platform-tests` and `argus-toolkit` repos to be -checked out side by side with the `trident` repo. This is because `platform-tests` -repo contains the common logic to execute test logic over SSH connection and -`argus-toolkit` repo contains the `netlaunch` and `virt-deploy` binaries, along -with logic to generate the OS deployment ISO. +`functional-test` target depends on `platform-tests` and `argus-toolkit` repos +to be checked out side by side with the `trident` repo. This is because +`platform-tests` repo contains the common logic to execute test logic over SSH +connection and `argus-toolkit` repo contains the `netlaunch` and `virt-deploy` +binaries, along with logic to generate the OS deployment ISO. Both `functional-test` and `patch-functional-test` targets leverage `pytest`. To get more detailed logs or do any changes to the `pytest` logic, you can modify @@ -240,15 +235,16 @@ code. ### Selective Test Execution -The functional test `make` targets support the variable `FILTER`. This is meant to -be used to filter the tests that are executed. For example, if you want to execute -only the tests from a certain rust crate, you can do: +The functional test `make` targets support the variable `FILTER`. This is meant +to be used to filter the tests that are executed. For example, if you want to +execute only the tests from a certain rust crate, you can do: ```bash make functional-test FILTER=ft.json:: ``` -You can narrow down the filter by adding the modules, up to each individual test. +You can narrow down the filter by adding the modules, up to each individual +test. ```bash make functional-test FILTER=ft.json:::::: @@ -294,11 +290,13 @@ End to end tests should: #### Install prerequisites for BM Host communication - baremetal-prep.yml -![Prerequisite Installation - BM Host](./diagrams/install-prerequisites-for-bm-host.png) +![Prerequisite Installation - BM +Host](./diagrams/install-prerequisites-for-bm-host.png) #### Create prerequisites required for the E2E tests - trident-prep.yml -![Prerequisite Installation - E2E](./diagrams/install-prerequisites-for-e2e-tests.png) +![Prerequisite Installation - +E2E](./diagrams/install-prerequisites-for-e2e-tests.png) #### Deploy Baremetal Environment - baremetal-deploy.yml @@ -310,7 +308,8 @@ End to end tests should: #### Update trident.yaml to reflect the OAM IP, HTTP server and SSH Key Details - baremetal-update-trident-host-config.yml -![Update trident.yaml to reflect the OAM IP, HTTP server and SSH Key Details](./diagrams/update-trident-yaml.png) +![Update trident.yaml to reflect the OAM IP, HTTP server and SSH Key +Details](./diagrams/update-trident-yaml.png) #### Boot baremetal lab machine - .pipelines/templates/stages/testing_baremetal/deploy_on_bm.py diff --git a/functional_tests/conftest.py b/functional_tests/conftest.py index 47d3ca25e..cbf8bbcba 100644 --- a/functional_tests/conftest.py +++ b/functional_tests/conftest.py @@ -21,10 +21,6 @@ """Location of the Trident repository.""" TRIDENT_REPO_DIR_PATH = Path(__file__).resolve().parent.parent -NETLAUNCH_BIN_REL_PATH = Path("bin/netlaunch") - -NETLAUNCH_BIN_PATH = TRIDENT_REPO_DIR_PATH / NETLAUNCH_BIN_REL_PATH - def __get_argus_toolkit_path(): """Returns the path to the argus-toolkit repository.""" @@ -42,30 +38,15 @@ def __get_argus_toolkit_path(): user specified in the trident-setup.yaml.""" TEST_USER = "testuser" -"""The name of the file containing the remote address of the VM.""" -REMOTE_ADDR_FILENAME = "remote-addr" - """The name of the file containing the known hosts for SSH connections.""" KNOWN_HOSTS_FILENAME = "known_hosts" VM_SSH_NODE_CACHE_KEY = "vm_ssh_node" +FT_BASE_IMAGE = TRIDENT_REPO_DIR_PATH / "artifacts" / "trident-functest.qcow2" -def __get_installer_iso_path(): - """Returns the path to the installer ISO.""" - envvar = os.environ.get("INSTALLER_ISO_PATH", None) - if envvar: - return Path(envvar).resolve() - return TRIDENT_REPO_DIR_PATH / "bin" / "trident-mos.iso" - - -"""Location of the installer ISO. -Defined in the makefile. -""" -INSTALLER_ISO_PATH = __get_installer_iso_path() - -"""Location of the directory netlaunch will serve from""" -NETLAUNCH_SERVE_DIRECTORY = TRIDENT_REPO_DIR_PATH / "artifacts" / "test-image" +"""Target location of the osmodifier binary in the test host.""" +OS_MODIFIER_BIN_TARGET_PATH = Path("/usr/bin/osmodifier") def pytest_addoption(parser): @@ -104,7 +85,10 @@ def pytest_addoption(parser): ) parser.addoption( - "--redeploy", action="store_true", help="Redeploy OS using Trident." + "--osmodifier", + help="Path to the osmodifier binary to copy into the test host.", + default=TRIDENT_REPO_DIR_PATH / "artifacts" / "osmodifier", + type=Path, ) @@ -269,14 +253,24 @@ def argus_runcmd(cmd, check=True, **kwargs): subprocess.run(cmd, check=check, cwd=ARGUS_REPO_DIR_PATH, **kwargs) -def upload_test_binaries(build_output_path: Path, force_upload, ssh_node): +def upload_test_binaries(build_output_path: Path, force_upload, ssh_node: SshNode): """Uploads all test binaries to the VM. Unless force_upload is set, only binaries that are not fresh are uploaded. You need to make sure that you dont rebuild the test binaries between the build and the upload, as the freshness is indicated by the cargo build output. """ ssh_node.execute("mkdir -p tests") - for line in open(build_output_path): + with open(build_output_path, "r") as f: + lines = f.readlines() + + logging.info(f"Found {len(lines)} lines in build output.") + + if not lines: + raise ValueError( + f"No test binaries found in {build_output_path}. Please ensure the build output is correct." + ) + + for line in lines: report = json.loads(line) if ( "target" in report @@ -285,12 +279,17 @@ def upload_test_binaries(build_output_path: Path, force_upload, ssh_node): and "executable" in report and report["executable"] ): - if force_upload or not report["fresh"]: - test_binary = report["executable"] - filename = os.path.basename(test_binary) - stripped_name = filename.split("-", 2)[0] - ssh_node.copy(test_binary, "tests/{}".format(stripped_name)) - ssh_node.execute("chmod +x tests/{}".format(stripped_name)) + if report["fresh"] and not force_upload: + continue + + test_binary = Path(report["executable"]) + stripped_name = test_binary.name.split("-", 2)[0] + remote_path = Path("tests/") / stripped_name + logging.info( + f"Uploading {test_binary} as {remote_path} ({test_binary.stat().st_size} bytes)" + ) + ssh_node.copy(test_binary, remote_path) + ssh_node.execute(f"chmod +x {remote_path}") @pytest.fixture(scope="session") @@ -299,16 +298,11 @@ def ssh_key_path(request) -> Path: @pytest.fixture(scope="session") -def ssh_key_public(request) -> Path: +def ssh_key_public(request) -> str: with open(request.config.getoption("--ssh-key"), "r") as f: return f.read().strip() -@pytest.fixture(scope="session") -def redeploy(request) -> bool: - return bool(request.config.getoption("--redeploy")) - - @pytest.fixture(scope="session") def reuse_environment(request) -> bool: return bool(request.config.getoption("--reuse-environment")) @@ -344,14 +338,9 @@ def test_dir_path(request, reuse_environment) -> Optional[Path]: @pytest.fixture(scope="session") -def remote_addr_path(test_dir_path) -> Path: - return test_dir_path / REMOTE_ADDR_FILENAME - - -@pytest.fixture(scope="session") -def known_hosts_path(test_dir_path, reuse_environment, redeploy) -> Path: +def known_hosts_path(test_dir_path, reuse_environment) -> Path: kh = test_dir_path / KNOWN_HOSTS_FILENAME - if reuse_environment and not redeploy: + if reuse_environment: if not kh.is_file(): pytest.fail( "No known hosts file found in test directory. You might need to recreate the test environment using make functional-test" @@ -370,6 +359,8 @@ def vm(request, ssh_key_path, known_hosts_path) -> SshNode: if ssh_node_address is None: pytest.skip("VM not setup!") + logging.info(f"Using VM at {ssh_node_address}") + priv_key = ssh_key_path.with_suffix("") logging.info(f"Using SSH key {priv_key}") @@ -382,6 +373,13 @@ def vm(request, ssh_key_path, known_hosts_path) -> SshNode: known_hosts_path=known_hosts_path, ) + # Upload OS modifier binary to the VM. + osmodifier_path = request.config.getoption("--osmodifier") + logging.info(f"Copying osmodifier from {osmodifier_path} to VM") + ssh_node.copy(osmodifier_path, Path("osmodifier")) + ssh_node.execute("chmod +x osmodifier") + ssh_node.execute(f"sudo mv osmodifier {OS_MODIFIER_BIN_TARGET_PATH}") + if build_output: upload_test_binaries(build_output, force_upload, ssh_node) diff --git a/functional_tests/custom/test_trident_e2e.py b/functional_tests/custom/test_trident_e2e.py index 99a5b5f63..061357653 100644 --- a/functional_tests/custom/test_trident_e2e.py +++ b/functional_tests/custom/test_trident_e2e.py @@ -1,129 +1,141 @@ -import yaml -import os -import pytest +## NOTE: +## These tests are currently disabled as they don't work with the new fastVM +## implementation because it is not deployed by Trident. +## +## They are being kept here for reference and can be re-enabled if trident is +## copied into the machine. +## +## All these tests, expect for test_trident_start_network, are currently +## covered by some other test, either FT or E2E. SO it is possible that we will +## just fully remove this file. -from assertpy import assert_that # type: ignore -from functional_tests.tools.trident import TridentTool -from functional_tests.conftest import TRIDENT_REPO_DIR_PATH +# import yaml +# import os +# import pytest +# from assertpy import assert_that # type: ignore -class HostStatusSafeLoader(yaml.SafeLoader): - def accept_image(self, node): - return self.construct_mapping(node) +# from functional_tests.tools.trident import TridentTool +# from functional_tests.conftest import TRIDENT_REPO_DIR_PATH -HostStatusSafeLoader.add_constructor("!image", HostStatusSafeLoader.accept_image) +# class HostStatusSafeLoader(yaml.SafeLoader): +# def accept_image(self, node): +# return self.construct_mapping(node) -@pytest.mark.functional -@pytest.mark.core -def test_trident_update(vm): - """Basic Trident run validation.""" - trident = TridentTool(vm) - result = trident.commit() - assert_that(result.exit_code).is_equal_to(0) +# HostStatusSafeLoader.add_constructor("!image", HostStatusSafeLoader.accept_image) - result = trident.commit(False) - assert_that(result.exit_code).is_equal_to(2) - assert_that( - result.stderr.index("Failed to run due to missing root privileges") != -1 - ) - pass +# @pytest.mark.functional +# @pytest.mark.core +# def test_trident_update(vm): +# """Basic Trident run validation.""" +# trident = TridentTool(vm) +# result = trident.commit() +# assert_that(result.exit_code).is_equal_to(0) +# result = trident.commit(False) +# assert_that(result.exit_code).is_equal_to(2) +# assert_that( +# result.stderr.index("Failed to run due to missing root privileges") != -1 +# ) -@pytest.mark.functional -@pytest.mark.core -def test_trident_get(vm): - """Basic trident get validation.""" - trident = TridentTool(vm) +# pass - host_status = trident.get() - host_status = yaml.load(host_status, Loader=HostStatusSafeLoader) - # TODO remove the placeholder logic by patching the template with the actual - # values, which we can fetch using lsblk, sfdisk and information about the - # images we put into the HostConfiguraion. - del host_status["spec"] - placeholder = "placeholder" - for id in host_status["partitionPaths"]: - host_status["partitionPaths"][id] = placeholder - host_status["diskUuids"] = {placeholder: placeholder} - with open( - TRIDENT_REPO_DIR_PATH / "functional_tests/host-status-template.yaml", "r" - ) as file: - host_status_expected = yaml.load(file, Loader=HostStatusSafeLoader) - assert host_status == host_status_expected - pass +# @pytest.mark.functional +# @pytest.mark.core +# def test_trident_get(vm): +# """Basic trident get validation.""" +# trident = TridentTool(vm) +# host_status = trident.get() +# host_status = yaml.load(host_status, Loader=HostStatusSafeLoader) +# # TODO remove the placeholder logic by patching the template with the actual +# # values, which we can fetch using lsblk, sfdisk and information about the +# # images we put into the HostConfiguraion. +# del host_status["spec"] +# placeholder = "placeholder" +# for id in host_status["partitionPaths"]: +# host_status["partitionPaths"][id] = placeholder +# host_status["diskUuids"] = {placeholder: placeholder} +# with open( +# TRIDENT_REPO_DIR_PATH / "functional_tests/host-status-template.yaml", "r" +# ) as file: +# host_status_expected = yaml.load(file, Loader=HostStatusSafeLoader) +# assert host_status == host_status_expected -@pytest.mark.functional -@pytest.mark.core -def test_trident_offline_initialize(vm): - """Basic trident offline initialize validation.""" - trident = TridentTool(vm) - host_status = trident.get() +# pass - # Load it as a yaml - host_status = yaml.load(host_status, Loader=HostStatusSafeLoader) - working_dir = "/tmp/datastore" +# @pytest.mark.functional +# @pytest.mark.core +# def test_trident_offline_initialize(vm): +# """Basic trident offline initialize validation.""" +# trident = TridentTool(vm) +# host_status = trident.get() - result = vm.execute("rm -rf " + working_dir) - assert_that(result.exit_code).is_equal_to(0) - vm.mkdir(working_dir) +# # Load it as a yaml +# host_status = yaml.load(host_status, Loader=HostStatusSafeLoader) - datastore_path = f"/var/lib/trident/offline/datastore.sqlite" - vm.execute(f"echo 'DatastorePath={datastore_path}' > /etc/trident/trident.conf") +# working_dir = "/tmp/datastore" - # Update the datastore location - host_status["spec"]["trident"] = {"datastorePath": datastore_path} +# result = vm.execute("rm -rf " + working_dir) +# assert_that(result.exit_code).is_equal_to(0) +# vm.mkdir(working_dir) - # Create mirror directory - if not os.path.exists(working_dir): - os.mkdir(working_dir) +# datastore_path = f"/var/lib/trident/offline/datastore.sqlite" +# vm.execute(f"echo 'DatastorePath={datastore_path}' > /etc/trident/trident.conf") - # Store it in a temporary file - host_status_path = f"{working_dir}/host-status.yaml" - with open(host_status_path, "w") as file: - yaml.dump(host_status, file) - vm.copy(host_status_path, host_status_path) +# # Update the datastore location +# host_status["spec"]["trident"] = {"datastorePath": datastore_path} - trident.offline_initialize(host_status_path) +# # Create mirror directory +# if not os.path.exists(working_dir): +# os.mkdir(working_dir) - vm.execute(f"sudo chown testuser {datastore_path}") +# # Store it in a temporary file +# host_status_path = f"{working_dir}/host-status.yaml" +# with open(host_status_path, "w") as file: +# yaml.dump(host_status, file) +# vm.copy(host_status_path, host_status_path) - # Use Trident get with the new config to load the status from the datastore - loaded_host_status = yaml.load(trident.get(), Loader=HostStatusSafeLoader) +# trident.offline_initialize(host_status_path) - host_status["spec"].pop("trident") - loaded_host_status["spec"].pop("trident") +# vm.execute(f"sudo chown testuser {datastore_path}") - # Check if the loaded status is the same as the original status - assert host_status == loaded_host_status +# # Use Trident get with the new config to load the status from the datastore +# loaded_host_status = yaml.load(trident.get(), Loader=HostStatusSafeLoader) - # Remove agent config so subsequent FTs use the real datastore - vm.execute(f"rm /etc/trident/trident.conf") +# host_status["spec"].pop("trident") +# loaded_host_status["spec"].pop("trident") - pass +# # Check if the loaded status is the same as the original status +# assert host_status == loaded_host_status +# # Remove agent config so subsequent FTs use the real datastore +# vm.execute(f"rm /etc/trident/trident.conf") -@pytest.mark.functional -@pytest.mark.core -def test_trident_start_network(vm): - """Basic trident start-network validation.""" +# pass - vm.mkdir("/etc/trident") - vm.execute("sudo chmod 777 /etc/trident") - vm.copy( - TRIDENT_REPO_DIR_PATH / "functional_tests/trident-setup.yaml", - "/etc/trident/config.yaml", - ) - trident = TridentTool(vm) - trident.start_network() +# @pytest.mark.functional +# @pytest.mark.core +# def test_trident_start_network(vm): +# """Basic trident start-network validation.""" - vm.execute("rm /etc/trident/config.yaml") +# vm.mkdir("/etc/trident") +# vm.execute("sudo chmod 777 /etc/trident") +# vm.copy( +# TRIDENT_REPO_DIR_PATH / "functional_tests/trident-setup.yaml", +# "/etc/trident/config.yaml", +# ) - pass +# trident = TridentTool(vm) +# trident.start_network() + +# vm.execute("rm /etc/trident/config.yaml") + +# pass diff --git a/functional_tests/test_setup.py b/functional_tests/test_setup.py index 765e112d7..948d1face 100644 --- a/functional_tests/test_setup.py +++ b/functional_tests/test_setup.py @@ -1,138 +1,125 @@ -import subprocess -import time -import pytest +import json import os -import tempfile import logging -import yaml +import subprocess +import tempfile +import time +from typing import Dict from pathlib import Path +from subprocess import CalledProcessError, TimeoutExpired from .conftest import ( - INSTALLER_ISO_PATH, - NETLAUNCH_SERVE_DIRECTORY, argus_runcmd, - trident_runcmd, ARGUS_REPO_DIR_PATH, - TRIDENT_REPO_DIR_PATH, - NETLAUNCH_BIN_PATH, VM_SSH_NODE_CACHE_KEY, + FT_BASE_IMAGE, + TEST_USER, ) from .ssh_node import SshNode +log = logging.getLogger(__name__) + +CLOUD_INIT_USER_TEMPLATE = """ +#cloud-config +users: + - name: {username} + ssh_authorized_keys: + - {ssh_pub_key} + sudo: ['ALL=(ALL) NOPASSWD:ALL'] +""" + -def create_vm(create_params): +def create_vm(create_params) -> Dict[str, str]: + log.info("Creating VM with parameters: %s", create_params) """Creates a VM with the given parameters, using virt-deploy.""" argus_runcmd([ARGUS_REPO_DIR_PATH / "virt-deploy", "create"] + create_params) + with open(ARGUS_REPO_DIR_PATH / "virt-deploy-metadata.json", "r") as file: + metadata = json.load(file) -def disable_phonehome(ssh_node: SshNode): - """Disables phonehome in the VM to allow faster rerunning of Trident.""" - ssh_node.execute("sudo sed -i 's/^\\s*phonehome: .*//' /etc/trident/config.yaml") - - -def prepare_hostconfig(test_dir_path: Path, ssh_pub_key: str): - """Sets up the host configuration file for the VM.""" - - # Add user's public key to trident-setup.yaml - with open( - TRIDENT_REPO_DIR_PATH / "functional_tests/trident-setup.yaml", "r" - ) as file: - trident_setup = yaml.safe_load(file) - trident_setup["os"]["users"][0]["sshPublicKeys"] = [ssh_pub_key] - - prepped_host_config_path = test_dir_path / "trident-setup.yaml" - with open(prepped_host_config_path, "w") as file: - yaml.dump(trident_setup, file) - - return prepped_host_config_path - - -def deploy_vm( - test_dir_path: Path, - ssh_pub_key: str, - known_hosts_path: Path, - remote_addr_path: Path, -) -> str: - """# Provision a VM with the given parameters, using virt-deploy to create the VM - and netlaunch to deploy the OS. Returns the ip address of the VM. - """ - - host_config_path = prepare_hostconfig(test_dir_path, ssh_pub_key) - - trident_runcmd( - [ - NETLAUNCH_BIN_PATH, - "-i", - INSTALLER_ISO_PATH, - "-c", - ARGUS_REPO_DIR_PATH / "vm-netlaunch.yaml", - "-t", - host_config_path, - "-l", - "-r", - remote_addr_path, - "-s", - NETLAUNCH_SERVE_DIRECTORY, - ] - ) + return metadata["virtualmachines"][0] - # Temporary solution to initialize the known_hosts file until we can inject - # a predictable key. - with open(remote_addr_path, "r") as file: - remote_addr = file.read().strip() - for i in range(10): +def wait_online(ip: str, known_hosts_path: Path, timeout: int = 60) -> None: + """Waits for the VM to be online by checking SSH connectivity.""" + start_time = time.time() + while time.time() - start_time < timeout: try: - with open(known_hosts_path, "w") as file: - subprocess.run(["ssh-keyscan", remote_addr], stdout=file, check=True) - break - except: - time.sleep(1) - - return remote_addr + with open(known_hosts_path, "w") as f: + subprocess.run( + [ + "ssh-keyscan", + ip, + ], + stdout=f, + check=True, + timeout=5, + ) + return + except (CalledProcessError, TimeoutExpired) as e: + time.sleep(5) + + raise TimeoutError(f"VM with IP {ip} did not come online within {timeout} seconds.") + + +def test_create_vm(request, known_hosts_path, ssh_key_public): + """Test function to create a VM with virt-deploy""" + if request.config.getoption("--reuse-environment"): + log.info("Skipping VM creation as --reuse-environment is set.") + return -def test_create_vm(request): - """Test function to create a VM with virt-deploy""" request.config.cache.set(VM_SSH_NODE_CACHE_KEY, None) - if not request.config.getoption("--reuse-environment"): - # Create one VM with default flags, cpus, memory, but with two 16GiB disks. - create_vm([":::16,16"]) - - -@pytest.mark.depends("test_create_vm") -def test_deploy_vm( - request, - test_dir_path, - reuse_environment, - redeploy, - remote_addr_path, - known_hosts_path, - ssh_key_public, -): - if reuse_environment and not redeploy: - # Get the IP address from the remote_addr file of the existing VM. - with open(remote_addr_path, "r") as file: - address = file.read().strip() - else: - if ( - not ARGUS_REPO_DIR_PATH.is_dir() - or not (ARGUS_REPO_DIR_PATH / "virt-deploy").is_file() - ): - pytest.fail(f"{ARGUS_REPO_DIR_PATH} is not a argus-toolkit repo directory") - - # Deploy OS to VM. - address = deploy_vm( - test_dir_path, - ssh_key_public, - known_hosts_path, - remote_addr_path, + + with tempfile.TemporaryDirectory() as temp_dir: + work_dir = Path(temp_dir) + # Create a cloud-init metadata file for the VM. + cloud_init_meta = work_dir / "cloud-init-meta.yaml" + with open(cloud_init_meta, "w") as file: + file.write("#cloud-config\n") + + # Create a cloud-init user-data file for the VM. + cloud_init_user_data = work_dir / "cloud-init-user-data.yaml" + with open(cloud_init_user_data, "w") as file: + file.write( + CLOUD_INIT_USER_TEMPLATE.format( + username=TEST_USER, + ssh_pub_key=ssh_key_public, + ) + ) + + # Create one VM with default flags, cpus, memory, but with two 16GiB + # disks. Pass the base Image as the OS disk. And Pass cloud init-params + # to set up a user and ssh access. + vm_data = create_vm( + [ + ":::16,16", + "--os-disk", + FT_BASE_IMAGE, + "--ci-user", + cloud_init_user_data, + "--ci-meta", + cloud_init_meta, + ] ) - request.config.cache.set(VM_SSH_NODE_CACHE_KEY, address) + vm_name = vm_data["name"] + vm_ip = vm_data["ip"] + + subprocess.run( + ["virsh", "start", vm_name], + check=True, + ) + + wait_online(vm_ip, known_hosts_path, timeout=60) + + request.config.cache.set(VM_SSH_NODE_CACHE_KEY, vm_ip) + + +# def test_wait_online(known_hosts_path): +# wait_online("192.168.242.2", known_hosts_path, timeout=60) -@pytest.mark.depends("test_deploy_vm") -def test_deployment(vm): +def test_deployment(vm: SshNode): vm.execute("true") diff --git a/osutils/src/encryption.rs b/osutils/src/encryption.rs index 251550075..cf28cefd2 100644 --- a/osutils/src/encryption.rs +++ b/osutils/src/encryption.rs @@ -336,18 +336,18 @@ mod functional_test { // Create a temporary file to store the recovery key file let key_file_tmp = NamedTempFile::new().unwrap(); - let key_file_path = key_file_tmp.path().to_owned(); - fs::set_permissions(&key_file_path, Permissions::from_mode(0o600)).unwrap(); - generate_recovery_key_file(&key_file_path).unwrap(); + let key_file_path = key_file_tmp.path(); + fs::set_permissions(key_file_path, Permissions::from_mode(0o600)).unwrap(); + generate_recovery_key_file(key_file_path).unwrap(); // Run `cryptsetup-luksFormat` on the partition - cryptsetup_luksformat(&key_file_path, &partition1.node).unwrap(); + cryptsetup_luksformat(key_file_path, &partition1.node).unwrap(); // Run `systemd-cryptenroll` on the partition - systemd_cryptenroll(&key_file_path, &partition1.node, BitFlags::from(Pcr::Pcr7)).unwrap(); + systemd_cryptenroll(key_file_path, &partition1.node, BitFlags::from(Pcr::Pcr7)).unwrap(); // Open the encrypted volume, to make the block device available - cryptsetup_open(&key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); + cryptsetup_open(key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); // Format the unlocked volume with ext4 mkfs::run(Path::new(ENCRYPTED_VOLUME_PATH), MkfsFileSystemType::Ext4).unwrap(); @@ -396,7 +396,7 @@ mod functional_test { cryptsetup_close(ENCRYPTED_VOLUME_NAME).unwrap(); // Re-open the encrypted volume - cryptsetup_open(&key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); + cryptsetup_open(key_file_path, &partition1.node, ENCRYPTED_VOLUME_NAME).unwrap(); // Re-mount the encrypted volume Dependency::Mount diff --git a/osutils/src/sfdisk.rs b/osutils/src/sfdisk.rs index ff041bbf4..caa9940c9 100644 --- a/osutils/src/sfdisk.rs +++ b/osutils/src/sfdisk.rs @@ -398,55 +398,48 @@ mod functional_test { /// /// ```json /// { - /// "partitiontable": { - /// "label": "gpt", - /// "id": "71F5C3EB-6D53-414B-9FF4-0953E6291577", - /// "device": "/dev/sda", - /// "unit": "sectors", - /// "firstlba": 2048, - /// "lastlba": 33554398, - /// "sectorsize": 512, - /// "partitions": [ - /// { - /// "node": "/dev/sda1", - /// "start": 2048, - /// "size": 102400, - /// "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - /// "uuid": "8D738FD1-9B6F-4B6D-B174-021954453D68", - /// "name": "esp" - /// },{ - /// "node": "/dev/sda2", - /// "start": 104448, - /// "size": 8388608, - /// "type": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", - /// "uuid": "71982B79-7759-449F-8D68-ACA7625AC1E2", - /// "name": "root-a", - /// "attrs": "GUID:59" - /// },{ - /// "node": "/dev/sda3", - /// "start": 8493056, - /// "size": 8388608, - /// "type": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", - /// "uuid": "1864172F-3594-4F7A-B447-EBCA0B115DC6", - /// "name": "root-b", - /// "attrs": "GUID:59" - /// },{ - /// "node": "/dev/sda4", - /// "start": 16881664, - /// "size": 4194304, - /// "type": "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", - /// "uuid": "ED608DB8-58D6-484B-B309-B03CD3615037", - /// "name": "swap" - /// },{ - /// "node": "/dev/sda5", - /// "start": 21075968, - /// "size": 204800, - /// "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - /// "uuid": "7DE2DA6E-4512-4091-B0B7-EC432DA971AA", - /// "name": "trident" - /// } - /// ] - /// } + /// "partitiontable": { + /// "label": "gpt", + /// "id": "BC1DB325-B1C0-4D6F-B98F-F9F24AB1C8EF", + /// "device": "/dev/sda", + /// "unit": "sectors", + /// "firstlba": 2048, + /// "lastlba": 21282782, + /// "sectorsize": 512, + /// "partitions": [ + /// { + /// "node": "/dev/sda1", + /// "start": 2048, + /// "size": 102400, + /// "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + /// "uuid": "" + /// },{ + /// "node": "/dev/sda2", + /// "start": 104448, + /// "size": 8388608, + /// "type": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", + /// "uuid": "" + /// },{ + /// "node": "/dev/sda3", + /// "start": 8493056, + /// "size": 8388608, + /// "type": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", + /// "uuid": "" + /// },{ + /// "node": "/dev/sda4", + /// "start": 16881664, + /// "size": 4194304, + /// "type": "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", + /// "uuid": "" + /// },{ + /// "node": "/dev/sda5", + /// "start": 21075968, + /// "size": 204800, + /// "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + /// "uuid": "" + /// } + /// ] + /// } /// } /// ``` /// @@ -458,9 +451,9 @@ mod functional_test { assert_eq!(disk.unit, SfDiskUnit::Sectors); print!("disk: {:#?}", disk); assert_eq!(disk.firstlba, 2048); - assert_eq!(disk.lastlba, 33554398); + assert_eq!(disk.lastlba, 31457246); assert_eq!(disk.sectorsize, 512); - assert_eq!(disk.capacity, 17_178_803_712); + assert_eq!(disk.capacity, 16_105_061_888); assert_eq!(disk.partitions.len(), 5); let expected_partitions = [ diff --git a/src/engine/boot/grub.rs b/src/engine/boot/grub.rs index e72d63d13..30d696feb 100644 --- a/src/engine/boot/grub.rs +++ b/src/engine/boot/grub.rs @@ -263,7 +263,10 @@ pub(crate) mod functional_test { use const_format::formatcp; use maplit::btreemap; - use crate::{engine::storage::raid, OS_MODIFIER_BINARY_PATH}; + use crate::{ + engine::{boot::get_update_esp_dir_name, storage::raid}, + OS_MODIFIER_BINARY_PATH, + }; use osutils::{ block_devices, @@ -285,6 +288,38 @@ pub(crate) mod functional_test { status::ServicingType, }; + struct DropFile(PathBuf); + impl Drop for DropFile { + fn drop(&mut self) { + if let Err(e) = fs::remove_file(&self.0) { + eprintln!("Failed to remove file '{}': {}", self.0.display(), e); + } + } + } + + fn setup_mock_grub_configs(ctx: &EngineContext) -> (DropFile, DropFile) { + let grub_esp = include_str!("test_files/grub_esp.cfg"); + let grub_boot = include_str!("test_files/grub_boot.cfg"); + + let grub_esp_path = Path::new(ESP_MOUNT_POINT_PATH) + .join(ESP_EFI_DIRECTORY) + .join(get_update_esp_dir_name(ctx).expect("Failed to get update esp dir name")) + .join(GRUB2_CONFIG_FILENAME); + let grub_boot_path = Path::new(ROOT_MOUNT_POINT_PATH).join(GRUB2_CONFIG_RELATIVE_PATH); + + fs::create_dir_all(grub_esp_path.parent().unwrap()) + .expect("Failed to create directory for grub esp config"); + fs::create_dir_all(grub_boot_path.parent().unwrap()) + .expect("Failed to create directory for grub boot config"); + + fs::write(&grub_esp_path, grub_esp).expect("Failed to write grub esp config"); + let drop_file_esp = DropFile(grub_esp_path.clone()); + fs::write(&grub_boot_path, grub_boot).expect("Failed to write grub boot config"); + let drop_file_boot = DropFile(grub_boot_path.clone()); + + (drop_file_esp, drop_file_boot) + } + pub fn test_execute_and_resulting_layout(is_single_disk_raid: bool, unequal_partitions: bool) { let disk_bus_path = PathBuf::from(TEST_DISK_DEVICE_PATH); @@ -554,6 +589,8 @@ pub(crate) mod functional_test { mkfs::run(root_device_path, MkfsFileSystemType::Ext4).unwrap(); + let _a = setup_mock_grub_configs(ctx); + update_configs(ctx, Path::new(OS_MODIFIER_BINARY_PATH)) } @@ -617,6 +654,8 @@ pub(crate) mod functional_test { let root_device_path = PathBuf::from(formatcp!("{TEST_DISK_DEVICE_PATH}2")); mkfs::run(&root_device_path, MkfsFileSystemType::Ext4).unwrap(); + let _a = setup_mock_grub_configs(&ctx); + update_configs(&ctx, Path::new(OS_MODIFIER_BINARY_PATH)).unwrap(); } @@ -692,6 +731,9 @@ pub(crate) mod functional_test { let root_device_path = PathBuf::from(formatcp!("{TEST_DISK_DEVICE_PATH}2")); mkfs::run(&root_device_path, MkfsFileSystemType::Ext4).unwrap(); + + let _a = setup_mock_grub_configs(&ctx); + update_configs(&ctx, Path::new(OS_MODIFIER_BINARY_PATH)).unwrap(); } @@ -742,6 +784,8 @@ pub(crate) mod functional_test { ..Default::default() }; + let _a = setup_mock_grub_configs(&ctx); + let result = update_configs(&ctx, Path::new(ROOT_MOUNT_POINT_PATH)); assert_eq!( result.unwrap_err().to_string(), @@ -796,6 +840,8 @@ pub(crate) mod functional_test { ..Default::default() }; + let _a = setup_mock_grub_configs(&ctx); + let result = update_configs(&ctx, Path::new(ROOT_MOUNT_POINT_PATH)); assert_eq!(result.unwrap_err().to_string(), "Root device path is none"); diff --git a/src/engine/boot/test_files/grub_boot.cfg b/src/engine/boot/test_files/grub_boot.cfg new file mode 100644 index 000000000..6a68d40b8 --- /dev/null +++ b/src/engine/boot/test_files/grub_boot.cfg @@ -0,0 +1,137 @@ +# +# THIS FILE WAS EXTRACTED FROM THE GRUB-BASED VERSION OF THE FUNCTIONAL TEST +# IMAGE AT /boot/grub2/grub.cfg +# + +# +# DO NOT EDIT THIS FILE +# +# It is automatically generated by grub2-mkconfig using templates +# from /etc/grub.d and settings from /etc/default/grub +# + +### BEGIN /etc/grub.d/00_header ### +if [ -s $prefix/grubenv ]; then + load_env +fi +if [ "${next_entry}" ] ; then + set default="${next_entry}" + set next_entry= + save_env next_entry + set boot_once=true +else + set default="0" +fi + +if [ x"${feature_menuentry_id}" = xy ]; then + menuentry_id_option="--id" +else + menuentry_id_option="" +fi + +export menuentry_id_option + +if [ "${prev_saved_entry}" ]; then + set saved_entry="${prev_saved_entry}" + save_env saved_entry + set prev_saved_entry= + save_env prev_saved_entry + set boot_once=true +fi + +function savedefault { + if [ -z "${boot_once}" ]; then + saved_entry="${chosen}" + save_env saved_entry + fi +} + +function load_video { + if [ x$feature_all_video_module = xy ]; then + insmod all_video + else + insmod efi_gop + insmod efi_uga + insmod ieee1275_fb + insmod vbe + insmod vga + insmod video_bochs + insmod video_cirrus + fi +} + +terminal_output console +if [ x$feature_timeout_style = xy ] ; then + set timeout_style=menu + set timeout=0 +# Fallback normal timeout code in case the timeout_style feature is +# unavailable. +else + set timeout=0 +fi +### END /etc/grub.d/00_header ### + +### BEGIN /etc/grub.d/10_linux ### +menuentry 'AzureLinux GNU/Linux, with Linux 6.6.85.1-2.azl3' --class azurelinux --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-6.6.85.1-2.azl3-advanced-/dev/sdb2' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_gpt + insmod ext2 + set root='hd0,gpt2' + if [ x$feature_platform_search_hint = xy ]; then + search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2 1adc13e8-f140-4109-9eee-bd8e1efe96b8 + else + search --no-floppy --fs-uuid --set=root 1adc13e8-f140-4109-9eee-bd8e1efe96b8 + fi + echo 'Loading Linux 6.6.85.1-2.azl3 ...' + linux /boot/vmlinuz-6.6.85.1-2.azl3 root=UUID=1adc13e8-f140-4109-9eee-bd8e1efe96b8 ro security=selinux selinux=1 rd.auto=1 net.ifnames=0 lockdown=integrity console=tty0 console=ttyS0 rd.info log_buf_len=1M $kernelopts + echo 'Loading initial ramdisk ...' + initrd /boot/initramfs-6.6.85.1-2.azl3.img +} +menuentry 'AzureLinux GNU/Linux, with Linux 6.6.85.1-2.azl3 (recovery mode)' --class azurelinux --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-6.6.85.1-2.azl3-recovery-/dev/sdb2' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_gpt + insmod ext2 + set root='hd0,gpt2' + if [ x$feature_platform_search_hint = xy ]; then + search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2 1adc13e8-f140-4109-9eee-bd8e1efe96b8 + else + search --no-floppy --fs-uuid --set=root 1adc13e8-f140-4109-9eee-bd8e1efe96b8 + fi + echo 'Loading Linux 6.6.85.1-2.azl3 ...' + linux /boot/vmlinuz-6.6.85.1-2.azl3 root=UUID=1adc13e8-f140-4109-9eee-bd8e1efe96b8 ro single security=selinux selinux=1 rd.auto=1 net.ifnames=0 lockdown=integrity + echo 'Loading initial ramdisk ...' + initrd /boot/initramfs-6.6.85.1-2.azl3.img +} + +### END /etc/grub.d/10_linux ### + +### BEGIN /etc/grub.d/20_linux_xen ### + +### END /etc/grub.d/20_linux_xen ### + +### BEGIN /etc/grub.d/30_os-prober ### +### END /etc/grub.d/30_os-prober ### + +### BEGIN /etc/grub.d/30_uefi-firmware ### +menuentry 'UEFI Firmware Settings' $menuentry_id_option 'uefi-firmware' { + fwsetup +} +### END /etc/grub.d/30_uefi-firmware ### + +### BEGIN /etc/grub.d/40_custom ### +# This file provides an easy way to add custom menu entries. Simply type the +# menu entries you want to add after this comment. Be careful not to change +# the 'exec tail' line above. +### END /etc/grub.d/40_custom ### + +### BEGIN /etc/grub.d/41_custom ### +if [ -f ${config_directory}/custom.cfg ]; then + source ${config_directory}/custom.cfg +elif [ -z "${config_directory}" -a -f $prefix/custom.cfg ]; then + source $prefix/custom.cfg +fi +### END /etc/grub.d/41_custom ### diff --git a/src/engine/boot/test_files/grub_esp.cfg b/src/engine/boot/test_files/grub_esp.cfg new file mode 100644 index 000000000..945a1294b --- /dev/null +++ b/src/engine/boot/test_files/grub_esp.cfg @@ -0,0 +1,13 @@ +# +# THIS FILE WAS EXTRACTED FROM THE GRUB-BASED VERSION OF THE FUNCTIONAL TEST +# IMAGE at /boot/efi/boot/grub2/grub.cfg +# + +# The bootUUID in which the menuentry grub.cfg is defined; +# Can be either its own separate partition or part of the rootfs partition. +search -n -u 1adc13e8-f140-4109-9eee-bd8e1efe96b8 -s +# For images using grub2-mkconfig, $prefix is the variable +# grub expects to be populated with the proper path to the grub.cfg, grubenv. +# - $prefix: the path to /boot/grub2/ relative to the bootUUID +set prefix=($root)"/boot/grub2" +configfile $prefix/grub.cfg diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 7a7281b62..838718b5a 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -408,6 +408,18 @@ mod functional_test { // Create mock datastore directory and log file fs::create_dir_all(&datastore_path).unwrap(); + // ENSURE THE LOG AND METRICS FILES EXIST + fs::write( + TRIDENT_BACKGROUND_LOG_PATH, + "{\"message\":\"This is a mock background log file.\"}", + ) + .unwrap(); + fs::write( + TRIDENT_METRICS_FILE_PATH, + "{\"metric\":\"This is a mock metrics file.\"}", + ) + .unwrap(); + // Compose the log dir let log_dir = join_relative(newroot_path, datastore_dir); fs::create_dir_all(&log_dir).unwrap(); @@ -432,17 +444,13 @@ mod functional_test { // Create mock datastore directory and log file fs::create_dir_all(&datastore_path).unwrap(); - // Create a temp copy of TRIDENT_BACKGROUND_LOG_PATH - let temp_log_path = TRIDENT_BACKGROUND_LOG_PATH.to_owned() + ".temp"; - fs::copy(TRIDENT_BACKGROUND_LOG_PATH, &temp_log_path).unwrap(); - // Remove TRIDENT_BACKGROUND_LOG_PATH - fs::remove_file(TRIDENT_BACKGROUND_LOG_PATH).unwrap(); - - // Create a temp copy of TRIDENT_METRICS_FILE_PATH - let temp_metrics_path = TRIDENT_METRICS_FILE_PATH.to_owned() + ".temp"; - fs::copy(TRIDENT_METRICS_FILE_PATH, &temp_metrics_path).unwrap(); - // Remove TRIDENT_METRICS_FILE_PATH - fs::remove_file(TRIDENT_METRICS_FILE_PATH).unwrap(); + // ENSURE THE LOG AND METRICS FILES DO NOT EXIST + if Path::new(TRIDENT_BACKGROUND_LOG_PATH).exists() { + fs::remove_file(TRIDENT_BACKGROUND_LOG_PATH).unwrap(); + } + if Path::new(TRIDENT_METRICS_FILE_PATH).exists() { + fs::remove_file(TRIDENT_METRICS_FILE_PATH).unwrap(); + } // Persist the background log and metrics file let servicing_state = ServicingState::AbUpdateFinalized; @@ -452,11 +460,5 @@ mod functional_test { !persisted_log_and_metrics_exist(datastore_dir, servicing_state), "Trident background log and metrics should not be persisted." ); - - // Re-create TRIDENT_BACKGROUND_LOG_PATH by copying from the temp file - fs::copy(&temp_log_path, TRIDENT_BACKGROUND_LOG_PATH).unwrap(); - - // Re-create TRIDENT_METRICS_FILE_PATH by copying from the temp file - fs::copy(&temp_metrics_path, TRIDENT_METRICS_FILE_PATH).unwrap(); } } From 534429c201f2686f4063b6357dbb6c9bc0711c61 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Fri, 13 Jun 2025 23:34:52 +0000 Subject: [PATCH 77/99] Merged PR 23519: engineering: Permissions to allow installer image to run in enforcing mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Necessary so `rerun` does not fail with !23518 . pre2e run pulling test-images from feature branch where installers have SELinux in enforcing mode: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=835951&view=results ---- #### AI description (iteration 1) #### PR Classification Engineering change updating SELinux policy rules to enable the installer image to run in enforcing mode. #### PR Summary This PR adjusts SELinux permissions in the `trident.te` file to support enforcing mode operation. - `selinux-policy-trident/trident.te`: Added `sys_module` to the allowed capabilities for `trident_t`. - `selinux-policy-trident/trident.te`: Expanded `var_t:dir` permissions to include `relabelto` along with `mounton`. Related work items: #12560 --- selinux-policy-trident/trident.te | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index b2eb7da77..34e1777ae 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -238,7 +238,7 @@ optional_policy(` allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; allow trident_t self:alg_socket { accept bind create read write }; -allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice linux_immutable }; +allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice linux_immutable sys_module }; allow trident_t self:fifo_file manage_fifo_file_perms; allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; @@ -458,7 +458,7 @@ allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto mounton }; allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; allow trident_t uuidd_exec_t:file getattr; -allow trident_t var_t:dir mounton; +allow trident_t var_t:dir { mounton relabelto }; allow trident_t var_run_t:dir { add_name create remove_name write }; allow trident_t var_run_t:file { create getattr lock open read unlink write }; From 494eee6f899cc223bd1d3a8dab4d1920e19969cb Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Mon, 16 Jun 2025 16:13:36 +0000 Subject: [PATCH 78/99] Merged PR 23530: bug: Add permissions so pipelines succeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description `memory-constraint-combined`, `misc`, and `root-verity` now succeed when run in an installer environment that has SELinux in enforcing mode. Among other permissions, was missing ability to mount NTFS (`misc`) and relabel various directories and files (`root-verity`). Succeeding pipeline: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=837611&view=results ![image.png](https://dev.azure.com/mariner-org/2311650c-e79e-4301-b4d2-96543fdd84ff/_apis/git/repositories/895b6b3d-5077-488a-8001-ab6b5a14c1a3/pullRequests/23530/attachments/image.png) Related work items: #12567, #12569 --- selinux-policy-trident/trident.te | 96 ++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index 34e1777ae..79b9c3984 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -62,6 +62,8 @@ require { type fs_t; type fsadm_exec_t; type fsadm_t; + type fuse_device_t; + type fusefs_t; type getty_exec_t; type gpg_agent_exec_t; type gpg_pinentry_exec_t; @@ -238,14 +240,14 @@ optional_policy(` allow trident_t self:capability { dac_override dac_read_search sys_ptrace sys_rawio }; allow trident_t self:alg_socket { accept bind create read write }; -allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice linux_immutable sys_module }; +allow trident_t self:capability { audit_write chown mknod net_admin sys_chroot sys_resource sys_admin fowner fsetid sys_boot ipc_lock sys_nice linux_immutable sys_module setgid setuid }; allow trident_t self:fifo_file manage_fifo_file_perms; allow trident_t self:netlink_audit_socket { create nlmsg_relay read write }; allow trident_t self:netlink_kobject_uevent_socket { bind create getattr getopt read setopt }; allow trident_t self:netlink_route_socket { bind create getattr nlmsg_read read write }; allow trident_t self:process { getsched setsched getcap setpgid signull getattr signal }; allow trident_t self:tcp_socket { connect create getattr getopt read setopt shutdown write }; -allow trident_t self:unix_dgram_socket { connect create }; +allow trident_t self:unix_dgram_socket { connect create write }; allow trident_t self:key { search write }; allow trident_t self:sem { associate create destroy read unix_read unix_write write }; @@ -260,35 +262,37 @@ allow trident_t trident_var_lib_t:file { getattr setattr create open read write # Allow Trident to relabel its executable allow trident_t trident_exec_t:file relabelto; -allow trident_t audit_spool_t:dir { getattr open read }; +allow trident_t audit_spool_t:dir { getattr open read relabelto }; allow trident_t auditctl_exec_t:file getattr; allow trident_t auditd_exec_t:file getattr; +allow trident_t auditd_log_t:dir relabelto; allow trident_t auditd_unit_t:file getattr; allow trident_t admin_passwd_exec_t:file getattr; allow trident_t anacron_exec_t:file getattr; allow trident_t audisp_remote_exec_t:file getattr; allow trident_t bluetooth_unit_t:file getattr; -allow trident_t boot_t:dir { mounton create }; +allow trident_t boot_t:dir { mounton create relabelto }; +allow trident_t boot_t:file relabelto; allow trident_t bpf_t:dir search; allow trident_t cgroup_t:filesystem getattr; allow trident_t chfn_exec_t:file getattr; allow trident_t chkpwd_exec_t:file getattr; allow trident_t chronyc_exec_t:file getattr; allow trident_t chronyd_unit_t:file getattr; -allow trident_t chronyd_var_lib_t:dir { getattr open read }; -allow trident_t chronyd_var_log_t:dir { getattr open read }; +allow trident_t chronyd_var_lib_t:dir { getattr open read relabelto }; +allow trident_t chronyd_var_log_t:dir { getattr open read relabelto }; allow trident_t cloud_init_exec_t:file getattr; -allow trident_t cloud_init_state_t:dir list_dir_perms; +allow trident_t cloud_init_state_t:dir { list_dir_perms relabelto }; allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; allow trident_t cloud_init_state_t:file getattr; -allow trident_t colord_var_lib_t:dir { getattr open read }; +allow trident_t colord_var_lib_t:dir { getattr open read relabelto }; allow trident_t container_unit_t:file getattr; allow trident_t crack_db_t:dir { getattr open search read }; allow trident_t crack_db_t:file getattr; allow trident_t crack_db_t:lnk_file getattr; allow trident_t crack_exec_t:file getattr; -allow trident_t cron_spool_t:dir read; -allow trident_t crond_unit_t:file getattr; +allow trident_t cron_spool_t:dir { read relabelto }; +allow trident_t crond_unit_t:file { getattr read open ioctl }; allow trident_t dbusd_unit_t:file getattr; allow trident_t debugfs_t:filesystem getattr; allow trident_t debugfs_t:dir search; @@ -296,9 +300,9 @@ allow trident_t default_t:dir { getattr open read relabelto search }; allow trident_t default_t:file relabelto; allow trident_t device_t:filesystem { getattr mount unmount }; allow trident_t devpts_t:chr_file { read write ioctl getattr }; -allow trident_t devlog_t:sock_file getattr; +allow trident_t devlog_t:sock_file { getattr write }; allow trident_t dhcpc_exec_t:file getattr; -allow trident_t dhcpc_state_t:dir { getattr open read }; +allow trident_t dhcpc_state_t:dir { getattr open read relabelto }; allow trident_t dhcpd_unit_t:file getattr; allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t dosfs_t:filesystem { getattr mount unmount }; @@ -306,11 +310,16 @@ allow trident_t efivarfs_t:filesystem getattr; allow trident_t efivarfs_t:dir search; allow trident_t etc_runtime_t:file { getattr open read relabelto relabelfrom setattr unlink }; allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; +allow trident_t etc_t:dir { mounton relabelfrom }; +allow trident_t faillog_t:file relabelto; allow trident_t fs_t:filesystem { mount unmount }; allow trident_t fsadm_t:process { siginh rlimitinh noatsecure transition }; allow trident_t fsadm_t:fd use; allow trident_t fsadm_t:fifo_file { read write }; allow trident_t fsadm_exec_t:file { getattr open read execute execute_no_trans relabelto setattr unlink write }; +allow trident_t fuse_device_t:chr_file { read write open }; +allow trident_t fusefs_t:dir getattr; +allow trident_t fusefs_t:filesystem { mount unmount getattr }; allow trident_t getty_exec_t:file getattr; allow trident_t gpg_pinentry_exec_t:file getattr; allow trident_t gpg_secret_t:file getattr; @@ -324,32 +333,35 @@ allow trident_t init_t:unix_stream_socket connectto; allow trident_t initctl_t:fifo_file getattr; allow trident_t init_runtime_t:dir { add_name write }; allow trident_t init_runtime_t:file { create getattr open write }; -allow trident_t init_var_lib_t:dir { getattr open read search }; +allow trident_t init_var_lib_t:dir { getattr open read search relabelto }; allow trident_t iptables_unit_t:file getattr; allow trident_t kernel_t:process setsched; allow trident_t kernel_t:system { module_request ipc_info }; +allow trident_t kernel_t:unix_dgram_socket sendto; allow trident_t kmod_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; allow trident_t krb5kdc_exec_t:file getattr; +allow trident_t lastlog_t:file relabelto; allow trident_t ld_so_t:file { execute_no_trans relabelto setattr unlink write }; -allow trident_t ldconfig_cache_t:dir { getattr open read search }; -allow trident_t ldconfig_cache_t:file getattr; +allow trident_t ldconfig_cache_t:dir { getattr open read search relabelto }; +allow trident_t ldconfig_cache_t:file { getattr relabelto }; allow trident_t lib_t:file { create relabelto rename setattr unlink write }; allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t locale_t:dir { add_name relabelto remove_name rmdir setattr write }; allow trident_t locale_t:file { link relabelto rename setattr unlink write }; allow trident_t locate_exec_t:file getattr; allow trident_t logrotate_unit_t:file getattr; -allow trident_t logrotate_var_lib_t:dir { getattr open read }; +allow trident_t logrotate_var_lib_t:dir { getattr open read relabelto }; allow trident_t lost_found_t:dir { getattr open read relabelto }; allow trident_t lvm_metadata_t:dir { getattr open read }; allow trident_t lvm_unit_t:file getattr; -allow trident_t mail_spool_t:dir list_dir_perms; +allow trident_t mail_spool_t:dir { list_dir_perms relabelto }; allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; allow trident_t mdadm_runtime_t:dir { add_name remove_name search write }; allow trident_t memory_pressure_t:file { read open getattr setattr }; allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; -allow trident_t modules_conf_t:file { relabelto setattr unlink }; +allow trident_t modules_conf_t:file { create relabelto setattr unlink write }; +allow trident_t modules_conf_t:dir { write add_name }; allow trident_t modules_dep_t:file { getattr ioctl map open read relabelto setattr unlink }; allow trident_t modules_object_t:file { getattr open read relabelto setattr unlink }; allow trident_t mount_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; @@ -368,12 +380,22 @@ allow trident_t proc_net_t:file { open read }; allow trident_t root_t:dir { add_name create mounton write }; allow trident_t root_t:file { create getattr map open read relabelfrom write }; allow trident_t rpm_unit_t:file getattr; +allow trident_t rpm_var_cache_t:dir relabelto; +allow trident_t rpm_var_cache_t:file relabelto; +allow trident_t rpm_var_lib_t:dir relabelto; +allow trident_t rpm_var_lib_t:file relabelto; allow trident_t security_t:file { map write }; allow trident_t security_t:filesystem getattr; +allow trident_t selinux_config_t:dir relabelfrom; +allow trident_t selinux_config_t:file relabelfrom; allow trident_t semanage_exec_t:file { execute execute_no_trans entrypoint getattr ioctl open read map }; +allow trident_t semanage_read_lock_t:file relabelto; +allow trident_t semanage_store_t:dir relabelto; +allow trident_t semanage_store_t:file relabelto; +allow trident_t semanage_trans_lock_t:file relabelto; allow trident_t setfiles_exec_t:file entrypoint; -allow trident_t shadow_t:file { getattr open read relabelto setattr unlink write }; -allow trident_t shadow_lock_t:file { create getattr lock open read unlink write }; +allow trident_t shadow_t:file { getattr open read relabelto setattr link unlink write relabelfrom }; +allow trident_t shadow_lock_t:file { create getattr lock open read link unlink write setattr relabelfrom }; allow trident_t shell_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; allow trident_t ssh_agent_exec_t:file getattr; allow trident_t ssh_exec_t:file { execute getattr }; @@ -395,6 +417,10 @@ allow trident_t syslog_conf_t:file { getattr open read relabelto setattr unlink allow trident_t syslogd_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t syslogd_runtime_t:dir search; allow trident_t syslogd_unit_t:file { getattr open read relabelto setattr unlink }; +allow trident_t system_cron_spool_t:dir relabelto; +allow trident_t system_dbusd_var_lib_t:dir relabelto; +allow trident_t system_dbusd_var_lib_t:lnk_file relabelto; +allow trident_t system_map_t:file relabelto; allow trident_t systemd_analyze_exec_t:file getattr; allow trident_t systemd_backlight_exec_t:file getattr; allow trident_t systemd_backlight_unit_t:file getattr; @@ -403,7 +429,7 @@ allow trident_t systemd_binfmt_unit_t:file getattr; allow trident_t systemd_cgroups_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t systemd_cgtop_exec_t:file getattr; allow trident_t systemd_coredump_exec_t:file { execute getattr map open read relabelto setattr unlink write }; -allow trident_t systemd_coredump_var_lib_t:dir { getattr open read }; +allow trident_t systemd_coredump_var_lib_t:dir { getattr open read relabelto }; allow trident_t systemd_factory_conf_t:dir { getattr open read search }; allow trident_t systemd_factory_conf_t:file getattr; allow trident_t systemd_generator_exec_t:file { execute getattr map open read relabelto setattr unlink write }; @@ -412,7 +438,8 @@ allow trident_t systemd_homework_exec_t:file getattr; allow trident_t systemd_hostnamed_exec_t:file { execute getattr }; allow trident_t systemd_hw_exec_t:file getattr; allow trident_t systemd_hwdb_t:file { getattr open read relabelto setattr unlink }; -allow trident_t systemd_journal_t:file relabelfrom_file_perms; +allow trident_t systemd_journal_t:file { relabelfrom_file_perms map open read relabelto }; +allow trident_t systemd_journal_t:dir relabelto; allow trident_t systemd_journalctl_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t systemd_locale_exec_t:file getattr; allow trident_t systemd_logind_exec_t:file getattr; @@ -437,30 +464,43 @@ allow trident_t systemd_tmpfiles_exec_t:file { execute getattr map open read rel allow trident_t systemd_unit_t:dir { read add_name create write }; allow trident_t systemd_unit_t:file { getattr ioctl link open read relabelto rename setattr unlink write create }; allow trident_t systemd_unit_t:lnk_file { getattr read create }; +allow trident_t systemd_unit_t:service { start status }; allow trident_t systemd_update_done_exec_t:file getattr; allow trident_t systemd_user_manager_unit_t:file getattr; allow trident_t systemd_user_runtime_dir_exec_t:file getattr; allow trident_t systemd_userdbd_exec_t:file getattr; allow trident_t systemd_userdbd_unit_t:file getattr; +allow trident_t tcsd_var_lib_t:dir relabelto; allow trident_t tmp_t:chr_file { create getattr unlink }; -allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write }; -allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write map }; +allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write relabelto }; +allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write map execute link }; allow trident_t tmp_t:lnk_file { create getattr read rename unlink }; -allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map }; +allow trident_t tmpfs_t:file { append create execute getattr ioctl mounton open read relabelto rename setattr unlink write map link }; allow trident_t tmpfs_t:filesystem { getattr mount unmount }; allow trident_t udev_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; allow trident_t udev_runtime_t:dir { read watch }; -allow trident_t unlabeled_t:dir { create add_name getattr setattr open read remove_name search write mounton relabelfrom }; -allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink write ioctl relabelfrom }; +allow trident_t unlabeled_t:chr_file { create getattr link rename unlink }; +allow trident_t unlabeled_t:dir { create add_name getattr setattr open read remove_name search write mounton relabelfrom ioctl rename reparent rmdir }; +allow trident_t unlabeled_t:file { create getattr lock open read setattr unlink write ioctl relabelfrom append execute execute_no_trans link map relabelto rename }; +allow trident_t unlabeled_t:lnk_file { create getattr read relabelfrom rename unlink }; allow trident_t unreserved_port_t:tcp_socket name_connect; allow trident_t updpwd_exec_t:file getattr; allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto mounton }; allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; allow trident_t uuidd_exec_t:file getattr; +allow trident_t uuidd_var_lib_t:dir relabelto; allow trident_t var_t:dir { mounton relabelto }; +allow trident_t var_lib_t:dir relabelto; +allow trident_t var_lib_t:file relabelto; +allow trident_t var_lock_t:lnk_file relabelto; +allow trident_t var_log_t:dir relabelto; +allow trident_t var_log_t:lnk_file relabelto; allow trident_t var_run_t:dir { add_name create remove_name write }; allow trident_t var_run_t:file { create getattr lock open read unlink write }; +allow trident_t var_run_t:lnk_file relabelto; +allow trident_t var_spool_t:dir relabelto; +allow trident_t wtmp_t:file relabelto; #============= interfaces ============== ########################################### @@ -723,6 +763,7 @@ role unconfined_r types fsadm_t; allow fsadm_t efivarfs_t:filesystem getattr; allow fsadm_t trident_t:process { siginh rlimitinh noatsecure transition sigchld }; allow fsadm_t fixed_disk_device_t:blk_file { open read write getattr ioctl }; +allow fsadm_t unlabeled_t:file map; # Create, read, write, and delete files on a efivarfs filesystem fs_manage_efivarfs_files(fsadm_t) @@ -776,6 +817,7 @@ allow systemd_generator_t home_root_t:dir read; allow udev_t cloud_init_t:fd use; allow udev_t cloud_init_t:fifo_file { append write getattr }; allow udev_t lvm_t:process { noatsecure rlimitinh siginh }; +allow udev_t unlabeled_t:file getattr; files_read_generic_tmp_files(udev_t) From f225aa40865304c26446e359e585e0d341d3ee9d Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 16 Jun 2025 20:33:30 +0000 Subject: [PATCH 79/99] Merged PR 23548: doc: typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Fix some typos Related work items: #12544 --- dev-docs/diagrams/overall-testing-on-baremetal.mmd | 2 +- dev-docs/testing.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-docs/diagrams/overall-testing-on-baremetal.mmd b/dev-docs/diagrams/overall-testing-on-baremetal.mmd index f685424ac..e27ef239e 100644 --- a/dev-docs/diagrams/overall-testing-on-baremetal.mmd +++ b/dev-docs/diagrams/overall-testing-on-baremetal.mmd @@ -16,7 +16,7 @@ F0 --> D["Strategy Matrix"] subgraph Job to execute the test - trident_baremetal_tests D --> E[Loop for each test from the list of E2E Tests] E --> G[Run E2E Test] - subgraph Seperate step for each test in Strategy Matrix + subgraph Separate step for each test in Strategy Matrix G --> H[Checkout Repositories] H --> I[Install prerequisites for BM Host communication - baremetal-prep.yml] I --> J[Create SSH key and install prerequisites required for the E2E tests - trident-prep.yml] diff --git a/dev-docs/testing.md b/dev-docs/testing.md index 2a0140422..46331d6da 100644 --- a/dev-docs/testing.md +++ b/dev-docs/testing.md @@ -89,11 +89,11 @@ Functional tests should: Functional tests are structured as follows: - `/functional_tests`: Contains the functional test code, leveraging `pytest` - and common SSH interface from `platform-tests` repo. `pytest` creates the test - VM using is Fixtures concept and while currently only a single VM is created - to run all the tests, this could be easily extended to support seperate VMs - for different tests. Most of the time, no changes will be required to this - layer while developing functional tests. + and common SSH interface from `platform-tests` repo. `pytest` fixtures create + the test VM using virt-deploy. Currently, only one VM is created to run all + the tests, but this could be easily extended to support separate VMs for + different tests. Most of the time, no changes will be required to this layer + while developing functional tests. - `/functional_tests/trident-setup.yaml`: Contains the initial host configuration for the VM that will be used to execute the functional tests. - `/functional_tests/custom/../*.py`: Manually authored Pytest modules for more From b51bd941cc60f02e849eed5b63e7db7a3097a069 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 16 Jun 2025 21:21:43 +0000 Subject: [PATCH 80/99] Merged PR 23513: Add helpful information to log when mounting over non-empty directory error occurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Add existing contents in warn!, example output: `Mount path: '/tmp/.tmpV5CnFt' already exists and is non-empty: /tmp/.tmpV5CnFt/temp_dir` Related work items: #10330 --- src/engine/newroot.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/engine/newroot.rs b/src/engine/newroot.rs index 6578c49e3..c36b1f323 100644 --- a/src/engine/newroot.rs +++ b/src/engine/newroot.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use anyhow::{bail, ensure, Context, Error}; +use anyhow::{anyhow, bail, ensure, Context, Error}; use log::{debug, error, trace, warn}; use sys_mount::{MountBuilder, MountFlags}; @@ -496,11 +496,31 @@ fn prepare_mount_directory(target_path: &Path, is_newroot: bool) -> Result<(), E ); // Check if the directory is empty if let Ok(entries) = fs::read_dir(target_path) { - ensure!( - entries.count() == 0, - "Mount path '{}' is not empty", - target_path.display() - ); + let entries_list = entries + .filter_map(|e| match e { + Ok(ee) => Some(ee.path().to_string_lossy().into_owned()), + Err(err) => { + warn!( + "Failed to read entry in mount path '{}': {}", + target_path.display(), + err + ); + None + } + }) + .collect::>() + .join(", "); + if !entries_list.is_empty() { + error!( + "Mount path '{}' already exists and is non-empty: {}\n", + target_path.display(), + entries_list + ); + return Err(anyhow!( + "Mount path '{}' is not empty", + target_path.display() + )); + } } Ok(()) } else { From 443bc3ce2b82924c4ff4f5c6ba97400bae46d0bd Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Mon, 16 Jun 2025 22:39:37 +0000 Subject: [PATCH 81/99] Merged PR 23423: Migrate servicing tests to storm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Migrate servicing tests from bash scripts to storm helper. qemu, uki servicing tests validated: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=829876&view=results azure servicing tests validated: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=833960&view=results (interestingly, new e2e-pr-azure servicing tests run for 20-25 minutes rather than 35-40 minutes ... 10min saving on different replication mode for azure image) ---- #### AI description (iteration 1) #### PR Classification This pull request migrates the servicing tests from legacy Bash scripts to a new Storm framework-based test helper. #### PR Summary This PR introduces a new testing infrastructure for servicing tests by implementing utility functions and a helper in Storm, and updating pipeline definitions to use the new commands. - **`tools/storm/utils/servicing.go`**: Added comprehensive utilities for VM deployment, update loops, log collection, image publishing, and cleanup across QEMU and Azure. - **`tools/storm/helpers/servicing.go` & `tools/storm/helpers/init.go`**: Implemented a new `ServicingTestsHelper` that registers test cases (deploy-vm, check-deployment, update-loop, collect-logs, cleanup-vm, publish-sig-image) with Storm. - **`/.pipelines/templates/stages/testing_servicing/testing-template.yml`**: Updated pipeline steps to call the new Storm helper with appropriate flags replacing old Bash scripts. - **`/.pipelines/templates/MockOB.yml`**: Modified to ensure artifact publishing occurs in all conditions. Related work items: #12590 --- .pipelines/templates/MockOB.yml | 1 + .../testing_servicing/testing-template.yml | 185 ++--- scripts/loop-update/README.md | 46 +- scripts/loop-update/check-deployment.sh | 43 +- scripts/loop-update/common.sh | 315 -------- scripts/loop-update/deploy-vm.sh | 99 --- scripts/loop-update/fetch-logs.sh | 73 -- scripts/loop-update/loop-update.sh | 226 ------ .../loop-update/publish-sig-image-prepare.sh | 87 --- scripts/loop-update/publish-sig-image.sh | 50 -- scripts/loop-update/servicing-tests.sh | 73 ++ tools/cmd/storm-trident/main.go | 4 + tools/storm/servicing/tests/azure.go | 13 + tools/storm/servicing/tests/logs.go | 34 + tools/storm/servicing/tests/update.go | 455 +++++++++++ tools/storm/servicing/tests/vm.go | 68 ++ tools/storm/servicing/trident.go | 138 ++++ tools/storm/servicing/utils/azure/azure.go | 707 ++++++++++++++++++ tools/storm/servicing/utils/config/config.go | 41 + tools/storm/servicing/utils/file/file.go | 35 + tools/storm/servicing/utils/qemu/qemu.go | 451 +++++++++++ tools/storm/servicing/utils/ssh/ssh.go | 160 ++++ tools/storm/utils/vmip.go | 34 + 23 files changed, 2348 insertions(+), 990 deletions(-) delete mode 100755 scripts/loop-update/common.sh delete mode 100755 scripts/loop-update/deploy-vm.sh delete mode 100755 scripts/loop-update/fetch-logs.sh delete mode 100755 scripts/loop-update/loop-update.sh delete mode 100755 scripts/loop-update/publish-sig-image-prepare.sh delete mode 100755 scripts/loop-update/publish-sig-image.sh create mode 100755 scripts/loop-update/servicing-tests.sh create mode 100644 tools/storm/servicing/tests/azure.go create mode 100644 tools/storm/servicing/tests/logs.go create mode 100644 tools/storm/servicing/tests/update.go create mode 100644 tools/storm/servicing/tests/vm.go create mode 100644 tools/storm/servicing/trident.go create mode 100644 tools/storm/servicing/utils/azure/azure.go create mode 100644 tools/storm/servicing/utils/config/config.go create mode 100644 tools/storm/servicing/utils/file/file.go create mode 100644 tools/storm/servicing/utils/qemu/qemu.go create mode 100644 tools/storm/servicing/utils/ssh/ssh.go create mode 100644 tools/storm/utils/vmip.go diff --git a/.pipelines/templates/MockOB.yml b/.pipelines/templates/MockOB.yml index 4912a434e..a656b3d7a 100644 --- a/.pipelines/templates/MockOB.yml +++ b/.pipelines/templates/MockOB.yml @@ -111,3 +111,4 @@ stages: - publish: ${{ variables.ob_outputDirectory }} artifact: ${{ variables.ob_customArtifactName }} + condition: always() diff --git a/.pipelines/templates/stages/testing_servicing/testing-template.yml b/.pipelines/templates/stages/testing_servicing/testing-template.yml index 202a334d3..83c40354f 100644 --- a/.pipelines/templates/stages/testing_servicing/testing-template.yml +++ b/.pipelines/templates/stages/testing_servicing/testing-template.yml @@ -45,36 +45,6 @@ parameters: default: "trident-ubuntu-1es-pool-eastus2" jobs: - - ${{ if eq(parameters.platform, 'azure') }}: - - job: PublishAzureImage - displayName: Publish Azure Image - timeoutInMinutes: 20 - pool: - type: linux - name: ${{ parameters.pool }} - hostArchitecture: amd64 - variables: - ob_outputDirectory: $(Build.SourcesDirectory)/logs - steps: - - task: DownloadPipelineArtifact@2 - inputs: - buildType: current - artifactName: image-${{ parameters.platform }}-base - targetPath: "$(Build.ArtifactStagingDirectory)/" - displayName: Download Base Image - - bash: | - set -eux - az login --identity - ./scripts/loop-update/publish-sig-image.sh - displayName: Publish Base Image - env: - SUBSCRIPTION: 04cdc145-a4f9-42d4-9868-c46d23d0c63f # CoreOS_Mariner_BMP_Staging - IMAGE_DEFINITION: "trident-vm-grub-verity-testimage-$(System.DefinitionId)" - ARTIFACTS: $(Build.ArtifactStagingDirectory) - STORAGE_ACCOUNT: "azlinuxbmpstagingeastus2" - RESOURCE_GROUP: "azlinux_bmp_staging_eastus2" - AZCOPY_AUTO_LOGIN_TYPE: "MSI" - - job: UpdateTesting_${{ parameters.flavor }} displayName: Update Testing - ${{ parameters.flavor }} timeoutInMinutes: ${{ parameters.updateCheckTimeoutInMinutes }} @@ -84,9 +54,6 @@ jobs: hostArchitecture: amd64 strategy: parallel: ${{ parameters.workers }} - dependsOn: - - ${{ if eq(parameters.platform, 'azure') }}: - - PublishAzureImage variables: tridentSourceDirectory: $(Build.SourcesDirectory) @@ -95,6 +62,9 @@ jobs: IMAGE_DEFINITION: "trident-vm-grub-verity-testimage-$(System.DefinitionId)" TEST_RESOURCE_GROUP: trident-vm-servicing-validation-$(Build.BuildId)-$(System.JobPositionInPhase) SUBSCRIPTION: 04cdc145-a4f9-42d4-9868-c46d23d0c63f # CoreOS_Mariner_BMP_Staging + STORAGE_ACCOUNT: "azlinuxbmpstagingeastus2" + RESOURCE_GROUP: "azlinux_bmp_staging_eastus2" + SUBNET_ID: /subscriptions/04cdc145-a4f9-42d4-9868-c46d23d0c63f/resourceGroups/trident-vm_servicing-azure-vnet/providers/Microsoft.Network/virtualNetworks/poolpeeringvnet/subnets/default steps: - bash: | @@ -102,6 +72,14 @@ jobs: echo "##vso[task.setvariable variable=TEST_RESOURCE_GROUP;]trident-vm-servicing-validation-$(Build.BuildId)-$(printf '%03d' $(System.JobPositionInPhase))" displayName: "Set variables" + - ${{ if eq(parameters.platform, 'azure') }}: + - task: DownloadPipelineArtifact@2 + inputs: + buildType: current + artifactName: image-${{ parameters.platform }}-base + targetPath: "$(Build.ArtifactStagingDirectory)/" + displayName: Download Base Image + - ${{ if eq(parameters.platform, 'qemu') }}: - task: DownloadPipelineArtifact@2 inputs: @@ -152,70 +130,50 @@ jobs: - bash: | set -eux - if [ "$TEST_PLATFORM" == "azure" ]; then + + SUDO="sudo" + if [ "${{ parameters.platform }}" == "azure" ]; then az login --identity + SUDO="" fi - ./scripts/loop-update/deploy-vm.sh - displayName: "Deploy VM" - env: - VERBOSE: ${{ parameters.verboseLogging }} - ARTIFACTS: $(Build.ArtifactStagingDirectory) - OUTPUT: $(ob_outputDirectory) - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - IMAGE_DEFINITION: $(IMAGE_DEFINITION) - TEST_PLATFORM: ${{ parameters.platform }} - SUBSCRIPTION: $(SUBSCRIPTION) - SECURE_BOOT: ${{ ne(parameters.flavor, 'uki') }} - VALIDATION_SUBNET_ID: /subscriptions/04cdc145-a4f9-42d4-9868-c46d23d0c63f/resourceGroups/trident-vm_servicing-azure-vnet/providers/Microsoft.Network/virtualNetworks/poolpeeringvnet/subnets/default - timeoutInMinutes: 5 - - bash: ./scripts/loop-update/check-deployment.sh - displayName: "Check that Trident can adopt the deployment" - env: - TEST_PLATFORM: ${{ parameters.platform }} + FLAGS="" + if [ "${{ parameters.verboseLogging }}" == "True" ]; then + FLAGS="$FLAGS --verbose" + fi + if [ "${{ parameters.flavor }}" != "uki" ]; then + FLAGS="$FLAGS --secure-boot" + fi - - bash: ./scripts/loop-update/loop-update.sh - env: - ARTIFACTS: $(Build.ArtifactStagingDirectory) - OUTPUT: $(ob_outputDirectory) - VERBOSE: ${{ parameters.verboseLogging }} - RETRY_COUNT: ${{ parameters.updateIterationCount }} - EXPECTED_VOLUME: "volume-b" - ROLLBACK: "false" - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - TEST_PLATFORM: ${{ parameters.platform }} - displayName: "Check that Trident can perform A/B update" - condition: succeeded() - - # E2E rollback test: Trigger an A/B update back into runtime OS A, then cause a rollback - # by triggering an artificial reboot. Then, check that the firmware performed a rollback - # into B correctly. Finally, trigger two A/B updates, the first one using the same Host - # Configuration, and validate that they succeed. Rollback testing will only be run when - # the rollbackTesting parameter is true. The scaling test logic will set it to false. - - - bash: ./scripts/loop-update/loop-update.sh + $SUDO ./bin/storm-trident run servicing $FLAGS \ + --artifacts-dir $(Build.ArtifactStagingDirectory) \ + --output-path $(ob_outputDirectory) \ + --subscription $(SUBSCRIPTION) \ + --image-definition $(IMAGE_DEFINITION) \ + --storage-account $(STORAGE_ACCOUNT) \ + --storage-account-resource-group $(RESOURCE_GROUP) \ + --test-resource-group $(TEST_RESOURCE_GROUP) \ + --platform ${{ parameters.platform }} \ + --subnet-id $(SUBNET_ID) \ + --ssh-private-key-path $HOME/.ssh/id_rsa \ + --ssh-public-key-path $HOME/.ssh/id_rsa.pub \ + --retry-count ${{ parameters.updateIterationCount }} \ + --rollback-retry-count ${{ parameters.updateIterationCount }} \ + --build-id $(Build.BuildId) \ + --force-cleanup + + set +x + echo "##vso[task.setvariable variable=STORM_SCENARIO_FINISHED;]true" + + displayName: "Servicing test" + timeoutInMinutes: 20 env: - ARTIFACTS: $(Build.ArtifactStagingDirectory) - OUTPUT: $(ob_outputDirectory) - VERBOSE: ${{ parameters.verboseLogging }} - RETRY_COUNT: 3 - EXPECTED_VOLUME: "volume-b" - ROLLBACK: "true" - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - TEST_PLATFORM: ${{ parameters.platform }} - displayName: "Check that Trident can roll back and perform A/B update after" - condition: and(succeeded(), eq(${{ parameters.rollbackTesting }}, true)) - - # TODO add more e2e tests here (Task 8813) - + AZCOPY_AUTO_LOGIN_TYPE: "MSI" + - bash: | set -eux - ./scripts/loop-update/fetch-logs.sh $(ob_outputDirectory)/ - - ./scripts/loop-update/cleanup-vm.sh - if [ "$TEST_PLATFORM" == "qemu" ]; then - mkdir -p $(ob_outputDirectory) + if [ "${{ parameters.platform }}" == "qemu" ]; then sudo zstd -T0 $(Build.ArtifactStagingDirectory)/booted.qcow2 sudo mv $(Build.ArtifactStagingDirectory)/booted.qcow2.zst $(ob_outputDirectory)/ fi @@ -223,21 +181,42 @@ jobs: # https://learn.microsoft.com/en-us/azure/virtual-machines/linux/download-vhd?tabs=azure-cli # for Azure images workingDirectory: $(tridentSourceDirectory) - env: - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - TEST_PLATFORM: ${{ parameters.platform }} - SUBSCRIPTION: $(SUBSCRIPTION) condition: failed() displayName: "Publish logs and OS disk on failure" timeoutInMinutes: 5 - - bash: | - set -eux - ./scripts/loop-update/cleanup-vm.sh - displayName: "Cleanup VM" - workingDirectory: $(tridentSourceDirectory) - condition: always() - env: - TEST_RESOURCE_GROUP: $(TEST_RESOURCE_GROUP) - TEST_PLATFORM: ${{ parameters.platform }} - SUBSCRIPTION: $(SUBSCRIPTION) + - ${{ if eq(parameters.platform, 'azure') }}: + - bash: | + set -eux + + # If platform is azure AND the test failed to finish, run cleanup to + # ensure there are no azure resources left behind + if [ "${STORM_SCENARIO_FINISHED}" != "true" ]; then + az login --identity + + FLAGS="" + if [ "${{ parameters.verboseLogging }}" == "True" ]; then + FLAGS="$FLAGS --verbose" + fi + + ./bin/storm-trident run servicing $FLAGS \ + --artifacts-dir $(Build.ArtifactStagingDirectory) \ + --output-path $(ob_outputDirectory) \ + --subscription $(SUBSCRIPTION) \ + --image-definition $(IMAGE_DEFINITION) \ + --storage-account $(STORAGE_ACCOUNT) \ + --storage-account-resource-group $(RESOURCE_GROUP) \ + --test-resource-group $(TEST_RESOURCE_GROUP) \ + --platform ${{ parameters.platform }} \ + --subnet-id $(SUBNET_ID) \ + --ssh-private-key-path $HOME/.ssh/id_rsa \ + --ssh-public-key-path $HOME/.ssh/id_rsa.pub \ + --retry-count ${{ parameters.updateIterationCount }} \ + --rollback-retry-count ${{ parameters.updateIterationCount }} \ + --build-id $(Build.BuildId) \ + --test-case-to-run cleanup-vm + fi + + displayName: "Cleanup even if timeout" + timeoutInMinutes: 20 + condition: always() diff --git a/scripts/loop-update/README.md b/scripts/loop-update/README.md index 9a189557d..ef41e4ae8 100644 --- a/scripts/loop-update/README.md +++ b/scripts/loop-update/README.md @@ -15,27 +15,25 @@ the scaling pipeline. They can be also used locally. servicing and two sets of update images. The produced images are moved to `artifacts`. Generally needs to be only rerun when you want to refresh the images to be used. - -- For Azure VMs, you will need to publish the image once, before it could be - used to create VMs. To publish the image, call `publish-sig-image.sh`. - -- `deploy-vm.sh`: Creates a VM instance with the base image and starts the VM. - It ensures the VM gets to the login prompt. - -- `check-deployment.sh`: Fetches the Host Status of the freshly deployed VM to - ensure it is in an expected state. You need to deploy the VM first using the - script above. - -- `loop-update.sh`: Loops through the update images and applies them to the VM. - It ensures the VM gets to the login prompt after each update and confirms the - Host Status is as expected. This script will power off and restart the VM - every 10 runs. By default, it will execute 20 loops, and you can change this - by setting `RETRY_COUNT` environment variable. - -- `cleanup-vm.sh`: Deletes the VM instance. The deploy scripts will delete - automatically before creating the new VMs. The other scripts use a presence of - a QEMU VM to decide whether to target Azure or QEMU, so if you are switching - between the two, you may need to delete the VM using this script first. - -- `common.sh`: Not used directly. Contains common functions used by the other - scripts. + +- The servicing tests are backed by a storm scenario (../tools/storm/servicing). + +- To run the scenario, you can use the `servicing-tests.sh` script or by invoking + the storm binary directly. + +- The servicing tests are composed of several storm test cases: + +1. For Azure VMs, `publish-sig-image` is the first testcase and it will + configure an appropriate qcow2 image as needed for an Azure VM and + upload it. +2. For all VMs, `deploy-vm` is the next phase and will create a VM on the + selected platform. +3. For all VMs, `check-deployment` will verify that the VM has been started + and that it booted from the expected volume.` +4. For all VMs, `update-loop` will update the VM the specified number of + times, applying the update images and checking that the VM is in the + expected state. +5. For all VMs, `rollback` will validate that rollback and update works. +6. For all VMs, `collect-logs` will collect logs from the VM. +7. For all VMs, `cleanup-vm` will delete the VM. + diff --git a/scripts/loop-update/check-deployment.sh b/scripts/loop-update/check-deployment.sh index 6f959fd5c..5e4a42b87 100755 --- a/scripts/loop-update/check-deployment.sh +++ b/scripts/loop-update/check-deployment.sh @@ -3,18 +3,35 @@ set -euxo pipefail . $(dirname $0)/common.sh -VM_IP=`getIp` - -# Help diagnose https://dev.azure.com/mariner-org/ECF/_workitems/edit/11273 and -# fail explicitly if multiple IPs are found -if [ "$TEST_PLATFORM" == "qemu" ]; then - if [ `echo $VM_IP | wc -w` -gt 1 ]; then - echo "Multiple IPs found:" - echo $VM_IP - sudo virsh domifaddr $VM_NAME - adoError "Multiple IPs found" - exit 1 - fi +SUDO="sudo" +if [ "$TEST_PLATFORM" == "azure" ]; then + SUDO="" fi -checkActiveVolume "volume-a" 0 \ No newline at end of file +$SUDO ./bin/storm-trident helper servicing-tests \ + --output-path $OUTPUT \ + --artifacts-dir $ARTIFACTS \ + --retry-count $RETRY_COUNT \ + --expected-volume volume-b \ + --storage-account-resource-group $RESOURCE_GROUP \ + --ssh-private-key-path ~/.ssh/id_rsa \ + --user $SSU_USER \ + --platform $TEST_PLATFORM \ + --name $VM_NAME \ + --serial-log $VM_SERIAL_LOG \ + --who-am-i $ALIAS \ + --subscription $SUBSCRIPTION \ + --image-definition $IMAGE_DEFINITION \ + --region $PUBLISH_LOCATION \ + --gallery-resource-group $GALLERY_RESOURCE_GROUP \ + --storage-account $STORAGE_ACCOUNT \ + --gallery-name $GALLERY_NAME \ + --offer $OFFER \ + --test-resource-group $TEST_RESOURCE_GROUP \ + --size $TEST_VM_SIZE \ + --ssh-public-key-path $SSH_PUBLIC_KEY_PATH \ + --user $SSH_USER \ + --update-port-a $UPDATE_PORT_A \ + --update-port-b $UPDATE_PORT_B \ + --expected-volume volume-a \ + --test-case-to-run check-deployment diff --git a/scripts/loop-update/common.sh b/scripts/loop-update/common.sh deleted file mode 100755 index be12540c0..000000000 --- a/scripts/loop-update/common.sh +++ /dev/null @@ -1,315 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -ARTIFACTS=${ARTIFACTS:-artifacts} -VM_NAME=${VM_NAME:-trident-vm-verity-test} -VM_SERIAL_LOG=${VM_SERIAL_LOG:-/tmp/$VM_NAME.log} -VERBOSE=${VERBOSE:-False} -OUTPUT=${OUTPUT:-} - -ALIAS=${ALIAS:-`whoami`} - -SUBSCRIPTION=${SUBSCRIPTION:-b8a0db63-c5fa-4198-8e2a-f9d6ff52465e} # CoreOS_AzureLinux_BMP_dev -IMAGE_DEFINITION=${IMAGE_DEFINITION:-trident-vm-grub-verity-azure-testimage} -RESOURCE_GROUP=${RESOURCE_GROUP:-azlinux_bmp_dev} -PUBLISH_LOCATION=${PUBLISH_LOCATION:-eastus2} -GALLERY_RESOURCE_GROUP=${GALLERY_RESOURCE_GROUP:-$ALIAS-trident-rg} -STORAGE_ACCOUNT=${STORAGE_ACCOUNT:-azlinuxbmpdev} -GALLERY_NAME=${GALLERY_NAME:-${ALIAS}_trident_gallery} -PUBLISHER=${PUBLISHER:-$ALIAS} -OFFER=${OFFER:-trident-vm-grub-verity-azure-offer} -export AZCOPY_AUTO_LOGIN_TYPE=${AZCOPY_AUTO_LOGIN_TYPE:-AZCLI} -TEST_RESOURCE_GROUP=${TEST_RESOURCE_GROUP:-$GALLERY_RESOURCE_GROUP-test} -TEST_VM_SIZE=${TEST_VM_SIZE:-Standard_D2ds_v5} -SSH_PUBLIC_KEY_PATH=${SSH_PUBLIC_KEY_PATH:-~/.ssh/id_rsa.pub} - -# Third parent of this script -TRIDENT_SOURCE_DIRECTORY=$(dirname $(dirname $(dirname $(realpath $0)))) -SSH_USER=testuser - -UPDATE_PORT_A=8000 -UPDATE_PORT_B=8001 - -function getIp() { - if [ "$TEST_PLATFORM" == "qemu" ]; then - while [ `sudo virsh domifaddr $VM_NAME | grep -c "ipv4"` -eq 0 ]; do sleep 1; done - sudo virsh domifaddr $VM_NAME | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 - elif [ "$TEST_PLATFORM" == "azure" ]; then - IP_TYPE=publicIps - if [ ! -z "${BUILD_BUILDNUMBER:-}" ]; then - IP_TYPE=privateIps - fi - az vm show -d -g $TEST_RESOURCE_GROUP -n $VM_NAME --query $IP_TYPE -o tsv - fi -} - -function sshCommand() { - local COMMAND=$1 - - # BatchMode - running from a script, disable any interactive prompts - # ConnectTimeout - how long to wait for the connection to be established - # ServerAliveCountMax - how many keepalive packets can be missed before the connection is closed - # ServerAliveInterval - how often to send keepalive packets - # StrictHostKeyChecking - disable host key checking; TODO: remove this and - # use the known_hosts file instead - # UserKnownHostsFile - disable known hosts file to simplify local runs - ssh \ - -o BatchMode=yes \ - -o ConnectTimeout=10 \ - -o ServerAliveCountMax=3 \ - -o ServerAliveInterval=5 \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -i ../test-images/build/id_rsa \ - $SSH_USER@$VM_IP \ - "$COMMAND" -} - -function sshProxyPort() { - local PORT=$1 - - ssh \ - -R $PORT:localhost:$PORT -N \ - -o BatchMode=yes \ - -o ConnectTimeout=10 \ - -o ServerAliveCountMax=3 \ - -o ServerAliveInterval=5 \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -i ../test-images/build/id_rsa \ - $SSH_USER@$VM_IP & -} - -function adoError() { - local MESSAGE=$1 - - set +x - echo "##vso[task.logissue type=error]$MESSAGE" - set -x -} - -function adoWarning() { - local MESSAGE=$1 - - set +x - echo "##vso[task.logissue type=warning]$MESSAGE" - set -x -} - -function checkActiveVolume() { - local VOLUME=$1 - local ITERATION=$2 - - bin/storm-trident helper check-ssh ~/.ssh/id_rsa $VM_IP $SSH_USER none --check-active-volume $VOLUME -} - -function validateRollback() { - HOST_STATUS=`sshCommand "set -o pipefail; sudo systemd-run --pipe --property=After=trident.service trident get"` - # Validate that lastError.category is set to "servicing" - CATEGORY=$(echo "$HOST_STATUS" | yq eval '.lastError.category' -) - if [ "$CATEGORY" != "servicing" ]; then - sshCommand "sudo trident get" - echo "Category of last error is not 'servicing', but '$CATEGORY'" - adoError "Category of last error is not 'servicing', but '$CATEGORY'" - exit 1 - fi - - # Validate that lastError.error contains the expected content - ERROR=$(echo "$HOST_STATUS" | yq eval '.lastError.error' -) - if [[ "$ERROR" != *"!ab-update-reboot-check"* ]]; then - echo "Type of last error is not '!ab-update-reboot-check', but '$ERROR'" - adoError "Type of last error is not '!ab-update-reboot-check', but '$ERROR'" - exit 1 - fi - - # Validate that lastError.message matches the expected format - MESSAGE=$(echo "$HOST_STATUS" | yq eval '.lastError.message' -) - if ! echo "$MESSAGE" | grep -Eq '^A/B update failed as host booted from .+ instead of the expected device .+$'; then - echo "Message of last error does not match the expected format: '$MESSAGE'" - adoError "Message of last error does not match the expected format: '$MESSAGE'" - exit 1 - fi - - echo "Rollback validation succeeded" -} - -function truncateLog() { - if sudo virsh dominfo $VM_NAME > /dev/null; then - sudo truncate -s 0 "$VM_SERIAL_LOG" - fi -} - -function analyzeSerialLog() { - local LOG=$1 - - LAST_LINE=$(tail -n 1 $LOG) - if [[ $LAST_LINE == *"tpm tpm0: Operation Timed out"* ]]; then - echo "Error found in serial log: tpm tpm0: Operation Timed out" - adoError "tpm tpm0: Operation Timed out" - else - echo "No error found in serial log" - echo "Last line of serial log: $LAST_LINE" - adoError "Last line of serial log: $LAST_LINE" - fi -} - -function waitForLogin() { - set +e - local ITERATION=$1 - - LOGGING="" - if [ $VERBOSE == True ]; then - echo "VM serial log:" - LOGGING="-v" - fi - - # Keeping errors masked, as we want to handle the failure explicitly - sudo $TRIDENT_SOURCE_DIRECTORY/e2e_tests/helpers/wait_for_login.py \ - -d "$VM_SERIAL_LOG" \ - -o ./serial.log \ - -t 120 \ - $LOGGING - - WAIT_FOR_LOGIN_EXITCODE=$? - - if [ "$OUTPUT" != "" ]; then - mkdir -p $OUTPUT - PAD_ITERATION=$(printf "%03d" $ITERATION) - OUTPUT_FILENAME=$PAD_ITERATION-serial.log - if [ "${ROLLBACK:-}" == "true" ]; then - OUTPUT_FILENAME=$PAD_ITERATION-rollback-serial.log - fi - sudo cp ./serial.log $OUTPUT/$OUTPUT_FILENAME - fi - - if [ $WAIT_FOR_LOGIN_EXITCODE -ne 0 ]; then - echo "Failed to reach login prompt for the VM" - adoError "Failed to reach login prompt for the VM for iteration $ITERATION" - - analyzeSerialLog ./serial.log - - if [ "$TEST_PLATFORM" == "qemu" ]; then - virsh dominfo $VM_NAME - fi - - df -h - exit $WAIT_FOR_LOGIN_EXITCODE - fi - set -e -} - -function getLatestVersion() { - local G_RG_NAME=$1 - local G_NAME=$2 - local I_NAME=$3 - - # TODO improve the sorting - az sig image-version list -g $G_RG_NAME -r $G_NAME -i $I_NAME --query '[].name' -o tsv | sort -t "." -k1,1n -k2,2n -k3,3n | tail -1 -} - -function getImageVersion() { - local OP=${1:-} - if [ -z "${BUILD_BUILDNUMBER:-}" ]; then - image_version=$(getLatestVersion $GALLERY_RESOURCE_GROUP $GALLERY_NAME $IMAGE_DEFINITION) - if [ -z $image_version ]; then - image_version=0.0.1 - else - if [ "$OP" == "increment" ]; then - # Increment the semver version - image_version=$(echo $image_version | awk -F. '{print $1"."$2"."$3+1}') - fi - fi - else - image_version="0.0.$BUILD_BUILDID" - fi - - echo $image_version -} - -function resizeImage() { - local IMAGE_PATH=$1 - # VHD images on Azure must have a virtual size aligned to 1MB. https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-generic#resize-vhds - raw_file="resize.raw" - sudo qemu-img convert -f vpc -O raw $IMAGE_PATH $raw_file - MB=$((1024*1024)) - size=$(qemu-img info -f raw --output json "$raw_file" | \ - gawk 'match($0, /"virtual-size": ([0-9]+),/, val) {print val[1]}') - - rounded_size=$(((($size+$MB-1)/$MB)*$MB)) - - echo "Rounded Size = $rounded_size" - - sudo qemu-img resize $raw_file $rounded_size - - sudo qemu-img convert -f raw -o subformat=fixed,force_size -O vpc $raw_file $IMAGE_PATH -} - -function killUpdateServer() { - local UPDATE_PORT=$1 - - set +e - KILL_PID=$(lsof -ti tcp:${UPDATE_PORT}) - PROCESS_FOUND=$? - set -e - if [ $PROCESS_FOUND -eq 0 ]; then - echo "Process already running on the Trident update server port: '${UPDATE_PORT}'. Killing process '$KILL_PID'." - kill -9 $KILL_PID > /dev/null 2>&1 || true - fi -} - -function logAssignedRoles() { - # Log the managed identity roles on the subscription - # 1cd7f210-4327-4ef9-b33f-f64d342cc431 is trident-servicing-test managed - # identity, assigned to the pool - set +e - echo "Assigned roles: `az role assignment list --assignee 1cd7f210-4327-4ef9-b33f-f64d342cc431 --scope "/subscriptions/$SUBSCRIPTION"`" 1>&2 - set -e -} - -function azCommand() { - local COMMAND="$@" - - set +e - # Capture the output, so we only print it once - OUTPUT=`az $COMMAND` - RESULT=$? - logAssignedRoles - set -e - if [ $RESULT -ne 0 ]; then - # Only for pipelines - if [ ! -z "${BUILD_BUILDNUMBER:-}" ]; then - adoWarning "Managed identity does not have access to the subscription (command '$COMMAND' exit code is $RESULT), retrying..." - az login --identity > /dev/null - fi - az $COMMAND - else - echo -n $OUTPUT - fi -} - -function ensureAzureAccess() { - local RESOURCE_GROUP=$1 - - SUCCESS=0 - for i in {1..10}; do - set +e - EXISTS=`az group exists -n "$RESOURCE_GROUP"` - RESULT=$? - set -e - logAssignedRoles - # If the check did not fail and actually returned a value, we should - # have access - if [ $RESULT -eq 0 ] && [ ! -z "$EXISTS" ]; then - SUCCESS=1 - break - fi - echo "Managed identity does not have access to the subscription, retrying..." - sleep 5 - az login --identity - done - if [ $SUCCESS -eq 0 ]; then - echo "Managed identity does not have access to the subscription" - adoError "Managed identity does not have access to the subscription" - exit 1 - fi -} \ No newline at end of file diff --git a/scripts/loop-update/deploy-vm.sh b/scripts/loop-update/deploy-vm.sh deleted file mode 100755 index 9f9004e5c..000000000 --- a/scripts/loop-update/deploy-vm.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -set -euo pipefail - -. $(dirname "$0")/common.sh - -if [ "$TEST_PLATFORM" == "qemu" ]; then - sudo virsh list --all - sudo virsh destroy "$VM_NAME" || true - sudo virsh undefine "$VM_NAME" --nvram || true - - IMAGE_FILES="$(find $ARTIFACTS -type f -name 'trident-vm-*-testimage.qcow2')" - IMAGE_FILES_COUNT=$(echo "$IMAGE_FILES" | wc -l) - - if [ $IMAGE_FILES_COUNT -lt 1 ]; then - echo "Image file not found!" - exit 1 - elif [ $IMAGE_FILES_COUNT -gt 1 ]; then - echo "Multiple image files found:" - echo $IMAGE_FILES - exit 1 - else - echo "Image file found: $IMAGE_FILES" - fi - - IMAGE_FILE=$(echo $IMAGE_FILES | head -1) - - BOOT_IMAGE="$ARTIFACTS/booted.qcow2" - cp "$IMAGE_FILE" "$BOOT_IMAGE" - - BOOT_CONFIG="--machine q35 --boot uefi,loader_secure=yes" - if [ "${SECURE_BOOT:-}" == "False" ]; then - BOOT_CONFIG="--boot uefi,loader_secure=no" - fi - - sudo virt-install \ - --name "$VM_NAME" \ - --memory 2048 \ - --vcpus 2 \ - --os-variant generic \ - --import \ - --disk "$BOOT_IMAGE,bus=sata" \ - --network default \ - $BOOT_CONFIG \ - --noautoconsole \ - --serial "file,path=$VM_SERIAL_LOG" - - until [ -f "$VM_SERIAL_LOG" ] - do - sleep 0.1 - done - - waitForLogin 0 -elif [ "$TEST_PLATFORM" == "azure" ]; then - az account set -s "$SUBSCRIPTION" - - # Ensure access when running in Azure DevOps, since this has not been - # working reliably in the past - if [ ! -z "${BUILD_BUILDNUMBER:-}" ]; then - ensureAzureAccess "$TEST_RESOURCE_GROUP" - fi - if [ "`azCommand group exists -n "$TEST_RESOURCE_GROUP"`" == "true" ]; then - azCommand group delete -n "$TEST_RESOURCE_GROUP" -y - fi - - azCommand group create -n "$TEST_RESOURCE_GROUP" -l "$PUBLISH_LOCATION" --tags creationTime=$(date +%s) - - if [ -n "${VALIDATION_SUBNET_ID:-}" ]; then - SUBNET_ARG="--subnet $VALIDATION_SUBNET_ID" - fi - - VERSION=`getImageVersion` - azCommand vm create \ - --resource-group "$TEST_RESOURCE_GROUP" \ - --name "$VM_NAME" \ - --size "$TEST_VM_SIZE" \ - --os-disk-size-gb 60 \ - --admin-username "$SSH_USER" \ - --ssh-key-values "$SSH_PUBLIC_KEY_PATH" \ - --image "/subscriptions/$SUBSCRIPTION/resourceGroups/$GALLERY_RESOURCE_GROUP/providers/Microsoft.Compute/galleries/$GALLERY_NAME/images/$IMAGE_DEFINITION/versions/$VERSION" \ - --location "$PUBLISH_LOCATION" \ - --security-type TrustedLaunch \ - --enable-secure-boot true \ - --enable-vtpm true \ - --no-wait \ - $SUBNET_ARG - - # Attempt to enable the boot diagnostics early on - while ! azCommand vm boot-diagnostics enable --name "$VM_NAME" -g "$TEST_RESOURCE_GROUP"; do - sleep 1 - done - - # Wait for the boot diagnostics to be available - while azCommand vm boot-diagnostics get-boot-log --name "$VM_NAME" --resource-group "$TEST_RESOURCE_GROUP" | grep "BlobNotFound"; do - sleep 5 - done - - # Use az cli to confirm the VM deployment status is successful - while [ "`azCommand vm show -d -g "$TEST_RESOURCE_GROUP" -n "$VM_NAME" --query provisioningState -o tsv`" != "Succeeded" ]; do sleep 1; done -fi diff --git a/scripts/loop-update/fetch-logs.sh b/scripts/loop-update/fetch-logs.sh deleted file mode 100755 index 6bdc339b2..000000000 --- a/scripts/loop-update/fetch-logs.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -set -euo pipefail - -. $(dirname $0)/common.sh -OUTPUT_DIR="$1" - -downloadJournalLog() { - local DEST=$1 - - local JOURNAL_LOG=/tmp/journal.log - - # Blocking error causing abort, so we can do other cleanup tasks - set +e - sshCommand "sudo journalctl --no-pager > $JOURNAL_LOG && sudo chmod 644 $JOURNAL_LOG" - if [ $? -eq 0 ]; then - scpDownloadFile $JOURNAL_LOG $DEST - else - echo "Failed to download journal log" - fi - set -e -} - -scpDownloadFile() { - local SRC=$1 - local DEST=$2 - - scp -i ../test-images/build/id_rsa -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSH_USER@$VM_IP:$SRC $DEST -} - -downloadCrashdumps() { - local DEST="$1" - - local CRASHDUMP_DIR=/var/crash - - if sshCommand "ls $CRASHDUMP_DIR/*"; then - echo "Crash files found on host" - adoError "Crash files found on host" - sshCommand "sudo mv $CRASHDUMP_DIR/* /tmp/crash && sudo chmod -R 644 /tmp/crash && sudo chmod +x /tmp/crash" - scpDownloadFile "/tmp/crash/*" "$DEST/" - tail -n 50 "$DEST/vmcore-dmesg.txt" - else - echo "No crash files found on host" - fi -} - -downloadAzureSerialLog() { - local DEST="$1" - - # Output of az vm boot-diagnostics get-boot-log is not very readable, so - # clean it up a bit: - # - convert \r\n to newlines - # - remove unicode characters - # - remove lines with only dashes - # - remove empty lines - # - remove lines with only quotes - az vm boot-diagnostics get-boot-log --name "$VM_NAME" --resource-group "$TEST_RESOURCE_GROUP" | - sed -r 's/\\r\\n/\n/g;s/\\u[a-z0-9]{4}//g;/^-+$/d;/^$/d' | sed -r '/^"$/d' > "$DEST" -} - -if [ "$TEST_PLATFORM" == "azure" ]; then - downloadAzureSerialLog $OUTPUT_DIR/serial.log - if [ $VERBOSE == True ]; then - cat $OUTPUT_DIR/serial.log - else - echo "Serial log saved to $OUTPUT_DIR/serial.log" - fi - analyzeSerialLog $OUTPUT_DIR/serial.log -fi - -VM_IP=`getIp` - -downloadJournalLog $OUTPUT_DIR/journal.log -downloadCrashdumps $OUTPUT_DIR/ diff --git a/scripts/loop-update/loop-update.sh b/scripts/loop-update/loop-update.sh deleted file mode 100755 index ff18c6bfb..000000000 --- a/scripts/loop-update/loop-update.sh +++ /dev/null @@ -1,226 +0,0 @@ -#!/bin/bash -set -euo pipefail - -. $(dirname $0)/common.sh - -if [ "$OUTPUT" != "" ]; then - mkdir -p $OUTPUT -fi - -# When ROLLBACK is set to true, the script will trigger a rollback scenario -# during the first update iteration. The rollback scenario will ensure that post -# rebooting into the updated OS, the OS will reboot again, thus letting the -# firmware boot back into the original OS, as the update was never completed -# successfully, since Trident did not run post reboot to certify the update. -ROLLBACK=${ROLLBACK:-false} - -killUpdateServer $UPDATE_PORT_A -killUpdateServer $UPDATE_PORT_B - -ls -l $ARTIFACTS/update-a -ls -l $ARTIFACTS/update-b - -COSI_FILES="$(find $ARTIFACTS/update-a -type f -name '*.cosi')" -COSI_FILES_COUNT=$(echo "$COSI_FILES" | wc -l) - -if [ $COSI_FILES_COUNT -lt 1 ]; then - echo "COSI file not found!" - exit 1 -elif [ $COSI_FILES_COUNT -gt 1 ]; then - echo "Multiple COSI files found:" - echo $COSI_FILES - exit 1 -else - echo "COSI file found: $COSI_FILES" -fi - -COSI_FILE=$(echo $COSI_FILES | head -1) -COSI_FILE=$(basename $COSI_FILE) - -if ! [ -f bin/netlisten ]; then - echo "netlisten not found!" - exit 1 -fi - -bin/netlisten -p $UPDATE_PORT_A -s $ARTIFACTS/update-a --force-color --full-logstream logstream-full-update-a.log & -bin/netlisten -p $UPDATE_PORT_B -s $ARTIFACTS/update-b --force-color --full-logstream logstream-full-update-a.log & - -EXPECTED_VOLUME=${EXPECTED_VOLUME:-volume-b} -UPDATE_CONFIG=/var/lib/trident/update-config.yaml -# When triggering A/B update to B, we want to use images in update-config.yaml; to A, we want to -# use update-config2.yaml. However, if this is the rollback scenario, EXPECTED_VOLUME is current -# volume, so the value of UPDATE_CONFIG needs to be flipped. -if [ "$EXPECTED_VOLUME" == "volume-a" ] && [ "$ROLLBACK" == "false" ]; then - UPDATE_CONFIG=/var/lib/trident/update-config2.yaml -elif [ "$EXPECTED_VOLUME" == "volume-b" ] && [ "$ROLLBACK" == "true" ]; then - UPDATE_CONFIG=/var/lib/trident/update-config2.yaml -fi - -RETRY_COUNT=${RETRY_COUNT:-20} - -VM_IP=`getIp` - -# Update the update-config.yaml file with the COSI file and host address address -# of the http server -sshCommand "sudo sed -i 's!verity.cosi!files/$COSI_FILE!' /var/lib/trident/update-config.yaml" -sshCommand "sudo sed -i 's/192.168.122.1/localhost/' /var/lib/trident/update-config.yaml" - -sshCommand "sudo cp /var/lib/trident/update-config.yaml /var/lib/trident/update-config2.yaml && sudo sed -i 's/8000/8001/' /var/lib/trident/update-config2.yaml" - -for i in $(seq 1 $RETRY_COUNT); do - - if [ "$TEST_PLATFORM" == "qemu" ]; then - # For every 10th update, reboot the VM to ensure that we can handle reboots - if [ $((i % 10)) -eq 0 ]; then - echo "" - echo "***************************" - echo "** Rebooting VM **" - echo "***************************" - echo "" - - truncateLog - sudo virsh shutdown $VM_NAME - until [ `sudo virsh list | grep -c $VM_NAME` -eq 0 ]; do sleep 1; done - sudo virsh start $VM_NAME - waitForLogin $i - fi - fi - - echo "" - echo "***************************" - echo "** Starting update $i **" - echo "***************************" - echo "" - - truncateLog - LOGGING="-v WARN" - if [ $VERBOSE == True ]; then - LOGGING="-v DEBUG" - fi - - sshProxyPort $UPDATE_PORT_A - sshProxyPort $UPDATE_PORT_B - - if sshCommand "ls /var/crash/*"; then - echo "Crash files found on host" - adoError "Crash files found on host during iteration $i" - exit 1 - fi - - # If this is a rollback scenario, inject the script to trigger rollback into UPDATE_CONFIG - if [ "$ROLLBACK" == "true" ] && [ $i -eq 1 ]; then - TRIGGER_ROLLBACK_SCRIPT=.pipelines/templates/stages/testing_common/scripts/trigger-rollback.sh - SCRIPT_HOST_COPY=/var/lib/trident/trigger-rollback.sh - sshCommand "sudo tee $SCRIPT_HOST_COPY > /dev/null" < $TRIGGER_ROLLBACK_SCRIPT - sshCommand "sudo chmod +x $SCRIPT_HOST_COPY" - - # The VM host does not have yq installed, so create a local copy of the update config - # and inject the trigger-rollback script into it - COPY_CONFIG="./config.yaml" - sshCommand "sudo cat $UPDATE_CONFIG" > $COPY_CONFIG - yq eval ".scripts.postProvision += [{ - \"name\": \"mount-var\", - \"runOn\": [\"ab-update\"], - \"content\": \"mkdir -p \$TARGET_ROOT/tmp/var && mount --bind /var \$TARGET_ROOT/tmp/var\" - }]" -i $COPY_CONFIG - yq eval " - .scripts.postConfigure += [{ - \"name\": \"trigger-rollback\", - \"runOn\": [\"ab-update\"], - \"path\": \"$SCRIPT_HOST_COPY\" - }] - " -i $COPY_CONFIG - - # Set writableEtcOverlayHooks flag under internalParams to true, so that the script - # can create a new systemd service - yq eval ".internalParams.writableEtcOverlayHooks = true" -i $COPY_CONFIG - sshCommand "sudo tee $UPDATE_CONFIG > /dev/null" < $COPY_CONFIG - - # Print out the contents of the update config to validate that the script was injected - echo "Updated Host Configuration:" - sshCommand "sudo cat $UPDATE_CONFIG" - fi - - sshCommand "sudo cat $UPDATE_CONFIG" - - # Masking errors as we want to report the specific failure if it happens - set +e - - sshCommand "sudo trident update $LOGGING $UPDATE_CONFIG --allowed-operations stage" - STAGE_RESULT=$? - - if [ "$OUTPUT" != "" ]; then - PAD_ITERATION=$(printf "%03d" $i) - scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSH_USER@$VM_IP:/var/log/trident-full.log $OUTPUT/$PAD_ITERATION-staged-trident-full.log - fi - - if [ $STAGE_RESULT -ne 0 ]; then - echo "Failed to stage update" - adoError "Failed to stage update for iteration $i" - exit 1 - fi - - set -e - - # Masking errors as the VM will be rebooting - set +e - - sshCommand "sudo trident update $LOGGING $UPDATE_CONFIG --allowed-operations finalize" - - LOGGING="" - if [ $VERBOSE == True ]; then - echo "VM serial log:" - LOGGING="-v" - fi - - if [ "$TEST_PLATFORM" == "qemu" ]; then - waitForLogin $i - elif [ "$TEST_PLATFORM" == "azure" ]; then - sleep 15 - SUCCESS=false - for j in $(seq 1 10); do - if sshCommand hostname; then - SUCCESS=true - break - fi - sleep 5 - done - if [ "$SUCCESS" == false ]; then - echo "VM did not come back up after update" - adoError "VM did not come back up after update for iteration $i" - exit 1 - fi - fi - set -e - - # Check that Trident updated correctly - NEW_IP=`getIp` - if [ "$NEW_IP" != "$VM_IP" ]; then - echo "VM IP changed from $VM_IP to $NEW_IP" - exit 1 - fi - checkActiveVolume $EXPECTED_VOLUME $i - - # If this is a rollback scenario and we're on 1st iteration, validate that firmware - # performed a rollback and that Trident detected it successfully - if [ "$ROLLBACK" == "true" ] && [ $i -eq 1 ]; then - validateRollback - fi - - if [ $VERBOSE == True ]; then - sshCommand "sudo trident get" - fi - - if [ "$EXPECTED_VOLUME" == "volume-a" ]; then - EXPECTED_VOLUME="volume-b" - # If this is a rollback scenario and we're on 1st iteration, do not update the config path - if [ "$ROLLBACK" != "true" ] || [ $i -ne 1 ]; then - UPDATE_CONFIG="/var/lib/trident/update-config.yaml" - fi - else - EXPECTED_VOLUME="volume-a" - if [ "$ROLLBACK" != "true" ] || [ $i -ne 1 ]; then - UPDATE_CONFIG="/var/lib/trident/update-config2.yaml" - fi - fi -done \ No newline at end of file diff --git a/scripts/loop-update/publish-sig-image-prepare.sh b/scripts/loop-update/publish-sig-image-prepare.sh deleted file mode 100755 index 9fd5d50f4..000000000 --- a/scripts/loop-update/publish-sig-image-prepare.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -SCRIPTS_DIR="$( dirname "$0" )" -. "$SCRIPTS_DIR/common.sh" - -az account set --subscription "$SUBSCRIPTION" - -if [ -z "${STORAGE_CONTAINER_NAME:-}" ]; then - echo "STORAGE_CONTAINER_NAME is not set. Exiting..." - exit 1 -fi - -if [ -z "${IMAGE_DEFINITION:-}" ]; then - echo "IMAGE_DEFINITION is not set. Exiting..." - exit 1 -fi -# Ensure access when running in Azure DevOps, since this has not been -# working reliably in the past -if [ ! -z "${BUILD_BUILDNUMBER:-}" ]; then - ensureAzureAccess "$RESOURCE_GROUP" -fi - -if [ "`azCommand group exists -n "$RESOURCE_GROUP"`" == "false" ]; then - azCommand group create -n "$RESOURCE_GROUP" -l "$PUBLISH_LOCATION" -fi -if [ "`azCommand group exists -n "$GALLERY_RESOURCE_GROUP"`" == "false" ]; then - azCommand group create -n "$GALLERY_RESOURCE_GROUP" -l "$PUBLISH_LOCATION" -fi - -# Ensure STORAGE_ACCOUNT exists and the managed identity has access -STORAGE_ACCOUNT_RESOURCE_ID="/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" -if ! azCommand storage account show --ids "$STORAGE_ACCOUNT_RESOURCE_ID"; then - echo "Could not find storage account '$STORAGE_ACCOUNT' in the expected location. Creating the storage account." - - if [ "`azCommand storage account check-name --name "$STORAGE_ACCOUNT" --query nameAvailable`" == "false" ]; then - echo "Storage account name $STORAGE_ACCOUNT is not available" - exit 1 - fi - azCommand storage account create -g "$RESOURCE_GROUP" -n "$STORAGE_ACCOUNT" -l "$PUBLISH_LOCATION" --allow-shared-key-access false -fi - -# Ensure "build_target" storage container exists -CONTAINER_EXISTS="$(azCommand storage container exists --account-name "$STORAGE_ACCOUNT" --name "$STORAGE_CONTAINER_NAME" --auth-mode login | jq .exists)" -if [[ "$CONTAINER_EXISTS" != "true" ]]; then - echo "Could not find container '$STORAGE_CONTAINER_NAME'. Creating container '$STORAGE_CONTAINER_NAME' in storage account '$STORAGE_ACCOUNT'..." - azCommand storage container create --account-name "$STORAGE_ACCOUNT" --name "$STORAGE_CONTAINER_NAME" --auth-mode login -fi - -# Ensure STEAMBOAT_GALLERY_NAME exists -if ! azCommand sig show -r "$GALLERY_NAME" -g "$GALLERY_RESOURCE_GROUP"; then - echo "Could not find image gallery '$GALLERY_NAME' in resource group '$GALLERY_RESOURCE_GROUP'. Creating the gallery." - azCommand sig create -g "$GALLERY_RESOURCE_GROUP" -r "$GALLERY_NAME" -l "$PUBLISH_LOCATION" -fi - -# Ensure the "build_target" image-definition exists -# Note: We publish only the VHD from the secure-prod the SIG -IMAGE_DEFINITION_EXISTS="$(azCommand sig image-definition list -r "$GALLERY_NAME" -g "$GALLERY_RESOURCE_GROUP" | grep "name" | grep -c "$IMAGE_DEFINITION" || :;)" # the "|| :;" prevents grep from halting the script when it finds no matches and exits with exit code 1 -if [[ "$IMAGE_DEFINITION_EXISTS" -eq 0 ]]; then - echo "Could not find image-definition '$IMAGE_DEFINITION'. Creating definition '$IMAGE_DEFINITION' in gallery '$GALLERY_NAME'..." - azCommand sig image-definition create -i "$IMAGE_DEFINITION" --publisher "$PUBLISHER" --offer "$OFFER" --sku "$IMAGE_DEFINITION" -r "$GALLERY_NAME" -g "$GALLERY_RESOURCE_GROUP" --os-type Linux -fi - -if ! which azcopy; then - # Install az-copy dependency - PIPELINE_AGENT_OS="$(cat "/etc/os-release" | grep "^ID=" | cut -d = -f 2)" - PIPELINE_AGENT_OS_VERSION="$(cat "/etc/os-release" | grep "^VERSION_ID=" | cut -d = -f 2 | tr -d '"')" - AZCOPY_DOWNLOAD_URL="https://packages.microsoft.com/config/$PIPELINE_AGENT_OS/$PIPELINE_AGENT_OS_VERSION/packages-microsoft-prod.deb" - curl -sSL -O "$AZCOPY_DOWNLOAD_URL" - CURL_STATUS=$? - if [ $CURL_STATUS -ne 0 ]; then - echo "Failed to download the debian package repo while attempting to install azcopy. The URL '$AZCOPY_DOWNLOAD_URL' returned the curl exit status: $CURL_STATUS" - echo "Suggestion: Are you using a new, non-ubuntu, pipeline agent? If yes, add azcopy installation logic for the new build agent." - exit 1 - fi - sudo dpkg -i packages-microsoft-prod.deb - rm packages-microsoft-prod.deb - sudo apt-get update -y - sudo apt-get install azcopy -y - azcopy --version - AZCOPY_STATUS=$? - if [ $AZCOPY_STATUS -ne 0 ]; then - echo "Failed to install azcopy." - exit 1 - fi -fi diff --git a/scripts/loop-update/publish-sig-image.sh b/scripts/loop-update/publish-sig-image.sh deleted file mode 100755 index 585c4163f..000000000 --- a/scripts/loop-update/publish-sig-image.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -SCRIPTS_DIR="$( dirname "$0" )" -. "$SCRIPTS_DIR/common.sh" - -az account set --subscription "$SUBSCRIPTION" - -CURRENT_DATE="$(date +'%y%m%d')" -CURRENT_TIME="$(date +'%H%M%S')" - -STORAGE_ACCOUNT_URL="https://$STORAGE_ACCOUNT.blob.core.windows.net" -STORAGE_ACCOUNT_RESOURCE_ID="/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" - -export STORAGE_CONTAINER_NAME="${STORAGE_CONTAINER_NAME:-$ALIAS-test}" -"$SCRIPTS_DIR/publish-sig-image-prepare.sh" -export IMAGE_PATH="${IMAGE_PATH:-$ARTIFACTS/trident-vm-grub-verity-azure-testimage.vhd}" - -IMAGE_VERSION="`getImageVersion increment`" -echo using image version $IMAGE_VERSION - -if azCommand sig image-version show \ - --resource-group "$GALLERY_RESOURCE_GROUP" \ - --gallery-name "$GALLERY_NAME" \ - --gallery-image-definition "$IMAGE_DEFINITION" \ - --gallery-image-version "$IMAGE_VERSION"; then - echo "Image version $IMAGE_VERSION already exists. Exiting..." - exit 0 -fi - -STORAGE_BLOB_NAME="${CURRENT_DATE##+(0)}.${CURRENT_TIME##+(0)}-$IMAGE_VERSION.vhd" -STORAGE_BLOB_ENDPOINT="$STORAGE_ACCOUNT_URL/$STORAGE_CONTAINER_NAME/$STORAGE_BLOB_NAME" - -# Get the path to the VHD file -resizeImage "$IMAGE_PATH" - -# Upload the image artifact to Steamboat Storage Account -azcopy copy "$IMAGE_PATH" "$STORAGE_BLOB_ENDPOINT" - -# Create Image Version from storage account blob -azCommand sig image-version create \ - --resource-group "$GALLERY_RESOURCE_GROUP" \ - --gallery-name "$GALLERY_NAME" \ - --gallery-image-definition "$IMAGE_DEFINITION" \ - --gallery-image-version "$IMAGE_VERSION" \ - --target-regions "$PUBLISH_LOCATION" \ - --location "$PUBLISH_LOCATION" \ - --os-vhd-storage-account "$STORAGE_ACCOUNT_RESOURCE_ID" \ - --os-vhd-uri "$STORAGE_BLOB_ENDPOINT" diff --git a/scripts/loop-update/servicing-tests.sh b/scripts/loop-update/servicing-tests.sh new file mode 100755 index 000000000..357645c8e --- /dev/null +++ b/scripts/loop-update/servicing-tests.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -euxo pipefail + +ARTIFACTS=${ARTIFACTS:-artifacts} +VM_NAME=${VM_NAME:-trident-vm-verity-test} +TEST_PLATFORM=${TEST_PLATFORM:-qemu} +VM_SERIAL_LOG=${VM_SERIAL_LOG:-/tmp/$VM_NAME.log} +VERBOSE=${VERBOSE:-False} +WATCH=${WATCH:-False} +OUTPUT=${OUTPUT:-} + +ALIAS=${ALIAS:-`whoami`} + +SUBSCRIPTION=${SUBSCRIPTION:-b8a0db63-c5fa-4198-8e2a-f9d6ff52465e} # CoreOS_AzureLinux_BMP_dev +IMAGE_DEFINITION=${IMAGE_DEFINITION:-trident-vm-grub-verity-azure-testimage} +RESOURCE_GROUP=${RESOURCE_GROUP:-azlinux_bmp_dev} +PUBLISH_LOCATION=${PUBLISH_LOCATION:-eastus2} +GALLERY_RESOURCE_GROUP=${GALLERY_RESOURCE_GROUP:-$ALIAS-trident-rg} +STORAGE_ACCOUNT=${STORAGE_ACCOUNT:-azlinuxbmpdev} +GALLERY_NAME=${GALLERY_NAME:-${ALIAS}_trident_gallery} +PUBLISHER=${PUBLISHER:-$ALIAS} +OFFER=${OFFER:-trident-vm-grub-verity-azure-offer} +export AZCOPY_AUTO_LOGIN_TYPE=${AZCOPY_AUTO_LOGIN_TYPE:-AZCLI} +TEST_RESOURCE_GROUP=${TEST_RESOURCE_GROUP:-$GALLERY_RESOURCE_GROUP-test} +TEST_VM_SIZE=${TEST_VM_SIZE:-Standard_D2ds_v5} +SSH_PRIVATE_KEY_PATH=${SSH_PRIVATE_KEY_PATH:-~/.ssh/id_rsa} +SSH_PUBLIC_KEY_PATH=${SSH_PUBLIC_KEY_PATH:-$SSH_PRIVATE_KEY_PATH.pub} +RETRY_COUNT=${RETRY_COUNT:-20} + +SSH_USER=testuser +UPDATE_PORT_A=8000 +UPDATE_PORT_B=8001 + + +SUDO="sudo" +if [ "$TEST_PLATFORM" == "azure" ]; then + az login --identity + SUDO="" +fi + +FLAGS="" +if [ "$VERBOSE" == "true" ]; then + FLAGS="$FLAGS --verbose" +fi +if [ "$WATCH" == "true" ]; then + FLAGS="$FLAGS -w" +fi + +$SUDO ./bin/storm-trident run servicing $FLAGS \ + --artifacts-dir $ARTIFACTS \ + --output-path "$OUTPUT" \ + --storage-account-resource-group $RESOURCE_GROUP \ + --name $VM_NAME \ + --serial-log $VM_SERIAL_LOG \ + --platform $TEST_PLATFORM \ + --retry-count $RETRY_COUNT \ + --rollback-retry-count $RETRY_COUNT \ + --who-am-i $ALIAS \ + --subscription $SUBSCRIPTION \ + --image-definition $IMAGE_DEFINITION \ + --region $PUBLISH_LOCATION \ + --gallery-resource-group $GALLERY_RESOURCE_GROUP \ + --storage-account $STORAGE_ACCOUNT \ + --gallery-name $GALLERY_NAME \ + --offer $OFFER \ + --test-resource-group $TEST_RESOURCE_GROUP \ + --size $TEST_VM_SIZE \ + --ssh-private-key-path $SSH_PRIVATE_KEY_PATH \ + --ssh-public-key-path $SSH_PUBLIC_KEY_PATH \ + --user $SSH_USER \ + --update-port-a $UPDATE_PORT_A \ + --update-port-b $UPDATE_PORT_B \ + --force-cleanup diff --git a/tools/cmd/storm-trident/main.go b/tools/cmd/storm-trident/main.go index 77a032a9c..5c8d4774a 100644 --- a/tools/cmd/storm-trident/main.go +++ b/tools/cmd/storm-trident/main.go @@ -3,6 +3,7 @@ package main import ( "storm" "tridenttools/storm/helpers" + "tridenttools/storm/servicing" ) func main() { @@ -14,6 +15,9 @@ func main() { // storm.AddScenario(&scenario) // } + // Add Trident servicing scenario + storm.AddScenario(&servicing.TridentServicingScenario{}) + // Register Trident helpers for _, helper := range helpers.TRIDENT_HELPERS { storm.AddHelper(helper) diff --git a/tools/storm/servicing/tests/azure.go b/tools/storm/servicing/tests/azure.go new file mode 100644 index 000000000..d7b822c83 --- /dev/null +++ b/tools/storm/servicing/tests/azure.go @@ -0,0 +1,13 @@ +package tests + +import ( + "fmt" + "tridenttools/storm/servicing/utils/config" +) + +func PublishSigImage(cfg config.ServicingConfig) error { + if err := cfg.AzureConfig.PublishSigImage(cfg.TestConfig.ArtifactsDir, cfg.TestConfig.BuildId); err != nil { + return fmt.Errorf("failed to publish Azure Shared Image Gallery image: %w", err) + } + return nil +} diff --git a/tools/storm/servicing/tests/logs.go b/tools/storm/servicing/tests/logs.go new file mode 100644 index 000000000..1285559db --- /dev/null +++ b/tools/storm/servicing/tests/logs.go @@ -0,0 +1,34 @@ +package tests + +import ( + "tridenttools/storm/servicing/utils/config" + "tridenttools/storm/servicing/utils/ssh" + "tridenttools/storm/utils" + + "github.com/sirupsen/logrus" +) + +func FetchLogs(cfg config.ServicingConfig) error { + vmIP, err := utils.GetVmIP(cfg) + if err != nil { + return err + } + // Best effort: download journal log + logrus.Tracef("Make journal log available for download") + _, err = ssh.SshCommand(cfg.VMConfig, vmIP, "sudo journalctl --no-pager > /tmp/journal.log && sudo chmod 644 /tmp/journal.log") + if err == nil { + // Download file via scp if creating journal.log succeeded + logrus.Tracef("Downloading journal log from VM '%s' to local machine", cfg.VMConfig.Name) + ssh.ScpDownloadFile(cfg.VMConfig, vmIP, "/tmp/journal.log", cfg.TestConfig.OutputPath+"/journal.log") + } + // Download crashdumps (simplified) + logrus.Tracef("Check for crash dumps on VM") + crashDumpOutput, err := ssh.SshCommand(cfg.VMConfig, vmIP, "ls /var/crash/*") + if err == nil { + logrus.Debugf("Crash files found on host: %s", crashDumpOutput) + logrus.Error("Crash files found on host") + ssh.SshCommand(cfg.VMConfig, vmIP, "sudo mv /var/crash/* /tmp/crash && sudo chmod -R 644 /tmp/crash && sudo chmod +x /tmp/crash") + ssh.ScpDownloadFile(cfg.VMConfig, vmIP, "/tmp/crash/*", cfg.TestConfig.OutputPath+"/") + } + return nil +} diff --git a/tools/storm/servicing/tests/update.go b/tools/storm/servicing/tests/update.go new file mode 100644 index 000000000..666d85e46 --- /dev/null +++ b/tools/storm/servicing/tests/update.go @@ -0,0 +1,455 @@ +package tests + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + "tridenttools/storm/servicing/utils/config" + "tridenttools/storm/servicing/utils/file" + "tridenttools/storm/servicing/utils/ssh" + "tridenttools/storm/utils" + + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +func UpdateLoop(cfg config.ServicingConfig) error { + return innerUpdateLoop(cfg, false) +} + +func Rollback(cfg config.ServicingConfig) error { + return innerUpdateLoop(cfg, true) +} + +func innerUpdateLoop(cfg config.ServicingConfig, rollback bool) error { + // Create context to ensure goroutines exit cleanly + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logrus.Tracef("Stop existing update servers if any") + // Kill any running update servers + killUpdateServer(cfg.TestConfig.UpdatePortA) + killUpdateServer(cfg.TestConfig.UpdatePortB) + + lsaCmd := exec.Command("ls", "-l", cfg.TestConfig.ArtifactsDir+"/update-a") + lsaOut, err := lsaCmd.Output() + if err != nil { + return fmt.Errorf("failed to list update-a directory: %w", err) + } + logrus.Tracef("Contents of update-a directory:\n%s", lsaOut) + + lsbCmd := exec.Command("ls", "-l", cfg.TestConfig.ArtifactsDir+"/update-b") + lsbOut, err := lsbCmd.Output() + if err != nil { + return fmt.Errorf("failed to list update-b directory: %w", err) + } + logrus.Tracef("Contents of update-b directory:\n%s", lsbOut) + + // Check for COSI files + cosiFile, err := file.FindFile(cfg.TestConfig.ArtifactsDir+"/update-a", ".*\\.cosi$") + if err != nil { + return fmt.Errorf("failed to find COSI file: %w", err) + } + logrus.Tracef("Found COSI file: %s", cosiFile) + cosiFileBase := cosiFile[strings.LastIndex(cosiFile, "/")+1:] + + logrus.Tracef("Start update servers (netlisten)") + // Start update servers (netlisten) + aStartedChannel := make(chan bool) + go startNetListenAndWait(ctx, cfg.TestConfig.UpdatePortA, "a", cfg.TestConfig.ArtifactsDir, aStartedChannel) + bStartedChannel := make(chan bool) + go startNetListenAndWait(ctx, cfg.TestConfig.UpdatePortB, "b", cfg.TestConfig.ArtifactsDir, bStartedChannel) + // Wait for both udpate servers to start + <-aStartedChannel + <-bStartedChannel + expectedVolume := "volume-b" + logrus.Tracef("Current expected volume: %s", expectedVolume) + + updateConfig := "/var/lib/trident/update-config.yaml" + if expectedVolume == "volume-a" && !rollback { + updateConfig = "/var/lib/trident/update-config2.yaml" + } else if expectedVolume == "volume-b" && rollback { + updateConfig = "/var/lib/trident/update-config2.yaml" + } + logrus.Tracef("Using update config file: %s", updateConfig) + + vmIP, err := utils.GetVmIP(cfg) + if err != nil { + return fmt.Errorf("failed to get VM IP: %w", err) + } + + // Run several commands to update/specialize update config files on VM + logrus.Tracef("Updating config files") + configChanges := + // use COSI file found in update-a and update-b directories + fmt.Sprintf("sudo sed -i 's!verity.cosi!files/%s!' /var/lib/trident/update-config.yaml && ", cosiFileBase) + + // use localhost as update server address + "sudo sed -i 's/192.168.122.1/localhost/' /var/lib/trident/update-config.yaml &&" + + // create second config file for b update + "sudo cp /var/lib/trident/update-config.yaml /var/lib/trident/update-config2.yaml && " + + // use update port b for second config + fmt.Sprintf("sudo sed -i 's/8000/%d/' /var/lib/trident/update-config2.yaml && ", cfg.TestConfig.UpdatePortB) + + // use udpate port a for first config + fmt.Sprintf("sudo sed -i 's/8000/%d/' /var/lib/trident/update-config.yaml", cfg.TestConfig.UpdatePortA) + configChangesOutput, err := ssh.SshCommand(cfg.VMConfig, vmIP, configChanges) + if err != nil { + logrus.Tracef("Failed to update config files:\n%s", configChangesOutput) + return fmt.Errorf("failed to create config for b updates") + } + + if cfg.TestConfig.Verbose { + configaOut, err := ssh.SshCommand(cfg.VMConfig, vmIP, "sudo cat /var/lib/trident/update-config.yaml") + if err != nil { + return fmt.Errorf("failed to get config a contents") + } + logrus.Tracef("Trident config-a contents:\n%s", configaOut) + configbOut, err := ssh.SshCommand(cfg.VMConfig, vmIP, "sudo cat /var/lib/trident/update-config2.yaml") + if err != nil { + return fmt.Errorf("failed to get config b contents") + } + logrus.Tracef("Trident config-b contents:\n%s", configbOut) + } + + // Main update loop (simplified) + loopCount := cfg.TestConfig.RetryCount + if rollback { + loopCount = cfg.TestConfig.RollbackRetryCount + } + for i := 1; i <= loopCount; i++ { + logrus.Infof("Update attempt #%d for VM '%s' (%s)", i, cfg.VMConfig.Name, cfg.VMConfig.Platform) + + if cfg.VMConfig.Platform == config.PlatformQEMU && i%10 == 0 { + // For every 10th update, reboot the VM (QEMU only) + if err := cfg.QemuConfig.RebootQemuVm(cfg.VMConfig.Name, i, cfg.TestConfig.OutputPath, cfg.TestConfig.Verbose); err != nil { + return fmt.Errorf("failed to reboot QEMU VM before update attempt #%d: %w", i, err) + } + if err := cfg.QemuConfig.TruncateLog(cfg.VMConfig.Name); err != nil { + return fmt.Errorf("failed to truncate log file before update attempt #%d: %w", i, err) + } + } + + logrus.Tracef("Setting up SSH proxy ports for update servers") + aStartedChannel := make(chan bool) + go ssh.StartSshProxyPortAndWait(ctx, cfg.TestConfig.UpdatePortA, vmIP, cfg.VMConfig.User, cfg.VMConfig.SshPrivateKeyPath, aStartedChannel) + bStartedChannel := make(chan bool) + go ssh.StartSshProxyPortAndWait(ctx, cfg.TestConfig.UpdatePortB, vmIP, cfg.VMConfig.User, cfg.VMConfig.SshPrivateKeyPath, bStartedChannel) + // Wait for both SSH proxy ports to be ready + <-aStartedChannel + <-bStartedChannel + + logrus.Tracef("Checking for crash dumps on host") + crashDumpOutput, err := ssh.SshCommand(cfg.VMConfig, vmIP, "ls /var/crash/*") + if err == nil { + logrus.Debugf("Crash files found on host during iteration %d: %s", i, crashDumpOutput) + logrus.Error("Crash files found on host") + return fmt.Errorf("crash files found on host during iteration %d", i) + } + + if rollback && i == 1 { + if err := prepareRollback(cfg, vmIP, updateConfig, expectedVolume, i); err != nil { + return fmt.Errorf("failed to prepare rollback for iteration %d: %w", i, err) + } + } + + if cfg.TestConfig.Verbose { + configContents, err := ssh.SshCommand(cfg.VMConfig, vmIP, fmt.Sprintf("sudo cat %s", updateConfig)) + if err != nil { + return fmt.Errorf("failed to read update config file after modification: %w", err) + } + logrus.Infof("Update Config Contents:\n%s", configContents) + } + + tridentLoggingArg := "-v WARN" + if cfg.TestConfig.Verbose { + tridentLoggingArg = "-v DEBUG" + } + + logrus.Tracef("Running Trident update staging command on VM") + combinedStagingOutput, stageErr := ssh.SshCommandCombinedOutput(cfg.VMConfig, vmIP, fmt.Sprintf("sudo trident update %s %s --allowed-operations stage", tridentLoggingArg, updateConfig)) + if cfg.TestConfig.Verbose { + logrus.Tracef("Staging output for iteration %d:\n%s", i, combinedStagingOutput) + } + + if cfg.TestConfig.OutputPath != "" { + logrus.Tracef("Download staging trident logs for iteration %d", i) + localPath := filepath.Join(cfg.TestConfig.OutputPath, fmt.Sprintf("%s-staged-trident-full.log", fmt.Sprintf("%03d", i))) + err = ssh.ScpDownloadFile(cfg.VMConfig, vmIP, "/var/log/trident-full.log", localPath) + if err != nil { + return fmt.Errorf("failed to download staged trident log: %w", err) + } + } + + if stageErr != nil { + logrus.Errorf("Failed to stage update for iteration %d: %v", i, stageErr) + return fmt.Errorf("failed to stage update for iteration %d: %w", i, stageErr) + } + + logrus.Tracef("Running Trident update finalize command on VM") + combinedFinalizeOutput, finalizeErr := ssh.SshCommandCombinedOutput(cfg.VMConfig, vmIP, fmt.Sprintf("sudo trident update %s %s --allowed-operations finalize", tridentLoggingArg, updateConfig)) + if cfg.TestConfig.Verbose { + logrus.Tracef("Finalize output for iteration %d:\n%s\n%v", i, combinedFinalizeOutput, finalizeErr) + } + + logrus.Tracef("Wait for VM to come back up after finalize reboot") + if cfg.VMConfig.Platform == config.PlatformQEMU { + err := cfg.QemuConfig.WaitForLogin(cfg.VMConfig.Name, cfg.TestConfig.OutputPath, cfg.TestConfig.Verbose, i) + if err != nil { + return fmt.Errorf("VM did not come back up after update for iteration %d: %w", i, err) + } + } else if cfg.VMConfig.Platform == config.PlatformAzure { + time.Sleep(15 * time.Second) + + success := false + for j := 0; j < 10; j++ { + if _, err = ssh.SshCommand(cfg.VMConfig, vmIP, "hostname"); err == nil { + success = true + break + } + time.Sleep(5 * time.Second) // Wait for the VM to stabilize + } + + if !success { + logrus.Info("VM did not come back up after update") + logrus.Errorf("VM did not come back up after update for iteration %d", i) + return fmt.Errorf("VM did not come back up after update for iteration %d", i) + } + } + + logrus.Tracef("Check if VM IP has changed after update") + newVmIP, err := utils.GetVmIP(cfg) + if err != nil { + return fmt.Errorf("failed to get new VM IP after update: %w", err) + } + if newVmIP != vmIP { + logrus.Infof("VM IP changed from %s to %s", vmIP, newVmIP) + return fmt.Errorf("VM IP changed during update from %s to %s", vmIP, newVmIP) + } + logrus.Infof("VM IP remains the same after update: %s", vmIP) + + logrus.Tracef("Validate active volume after update") + checkActiveVolumeErr := checkActiveVolume(cfg.VMConfig, vmIP, expectedVolume) + logrus.Tracef("Get journal logs after post-update reboot %d", i) + if _, postUpdateJournalLogErr := ssh.SshCommand(cfg.VMConfig, vmIP, "sudo journalctl --no-pager > /tmp/post-reboot-update-journal.log && sudo chmod 644 /tmp/post-reboot-update-journal.log"); postUpdateJournalLogErr == nil { + // Download file via scp if creating post-reboot-update-journal.log succeeded + padIteration := fmt.Sprintf("%03d", i) + logrus.Tracef("Downloading post-reboot-update-journal.log from VM '%s' to local machine", cfg.VMConfig.Name) + ssh.ScpDownloadFile(cfg.VMConfig, vmIP, "/tmp/post-reboot-update-journal.log", fmt.Sprintf("%s/%s-%s", cfg.TestConfig.OutputPath, padIteration, "post-reboot-update-journal.log")) + } + if checkActiveVolumeErr != nil { + return fmt.Errorf("failed to verify active volume after update: %w", checkActiveVolumeErr) + } + + if rollback && i == 1 { + logrus.Tracef("Validate rollback after first update") + validateRollback(cfg.VMConfig, vmIP) + } + + if cfg.TestConfig.Verbose { + hostStatusStr, err := ssh.SshCommand(cfg.VMConfig, vmIP, "sudo trident get") + if err != nil { + return fmt.Errorf("failed to get host status: %w", err) + } + logrus.Infof("Host Status after update:\n%s", hostStatusStr) + } + + if expectedVolume == "volume-a" { + expectedVolume = "volume-b" + if !rollback || i != 1 { + updateConfig = "/var/lib/trident/update-config.yaml" + } + } else { + expectedVolume = "volume-a" + if !rollback || i != 1 { + updateConfig = "/var/lib/trident/update-config2.yaml" + } + } + logrus.Tracef("Updated expected volume for next update to be: %s", expectedVolume) + logrus.Tracef("Updated config file for next update to be: %s", updateConfig) + } + return nil +} + +func prepareRollback(cfg config.ServicingConfig, vmIP string, updateConfig string, expectedVolume string, iteration int) error { + logrus.Tracef("Testing Rollback for iteration %d", iteration) + + triggerRollbackScript := ".pipelines/templates/stages/testing_common/scripts/trigger-rollback.sh" + scriptHostCopy := "/var/lib/trident/trigger-rollback.sh" + + logrus.Tracef("Copying rollback script to VM") + if err := ssh.ScpUploadFileWithSudo(cfg.VMConfig, vmIP, triggerRollbackScript, scriptHostCopy); err != nil { + return fmt.Errorf("failed to upload rollback script: %w", err) + } + logrus.Tracef("Make rollback script executable") + if _, err := ssh.SshCommand(cfg.VMConfig, vmIP, fmt.Sprintf("sudo chmod +x %s", scriptHostCopy)); err != nil { + return fmt.Errorf("failed to make rollback script executable: %w", err) + } + + localConfig := "./config.yaml" + logrus.Tracef("Downloading %s from VM to local machine: %s", updateConfig, updateConfig) + if err := ssh.ScpDownloadFile(cfg.VMConfig, vmIP, updateConfig, localConfig); err != nil { + return fmt.Errorf("failed to download update config file: %w", err) + } + + logrus.Tracef("Add postProvision step to local config file: %s", localConfig) + postProvisionCmd := exec.Command( + "yq", "eval", + ".scripts.postProvision += [{\"name\": \"mount-var\", \"runOn\": [\"ab-update\"], \"content\": \"mkdir -p $TARGET_ROOT/tmp/var && mount --bind /var $TARGET_ROOT/tmp/var\"}]", + "-i", localConfig) + if err := postProvisionCmd.Run(); err != nil { + return fmt.Errorf("failed to update postProvision scripts in config: %w", err) + } + + logrus.Tracef("Add postConfigure step to invoke rollback script to local config file: %s", localConfig) + postConfigureCmd := exec.Command( + "yq", "eval", + ".scripts.postConfigure += [{\"name\": \"trigger-rollback\", \"runOn\": [\"ab-update\"], \"path\": \""+scriptHostCopy+"\"}]", + "-i", localConfig) + if err := postConfigureCmd.Run(); err != nil { + return fmt.Errorf("failed to update postConfigure scripts in config: %w", err) + } + + // Set writableEtcOverlayHooks flag under internalParams to true, so that the script + // can create a new systemd service + logrus.Tracef("Set writableEtcOverlayHooks in local config file: %s", localConfig) + internalParamsCmd := exec.Command( + "yq", "eval", + ".internalParams.writableEtcOverlayHooks = true", + "-i", localConfig) + if err := internalParamsCmd.Run(); err != nil { + return fmt.Errorf("failed to set writableEtcOverlayHooks in config: %w", err) + } + + logrus.Tracef("Upload modified config file to VM: %s", updateConfig) + if err := ssh.ScpUploadFileWithSudo(cfg.VMConfig, vmIP, localConfig, updateConfig); err != nil { + return fmt.Errorf("failed to upload rollback script: %w", err) + } + return nil +} + +func killUpdateServer(port int) error { + logrus.Tracef("Kill process found using port %d", port) + cmd := exec.Command("lsof", "-ti", fmt.Sprintf("tcp:%d", port)) + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + // No process found is not an error for our use case + logrus.Tracef("No process found on port %d", port) + return nil + } + pids := strings.Fields(out.String()) + for _, pid := range pids { + logrus.Tracef("Kill process %v", pid) + killCmd := exec.Command("kill", "-9", pid) + _ = killCmd.Run() // Ignore errors for robustness + } + return nil +} + +func startNetListenAndWait(ctx context.Context, port int, partition string, artifactsDir string, startedChannel chan bool) error { + cmdPath := "bin/netlisten" + if _, err := os.Stat(cmdPath); os.IsNotExist(err) { + logrus.Error("bin/netlisten not found") + return fmt.Errorf("netlisten not found at %s: %w", cmdPath, err) + } + + cmdArgs := []string{ + "-p", fmt.Sprint(port), + "-s", fmt.Sprintf("%s/update-%s", artifactsDir, partition), + "--force-color", + "--full-logstream", fmt.Sprintf("logstream-full-update-%s.log", partition), + } + cmd := exec.CommandContext(ctx, cmdPath, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start netlisten for port %d: %w", port, err) + } + + // Signal that netlisten has started + startedChannel <- true + + // Wait for the command to finish + if err := cmd.Wait(); err != nil { + return fmt.Errorf("netlisten for port %d failed: %w", port, err) + } + logrus.Tracef("netlisten for port %d exited", port) + + return nil +} + +func validateRollback(cfg config.VMConfig, vmIP string) error { + // Get host status, but ensure this is done **after** trident.service runs + hostStatusStr, err := ssh.SshCommand(cfg, vmIP, "set -o pipefail; sudo systemd-run --pipe --property=After=trident.service trident get") + if err != nil { + return fmt.Errorf("failed to get host status: %w", err) + } + + // Parse the host status yaml + hostStatus := make(map[string]interface{}) + if err = yaml.Unmarshal([]byte(hostStatusStr), &hostStatus); err != nil { + return fmt.Errorf("failed to unmarshal YAML output: %w", err) + } + + // Validate that lastError.category is set to "servicing" + category, ok := hostStatus["lastError"].(map[interface{}]interface{})["category"].(string) + if ok && category != "servicing" { + logrus.Tracef("Host status: %s", hostStatusStr) + logrus.Errorf("Category of last error is not 'servicing', but '%s'", category) + return fmt.Errorf("category of last error is not 'servicing', but '%s'", category) + } + + // Validate that lastError.error contains the expected content + error, ok := hostStatus["lastError"].(map[interface{}]interface{})["error"].(string) + if ok && !strings.Contains(error, "!ab-update-reboot-check") { + logrus.Errorf("Type of last error is not '!ab-update-reboot-check', but '%s'", error) + return fmt.Errorf("type of last error is not '!ab-update-reboot-check', but '%s'", error) + } + + // Validate that lastError.message matches the expected format + message, ok := hostStatus["lastError"].(map[interface{}]interface{})["message"].(string) + if ok && !regexp.MustCompile(`^A/B update failed as host booted from .+ instead of the expected device .+$`).MatchString(message) { + logrus.Errorf("Message of last error does not match the expected format: '%s'", message) + return fmt.Errorf("message of last error does not match the expected format: '%s'", message) + } + + logrus.Info("Rollback validation succeeded") + return nil +} + +func checkActiveVolume(cfg config.VMConfig, vmIP string, expectedVolume string) error { + _, err := utils.Retry( + time.Second*600, + time.Second, + func(attempt int) (*bool, error) { + logrus.Tracef("Checking active volume (attempt %d)", attempt) + hostStatusStr, err := ssh.SshCommandWithRetries(cfg, vmIP, "sudo trident get", 5, 5) + if err != nil { + return nil, fmt.Errorf("failed to get host status: %w", err) + } + logrus.Tracef("Retrieved host status") + hostStatus := make(map[string]interface{}) + if err = yaml.Unmarshal([]byte(hostStatusStr), &hostStatus); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML output: %w", err) + } + logrus.Tracef("Parsed host status") + if hostStatus["servicingState"] != "provisioned" { + return nil, fmt.Errorf("trident state is not 'provisioned'") + } + logrus.Tracef("Host satus servicingState is 'provisioned'") + hsActiveVol := hostStatus["abActiveVolume"] + if hsActiveVol != expectedVolume { + return nil, fmt.Errorf("expected active volume '%s', got '%s'", expectedVolume, hsActiveVol) + } + logrus.Infof("Active volume '%s' matches expected volume '%s'", hsActiveVol, expectedVolume) + return nil, nil + }, + ) + return err +} diff --git a/tools/storm/servicing/tests/vm.go b/tools/storm/servicing/tests/vm.go new file mode 100644 index 000000000..9f9d05d60 --- /dev/null +++ b/tools/storm/servicing/tests/vm.go @@ -0,0 +1,68 @@ +package tests + +import ( + "fmt" + "tridenttools/storm/servicing/utils/config" + "tridenttools/storm/utils" + + "github.com/sirupsen/logrus" +) + +func CheckDeployment(cfg config.ServicingConfig) error { + logrus.Tracef("Get VM IP address(es)") + vmIPs, err := utils.GetAllVmIPAddresses(cfg) + if err != nil { + return fmt.Errorf("failed to get VM IP addresses: %w", err) + } + if len(vmIPs) == 0 { + return fmt.Errorf("no VM IP addresses found") + } + logrus.Infof("Found VM IP address(es): %v", vmIPs) + + // Help diagnose https://dev.azure.com/mariner-org/ECF/_workitems/edit/11273 and + // fail explicitly if multiple IPs are found + if cfg.VMConfig.Platform == config.PlatformQEMU { + if len(vmIPs) > 1 { + logrus.Errorf("Multiple IPs found: %v", vmIPs) + logrus.Error("Multiple IPs found, expected only one IP address") + return fmt.Errorf("multiple IPs found, expected only one IP address") + } + } + + logrus.Tracef("Check if VM is reachable and has expected active volume") + if err := checkActiveVolume(cfg.VMConfig, vmIPs[0], cfg.TestConfig.ExpectedVolume); err != nil { + return fmt.Errorf("failed to check active volume '%s': %w", cfg.TestConfig.ExpectedVolume, err) + } + + return nil +} + +func DeployVM(cfg config.ServicingConfig) error { + if cfg.VMConfig.Platform == config.PlatformQEMU { + logrus.Tracef("Deploying VM on QEMU platform with name '%s'", cfg.VMConfig.Name) + if err := cfg.QemuConfig.DeployQemuVM(cfg.VMConfig.Name, cfg.TestConfig.ArtifactsDir, cfg.TestConfig.OutputPath, cfg.TestConfig.Verbose); err != nil { + return fmt.Errorf("failed to deploy qemu vm: %w", err) + } + } else if cfg.VMConfig.Platform == config.PlatformAzure { + logrus.Tracef("Deploying VM on Azure platform with name '%s'", cfg.VMConfig.Name) + if err := cfg.AzureConfig.DeployAzureVM(cfg.VMConfig.Name, cfg.VMConfig.User, cfg.TestConfig.BuildId); err != nil { + return fmt.Errorf("failed to deploy azure vm: %w", err) + } + } + return nil +} + +func CleanupVM(cfg config.ServicingConfig) error { + if cfg.VMConfig.Platform == config.PlatformAzure { + if err := cfg.AzureConfig.CleanupAzureVM(); err != nil { + return fmt.Errorf("failed to cleanup Azure VM: %w", err) + } + } else if cfg.VMConfig.Platform == config.PlatformQEMU { + if err := cfg.QemuConfig.CleanupQemuVM(cfg.VMConfig.Name); err != nil { + return fmt.Errorf("failed to cleanup QEMU VM: %w", err) + } + } + killUpdateServer(cfg.TestConfig.UpdatePortA) + killUpdateServer(cfg.TestConfig.UpdatePortB) + return nil +} diff --git a/tools/storm/servicing/trident.go b/tools/storm/servicing/trident.go new file mode 100644 index 000000000..51fb1c8db --- /dev/null +++ b/tools/storm/servicing/trident.go @@ -0,0 +1,138 @@ +package servicing + +import ( + "fmt" + "os" + "path/filepath" + "tridenttools/storm/servicing/tests" + "tridenttools/storm/servicing/utils/azure" + "tridenttools/storm/servicing/utils/config" + "tridenttools/storm/servicing/utils/qemu" + + "storm" + + "github.com/sirupsen/logrus" +) + +type TridentServicingScenario struct { + args TridentServicingScenarioArgs +} + +type TridentServicingScenarioArgs struct { + config.TestConfig `embed:""` + config.VMConfig `embed:""` + qemu.QemuConfig `embed:""` + azure.AzureConfig `embed:""` + TestCaseToRun string `help:"Name of the test case to run. If not specified, all test cases will be run." default:"all"` +} + +func (s *TridentServicingScenario) Name() string { + return "servicing" +} + +func (s *TridentServicingScenario) Args() any { + return &s.args +} + +func (s *TridentServicingScenario) Tags() []string { + return []string{} +} + +func (s *TridentServicingScenario) StagePaths() []string { + return []string{} +} + +func (s *TridentServicingScenario) RegisterTestCases(r storm.TestRegistrar) error { + r.RegisterTestCase("publish-sig-image", s.publishSigImage) + r.RegisterTestCase("deploy-vm", s.deployVm) + r.RegisterTestCase("check-deployment", s.checkDeployment) + r.RegisterTestCase("update-loop", s.updateLoop) + r.RegisterTestCase("rollback", s.rollback) + r.RegisterTestCase("collect-logs", s.collectLogs) + r.RegisterTestCase("cleanup-vm", s.cleanupVm) + return nil +} + +func (s *TridentServicingScenario) RequiredFiles() []string { + return nil +} + +func (s TridentServicingScenario) Setup(ctx storm.SetupCleanupContext) error { + return nil +} + +func (h *TridentServicingScenario) Cleanup(ctx storm.SetupCleanupContext) error { + if h.args.TestConfig.ForceCleanup { + // Best effort to clean up azure resources in case there was a failure + tests.CleanupVM(config.ServicingConfig{ + TestConfig: h.args.TestConfig, + VMConfig: h.args.VMConfig, + QemuConfig: h.args.QemuConfig, + AzureConfig: h.args.AzureConfig, + }) + } + return nil +} + +func (h *TridentServicingScenario) runTestCase(tc storm.TestCase, testFunc func(config.ServicingConfig) error) error { + if tc.Name() != h.args.TestCaseToRun && h.args.TestCaseToRun != "all" { + tc.Skip(fmt.Sprintf("Test case '%s' does not align to TestCaseToRun '%s'", tc.Name(), h.args.TestCaseToRun)) + } else { + logrus.Infof("Running test case '%s'", tc.Name()) + // create test-specific output directory + testCaseSpecificConfig := h.args.TestConfig + testCaseSpecificConfig.OutputPath = h.args.TestConfig.OutputPath + if testCaseSpecificConfig.OutputPath != "" { + testCaseSpecificConfig.OutputPath = filepath.Join(testCaseSpecificConfig.OutputPath, tc.Name()) + if err := os.MkdirAll(testCaseSpecificConfig.OutputPath, 0755); err != nil { + tc.FailFromError(err) + } + } + err := testFunc(config.ServicingConfig{ + TestConfig: testCaseSpecificConfig, + VMConfig: h.args.VMConfig, + QemuConfig: h.args.QemuConfig, + AzureConfig: h.args.AzureConfig, + }) + if err != nil { + logrus.Infof("test case '%s' failed", tc.Name()) + tc.FailFromError(err) + } + logrus.Infof("test case '%s' passed", tc.Name()) + } + return nil + +} + +func (h *TridentServicingScenario) deployVm(tc storm.TestCase) error { + return h.runTestCase(tc, tests.DeployVM) +} + +func (h *TridentServicingScenario) checkDeployment(tc storm.TestCase) error { + return h.runTestCase(tc, tests.CheckDeployment) +} + +func (h *TridentServicingScenario) updateLoop(tc storm.TestCase) error { + return h.runTestCase(tc, tests.UpdateLoop) +} + +func (h *TridentServicingScenario) rollback(tc storm.TestCase) error { + return h.runTestCase(tc, tests.Rollback) +} + +func (h *TridentServicingScenario) collectLogs(tc storm.TestCase) error { + return h.runTestCase(tc, tests.FetchLogs) +} + +func (h *TridentServicingScenario) cleanupVm(tc storm.TestCase) error { + return h.runTestCase(tc, tests.CleanupVM) +} + +func (h *TridentServicingScenario) publishSigImage(tc storm.TestCase) error { + if h.args.Platform != config.PlatformAzure { + tc.Skip("Test case 'publish-sig-image' is only applicable for Azure platform") + return nil // No action needed for non-Azure platforms + } + + return h.runTestCase(tc, tests.PublishSigImage) +} diff --git a/tools/storm/servicing/utils/azure/azure.go b/tools/storm/servicing/utils/azure/azure.go new file mode 100644 index 000000000..61ba3ddd5 --- /dev/null +++ b/tools/storm/servicing/utils/azure/azure.go @@ -0,0 +1,707 @@ +// Package storm provides helpers for Trident loop-update Storm tests. +// This file contains helpers converted from Bash scripts in scripts/loop-update. +package azure + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +type AzureConfig struct { + Subscription string `help:"Azure subscription" default:"b8a0db63-c5fa-4198-8e2a-f9d6ff52465e"` + StorageAccountResourceGroup string `help:"Azure resource group" default:"azlinux_bmp_dev"` + StorageAccount string `help:"Azure storage account for VM artifacts" default:"azlinuxbmpdev"` + StorageContainerName string `help:"Azure storage continer for VM artifacts" default:""` + WhoAmI string `help:"User who is running the tests, used for tagging resources" default:""` + Region string `help:"Azure region" default:"eastus2"` + SubnetId string `help:"Azure subnet ID" default:"/subscriptions/04cdc145-a4f9-42d4-9868-c46d23d0c63f/resourceGroups/trident-vm_servicing-azure-vnet/providers/Microsoft.Network/virtualNetworks/poolpeeringvnet/subnets/default"` + SshPublicKeyPath string `help:"Path to SSH public key" default:"~/.ssh/id_rsa.pub"` + GalleryName string `help:"Azure Shared Image Gallery name" default:""` + GalleryResourceGroup string `help:"Azure Shared Image Gallery resource group" default:""` + ImageDefinition string `help:"Azure Shared Image Gallery image definition" default:"trident-vm-grub-verity-azure-testimage"` + Offer string `help:"Azure offer for the VM" default:"trident-vm-grub-verity-azure-offer"` + Size string `help:"Azure VM size" default:"Standard_D2ds_v5"` + TestResourceGroup string `help:"Azure resource group for the VM" default:""` +} + +func (cfg AzureConfig) GetStorageAccountUrl() string { + return fmt.Sprintf("https://%s.blob.core.windows.net", cfg.StorageAccount) +} + +func (cfg AzureConfig) GetStorageAccountId() string { + return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", cfg.Subscription, cfg.StorageAccountResourceGroup, cfg.StorageAccount) +} + +func (cfg AzureConfig) ImageVersionExists(imageVersion string) bool { + _, err := cfg.CallAzCli( + []string{ + "sig", "image-version", "show", + "--resource-group", cfg.GetGalleryResourceGroup(), + "--gallery-name", cfg.GetGalleryName(), + "--gallery-image-definition", cfg.ImageDefinition, + "--gallery-image-version", imageVersion, + }, + false, + ) + return err == nil +} + +func (cfg AzureConfig) CreateImageVersion(imageVersion string, storageAccountResourceId string, storageBlobEndpoint string) error { + logrus.Tracef("Create image version in Azure Shared Image Gallery") + output, err := cfg.CallAzCli( + []string{ + "sig", "image-version", "create", + "--resource-group", cfg.GetGalleryResourceGroup(), + "--gallery-name", cfg.GetGalleryName(), + "--gallery-image-definition", cfg.ImageDefinition, + "--gallery-image-version", imageVersion, + "--target-regions", cfg.Region, + "--location", cfg.Region, + "--replication-mode", "Shallow", + "--os-vhd-storage-account", storageAccountResourceId, + "--os-vhd-uri", storageBlobEndpoint, + }, + true, + ) + if err != nil { + logrus.Tracef("Failed to create image version in Azure SIG (%v): %s", err, output) + return fmt.Errorf("failed to create image version in Azure SIG: %w", err) + } + + return nil +} + +func (cfg AzureConfig) DeployAzureVM(vmName string, user string, buildId string) error { + if err := cfg.SetSubscription(); err != nil { + return fmt.Errorf("failed to set Azure subscription: %w", err) + } + + err := cfg.EnsureGroupExists(cfg.GetTestResourceGroup(), true) + if err != nil { + return fmt.Errorf("failed to create Azure resource group: %w", err) + } + + imageVersion := cfg.GetImageVersion(buildId, false) + + // Create the VM + vmCreateArgs := []string{ + "vm", "create", + "--resource-group", cfg.GetTestResourceGroup(), + "--name", vmName, + "--size", cfg.Size, + "--os-disk-size-gb", "60", + "--admin-username", user, + "--ssh-key-values", cfg.SshPublicKeyPath, + "--image", fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/galleries/%s/images/%s/versions/%s", + cfg.Subscription, cfg.GetGalleryResourceGroup(), cfg.GetGalleryName(), cfg.ImageDefinition, imageVersion), + "--location", cfg.Region, + "--security-type", "TrustedLaunch", + "--enable-secure-boot", "true", + "--enable-vtpm", "true", + "--no-wait", + } + if cfg.SubnetId != "" { + vmCreateArgs = append(vmCreateArgs, "--subnet", cfg.SubnetId) + } + logrus.Tracef("Creating Azure VM with args: %v", vmCreateArgs) + + createVmOutput, err := cfg.CallAzCli(vmCreateArgs, true) + logrus.Tracef("Azure VM creation (%v): %s", err, createVmOutput) + if err != nil { + return fmt.Errorf("failed to create Azure VM (%w):\n%s", err, createVmOutput) + } + + for { + out, err := cfg.CallAzCli( + []string{"vm", "boot-diagnostics", "enable", "--name", vmName, "--resource-group", cfg.GetTestResourceGroup()}, + true, + ) + if err == nil { + break + } + logrus.Tracef("Failed to enable boot diagnostics for VM '%s': %s", vmName, out) + time.Sleep(1 * time.Second) // Retry after a short delay + } + + for { + out, err := cfg.CallAzCli( + []string{"vm", "boot-diagnostics", "get-boot-log", "--name", vmName, "--resource-group", cfg.GetTestResourceGroup()}, + true, + ) + if err == nil { + if strings.Contains(string(out), "BlobNotFound") { + time.Sleep(5 * time.Second) // Wait before retrying + continue // Retry until the boot log is available + } + break + } + logrus.Tracef("Failed to get boot diagnostics log for VM '%s': %s", vmName, out) + time.Sleep(5 * time.Second) // Retry after a short delay + } + + for { + out, err := cfg.CallAzCli( + []string{"vm", "show", "-d", "-g", cfg.GetTestResourceGroup(), "-n", vmName, "--query", "provisioningState", "-o", "tsv"}, + false, + ) + if err != nil || strings.TrimSpace(string(out)) != "Succeeded" { + logrus.Tracef("VM '%s' provisioning state is not 'Succeeded': %s", vmName, out) + time.Sleep(1 * time.Second) // Wait before retrying + continue // Retry until the VM is successfully provisioned + } + break + } + return nil +} + +func (cfg AzureConfig) CleanupAzureVM() error { + if err := cfg.SetSubscription(); err != nil { + return fmt.Errorf("failed to set Azure subscription: %w", err) + } + if err := cfg.DeleteGroup(cfg.GetTestResourceGroup()); err != nil { + return fmt.Errorf("failed to delete Azure resource group: %w", err) + } + return nil +} + +func (cfg AzureConfig) PublishSigImage(artifactsDir string, buildId string) error { + if err := cfg.SetSubscription(); err != nil { + return fmt.Errorf("failed to set Azure subscription: %w", err) + } + + now := time.Now() + currentDate := now.Format("20060102") + currentTime := now.Format("150405") + + storageAccountUrl := cfg.GetStorageAccountUrl() + storageAccountResourceId := cfg.GetStorageAccountId() + storageContainerName := cfg.GetStorageContainerName() + imagePath := filepath.Join(artifactsDir, "trident-vm-grub-verity-azure-testimage.vhd") + + imageVersion := cfg.GetImageVersion(buildId, true) + logrus.Infof("Using image version %s", imageVersion) + + logrus.Tracef("Check if image version %s already exists", imageVersion) + _, err := cfg.CallAzCli( + []string{ + "sig", "image-version", "show", + "--resource-group", cfg.GetGalleryResourceGroup(), + "--gallery-name", cfg.GetGalleryName(), + "--gallery-image-definition", cfg.ImageDefinition, + "--gallery-image-version", imageVersion}, + false, + ) + if err == nil { + logrus.Infof("Image version %s already exists. Exiting...", imageVersion) + return nil // Image version already exists, no need to proceed + } + logrus.Tracef("Image version %s does not exist", imageVersion) + + logrus.Tracef("Prepare image for Azure Shared Image Gallery") + err = cfg.PrepareSigImage() + if err != nil { + return fmt.Errorf("failed to prepare image for Azure: %w", err) + } + + storageBlobName := fmt.Sprintf("%s.%s-%s.vhd", currentDate, currentTime, imageVersion) + storageBlobEndpoint := fmt.Sprintf("%s/%s/%s", storageAccountUrl, storageContainerName, storageBlobName) + + // Get the path to the VHD file + logrus.Tracef("Resize VHD image for upload") + if err := cfg.ResizeImage(artifactsDir, imagePath); err != nil { + return fmt.Errorf("failed to resize image: %w", err) + } + + logrus.Tracef("Ensure azcopy is installed") + if err := cfg.EnsureAzcopyExists(); err != nil { + return fmt.Errorf("failed to ensure azcopy exists: %w", err) + } + + // Upload the image artifact to Steamboat Storage Account + logrus.Tracef("Upload image to Azure Storage Blob: %s", storageBlobEndpoint) + if azcopyOutput, err := exec.Command("azcopy", "copy", imagePath, storageBlobEndpoint).CombinedOutput(); err != nil { + logrus.Tracef("Failed to upload image to Azure Storage Blob (%v): %s", err, azcopyOutput) + return fmt.Errorf("failed to upload image to Azure Storage: %w", err) + } + + logrus.Tracef("Create image version in Azure Shared Image Gallery") + createImageVersionOutput, err := cfg.CallAzCli( + []string{ + "sig", "image-version", "create", + "--resource-group", cfg.GetGalleryResourceGroup(), + "--gallery-name", cfg.GetGalleryName(), + "--gallery-image-definition", cfg.ImageDefinition, + "--gallery-image-version", imageVersion, + "--target-regions", cfg.Region, + "--location", cfg.Region, + "--replication-mode", "Shallow", + "--os-vhd-storage-account", storageAccountResourceId, + "--os-vhd-uri", storageBlobEndpoint, + }, + true, + ) + if err != nil { + logrus.Tracef("Failed to create image version in Azure SIG (%v): %s", err, createImageVersionOutput) + return fmt.Errorf("failed to create image version in Azure SIG: %w", err) + } + + return nil +} + +func (cfg AzureConfig) EnsureAzcopyExists() error { + // if ! which azcopy; then + if _, err := exec.LookPath("azcopy"); err != nil { + logrus.Info("azcopy not found, installing...") + // Install az-copy dependency + pipelineAgentOs, err := os.ReadFile("/etc/os-release") + if err != nil { + return fmt.Errorf("failed to read /etc/os-release: %w", err) + } + pipelineAgentOsId := "" + for _, lines := range strings.Split(string(pipelineAgentOs), "\n") { + if strings.HasPrefix(lines, "ID=") { + pipelineAgentOsId = strings.Trim(strings.Split(lines, "=")[1], "\"") + break + } + } + if pipelineAgentOsId == "" { + return fmt.Errorf("failed to determine OS ID from /etc/os-release") + } + + pipelineAgentOsVersion := "" + for _, lines := range strings.Split(string(pipelineAgentOs), "\n") { + if strings.HasPrefix(lines, "VERSION_ID=") { + pipelineAgentOsVersion = strings.Trim(strings.Split(lines, "=")[1], "\"") + break + } + } + if pipelineAgentOsVersion == "" { + return fmt.Errorf("failed to determine OS version from /etc/os-release") + } + + azcopyDownloadUrl := fmt.Sprintf("https://packages.microsoft.com/config/%s/%s/packages-microsoft-prod.deb", pipelineAgentOsId, pipelineAgentOsVersion) + if err := exec.Command("curl", "-sSL", "-O", azcopyDownloadUrl).Run(); err != nil { + logrus.Errorf("Failed to download the debian package repo while attempting to install azcopy: %v", err) + logrus.Error("Suggestion: Are you using a new, non-ubuntu, pipeline agent? If yes, add azcopy installation logic for the new build agent.") + return fmt.Errorf("failed to download the debian package repo while attempting to install azcopy: %w", err) + } + + if err := exec.Command("sudo", "dpkg", "-i", "packages-microsoft-prod.deb").Run(); err != nil { + return fmt.Errorf("failed to install debian package while attempting to install azcopy: %w", err) + } + if err := os.Remove("packages-microsoft-prod.deb"); err != nil { + return fmt.Errorf("failed to remove debian package file: %w", err) + } + if err := exec.Command("sudo", "apt-get", "update", "-y").Run(); err != nil { + return fmt.Errorf("failed to update package list while attempting to install azcopy: %w", err) + } + if err := exec.Command("sudo", "apt-get", "install", "azcopy", "-y").Run(); err != nil { + return fmt.Errorf("failed to install azcopy: %w", err) + } + out, err := exec.Command("azcopy", "--version").Output() + if err != nil { + return fmt.Errorf("failed to check azcopy version: %w", err) + } + logrus.Infof("azcopy version: %s", strings.TrimSpace(string(out))) + } + return nil +} + +func (cfg AzureConfig) ResizeImage(artifactsDir string, imagePath string) error { + // VHD images on Azure must have a virtual size aligned to 1MB. https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-generic#resize-vhds + MB := 1024 * 1024 + + rawFile := filepath.Join(artifactsDir, "resize.raw") + // Convert to raw format + if out, err := exec.Command("sudo", "qemu-img", "convert", "-f", "vpc", "-O", "raw", imagePath, rawFile).CombinedOutput(); err != nil { + logrus.Tracef("Failed to convert VHD to raw: %v\n%s", err, out) + return fmt.Errorf("failed to convert VHD to raw: %w", err) + } + + // Get the size of the raw image + out, err := exec.Command("qemu-img", "info", "-f", "raw", "--output", "json", rawFile).Output() + if err != nil { + return fmt.Errorf("failed to get raw image size: %w", err) + } + re := regexp.MustCompile(`"virtual-size":\s*([0-9]+)`) + matches := re.FindSubmatch(out) + if len(matches) < 2 { + return fmt.Errorf("failed to parse raw image size from output: %s", out) + } + size, err := strconv.Atoi(string(matches[1])) + if err != nil { + return fmt.Errorf("failed to convert raw image size to integer: %w", err) + } + + roundedSize := ((size + MB - 1) / MB) * MB + logrus.Infof("Rounded Size = %d", roundedSize) + + // Resize the raw image to the rounded size + if out, err := exec.Command("sudo", "qemu-img", "resize", rawFile, fmt.Sprintf("%d", roundedSize)).CombinedOutput(); err != nil { + logrus.Tracef("Failed to resize raw image: %v\n%s", err, out) + return fmt.Errorf("failed to resize raw image: %w", err) + } + // Convert back to original format + if out, err := exec.Command("sudo", "qemu-img", "convert", "-f", "raw", "-o", "subformat=fixed,force_size", "-O", "vpc", rawFile, imagePath).CombinedOutput(); err != nil { + logrus.Tracef("Failed to convert raw back to VHD: %v\n%s", err, out) + return fmt.Errorf("failed to convert raw back to VHD: %w", err) + } + // Remove the temporary raw file + if err := exec.Command("rm", rawFile).Run(); err != nil { + return fmt.Errorf("failed to remove temporary raw file: %w", err) + } + return nil +} + +func (cfg AzureConfig) PrepareSigImage() error { + logrus.Tracef("Set Azure subscription") + if err := cfg.SetSubscription(); err != nil { + return fmt.Errorf("failed to set Azure subscription: %w", err) + } + logrus.Tracef("Ensure Azure resource group '%s' exists", cfg.GetTestResourceGroup()) + if err := cfg.EnsureGroupExists(cfg.GetTestResourceGroup(), false); err != nil { + return fmt.Errorf("failed to ensure Azure resource group (%s) exists: %w", cfg.GetTestResourceGroup(), err) + } + logrus.Tracef("Ensure Azure gallery resource group '%s' exists", cfg.GetGalleryResourceGroup()) + if err := cfg.EnsureGroupExists(cfg.GetGalleryResourceGroup(), false); err != nil { + return fmt.Errorf("failed to ensure Azure gallery resource group (%s) exists: %w", cfg.GetGalleryResourceGroup(), err) + } + // Ensure storage account exists + if err := cfg.EnsureStorageAccountExists(); err != nil { + return fmt.Errorf("failed to ensure Azure storage account exists: %w", err) + } + // Ensure storage container exists + if err := cfg.EnsureStorageContainerExists(); err != nil { + return fmt.Errorf("failed to ensure Azure storage container exists: %w", err) + } + // Ensure gallery exists + if err := cfg.EnsureGalleryExists(); err != nil { + return fmt.Errorf("failed to ensure Azure image gallery exists: %w", err) + } + + // Ensure the image-definition exists + if err := cfg.EnsureImageDefinitionExists(); err != nil { + return fmt.Errorf("failed to ensure Azure image definition exists: %w", err) + } + + return nil +} + +func (cfg AzureConfig) GetWhoAmI() string { + if cfg.WhoAmI != "" { + return cfg.WhoAmI + } + whoami, err := exec.Command("whoami").Output() + if err != nil { + panic(fmt.Sprintf("Failed to get current user: %v", err)) + } + return strings.TrimSpace(string(whoami)) +} + +func (cfg AzureConfig) GetTestResourceGroup() string { + if cfg.TestResourceGroup != "" { + return cfg.TestResourceGroup + } + return fmt.Sprintf("%s-test", cfg.GetGalleryResourceGroup()) +} + +func (cfg AzureConfig) GetGalleryName() string { + if cfg.GalleryName != "" { + return cfg.GalleryName + } + return fmt.Sprintf("%s_trident_gallery", cfg.GetWhoAmI()) +} + +func (cfg AzureConfig) GetGalleryResourceGroup() string { + if cfg.GalleryResourceGroup != "" { + return cfg.GalleryResourceGroup + } + return fmt.Sprintf("%s-trident-rg", cfg.GetWhoAmI()) +} + +func (cfg AzureConfig) GetStorageContainerName() string { + if cfg.StorageContainerName != "" { + return cfg.StorageContainerName + } + return fmt.Sprintf("%s-test", cfg.GetWhoAmI()) +} + +func (cfg AzureConfig) GetAllVmIPAddresses(vmName string, buildId string) ([]string, error) { + ipType := "publicIps" + if buildId != "" { + ipType = "privateIps" // Use private IPs for build tests + } + logrus.Tracef("Fetching Azure VM IP addresses for type '%s'", ipType) + cmd := exec.Command("az", "vm", "show", "-d", "-g", cfg.GetTestResourceGroup(), "-n", vmName, "--query", ipType, "-o", "tsv") + out, err := cmd.Output() + if err != nil { + fullShowCmd := exec.Command("az", "vm", "show", "-d", "-g", cfg.GetTestResourceGroup(), "-n", vmName) + fullShowCmdOut, fullShowCmdErr := fullShowCmd.CombinedOutput() + logrus.Tracef("Failed to get Azure VM IP addresses, show vm: %v\n%s", fullShowCmdErr, fullShowCmdOut) + return nil, fmt.Errorf("failed to get Azure VM IP (%w): %s", err, out) + } + return strings.Split(strings.TrimSpace(string(out)), "\n"), nil +} + +func (cfg AzureConfig) GetLatestVersion() string { + // Get existing image versions from Azure Shared Image Gallery + out, err := exec.Command("az", "sig", "image-version", "list", + "--resource-group", cfg.GetGalleryResourceGroup(), + "--gallery-name", cfg.GetGalleryName(), + "--gallery-image-definition", cfg.ImageDefinition, + "--query", "[].name", + "-o", "tsv").Output() + if err != nil { + logrus.Errorf("Failed to get latest image version: %v", err) + return "" + } + versions := strings.Split(strings.TrimSpace(string(out)), "\n") + if len(versions) == 0 { + logrus.Info("No image versions found") + return "" + } + // Sort versions by semver + sort.Slice(versions, func(i, j int) bool { + v1 := strings.Split(versions[i], ".") + v2 := strings.Split(versions[j], ".") + if len(v1) != 3 || len(v2) != 3 { + return false // Invalid version format + } + for k := 0; k < 3; k++ { + if v1[k] != v2[k] { + return v1[k] < v2[k] + } + } + return false // Versions are equal + }) + return versions[len(versions)-1] // Return the latest version +} + +func (cfg AzureConfig) GetImageVersion(buildId string, increment bool) string { + imageVersion := "0.0.1" + if buildId == "" { + // If no build ID is provided, get the latest version + imageVersion = cfg.GetLatestVersion() + if imageVersion == "" { + // If no versions found, use a default version + imageVersion = "0.0.1" + } else if increment { + // If version was found and increment is true, + // increment the patch version + parts := strings.Split(imageVersion, ".") + if len(parts) != 3 { + logrus.Errorf("Invalid image version format: %s", imageVersion) + return "" + } + major, _ := strconv.Atoi(parts[0]) + minor, _ := strconv.Atoi(parts[1]) + patch, _ := strconv.Atoi(parts[2]) + patch++ // Increment the patch version + imageVersion = fmt.Sprintf("%d.%d.%d", major, minor, patch) + } + } else { + // Use the build ID as the patch version + imageVersion = fmt.Sprintf("0.0.%s", buildId) + } + + logrus.Infof("Image version: %s", imageVersion) + return imageVersion +} + +func (cfg AzureConfig) SetSubscription() error { + if _, err := cfg.CallAzCli([]string{"account", "set", "--subscription", cfg.Subscription}, false); err != nil { + return fmt.Errorf("failed to set Azure subscription: %w", err) + } + return nil +} + +func (cfg AzureConfig) EnsureGroupExists(groupName string, deleteExisting bool) error { + findGroupCmdOutput, err := cfg.CallAzCli([]string{"group", "exists", "-n", groupName}, false) + if err != nil { + return fmt.Errorf("failed to check if group exists: %w", err) + } + + if strings.TrimSpace(string(findGroupCmdOutput)) == "true" { + if !deleteExisting { + return nil + } + + // Resource group exists, delete it + if deleteOutput, err := cfg.CallAzCli([]string{"group", "delete", "-n", groupName, "-y"}, true); err != nil { + return fmt.Errorf("failed to delete Azure resource group (%w):\n%s", err, deleteOutput) + } + } + + if groupCreateOutput, err := cfg.CallAzCli([]string{"group", "create", "-n", groupName, "-l", cfg.Region, "--tags", fmt.Sprintf("creationTime=%d", time.Now().Unix())}, true); err != nil { + return fmt.Errorf("failed to create Azure resource group (%w):\n%s", err, groupCreateOutput) + } + return nil +} + +func (cfg AzureConfig) DeleteGroup(groupName string) error { + findGroupCmdOutput, err := cfg.CallAzCli([]string{"group", "exists", "-n", groupName}, false) + if err != nil { + return fmt.Errorf("failed to check if group exists: %w", err) + } + if strings.TrimSpace(string(findGroupCmdOutput)) != "true" { + return nil + } + if deleteOutput, err := cfg.CallAzCli([]string{"group", "delete", "-n", groupName, "-y"}, true); err != nil { + return fmt.Errorf("failed to delete Azure resource group (%w):\n%s", err, deleteOutput) + } + return nil +} + +func (cfg AzureConfig) EnsureStorageAccountExists() error { + // Ensure storage account exists + storageAccountResourceId := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", + cfg.Subscription, cfg.StorageAccountResourceGroup, cfg.StorageAccount) + + logrus.Tracef("Ensure Azure storage account '%s' exists in resource group '%s'", cfg.StorageAccount, cfg.StorageAccountResourceGroup) + storageAccountOutput, err := cfg.CallAzCli( + []string{ + "storage", "account", "show", + "--ids", storageAccountResourceId, + }, + false, + ) + if err != nil || strings.TrimSpace(string(storageAccountOutput)) == "" { + logrus.Infof("Could not find storage account '%s' in the expected location. Creating the storage account.", cfg.StorageAccount) + checkNameOut, err := cfg.CallAzCli( + []string{"storage", "account", "check-name", "--name", cfg.StorageAccount, "--query", "nameAvailable"}, + false, + ) + if err != nil || strings.TrimSpace(string(checkNameOut)) != "false" { + return fmt.Errorf("storage account name %s is not available", cfg.StorageAccount) + } + createStorageOutput, err := cfg.CallAzCli( + []string{ + "storage", "account", "create", + "-g", cfg.StorageAccountResourceGroup, + "-n", cfg.StorageAccount, + "-l", cfg.Region, + "--allow-shared-key-access", "false", + }, + true, + ) + if err != nil { + logrus.Tracef("Failed to create Azure storage account '%s': %s", cfg.StorageAccount, createStorageOutput) + return fmt.Errorf("failed to create Azure storage account: %w", err) + } + } + return nil +} + +func (cfg AzureConfig) EnsureStorageContainerExists() error { + logrus.Tracef("Ensure Azure storage container '%s' exists in storage account '%s'", cfg.GetStorageContainerName(), cfg.StorageAccount) + containerExistsOutput, err := cfg.CallAzCli( + []string{ + "storage", "container", "exists", + "--account-name", cfg.StorageAccount, + "--name", cfg.GetStorageContainerName(), + "--auth-mode", "login", + }, + false, + ) + if err != nil || !strings.Contains(string(containerExistsOutput), `"exists": true`) { + logrus.Tracef("Container '%s' not found, creating in storage account '%s'...", cfg.GetStorageContainerName(), cfg.StorageAccount) + createContainerOutput, err := cfg.CallAzCli( + []string{ + "storage", "container", "create", + "--account-name", cfg.StorageAccount, + "--name", cfg.GetStorageContainerName(), + "--auth-mode", "login", + }, + true, + ) + if err != nil { + logrus.Tracef("Failed to create Azure storage container '%s': %s", cfg.GetStorageContainerName(), createContainerOutput) + return fmt.Errorf("failed to create Azure storage container: %w", err) + } + } + return nil +} + +func (cfg AzureConfig) EnsureGalleryExists() error { + logrus.Tracef("Ensure Azure image gallery '%s' exists in resource group '%s'", cfg.GetGalleryName(), cfg.GetGalleryResourceGroup()) + galleryExistsOutput, err := cfg.CallAzCli( + []string{ + "sig", "show", + "-r", cfg.GetGalleryName(), + "-g", cfg.GetGalleryResourceGroup(), + }, + false, + ) + if err != nil || strings.TrimSpace(string(galleryExistsOutput)) == "" { + logrus.Infof("Could not find image gallery '%s' in resource group '%s'. Creating the gallery.", cfg.GetGalleryName(), cfg.GetGalleryResourceGroup()) + createGalleryOutput, err := cfg.CallAzCli( + []string{ + "sig", "create", + "-g", cfg.GetGalleryResourceGroup(), + "-r", cfg.GetGalleryName(), + "-l", cfg.Region, + }, + true) + if err != nil { + logrus.Tracef("Failed to create Azure image gallery '%s': %s", cfg.GetGalleryName(), createGalleryOutput) + return fmt.Errorf("failed to create Azure image gallery: %w", err) + } + } + return nil +} + +func (cfg AzureConfig) EnsureImageDefinitionExists() error { + logrus.Tracef("Ensure Azure image definition '%s' exists in gallery '%s'", cfg.ImageDefinition, cfg.GetGalleryName()) + imageDefinitionExistsOutput, err := cfg.CallAzCli( + []string{ + "sig", "image-definition", "list", + "-r", cfg.GetGalleryName(), + "-g", cfg.GetGalleryResourceGroup(), + "--query", fmt.Sprintf("[?name=='%s'].name", cfg.ImageDefinition), + "-o", "tsv", + }, + false, + ) + if err != nil || strings.TrimSpace(string(imageDefinitionExistsOutput)) == "" { + logrus.Infof("Could not find image-definition '%s'. Creating definition '%s' in gallery '%s'...", cfg.ImageDefinition, cfg.ImageDefinition, cfg.GetGalleryName()) + createImageDefOutput, err := cfg.CallAzCli( + []string{ + "sig", "image-definition", "create", + "-i", cfg.ImageDefinition, + "--publisher", cfg.GetWhoAmI(), + "--offer", cfg.Offer, + "--sku", cfg.ImageDefinition, + "-r", cfg.GetGalleryName(), + "-g", cfg.GetGalleryResourceGroup(), + "--os-type", "Linux", + }, + true, + ) + if err != nil { + logrus.Tracef("Failed to create Azure image definition '%s': %s", cfg.ImageDefinition, createImageDefOutput) + return fmt.Errorf("failed to create Azure image definition: %w", err) + } + } + return nil +} + +func (cfg AzureConfig) CallAzCli(azArgs []string, combined bool) (string, error) { + cmd := exec.Command("az", azArgs...) + var b bytes.Buffer + cmd.Stdout = &b + if combined { + cmd.Stderr = &b + } + err := cmd.Run() + return b.String(), err +} diff --git a/tools/storm/servicing/utils/config/config.go b/tools/storm/servicing/utils/config/config.go new file mode 100644 index 000000000..f7ad1a736 --- /dev/null +++ b/tools/storm/servicing/utils/config/config.go @@ -0,0 +1,41 @@ +package config + +import ( + "tridenttools/storm/servicing/utils/azure" + "tridenttools/storm/servicing/utils/qemu" +) + +// VMPlatformType represents the test platform (qemu or azure) +type VMPlatformType string + +const ( + PlatformQEMU VMPlatformType = "qemu" + PlatformAzure VMPlatformType = "azure" +) + +type VMConfig struct { + Name string `help:"Name of the VM" default:"trident-vm-verity-test"` + Platform VMPlatformType `help:"Platform for the VM (qemu or azure)" default:"qemu"` + User string `help:"User to use for SSH connection" default:"testuser"` + SshPrivateKeyPath string `help:"Path to the SSH private key file" default:"~/.ssh/id_rsa"` +} + +type TestConfig struct { + ArtifactsDir string `help:"Directory containing artifacts for the VM" default:"/tmp"` + OutputPath string `help:"Path to the output directory for logs and artifacts" default:"./output"` + Verbose bool `help:"Enable verbose logging" default:"false"` + RetryCount int `help:"Number of retry attempts for updates" default:"3"` + RollbackRetryCount int `help:"Number of retry attempts for updates" default:"3"` + UpdatePortA int `help:"Port for the first update server" default:"8000"` + UpdatePortB int `help:"Port for the second update server" default:"8001"` + BuildId string `help:"Build ID for the VM" default:""` + ExpectedVolume string `help:"Expected active volume after update" default:"volume-a"` + ForceCleanup bool `help:"Force cleanup of VM when test finishes" default:"false"` +} + +type ServicingConfig struct { + VMConfig VMConfig + TestConfig TestConfig + QemuConfig qemu.QemuConfig + AzureConfig azure.AzureConfig +} diff --git a/tools/storm/servicing/utils/file/file.go b/tools/storm/servicing/utils/file/file.go new file mode 100644 index 000000000..d6985d792 --- /dev/null +++ b/tools/storm/servicing/utils/file/file.go @@ -0,0 +1,35 @@ +// Package storm provides helpers for Trident loop-update Storm tests. +// This file contains helpers converted from Bash scripts in scripts/loop-update. +package file + +import ( + "fmt" + "os" + "path/filepath" + "regexp" +) + +func FindFile(dir, pattern string) (string, error) { + // Find image file + regexPattern, e := regexp.Compile(pattern) + if e != nil { + return "", fmt.Errorf("failed to match pattern: %w", e) + } + + matchingFiles := make([]string, 0) + e = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err == nil && !info.IsDir() && regexPattern.MatchString(info.Name()) { + matchingFiles = append(matchingFiles, path) + } + return nil + }) + if e != nil { + return "", fmt.Errorf("failed to find file: %w", e) + } + if len(matchingFiles) < 1 { + return "", fmt.Errorf("file not found") + } else if len(matchingFiles) > 1 { + return "", fmt.Errorf("multiple files found: %v", matchingFiles) + } + return matchingFiles[0], nil +} diff --git a/tools/storm/servicing/utils/qemu/qemu.go b/tools/storm/servicing/utils/qemu/qemu.go new file mode 100644 index 000000000..5877f941c --- /dev/null +++ b/tools/storm/servicing/utils/qemu/qemu.go @@ -0,0 +1,451 @@ +// Package storm provides helpers for Trident loop-update Storm tests. +// This file contains helpers converted from Bash scripts in scripts/loop-update. +package qemu + +import ( + "bufio" + "fmt" + "io" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + "tridenttools/storm/servicing/utils/file" + + "github.com/digitalocean/go-libvirt" + "github.com/sirupsen/logrus" +) + +type QemuConfig struct { + SecureBoot bool `help:"Enable secure boot for the VM" default:"false"` + SerialLog string `help:"Path to the serial log file" default:"/tmp/trident-vm-verity-test.log"` +} + +func (cfg QemuConfig) DeployQemuVM(vmName string, artifactsDir string, outputPath string, verbose bool) error { + logrus.Tracef("Deploying VM on QEMU platform with name '%s'", vmName) + + // Destroy and undefine any existing VM + if err := cfg.deleteLibvirtDomain(vmName); err != nil { + return fmt.Errorf("failed to delete existing domain '%s': %w", vmName, err) + } + + // Find image file + imageFile, err := file.FindFile(artifactsDir, "^trident-vm-.*-testimage.qcow2$") + if err != nil { + return fmt.Errorf("failed to find image file: %w", err) + } + logrus.Tracef("Found image file: %s", imageFile) + + bootImage := artifactsDir + "/booted.qcow2" + if err := exec.Command("cp", imageFile, bootImage).Run(); err != nil { + return fmt.Errorf("failed to copy image: %w", err) + } + logrus.Tracef("Copied image to boot image: %s", bootImage) + + err = cfg.createQemuVM(vmName, bootImage, true) + if err != nil { + return fmt.Errorf("failed to create VM: %w", err) + } + + // Wait for serial log + for { + if _, err := os.Stat(cfg.SerialLog); err == nil { + break + } + } + + logrus.Tracef("Check if VM is ready for login") + err = cfg.WaitForLogin(vmName, outputPath, verbose, 0) + if err != nil { + return fmt.Errorf("failed to wait for login after reboot: %w", err) + } + + return nil +} + +func (cfg QemuConfig) CleanupQemuVM(vmName string) error { + err := cfg.deleteLibvirtDomain(vmName) + if err != nil { + return fmt.Errorf("failed to cleanup vm '%s': %w", vmName, err) + } + return nil +} + +func (cfg QemuConfig) RebootQemuVm(vmName string, iteration int, outputPath string, verbose bool) error { + logrus.Tracef("Truncate log files before reboot") + if err := cfg.TruncateLog(vmName); err != nil { + return fmt.Errorf("failed to truncate log file: %w", err) + } + + lv, domain, err := getLibvirtDomainByname(vmName) + if err != nil { + return fmt.Errorf("failed to lookup domain by name '%s': %w", vmName, err) + } + + logrus.Tracef("Rebooting VM '%s' before update attempt #%d", vmName, iteration) + if err := lv.DomainShutdown(domain); err != nil { + return fmt.Errorf("failed to shutdown domain '%s': %w", vmName, err) + } + logrus.Tracef("Waiting for VM '%s' to shut down", vmName) + for { + domainState, _, err := lv.DomainGetState(domain, 0) + if err != nil { + return fmt.Errorf("failed to get domain state: %w", err) + } + if domainState == int32(libvirt.DomainShutoff) { + break // Domain is shut off, exit loop + } + } + logrus.Tracef("Domain '%s' is shut down, starting it again", vmName) + err = lv.DomainCreate(domain) + if err != nil { + return fmt.Errorf("failed to start domain '%s': %w", vmName, err) + } + logrus.Tracef("Waiting for VM '%s' to come back up after reboot", vmName) + err = cfg.WaitForLogin(vmName, outputPath, verbose, iteration) + if err != nil { + return fmt.Errorf("failed to wait for login after reboot: %w", err) + } + return nil +} + +func getLibvirtDomainByname(vmName string) (lv *libvirt.Libvirt, domain libvirt.Domain, err error) { + uri, _ := url.Parse(string(libvirt.QEMUSession)) + lv, err = libvirt.ConnectToURI(uri) + if err != nil { + return lv, domain, fmt.Errorf("failed to connect: %v", err) + } + + domain, err = lv.DomainLookupByName(vmName) + if err != nil { + return lv, domain, fmt.Errorf("failed to lookup domain by name '%s': %w", vmName, err) + } + + return lv, domain, nil +} + +func (cfg QemuConfig) getQemuVmIpAddresses(vmName string) ([]string, error) { + lv, domain, err := getLibvirtDomainByname(vmName) + if err != nil { + return nil, fmt.Errorf("failed to lookup domain by name '%s': %w", vmName, err) + } + logrus.Tracef("Found libvirt domain '%s' with ID %v", vmName, domain) + + ifaces, err := lv.DomainInterfaceAddresses(domain, uint32(libvirt.DomainInterfaceAddressesSrcLease), 0) // VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT retrieves info from the guest agent + if err != nil { + return nil, fmt.Errorf("failed to get domain interface addresses: %w", err) + } + logrus.Tracef("Found %d interfaces for domain '%s'", len(ifaces), vmName) + + ipAddressesFound := make([]string, 0) + + // Iterate through interfaces to find IP address + for _, val := range ifaces { + logrus.Tracef("Interface '%s' has %d addresses", val.Name, len(val.Addrs)) + if val.Addrs != nil { + logrus.Tracef("Interface '%s' has non-null addresses: %v", val.Name, val.Addrs) + for _, addr := range val.Addrs { + logrus.Tracef("Found address '%s' of type %d for interface '%s'", addr.Addr, addr.Type, val.Name) + if addr.Type == int32(libvirt.IPAddrTypeIpv4) { + logrus.Tracef("Found IPv4 address '%s' for interface '%s'", addr.Addr, val.Name) + ipAddressesFound = append(ipAddressesFound, addr.Addr) + } + } + } + } + return ipAddressesFound, nil +} + +func (cfg QemuConfig) GetAllVmIPAddresses(vmName string) ([]string, error) { + for { + ips, err := cfg.getQemuVmIpAddresses(vmName) + if err != nil || len(ips) == 0 { + logrus.Tracef("Failed to get QEMU VM IP addresses: %v", err) + + virshOutput, virshErr := exec.Command("sudo", "virsh", "domifaddr", vmName).CombinedOutput() + logrus.Tracef("virsh domifaddr output: %s\n%v", string(virshOutput), virshErr) + + time.Sleep(1 * time.Second) // Wait before retrying + continue // Retry until we get an IP address + } + + return ips, nil + } +} + +func (cfg QemuConfig) deleteLibvirtDomain(vmName string) error { + logrus.Tracef("Deleting libvirt domain '%s'", vmName) + lv, domain, err := getLibvirtDomainByname(vmName) + if err != nil { + logrus.Tracef("Failed to lookup domain by name '%s': %v", vmName, err) + return nil + } + + domainState, _, err := lv.DomainGetState(domain, 0) + if err != nil { + return fmt.Errorf("failed to get domain state: %w", err) + } + if domainState == int32(libvirt.DomainRunning) { + logrus.Tracef("Destroying libvirt domain '%s'", vmName) + err = lv.DomainDestroy(domain) // Stop the VM + if err != nil { + logrus.Tracef("failed to destroy domain '%s': %v", vmName, err) + return fmt.Errorf("failed to destroy domain '%s': %w", vmName, err) + } + } + + logrus.Tracef("Undefining libvirt domain '%s'", vmName) + err = lv.DomainUndefineFlags(domain, libvirt.DomainUndefineNvram) // Undefine the VM, including NVRAM + if err != nil { + logrus.Tracef("failed to undefine domain '%s': %v", vmName, err) + return fmt.Errorf("failed to undefine domain '%s': %w", vmName, err) + } + return nil +} + +func (cfg QemuConfig) createQemuVM(name string, bootImage string, useVirtInstall bool) error { + + // TODO: migrate to use virtdeploy + + if useVirtInstall { + logrus.Tracef("Using virt-install to create QEMU VM '%s'", name) + virtInstallArgs := []string{ + "virt-install", + "--name", name, + "--memory", "2048", + "--vcpus", "2", + "--os-variant", "generic", + "--import", + "--disk", fmt.Sprintf("%s,bus=sata", bootImage), + "--network", "default", + "--noautoconsole", + "--serial", fmt.Sprintf("file,path=%s", cfg.SerialLog), + } + if cfg.SecureBoot { + virtInstallArgs = append(virtInstallArgs, "--machine", "q35", "--boot", "uefi,loader_secure=yes") + } else { + virtInstallArgs = append(virtInstallArgs, "--boot", "uefi,loader_secure=no") + } + logrus.Tracef("Running virt-install command: %s", strings.Join(virtInstallArgs, " ")) + if err := exec.Command("sudo", virtInstallArgs...).Run(); err != nil { + return fmt.Errorf("failed to create QEMU VM '%s': %w", name, err) + } + } else { + logrus.Tracef("Using libvirt to create QEMU VM '%s'", name) + uri, _ := url.Parse(string(libvirt.QEMUSession)) + lv, err := libvirt.ConnectToURI(uri) + if err != nil { + return fmt.Errorf("failed to connect: %v", err) + } + + loaderPath := "/usr/share/OVMF/OVMF_CODE.secboot.fd" + if !cfg.SecureBoot { + loaderPath = "/usr/share/OVMF/OVMF_CODE.fd" + } + + domainXML := fmt.Sprintf(` + + %s + 2048 + 2 + + hvm + %s + + + + + + + + + + + + + + + + + + + + + + + + + +`, + name, loaderPath, bootImage, cfg.SerialLog, cfg.SerialLog) + + logrus.Tracef("Defining libvirt domain with XML: %s", domainXML) + domain, err := lv.DomainDefineXML(domainXML) + if err != nil { + return fmt.Errorf("failed to define domain: %w", err) + } + + logrus.Tracef("Starting libvirt domain '%s'", name) + if err := lv.DomainCreate(domain); err != nil { + return fmt.Errorf("failed to start domain: %w", err) + } + } + return nil +} + +func (cfg QemuConfig) TruncateLog(vmName string) error { + // If domain exists, truncate the serial log file + if _, _, err := getLibvirtDomainByname(vmName); err == nil { + if err := exec.Command("truncate", "-s", "0", cfg.SerialLog).Run(); err != nil { + return fmt.Errorf("failed to truncate log file: %w", err) + } + } + return nil +} + +func (cfg QemuConfig) WaitForLogin(vmName string, outputPath string, verbose bool, iteration int) error { + localSerialLog := "./serial.log" + // Wait for login prompt to appear in the serial log and save the log to localSerialLog + waitErr := innerWaitForLogin(cfg.SerialLog, verbose, iteration, localSerialLog) + // Copy serial log to output directory if specified + if outputPath != "" { + err := os.MkdirAll(outputPath, 0755) + if err != nil { + return fmt.Errorf("failed to create output directory '%s': %w", outputPath, err) + } + + outputFilename := fmt.Sprintf("%s-serial.log", fmt.Sprintf("%03d", iteration)) + if err := exec.Command("cp", localSerialLog, filepath.Join(outputPath, outputFilename)).Run(); err != nil { + return fmt.Errorf("failed to copy serial log to output directory: %w", err) + } + } + + if waitErr != nil { + // Create fairly generic error message + logrus.Errorf("Failed to reach login prompt for the VM for iteration %d: %v", iteration, waitErr) + // Attempt to create more meaningful error messages based on the serial log + analyzeSerialLog(cfg.SerialLog) + + // Output qemu domain info to try to help debug failure + dominfoOut, err := exec.Command("virsh", "dominfo", vmName).Output() + if err != nil { + logrus.Errorf("Failed to get domain info for VM '%s': %v", vmName, err) + } else { + logrus.Infof("Domain info for VM '%s': %s", vmName, dominfoOut) + } + + // Output disk usage to help debug failure + dfOut, err := exec.Command("df", "-h").Output() + if err != nil { + logrus.Errorf("Failed to run 'df -h': %v", err) + } else { + logrus.Infof("Disk usage:\n%s", dfOut) + } + } + return waitErr +} + +func printAndSave(line string, verbose bool, localSerialLog string, ansi_control_cleaner *regexp.Regexp, ansi_cleaner *regexp.Regexp) { + if line == "" { + return + } + + // Remove ANSI control codes + line = ansi_control_cleaner.ReplaceAllString(line, "") + if verbose { + logrus.Info(line) + } + if localSerialLog != "" { + // Remove all ANSI escape codes + line = ansi_cleaner.ReplaceAllString(line, "") + logFile, err := os.OpenFile(localSerialLog, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return + } + defer logFile.Close() + + _, err = logFile.WriteString(line + "\n") + if err != nil { + logrus.Errorf("Failed to append line to output file: %v", err) + } + } +} + +func analyzeSerialLog(serial string) error { + // Read the last line of the serial log + lastLine, err := exec.Command("tail", "-n", "1", serial).Output() + if err != nil { + return fmt.Errorf("failed to read last line of serial log: %w", err) + } + // Watch for specific failures and create error messages accordingly + if strings.Contains(string(lastLine), "tpm tpm0: Operation Timed out") { + logrus.Error("tpm tpm0: Operation Timed out") + } else { + // More generic error message based on last serial log line + logrus.Errorf("Last line of serial log: %s", lastLine) + } + return nil +} + +func innerWaitForLogin(vmSerialLog string, verbose bool, iteration int, localSerialLog string) error { + // ANSI escape code cleaner + ansi_cleaner := regexp.MustCompile(`(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]`) + // ANSI non-color escape code cleaner, matches only control codes + ansi_control_cleaner := regexp.MustCompile(`(\x9B|\x1B\[)[0-?]*[ -\/]*[@-ln-~]`) + + // Timeout for monitoring serial log for login prompt + timeout := time.Second * 120 + startTime := time.Now() + + // Create the file if it doesn't exist + file, err := os.OpenFile(vmSerialLog, os.O_RDWR, 0644) + if err != nil { + return fmt.Errorf("failed to open serial log file: %w", err) + } + defer file.Close() + + reader := bufio.NewReader(file) + lineBuffer := "" + for { + // Check if the current line contains the login prompt, and return if it does + if strings.Contains(lineBuffer, "login:") && !strings.Contains(lineBuffer, "mos") { + printAndSave(lineBuffer, verbose, localSerialLog, ansi_control_cleaner, ansi_cleaner) + return nil + } + + // Read a rune from reader, if EOF is encountered, retry until either a new + // character is read or the timeout is reached + var readRune rune + for { + if time.Since(startTime) >= timeout { + return fmt.Errorf("timeout waiting for login prompt after %d seconds", int(timeout.Seconds())) + } + // Read a rune from the serial log file + readRune, _, err = reader.ReadRune() + if err == io.EOF { + // Wait for new serial output + time.Sleep(10 * time.Millisecond) + continue + } + if err != nil { + return fmt.Errorf("failed to read from serial log: %w", err) + } + // Successfully read a rune, break out of the loop + break + } + // Handle the rune read from the serial log + runeStr := string(readRune) + if runeStr == "\n" { + // If the last character is a newline, print the line buffer + // and reset it + printAndSave(lineBuffer, verbose, localSerialLog, ansi_control_cleaner, ansi_cleaner) + lineBuffer = "" + } else { + // If non-newline, append the output to the buffer + lineBuffer += runeStr + } + } +} diff --git a/tools/storm/servicing/utils/ssh/ssh.go b/tools/storm/servicing/utils/ssh/ssh.go new file mode 100644 index 000000000..5c7d7f386 --- /dev/null +++ b/tools/storm/servicing/utils/ssh/ssh.go @@ -0,0 +1,160 @@ +package ssh + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + "tridenttools/storm/servicing/utils/config" + "tridenttools/storm/utils" + + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" +) + +func SshCommandWithRetries(cfg config.VMConfig, vmIP, command string, connectionRetryCount int, commandRetryCount int) (string, error) { + return innerSshCommand(cfg, vmIP, command, false, connectionRetryCount, commandRetryCount) +} + +func SshCommand(cfg config.VMConfig, vmIP, command string) (string, error) { + return innerSshCommand(cfg, vmIP, command, false, 0, 0) +} + +func SshCommandCombinedOutput(cfg config.VMConfig, vmIP, command string) (string, error) { + return innerSshCommand(cfg, vmIP, command, true, 0, 0) +} + +func StartSshProxyPortAndWait(ctx context.Context, port int, vmIP string, sshUser string, sshKeyPath string, startedChannel chan bool) error { + cmd := exec.CommandContext(ctx, + "ssh", + "-R", fmt.Sprintf("%d:localhost:%d", port, port), + "-N", + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=10", + "-o", "ServerAliveCountMax=3", + "-o", "ServerAliveInterval=5", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-i", sshKeyPath, + fmt.Sprintf("%s@%s", sshUser, vmIP), + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + logrus.Tracef("Starting SSH proxy for port %d to VM %s with user %s", port, vmIP, sshUser) + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start SSH proxy for port %d: %w", port, err) + } + // Signal that the SSH proxy has started + startedChannel <- true + // Wait for the command to finish + if err := cmd.Wait(); err != nil { + return fmt.Errorf("SSH proxy for port %d failed: %w", port, err) + } + logrus.Tracef("SSH proxy for port %d exited", port) + + return nil +} + +func ScpDownloadFile(cfg config.VMConfig, vmIP, src, dest string) error { + args := []string{ + "-i", cfg.SshPrivateKeyPath, + "-r", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + fmt.Sprintf("%s@%s:%s", cfg.User, vmIP, src), + dest, + } + logrus.Tracef("Running scp download with args: %v", args) + cmd := exec.Command("scp", args...) + return cmd.Run() +} + +func ScpUploadFile(cfg config.VMConfig, vmIP, src, dest string) error { + args := []string{ + "-i", cfg.SshPrivateKeyPath, + "-r", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + src, + fmt.Sprintf("%s@%s:%s", cfg.User, vmIP, dest), + } + logrus.Tracef("Running scp upload with args: %v", args) + cmd := exec.Command("scp", args...) + return cmd.Run() +} + +func ScpUploadFileWithSudo(cfg config.VMConfig, vmIP, src, dest string) error { + // Create a temporary file on the VM to upload the file + tmpFile, err := SshCommand(cfg, vmIP, "mktemp") + if err != nil { + return fmt.Errorf("failed to create temporary file on VM: %w", err) + } + // Use scp to upload file to temporary location + if err = ScpUploadFile(cfg, vmIP, src, tmpFile); err != nil { + return fmt.Errorf("failed to upload file to VM: %w", err) + } + // Move file to destination with sudo + if _, err = SshCommand(cfg, vmIP, fmt.Sprintf("sudo mv %s %s", tmpFile, dest)); err != nil { + return fmt.Errorf("failed to move file on VM: %w", err) + } + return nil +} + +func innerSshCommand(cfg config.VMConfig, vmIP, command string, combineOutput bool, connectionRetryCount int, commandRetryCount int) (string, error) { + sshCliSettings := utils.SshCliSettings{ + PrivateKeyPath: cfg.SshPrivateKeyPath, + Host: vmIP, + User: cfg.User, + Port: 22, + Timeout: 5, + } + var err error + client, err := utils.Retry( + time.Second*time.Duration(connectionRetryCount), + time.Second*time.Duration(1), + func(attempt int) (*ssh.Client, error) { + logrus.Tracef("SSH dial to '%s' (attempt %d)", sshCliSettings.FullHost(), attempt) + return utils.OpenSshClient(sshCliSettings) + }, + ) + if err != nil { + return "", fmt.Errorf("failed to create SSH client: %w", err) + } + defer client.Close() + + output, err := utils.Retry( + time.Second*time.Duration(commandRetryCount), + time.Second*time.Duration(1), + func(attempt int) (*string, error) { + session, err := client.NewSession() + if err != nil { + return nil, fmt.Errorf("failed to create SSH session: %w", err) + } + defer session.Close() + + var output []byte + if combineOutput { + output, err = session.CombinedOutput(command) + } else { + output, err = session.Output(command) + } + if err != nil { + return nil, fmt.Errorf("failed to run command '%s': %w\nOutput: %s", command, err, output) + } + + sanitizedOutput := strings.TrimSpace(string(output)) + return &sanitizedOutput, nil + }, + ) + if err != nil { + return "", fmt.Errorf("failed to run command '%s' on VM '%s': %w", command, vmIP, err) + } + if output == nil { + return "", fmt.Errorf("no output received from command '%s' on VM '%s'", command, vmIP) + } + logrus.Tracef("SSH command '%s' output on VM '%s': %s", command, vmIP, *output) + return *output, nil +} diff --git a/tools/storm/utils/vmip.go b/tools/storm/utils/vmip.go new file mode 100644 index 000000000..df5e8fc1a --- /dev/null +++ b/tools/storm/utils/vmip.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "tridenttools/storm/servicing/utils/config" +) + +func GetVmIP(cfg config.ServicingConfig) (string, error) { + allIps, err := GetAllVmIPAddresses(cfg) + if err != nil { + return "", fmt.Errorf("failed to get all VM IP addresses: %w", err) + } + if len(allIps) == 0 { + return "", fmt.Errorf("no IP addresses found for VM '%s'", cfg.VMConfig.Name) + } + return allIps[0], nil +} + +func GetAllVmIPAddresses(cfg config.ServicingConfig) ([]string, error) { + if cfg.VMConfig.Platform == config.PlatformQEMU { + ips, err := cfg.QemuConfig.GetAllVmIPAddresses(cfg.VMConfig.Name) + if err != nil { + return nil, fmt.Errorf("failed to get QEMU VM IP addresses: %w", err) + } + return ips, nil + } else if cfg.VMConfig.Platform == config.PlatformAzure { + ips, err := cfg.AzureConfig.GetAllVmIPAddresses(cfg.VMConfig.Name, cfg.TestConfig.BuildId) + if err != nil { + return nil, fmt.Errorf("failed to get Azure VM IP addresses: %w", err) + } + return ips, nil + } + return nil, fmt.Errorf("unknown platform: %s", cfg.VMConfig.Platform) +} From d5d922933c5e2269812cec4bbbc21630861e3377 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 16 Jun 2025 22:49:41 +0000 Subject: [PATCH 82/99] Merged PR 23553: engineering: Makefile rule to download functest image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Quick convenience rule to download image. Related work items: #12544 --- Makefile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Makefile b/Makefile index 1eed1d99c..ffde6ce12 100644 --- a/Makefile +++ b/Makefile @@ -420,6 +420,30 @@ run-netlaunch-sample: build-api-docs yq '.os.users += [{"name": "$(shell whoami)", "sshPublicKeys": ["$(shell cat ~/.ssh/id_rsa.pub)"], "sshMode": "key-only", "secondaryGroups": ["wheel"]}] | (.. | select(tag == "!!str")) |= sub("file:///trident_cdrom/data", "http://NETLAUNCH_HOST_ADDRESS/files") | del(.storage.encryption.recoveryKeyUrl) | (.storage.filesystems[] | select(has("source")) | .source).sha256 = "ignored" | .storage.verityFilesystems[].dataImage.sha256 = "ignored" | .storage.verityFilesystems[].hashImage.sha256 = "ignored"' docs/Reference/Host-Configuration/Samples/$(HOST_CONFIG) > $(TMP) TRIDENT_CONFIG=$(TMP) make run-netlaunch +# Downloads the latest Trident functional test image from the Azure DevOps pipeline. +artifacts/trident-functest.qcow2: + $(eval BRANCH ?= main) + $(eval RUN_ID ?= $(shell az pipelines runs list \ + --org "https://dev.azure.com/mariner-org" \ + --project "ECF" \ + --pipeline-ids 3371 \ + --branch $(BRANCH) \ + --query-order QueueTimeDesc \ + --result succeeded \ + --reason triggered \ + --top 1 \ + --query '[0].id')) + @echo PIPELINE RUN ID: $(RUN_ID) + + mkdir -p artifacts + rm -f $@ + az pipelines runs artifact download \ + --org 'https://dev.azure.com/mariner-org' \ + --project "ECF" \ + --run-id $(RUN_ID) \ + --path artifacts/ \ + --artifact-name 'trident-functest' + # Downloads regular, verity, and container COSI images from the latest successful # pipeline run. The images are downloaded to ./artifacts/test-image. .PHONY: download-runtime-images From 0c460abf3c17e28617d34d9b3f415d6608423110 Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Tue, 17 Jun 2025 18:22:21 +0000 Subject: [PATCH 83/99] Merged PR 23554: Refactor to create ESP subsystem and run encryption logic after ESP contents can be accessed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR re-factors the logic inside `engine` and `subsystems` to create a separate `EspSubsystem` and run the encryption logic after the ESP contents have been created and can be accessed. This is required so that a `pcrlock` policy can be created during the staging of the clean install. ---- #### AI description (iteration 1) #### PR Classification This PR refactors the codebase by isolating ESP logic into its own dedicated subsystem. #### PR Summary The pull request reorganizes ESP-related functionality by moving its implementation from the boot subsystem to a new ESP subsystem and updating module references accordingly. - `src/engine/boot/esp.rs` moved to `src/subsystems/esp.rs` without code changes. - `src/engine/boot/mod.rs` now omits direct ESP image deployment and removes the ESP module export. - `src/engine/clean_install.rs` and `src/engine/mod.rs` update imports to reference the new `EspSubsystem`. - Minor parameter renaming in `src/subsystems/storage/mod.rs` and module registration added in `src/subsystems/mod.rs`. Related work items: #12593 --- src/engine/boot/mod.rs | 14 +- src/engine/clean_install.rs | 4 +- src/engine/install_index.rs | 177 +++++++++++++++ src/engine/mod.rs | 3 + src/engine/storage/encryption.rs | 6 +- src/engine/storage/mod.rs | 7 +- src/{engine/boot => subsystems}/esp.rs | 286 +++++++------------------ src/subsystems/mod.rs | 1 + src/subsystems/storage/mod.rs | 4 +- 9 files changed, 267 insertions(+), 235 deletions(-) create mode 100644 src/engine/install_index.rs rename src/{engine/boot => subsystems}/esp.rs (79%) diff --git a/src/engine/boot/mod.rs b/src/engine/boot/mod.rs index 946805744..21b7b568b 100644 --- a/src/engine/boot/mod.rs +++ b/src/engine/boot/mod.rs @@ -16,9 +16,8 @@ use crate::{engine::Subsystem, OS_MODIFIER_NEWROOT_PATH}; use super::EngineContext; -pub(super) mod esp; pub(super) mod grub; -pub(super) mod uki; +pub mod uki; pub(crate) const ESP_EXTRACTION_DIRECTORY: &str = VAR_TMP_PATH; @@ -29,17 +28,6 @@ impl Subsystem for BootSubsystem { "boot" } - #[tracing::instrument(name = "boot_provision", skip_all)] - fn provision(&mut self, ctx: &EngineContext, mount_point: &Path) -> Result<(), TridentError> { - // Perform file-based deployment of ESP images, if needed, after filesystems have been - // mounted and initialized. - - // Deploy ESP image - esp::deploy_esp(ctx, mount_point).structured(ServicingError::DeployESPImages)?; - - Ok(()) - } - #[tracing::instrument(name = "boot_configuration", skip_all)] fn configure(&mut self, ctx: &EngineContext) -> Result<(), TridentError> { if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { diff --git a/src/engine/clean_install.rs b/src/engine/clean_install.rs index ed2fb5f38..03f2e4c5e 100644 --- a/src/engine/clean_install.rs +++ b/src/engine/clean_install.rs @@ -25,7 +25,7 @@ use trident_api::{ use crate::{ datastore::DataStore, - engine::{self, boot::esp, bootentries, storage, EngineContext, SUBSYSTEMS}, + engine::{self, bootentries, install_index, storage, EngineContext, SUBSYSTEMS}, monitor_metrics, osimage::OsImage, subsystems::hooks::HooksSubsystem, @@ -228,7 +228,7 @@ fn stage_clean_install( &ctx.partition_paths, AbVolumeSelection::VolumeA, )?; - ctx.install_index = esp::next_install_index(newroot_mount.path())?; + ctx.install_index = install_index::next_install_index(newroot_mount.path())?; engine::provision(subsystems, &ctx, newroot_mount.path())?; diff --git a/src/engine/install_index.rs b/src/engine/install_index.rs new file mode 100644 index 000000000..c26171b5a --- /dev/null +++ b/src/engine/install_index.rs @@ -0,0 +1,177 @@ +use std::path::Path; + +use log::{debug, trace}; + +use trident_api::{ + constants::{ESP_EFI_DIRECTORY, ESP_RELATIVE_MOUNT_POINT_PATH}, + error::{ReportError, TridentError, TridentResultExt, UnsupportedConfigurationError}, +}; + +use crate::engine::boot; + +/// Returns the next available install index for the current install. +pub fn next_install_index(mount_point: &Path) -> Result { + let esp_efi_path = mount_point + .join(ESP_RELATIVE_MOUNT_POINT_PATH) + .join(ESP_EFI_DIRECTORY); + + debug!( + "Looking for next available install index in '{}'", + esp_efi_path.display() + ); + let first_available_install_index = find_first_available_install_index(&esp_efi_path) + .message("Failed to find the first available install index")?; + + debug!("Selected first available install index: '{first_available_install_index}'",); + Ok(first_available_install_index) +} + +/// Tries to find the next available AzL install index by looking at the +/// ESP directory names present in the specified ESP EFI path. +fn find_first_available_install_index(esp_efi_path: &Path) -> Result { + Ok(boot::make_esp_dir_name_candidates() + // Take a limited number of candidates to avoid an infinite loop. + .take(1000) + // Go over all the candidates and find the first one that doesn't exist. + .find(|(idx, dir_names)| { + trace!("Checking if an install with index '{}' exists", idx); + // Returns true if all possible ESP directory names for this index + // do NOT exist. + dir_names.iter().all(|dir_names| { + let path = esp_efi_path.join(dir_names); + trace!("Checking if path '{}' exists", path.display()); + !path.exists() + }) + }) + .structured(UnsupportedConfigurationError::NoAvailableInstallIndex)? + .0) +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::fs; + use tempfile::TempDir; + + use crate::engine::boot::make_esp_dir_name_candidates; + + /// Simple case for find_first_available_install_index + #[test] + fn test_find_first_available_install_index_simple() { + let test_dir = TempDir::new().unwrap(); + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 0, "First available index should be 0"); + } + + /// Test that find_first_available_install_index will skip unavailable + /// indices + #[test] + fn test_find_first_available_install_index_existing_all() { + let test_dir = TempDir::new().unwrap(); + + // Create all ESP directories for indices 0-9 + make_esp_dir_name_candidates() + .take(10) + .for_each(|(_, dir_names)| { + for dir_name in dir_names { + fs::create_dir(test_dir.path().join(dir_name)).unwrap(); + } + }); + + // The first available index should be 10 + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 10, "First available index should be 10"); + } + + /// Test that find_first_available_install_index will skip unavailable + /// indices, even when only the A volume IDs are present + #[test] + fn test_find_first_available_install_index_existing_a() { + let test_dir = TempDir::new().unwrap(); + + // Create Volume A ESP directories for indices 0-9 + make_esp_dir_name_candidates() + .take(10) + .for_each(|(_, dir_names)| { + fs::create_dir(test_dir.path().join(&dir_names[0])).unwrap(); + }); + + // The first available index should be 10 + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 10, "First available index should be 10"); + } + + /// Test that find_first_available_install_index will skip unavailable + /// indices, even when only the B volume IDs are present + #[test] + fn test_find_first_available_install_index_existing_b() { + let test_dir = TempDir::new().unwrap(); + + // Create Volume B ESP directories for indices 0-9 + make_esp_dir_name_candidates() + .take(10) + .for_each(|(_, dir_names)| { + fs::create_dir(test_dir.path().join(&dir_names[1])).unwrap(); + }); + + // The first available index should be 10 + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 10, "First available index should be 10"); + } + + /// Test that find_first_available_install_index will skip unavailable + /// indices, even when only ONE ID is present per install. + #[test] + fn test_find_first_available_install_index_existing_mixed_1() { + let test_dir = TempDir::new().unwrap(); + + // Iterator to cycle between 0 and 1 + let mut volume_selector = (0..=1).cycle(); + + // Create alternating A/B Volume ESP directories for indices 0-9, starting with A + make_esp_dir_name_candidates() + .take(10) + .for_each(|(_, dir_names)| { + fs::create_dir( + test_dir + .path() + .join(&dir_names[volume_selector.next().unwrap()]), + ) + .unwrap(); + }); + + // The first available index should be 10 + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 10, "First available index should be 10"); + } + + /// Test that find_first_available_install_index will skip unavailable + /// indices, even when only ONE ID is present per install. + #[test] + fn test_find_first_available_install_index_existing_mixed_2() { + let test_dir = TempDir::new().unwrap(); + + // Iterator to cycle between 0 and 1 + let mut volume_selector = (0..=1).cycle(); + + // Advance the volume selector to start with B + volume_selector.next(); + + // Create alternating A/B Volume ESP directories for indices 0-9, starting with B + make_esp_dir_name_candidates() + .take(10) + .for_each(|(_, dir_names)| { + fs::create_dir( + test_dir + .path() + .join(&dir_names[volume_selector.next().unwrap()]), + ) + .unwrap(); + }); + + // The first available index should be 10 + let index = find_first_available_install_index(test_dir.path()).unwrap(); + assert_eq!(index, 10, "First available index should be 10"); + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 838718b5a..fa82272b0 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -21,6 +21,7 @@ use trident_api::{ use crate::{ engine::boot::BootSubsystem, subsystems::{ + esp::EspSubsystem, hooks::HooksSubsystem, initrd::InitrdSubsystem, management::ManagementSubsystem, @@ -48,6 +49,7 @@ pub mod storage; // Helper modules mod etc_overlay; +pub(crate) mod install_index; pub(crate) use clean_install::{clean_install, finalize_clean_install}; pub(crate) use context::{filesystem, EngineContext}; @@ -104,6 +106,7 @@ pub(crate) trait Subsystem: Send { lazy_static::lazy_static! { static ref SUBSYSTEMS: Mutex>> = Mutex::new(vec![ Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/src/engine/storage/encryption.rs b/src/engine/storage/encryption.rs index adbc315dd..b476ad6a7 100644 --- a/src/engine/storage/encryption.rs +++ b/src/engine/storage/encryption.rs @@ -60,9 +60,9 @@ enum EncryptionType { Reencrypt, } -/// Provisions all configured encrypted volumes. -#[tracing::instrument(name = "encryption_provision", fields(total_partition_size_bytes = tracing::field::Empty), skip_all)] -pub(super) fn provision( +/// Sets up and opens encrypted devices. +#[tracing::instrument(name = "encrypted_devices_creation", fields(total_partition_size_bytes = tracing::field::Empty), skip_all)] +pub(super) fn create_encrypted_devices( ctx: &EngineContext, host_config: &HostConfiguration, ) -> Result<(), TridentError> { diff --git a/src/engine/storage/mod.rs b/src/engine/storage/mod.rs index 13ea77e91..65c84bc9a 100644 --- a/src/engine/storage/mod.rs +++ b/src/engine/storage/mod.rs @@ -20,8 +20,6 @@ pub mod verity; use super::EngineContext; -const ENCRYPTION_SUBSYSTEM_NAME: &str = "encryption"; - #[tracing::instrument(skip_all)] pub(super) fn create_block_devices(ctx: &mut EngineContext) -> Result<(), TridentError> { trace!( @@ -39,9 +37,8 @@ pub(super) fn create_block_devices(ctx: &mut EngineContext) -> Result<(), Triden partitioning::create_partitions(ctx).structured(ServicingError::CreatePartitions)?; raid::create_sw_raid(ctx, &ctx.spec).structured(ServicingError::CreateRaid)?; - encryption::provision(ctx, &ctx.spec).message(format!( - "Step 'Provision' failed for subsystem '{ENCRYPTION_SUBSYSTEM_NAME}'" - ))?; + encryption::create_encrypted_devices(ctx, &ctx.spec) + .message("Failed to create and open encrypted devices")?; Ok(()) } diff --git a/src/engine/boot/esp.rs b/src/subsystems/esp.rs similarity index 79% rename from src/engine/boot/esp.rs rename to src/subsystems/esp.rs index 75a565d09..b604380d6 100644 --- a/src/engine/boot/esp.rs +++ b/src/subsystems/esp.rs @@ -18,17 +18,17 @@ use osutils::{ path, }; use trident_api::{ - constants::internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, - error::{ReportError, TridentError, TridentResultExt, UnsupportedConfigurationError}, -}; - -use crate::engine::{ - boot::{uki, ESP_EXTRACTION_DIRECTORY}, constants::{ + internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, EFI_DEFAULT_BIN_RELATIVE_PATH, ESP_EFI_DIRECTORY, ESP_RELATIVE_MOUNT_POINT_PATH, GRUB2_CONFIG_FILENAME, GRUB2_CONFIG_RELATIVE_PATH, }, - EngineContext, + error::{ReportError, ServicingError, TridentError}, +}; + +use crate::engine::{ + boot::{self, uki, ESP_EXTRACTION_DIRECTORY}, + EngineContext, Subsystem, }; /// Bootloader executables @@ -36,6 +36,68 @@ const BOOT_EFI: &str = BootloaderExecutable::Boot.current_name(); const GRUB_EFI: &str = BootloaderExecutable::Grub.current_name(); const GRUB_NOPREFIX_EFI: &str = BootloaderExecutable::GrubNoPrefix.current_name(); +#[derive(Default, Debug)] +pub struct EspSubsystem; +impl Subsystem for EspSubsystem { + fn name(&self) -> &'static str { + "esp" + } + + #[tracing::instrument(name = "esp_provision", skip_all)] + fn provision(&mut self, ctx: &EngineContext, mount_path: &Path) -> Result<(), TridentError> { + // Perform file-based deployment of ESP images, if needed, after filesystems have been + // mounted and initialized. + + // Deploy ESP image + deploy_esp(ctx, mount_path).structured(ServicingError::DeployESPImages)?; + + Ok(()) + } +} + +/// Performs file-based deployment of ESP images from the OS image. +fn deploy_esp(ctx: &EngineContext, mount_point: &Path) -> Result<(), Error> { + trace!("Deploying ESP from OS image"); + + let os_image = ctx + .image + .as_ref() + .context("OS image is required to deploy ESP from OS image")?; + + let esp_img = os_image + .esp_filesystem() + .context("Failed to get ESP image from OS image")?; + + let stream = esp_img + .image_file + .reader() + .context("Failed to get reader for ESP image from OS image")?; + + // Extract the ESP image to a temporary file in + // `/ESP_EXTRACTION_DIRECTORY`. This location is generally + // guaranteed to be writable and backed by a real block device, so we don't + // have to store a potentially large ESP image in memory. + let esp_extraction_dir = path::join_relative(mount_point, ESP_EXTRACTION_DIRECTORY); + + let (temp_file, computed_sha384) = load_raw_image( + &esp_extraction_dir, + os_image.source(), + HashingReader384::new(stream), + ) + .context("Failed to load raw image")?; + + if esp_img.image_file.sha384 != computed_sha384 { + bail!( + "SHA384 mismatch for disk image {}: expected {}, got {}", + os_image.source(), + esp_img.image_file.sha384, + computed_sha384 + ); + } + + copy_file_artifacts(temp_file.path(), ctx, mount_point) +} + /// Takes in a reader to the raw zstd-compressed ESP image and decompresses it /// into a temporary file under `//`. /// Returns a tuple containing the temporary file and the computed hash (SHA256 @@ -284,22 +346,6 @@ fn generate_boot_filepaths(temp_mount_dir: &Path) -> Result, Error> Ok(paths) } -pub fn next_install_index(mount_point: &Path) -> Result { - let esp_efi_path = mount_point - .join(ESP_RELATIVE_MOUNT_POINT_PATH) - .join(ESP_EFI_DIRECTORY); - - debug!( - "Looking for next available install index in '{}'", - esp_efi_path.display() - ); - let first_available_install_index = find_first_available_install_index(&esp_efi_path) - .message("Failed to find the first available install index")?; - - debug!("Selected first available install index: '{first_available_install_index}'",); - Ok(first_available_install_index) -} - /// Returns the path to the ESP directory where the boot files need to be copied to. /// /// Path will be in the form of `/boot/efi/EFI/`, where `` is the install ID as determined @@ -307,7 +353,7 @@ pub fn next_install_index(mount_point: &Path) -> Result { /// /// The function will find the next available install ID for this install and update the install /// index in the engine context. -pub fn generate_efi_bin_base_dir_path( +fn generate_efi_bin_base_dir_path( ctx: &EngineContext, mount_point: &Path, ) -> Result { @@ -318,76 +364,12 @@ pub fn generate_efi_bin_base_dir_path( // Return the path to the ESP directory with the ESP dir name Ok( - esp_efi_path.join(super::get_update_esp_dir_name(ctx).context( + esp_efi_path.join(boot::get_update_esp_dir_name(ctx).context( "Failed to get ESP directory name for the new OS. Engine context is in an invalid state.", )?), ) } -/// Tries to find the next available AzL install index by looking at the -/// ESP directory names present in the specified ESP EFI path. -fn find_first_available_install_index(esp_efi_path: &Path) -> Result { - Ok(super::make_esp_dir_name_candidates() - // Take a limited number of candidates to avoid an infinite loop. - .take(1000) - // Go over all the candidates and find the first one that doesn't exist. - .find(|(idx, dir_names)| { - trace!("Checking if an install with index '{}' exists", idx); - // Returns true if all possible ESP directory names for this index - // do NOT exist. - dir_names.iter().all(|dir_names| { - let path = esp_efi_path.join(dir_names); - trace!("Checking if path '{}' exists", path.display()); - !path.exists() - }) - }) - .structured(UnsupportedConfigurationError::NoAvailableInstallIndex)? - .0) -} - -/// Performs file-based deployment of ESP images from the OS image. -pub(super) fn deploy_esp(ctx: &EngineContext, mount_point: &Path) -> Result<(), Error> { - trace!("Deploying ESP from OS image"); - - let os_image = ctx - .image - .as_ref() - .context("OS image is required to deploy ESP from OS image")?; - - let esp_img = os_image - .esp_filesystem() - .context("Failed to get ESP image from OS image")?; - - let stream = esp_img - .image_file - .reader() - .context("Failed to get reader for ESP image from OS image")?; - - // Extract the ESP image to a temporary file in - // `/ESP_EXTRACTION_DIRECTORY`. This location is generally - // guaranteed to be writable and backed by a real block device, so we don't - // have to store a potentially large ESP image in memory. - let esp_extraction_dir = path::join_relative(mount_point, ESP_EXTRACTION_DIRECTORY); - - let (temp_file, computed_sha384) = load_raw_image( - &esp_extraction_dir, - os_image.source(), - HashingReader384::new(stream), - ) - .context("Failed to load raw image")?; - - if esp_img.image_file.sha384 != computed_sha384 { - bail!( - "SHA384 mismatch for disk image {}: expected {}, got {}", - os_image.source(), - esp_img.image_file.sha384, - computed_sha384 - ); - } - - copy_file_artifacts(temp_file.path(), ctx, mount_point) -} - #[cfg(test)] mod tests { use super::*; @@ -401,126 +383,10 @@ mod tests { status::{AbVolumeSelection, ServicingType}, }; - use crate::engine::boot::{get_update_esp_dir_name, make_esp_dir_name_candidates}; - - /// Simple case for find_first_available_install_index - #[test] - fn test_find_first_available_install_index_simple() { - let test_dir = TempDir::new().unwrap(); - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 0, "First available index should be 0"); - } - - /// Test that find_first_available_install_index will skip unavailable - /// indices - #[test] - fn test_find_first_available_install_index_existing_all() { - let test_dir = TempDir::new().unwrap(); - - // Create all ESP directories for indices 0-9 - make_esp_dir_name_candidates() - .take(10) - .for_each(|(_, dir_names)| { - for dir_name in dir_names { - fs::create_dir(test_dir.path().join(dir_name)).unwrap(); - } - }); - - // The first available index should be 10 - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 10, "First available index should be 10"); - } - - /// Test that find_first_available_install_index will skip unavailable - /// indices, even when only the A volume IDs are present - #[test] - fn test_find_first_available_install_index_existing_a() { - let test_dir = TempDir::new().unwrap(); - - // Create Volume A ESP directories for indices 0-9 - make_esp_dir_name_candidates() - .take(10) - .for_each(|(_, dir_names)| { - fs::create_dir(test_dir.path().join(&dir_names[0])).unwrap(); - }); - - // The first available index should be 10 - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 10, "First available index should be 10"); - } - - /// Test that find_first_available_install_index will skip unavailable - /// indices, even when only the B volume IDs are present - #[test] - fn test_find_first_available_install_index_existing_b() { - let test_dir = TempDir::new().unwrap(); - - // Create Volume B ESP directories for indices 0-9 - make_esp_dir_name_candidates() - .take(10) - .for_each(|(_, dir_names)| { - fs::create_dir(test_dir.path().join(&dir_names[1])).unwrap(); - }); - - // The first available index should be 10 - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 10, "First available index should be 10"); - } - - /// Test that find_first_available_install_index will skip unavailable - /// indices, even when only ONE ID is present per install. - #[test] - fn test_find_first_available_install_index_existing_mixed_1() { - let test_dir = TempDir::new().unwrap(); - - // Iterator to cycle between 0 and 1 - let mut volume_selector = (0..=1).cycle(); - - // Create alternating A/B Volume ESP directories for indices 0-9, starting with A - make_esp_dir_name_candidates() - .take(10) - .for_each(|(_, dir_names)| { - fs::create_dir( - test_dir - .path() - .join(&dir_names[volume_selector.next().unwrap()]), - ) - .unwrap(); - }); - - // The first available index should be 10 - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 10, "First available index should be 10"); - } - - /// Test that find_first_available_install_index will skip unavailable - /// indices, even when only ONE ID is present per install. - #[test] - fn test_find_first_available_install_index_existing_mixed_2() { - let test_dir = TempDir::new().unwrap(); - - // Iterator to cycle between 0 and 1 - let mut volume_selector = (0..=1).cycle(); - - // Advance the volume selector to start with B - volume_selector.next(); - - // Create alternating A/B Volume ESP directories for indices 0-9, starting with B - make_esp_dir_name_candidates() - .take(10) - .for_each(|(_, dir_names)| { - fs::create_dir( - test_dir - .path() - .join(&dir_names[volume_selector.next().unwrap()]), - ) - .unwrap(); - }); - - // The first available index should be 10 - let index = find_first_available_install_index(test_dir.path()).unwrap(); - assert_eq!(index, 10, "First available index should be 10"); - } + use crate::engine::{ + boot::{get_update_esp_dir_name, make_esp_dir_name_candidates}, + install_index, + }; #[test] fn test_generate_efi_bin_base_dir_path_clean_install() { @@ -546,7 +412,7 @@ mod tests { idx, test_dir.path().display() ); - ctx.install_index = next_install_index(test_dir.path()).unwrap(); + ctx.install_index = install_index::next_install_index(test_dir.path()).unwrap(); assert_eq!(idx, ctx.install_index); let esp_dir_path = generate_efi_bin_base_dir_path(&ctx, test_dir.path()).unwrap(); diff --git a/src/subsystems/mod.rs b/src/subsystems/mod.rs index 90a8cf4a9..2f882bf87 100644 --- a/src/subsystems/mod.rs +++ b/src/subsystems/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod esp; pub(crate) mod hooks; pub(crate) mod initrd; pub(crate) mod management; diff --git a/src/subsystems/storage/mod.rs b/src/subsystems/storage/mod.rs index a7c41d473..af2d4478b 100644 --- a/src/subsystems/storage/mod.rs +++ b/src/subsystems/storage/mod.rs @@ -166,12 +166,12 @@ impl Subsystem for StorageSubsystem { Ok(Some(ServicingType::NoActiveServicing)) } - fn provision(&mut self, ctx: &EngineContext, mount_point: &Path) -> Result<(), TridentError> { + fn provision(&mut self, ctx: &EngineContext, mount_path: &Path) -> Result<(), TridentError> { if ctx.servicing_type == ServicingType::CleanInstall && ctx.storage_graph.root_fs_is_verity() { debug!("Root verity is enabled, setting up machine-id"); - verity::create_machine_id(mount_point).structured(ServicingError::CreateMachineId)?; + verity::create_machine_id(mount_path).structured(ServicingError::CreateMachineId)?; } Ok(()) From a13e1002d79aaa77b304cd0b8d29cb758d29a899 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Tue, 17 Jun 2025 21:11:51 +0000 Subject: [PATCH 84/99] Merged PR 23514: Verity size validation does not account for hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Ensure verity hash is factored into size validation. In existing size validation, check if the fs is verity and compare uncompressed hash file size to configured hash device size. ---- #### AI description (iteration 1) #### PR Classification Bug fix addressing an error in filesystem size validation for verity devices. #### PR Summary This PR updates the storage size calculation for verity devices to include both the data and hash device sizes, ensuring accurate filesystem validation. - `trident_api/src/config/host/storage/storage_graph/graph.rs`: Modified the VerityDevice branch in block_device_size to compute and sum the sizes for both data and hash devices. - `trident_api/src/config/host/storage/storage_graph/graph.rs`: Updated test assertions to validate that the verity device size equals the combined sizes of data and hash devices. Related work items: #12062 --- src/subsystems/storage/osimage.rs | 129 +++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/src/subsystems/storage/osimage.rs b/src/subsystems/storage/osimage.rs index 5bc587c85..191cf0fc8 100644 --- a/src/subsystems/storage/osimage.rs +++ b/src/subsystems/storage/osimage.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet}, - path::Path, + path::{Path, PathBuf}, }; use const_format::formatcp; @@ -408,6 +408,16 @@ fn validate_filesystem_blkdev_fit( let fs_size = fs.image_file.uncompressed_size; trace!("The size of the filesystem associated with block device '{device_id}' is {fs_size} bytes."); + if let Some(fs_verity) = fs.verity.as_ref() { + // If the filesystem has a verity hash, we need to check the size of the + // block device that will contain the verity hash. + validate_hash_filesystem_blkdev_fit( + fs_verity.hash_image_file.uncompressed_size, + fs.mount_point.clone(), + graph, + )?; + } + let Some(blkdev_size) = graph.block_device_size(device_id) else { debug!("Could not find the size of the block device with id '{device_id}'. Block device may not have a fixed size."); continue; @@ -435,6 +445,53 @@ fn validate_filesystem_blkdev_fit( Ok(()) } +/// Validate that the size of a verity filesystem hash fits in the configured block device. +fn validate_hash_filesystem_blkdev_fit( + fs_verity_hash_file_size: u64, + fs_mount_point: PathBuf, + graph: &StorageGraph, +) -> Result<(), TridentError> { + // Get the verity block device corresponding to the filesystem + let verity_device = graph + .verity_device_for_filesystem(&fs_mount_point) + .structured(InternalError::Internal( + "No verity device found for mount point", + )) + .message(format!( + "Failed to find verity device for filesystem mounted at '{}'", + fs_mount_point.display() + ))?; + + // Get the size of the block device configured for the verity hash + let Some(blkdev_hash_size) = graph.block_device_size(&verity_device.hash_device_id) else { + debug!( + "Could not find the size of the block device with id '{}'. Block device may not have a fixed size.", + verity_device.hash_device_id + ); + return Ok(()); + }; + + debug!( + "Found filesystem with verity hash of size {} and block device with size {} for device '{}'", + ByteCount::from(fs_verity_hash_file_size).to_human_readable_approx(), + ByteCount::from(blkdev_hash_size).to_human_readable_approx(), + &verity_device.hash_device_id, + ); + + // Ensure that the filesystem hash will fit in the block device + if fs_verity_hash_file_size > blkdev_hash_size { + return Err(TridentError::new( + InvalidInputError::FilesystemSizeExceedsBlockDevice { + mount_point: fs_mount_point.to_string_lossy().into(), + device_id: verity_device.hash_device_id.to_string(), + fs_size: ByteCount::from(fs_verity_hash_file_size), + device_size: ByteCount::from(blkdev_hash_size), + }, + )); + }; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -1058,6 +1115,76 @@ mod tests { "Expected UnusedOsImageFilesystem error" ); } + + #[test] + fn test_validate_hash_filesystem_blkdev_fit() { + let required_size_gb = 1; + let required_partition_size = + PartitionSize::from_str(format!("{required_size_gb}G").as_str()).unwrap(); + let too_big_size_gb = 2; + let too_big_partition_size = + PartitionSize::from_str(format!("{too_big_size_gb}G").as_str()).unwrap(); + let mount_point = "/mnt/path/verity"; + let fs_mount_point = PathBuf::from(mount_point); + let graph = Storage { + disks: vec![Disk { + device: "/dev/sda".into(), + partitions: vec![ + Partition { + id: "data".into(), + partition_type: Default::default(), + size: required_partition_size, + }, + Partition { + id: "hash".into(), + partition_type: Default::default(), + size: required_partition_size, + }, + ], + ..Default::default() + }], + verity: vec![VerityDevice { + id: "verity".into(), + name: "verity".into(), + data_device_id: "data".into(), + hash_device_id: "hash".into(), + ..Default::default() + }], + filesystems: vec![FileSystem { + device_id: Some("verity".into()), + source: FileSystemSource::Image, + mount_point: Some(MountPoint::from_str(mount_point).unwrap()), + }], + ..Default::default() + } + .build_graph() + .unwrap(); + + // Test with exact matching block device size + validate_hash_filesystem_blkdev_fit( + required_partition_size.to_bytes().unwrap(), + fs_mount_point.clone(), + &graph, + ) + .unwrap(); + + // Test with too small block device size + let err = validate_hash_filesystem_blkdev_fit( + too_big_partition_size.to_bytes().unwrap(), + fs_mount_point.clone(), + &graph, + ) + .unwrap_err(); + assert_eq!( + err.kind(), + &ErrorKind::InvalidInput(InvalidInputError::FilesystemSizeExceedsBlockDevice { + mount_point: mount_point.to_string(), + device_id: "hash".to_string(), + fs_size: ByteCount::from(too_big_partition_size.to_bytes().unwrap()), + device_size: ByteCount::from(required_partition_size.to_bytes().unwrap()), + }) + ); + } } #[cfg(feature = "functional-test")] From 47e2fc8339c012c22cbce380b40ca5fbdc471dae Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 18 Jun 2025 01:05:37 +0000 Subject: [PATCH 85/99] Merged PR 23505: Add some retry to COSI download logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Endure COSI download failures due to temporary network glitches ---- #### AI description (iteration 1) #### PR Classification This PR introduces a new feature by adding a retry mechanism to the COSI download logic, improving the resiliency of HTTP requests. #### PR Summary The changes refactor the HTTP file requester to use a lambda function with a retry loop for both GET and HEAD operations, ensuring that transient HTTP failures are handled with incremental delays and proper logging. - `src/osimage/cosi/reader.rs`: Modified the `reader` function to encapsulate HTTP requests in a lambda and route them through a new `retriable_request_sender` that retries failed requests. - `src/osimage/cosi/reader.rs`: Implemented the `retriable_request_sender` function with a configurable retry count (set to 10), incremental sleep intervals, and warning logs for failed attempts. - `src/osimage/cosi/reader.rs`: Updated the `HttpFile` struct and its initialization to include and pass the retry count for improved download robustness. Related work items: #9518, #11001 --- src/osimage/cosi/reader.rs | 158 ++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 28 deletions(-) diff --git a/src/osimage/cosi/reader.rs b/src/osimage/cosi/reader.rs index eef653395..4eafd34c7 100644 --- a/src/osimage/cosi/reader.rs +++ b/src/osimage/cosi/reader.rs @@ -4,10 +4,12 @@ use std::{ fs::File, io::{Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom}, path::PathBuf, + thread, + time::{Duration, Instant}, }; use anyhow::{bail, ensure, Error}; -use log::{debug, trace}; +use log::{debug, trace, warn}; use reqwest::blocking::{Client, Response}; use url::Url; @@ -113,6 +115,7 @@ pub struct HttpFile { position: u64, size: u64, client: Client, + timeout_in_seconds: u64, } impl HttpFile { @@ -120,17 +123,12 @@ impl HttpFile { pub fn new(url: &Url) -> IoResult { debug!("Opening HTTP file '{}'", url); + let timeout_in_seconds = 5; + // Create a new client for this file. let client = Client::new(); - - // Query the server for the file size - let response = client - .head(url.as_str()) - .send() - .map_err(Self::http_to_io_err)? - .error_for_status() - .map_err(Self::http_to_io_err)?; - + let request_sender = || client.head(url.as_str()).send(); + let response = Self::retriable_request_sender(request_sender, timeout_in_seconds)?; trace!("HTTP file '{}' has status: {}", url, response.status()); // Get the file size from the response headers @@ -189,6 +187,7 @@ impl HttpFile { position: 0, size, client, + timeout_in_seconds, }) } @@ -218,26 +217,73 @@ impl HttpFile { /// Performs a request with optional range headers to get the file content. /// Returns the HTTP response. fn reader(&self, start: Option, end: Option) -> IoResult { - let mut request = self.client.get(self.url.as_str()); - - // Generate the range header when appropriate - let range_header = match (start, end) { - (Some(start), Some(end)) => Some(format!("bytes={}-{}", start, end)), - (Some(start), None) => Some(format!("bytes={}-", start)), - (None, Some(end)) => Some(format!("bytes=0-{}", end)), - (None, None) => None, + let request_sender = || { + let mut request = self.client.get(self.url.as_str()); + + // Generate the range header when appropriate + let range_header = match (start, end) { + (Some(start), Some(end)) => Some(format!("bytes={}-{}", start, end)), + (Some(start), None) => Some(format!("bytes={}-", start)), + (None, Some(end)) => Some(format!("bytes=0-{}", end)), + (None, None) => None, + }; + + // Add the range header to the request + if let Some(range) = range_header { + request = request.header("Range", range); + } + + request.send() }; - // Add the range header to the request - if let Some(range) = range_header { - request = request.header("Range", range); - } + Self::retriable_request_sender(request_sender, self.timeout_in_seconds) + } - request - .send() - .map_err(Self::http_to_io_err)? - .error_for_status() - .map_err(Self::http_to_io_err) + /// Performs an HTTP request and retries it for up to `timeout_in_seconds` if + /// it fails. The HTTP request is created and invoked by `request_sender`, a + /// closure that that returns a `reqwest::Result`. If the request is + /// successful, it returns the response. If the request fails after all retries, + /// it returns an IO error. + fn retriable_request_sender(request_sender: F, timeout_in_seconds: u64) -> IoResult + where + F: Fn() -> reqwest::Result, + { + let mut retry = 0; + let now = Instant::now(); + let timeout_time = now + Duration::from_secs(timeout_in_seconds); + let mut sleep_duration = Duration::from_millis(10); + loop { + if retry != 0 { + trace!("Retrying HTTP request (attempt {})", retry + 1); + } + match request_sender() { + Ok(response) => { + if response.status().is_success() { + return Ok(response); + } else if std::time::Instant::now() > timeout_time { + return response.error_for_status().map_err(Self::http_to_io_err); + } else { + warn!("HTTP request failed with status: {}", response.status()); + } + } + Err(e) => { + if Instant::now() > timeout_time { + return Err(Self::http_to_io_err(e)); + } + warn!("HTTP request failed: {}", e); + } + }; + // Sleep for a short duration before retrying + if Instant::now() + sleep_duration > timeout_time { + return Err(IoError::new( + IoErrorKind::TimedOut, + "HTTP request timed out", + )); + } + thread::sleep(sleep_duration); + sleep_duration *= 2; + retry += 1; + } } /// Performs a request of a specific section of the file. Returns the HTTP @@ -345,7 +391,62 @@ mod tests { use super::*; - use std::io::{SeekFrom, Write}; + use std::{ + io::{SeekFrom, Write}, + sync::{ + atomic::{AtomicU16, Ordering}, + Arc, + }, + }; + + #[test] + fn test_retriable_request_sender_retry_count() { + let tries = Arc::new(AtomicU16::new(0)); + let closure_tries = tries.clone(); + let request_sender = || { + closure_tries.fetch_add(1, Ordering::SeqCst); + let client = Client::new(); + client.get("").send() + }; + HttpFile::retriable_request_sender(request_sender, 2).unwrap_err(); + assert!(tries.load(Ordering::SeqCst) > 1); + } + + #[test] + fn test_retriable_request_sender_initial_failure() { + let relative_file_path = "/test.yaml"; + let mut server = mockito::Server::new(); + let data = "test document"; + let document_mock = server + .mock("GET", relative_file_path) + .with_body(data) + .with_header("content-length", &data.len().to_string()) + .with_header("content-type", "text/plain") + .with_status(200) + .expect(1) + .create(); + let url = Url::parse(&server.url()).unwrap(); + let request_url = url.join(relative_file_path).unwrap().to_string(); + + let tries = Arc::new(AtomicU16::new(0)); + let closure_tries = tries.clone(); + let request_sender = || { + closure_tries.fetch_add(1, Ordering::SeqCst); + if closure_tries.load(Ordering::SeqCst) < 2 { + let client = Client::new(); + return client.get("").send(); + } + let client = Client::new(); + client.get(&request_url).send() + }; + let document = HttpFile::retriable_request_sender(request_sender, 5) + .unwrap() + .text() + .unwrap(); + assert!(tries.load(Ordering::SeqCst) > 1); + assert_eq!(document, data); + document_mock.assert(); + } #[test] fn test_http_file_seek() { @@ -354,6 +455,7 @@ mod tests { position: 0, size: 100, // We have indices from 0 to 99 client: Client::new(), + timeout_in_seconds: 1, }; assert_eq!(http_file.seek(SeekFrom::Start(50)).unwrap(), 50); From c6c4b2a42cd4438a3aa9871828ec0dbb6301b8bc Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Wed, 18 Jun 2025 16:54:30 +0000 Subject: [PATCH 86/99] Merged PR 23573: engineering: Add swap to storage graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Adding swap mounts to the storage graph as their own pseudo-fs non-device variant. Related work items: #12600 --- .../Host-Configuration/API-Reference/.order | 2 +- .../API-Reference/Storage.md | 12 +++--- .../API-Reference/{SwapDevice.md => Swap.md} | 4 +- .../Host-Configuration/Storage-Rules.md | 25 ++++++------ docs/resources/trident-install.svg | 2 +- setsail/src/translator/partitions.rs | 6 +-- src/subsystems/storage/fstab.rs | 10 ++--- trident_api/schemas/host-config-schema.json | 8 ++-- trident_api/src/config/host/storage/mod.rs | 10 +++-- .../host/storage/storage_graph/conversions.rs | 9 ++++- .../host/storage/storage_graph/display.rs | 3 +- .../host/storage/storage_graph/error.rs | 1 + .../host/storage/storage_graph/graph.rs | 6 --- .../config/host/storage/storage_graph/node.rs | 39 ++++++++++++++++--- .../host/storage/storage_graph/rules/mod.rs | 18 ++++----- .../host/storage/storage_graph/types.rs | 21 +++------- trident_api/src/config/host/storage/swap.rs | 10 ++--- trident_api/src/config/mod.rs | 2 +- trident_api/src/samples/sample_hc.rs | 14 +++---- 19 files changed, 110 insertions(+), 92 deletions(-) rename docs/Reference/Host-Configuration/API-Reference/{SwapDevice.md => Swap.md} (85%) diff --git a/docs/Reference/Host-Configuration/API-Reference/.order b/docs/Reference/Host-Configuration/API-Reference/.order index 473873a10..348f9aca1 100644 --- a/docs/Reference/Host-Configuration/API-Reference/.order +++ b/docs/Reference/Host-Configuration/API-Reference/.order @@ -34,7 +34,7 @@ ServicingTypeSelection SoftwareRaidArray SshMode Storage -SwapDevice +Swap Trident User VerityCorruptionOption diff --git a/docs/Reference/Host-Configuration/API-Reference/Storage.md b/docs/Reference/Host-Configuration/API-Reference/Storage.md index e037e7c3c..0e4cc4a44 100644 --- a/docs/Reference/Host-Configuration/API-Reference/Storage.md +++ b/docs/Reference/Host-Configuration/API-Reference/Storage.md @@ -77,12 +77,12 @@ Swap device configuration. - Items of the array must have the type: - | Characteristic | Value | - | ---------------- | ----------------------------- | - | Type | `SwapDevice` | - | Link | [SwapDevice](./SwapDevice.md) | - | Shorthand Type | `string` | - | Shorthand Format | `Block Device ID` | + | Characteristic | Value | + | ---------------- | ----------------- | + | Type | `Swap` | + | Link | [Swap](./Swap.md) | + | Shorthand Type | `string` | + | Shorthand Format | `Block Device ID` | ### `verity` (optional) diff --git a/docs/Reference/Host-Configuration/API-Reference/SwapDevice.md b/docs/Reference/Host-Configuration/API-Reference/Swap.md similarity index 85% rename from docs/Reference/Host-Configuration/API-Reference/SwapDevice.md rename to docs/Reference/Host-Configuration/API-Reference/Swap.md index b5238c306..1cda0cdb5 100644 --- a/docs/Reference/Host-Configuration/API-Reference/SwapDevice.md +++ b/docs/Reference/Host-Configuration/API-Reference/Swap.md @@ -1,6 +1,6 @@ -# SwapDevice +# Swap | Characteristic | Value | | -------------- | -------- | @@ -10,7 +10,7 @@ ### `deviceId` **(required)** -The ID of the block device to use for this swap device. +The ID of the block device to use for this swap area. | Characteristic | Value | | -------------- | ----------------- | diff --git a/docs/Reference/Host-Configuration/Storage-Rules.md b/docs/Reference/Host-Configuration/Storage-Rules.md index d32dfc086..aef8ab09f 100644 --- a/docs/Reference/Host-Configuration/Storage-Rules.md +++ b/docs/Reference/Host-Configuration/Storage-Rules.md @@ -36,7 +36,6 @@ configuration, along with their descriptions. | ab-volume | An A/B volume | | encrypted-volume | An encrypted volume | | verity-device | A verity device | -| swap-device | A swap partition | ## Referrer Description @@ -49,7 +48,7 @@ configuration, along with their descriptions. | ab-volume | An A/B volume | | encrypted-volume | An encrypted volume | | verity-device | A verity device | -| swap-device | A swap device | +| swap-device | A swap mount | | filesystem-new | A new filesystem | | filesystem-image | A filesystem from an image | | filesystem-esp | An ESP/EFI filesystem | @@ -65,17 +64,17 @@ that can be referenced. A single cell in the table represents whether a referrer of a certain type can reference a block device of a certain type. -| Referrer ╲ Device | disk | partition | adopted-partition | raid-array | ab-volume | encrypted-volume | verity-device | swap-device | -| ------------------- | ---- | --------- | ----------------- | ---------- | --------- | ---------------- | ------------- | ----------- | -| raid-array | No | Yes | No | No | No | No | No | No | -| ab-volume | No | Yes | No | Yes | No | Yes | No | No | -| encrypted-volume | No | Yes | No | Yes | No | No | No | No | -| verity-device | No | Yes | No | Yes | Yes | No | No | No | -| swap-device | No | Yes | No | No | No | Yes | No | No | -| filesystem-new | No | Yes | No | Yes | Yes | Yes | No | No | -| filesystem-image | No | Yes | No | Yes | Yes | Yes | Yes | No | -| filesystem-esp | No | Yes | Yes | Yes | No | No | No | No | -| filesystem-adopted | No | No | Yes | No | No | No | No | No | +| Referrer ╲ Device | disk | partition | adopted-partition | raid-array | ab-volume | encrypted-volume | verity-device | +| ------------------- | ---- | --------- | ----------------- | ---------- | --------- | ---------------- | ------------- | +| raid-array | No | Yes | No | No | No | No | No | +| ab-volume | No | Yes | No | Yes | No | Yes | No | +| encrypted-volume | No | Yes | No | Yes | No | No | No | +| verity-device | No | Yes | No | Yes | Yes | No | No | +| swap-device | No | Yes | No | No | No | Yes | No | +| filesystem-new | No | Yes | No | Yes | Yes | Yes | No | +| filesystem-image | No | Yes | No | Yes | Yes | Yes | Yes | +| filesystem-esp | No | Yes | Yes | Yes | No | No | No | +| filesystem-adopted | No | No | Yes | No | No | No | No | ## Reference Count diff --git a/docs/resources/trident-install.svg b/docs/resources/trident-install.svg index 847e89b2e..3e5339dd5 100644 --- a/docs/resources/trident-install.svg +++ b/docs/resources/trident-install.svg @@ -466,4 +466,4 @@ Update is finalized on success - \ No newline at end of file + diff --git a/setsail/src/translator/partitions.rs b/setsail/src/translator/partitions.rs index fde81507d..e008053d5 100644 --- a/setsail/src/translator/partitions.rs +++ b/setsail/src/translator/partitions.rs @@ -6,7 +6,7 @@ use std::{ use trident_api::{ config::{ Disk, FileSystem, FileSystemSource, HostConfiguration, MountOptions, MountPoint, - NewFileSystemType, Partition, PartitionSize, PartitionTableType, PartitionType, SwapDevice, + NewFileSystemType, Partition, PartitionSize, PartitionTableType, PartitionType, Swap, }, misc::IdGenerator, }; @@ -28,7 +28,7 @@ pub fn translate(input: &ParsedData, hc: &mut HostConfiguration, errors: &mut Ve let mut filesystems: Vec = Vec::new(); // List of all swap devices - let mut swap_devices: Vec = Vec::new(); + let mut swap_devices: Vec = Vec::new(); // Go over all parsed partitions for part in input.partitions.iter() { @@ -92,7 +92,7 @@ pub fn translate(input: &ParsedData, hc: &mut HostConfiguration, errors: &mut Ve }); if let PartitionMount::Swap = part.mntpoint { - swap_devices.push(SwapDevice { + swap_devices.push(Swap { device_id: partition_id.clone(), }); diff --git a/src/subsystems/storage/fstab.rs b/src/subsystems/storage/fstab.rs index 062b371b8..8370d368b 100644 --- a/src/subsystems/storage/fstab.rs +++ b/src/subsystems/storage/fstab.rs @@ -7,7 +7,7 @@ use osutils::{ filesystems::TabFileSystemType, tabfile::{TabFile, TabFileEntry}, }; -use trident_api::{config::SwapDevice, BlockDeviceId}; +use trident_api::{config::Swap, BlockDeviceId}; use crate::engine::{filesystem::FileSystemData, EngineContext}; @@ -109,7 +109,7 @@ fn entry_from_fs_data( fn entry_from_swap( device_finder: impl Fn(&BlockDeviceId) -> Result, - swap: &SwapDevice, + swap: &Swap, ) -> Result { Ok(TabFileEntry::new_swap(device_finder(&swap.device_id)?)) } @@ -297,7 +297,7 @@ mod tests { assert_eq!( entry_from_swap( device_finder, - &SwapDevice { + &Swap { device_id: "swap".to_owned(), }, ) @@ -319,7 +319,7 @@ mod tests { servicing_type: ServicingType::CleanInstall, spec: HostConfiguration { storage: Storage { - swap: vec![SwapDevice { + swap: vec![Swap { device_id: "swap".to_owned(), }], ..Default::default() @@ -453,7 +453,7 @@ mod tests { source: FileSystemSource::New(NewFileSystemType::Ext4), }, ], - swap: vec![SwapDevice { + swap: vec![Swap { device_id: "swap".to_owned(), }], ..Default::default() diff --git a/trident_api/schemas/host-config-schema.json b/trident_api/schemas/host-config-schema.json index c60de845b..81a5129fd 100644 --- a/trident_api/schemas/host-config-schema.json +++ b/trident_api/schemas/host-config-schema.json @@ -1200,7 +1200,7 @@ "type": "array", "oneOf": [ { - "$ref": "#/definitions/SwapDevice" + "$ref": "#/definitions/Swap" }, { "type": "string" @@ -1209,7 +1209,7 @@ "items": { "oneOf": [ { - "$ref": "#/definitions/SwapDevice" + "$ref": "#/definitions/Swap" }, { "type": "string" @@ -1228,14 +1228,14 @@ }, "additionalProperties": false }, - "SwapDevice": { + "Swap": { "type": "object", "required": [ "deviceId" ], "properties": { "deviceId": { - "description": "The ID of the block device to use for this swap device.", + "description": "The ID of the block device to use for this swap area.", "type": "string", "format": "Block Device ID" } diff --git a/trident_api/src/config/host/storage/mod.rs b/trident_api/src/config/host/storage/mod.rs index 0b334f2cc..4e9bb4312 100644 --- a/trident_api/src/config/host/storage/mod.rs +++ b/trident_api/src/config/host/storage/mod.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "schemars")] use schemars::JsonSchema; -use swap::SwapDevice; +use swap::Swap; use crate::{ constants::{ @@ -87,10 +87,10 @@ pub struct Storage { #[cfg_attr( feature = "schemars", schemars( - schema_with = "crate::primitives::shortcuts::vec_string_or_struct_schema::" + schema_with = "crate::primitives::shortcuts::vec_string_or_struct_schema::" ) )] - pub swap: Vec, + pub swap: Vec, } impl Storage { @@ -157,6 +157,10 @@ impl Storage { builder.add_node(fs.into()); } + for swap in &self.swap { + builder.add_node(swap.into()); + } + // Try to build the graph builder.build() } diff --git a/trident_api/src/config/host/storage/storage_graph/conversions.rs b/trident_api/src/config/host/storage/storage_graph/conversions.rs index b7fa03f73..1b52afacc 100644 --- a/trident_api/src/config/host/storage/storage_graph/conversions.rs +++ b/trident_api/src/config/host/storage/storage_graph/conversions.rs @@ -2,7 +2,7 @@ use crate::config::{ AbVolumePair, AdoptedPartition, Disk, EncryptedVolume, FileSystem, FileSystemSource, Partition, - SoftwareRaidArray, VerityDevice, + SoftwareRaidArray, Swap, VerityDevice, }; use super::{ @@ -95,6 +95,13 @@ impl From<&FileSystem> for StorageGraphNode { } } +/// Get a StorageGraphNode from a SwapDevice reference. +impl From<&Swap> for StorageGraphNode { + fn from(swap: &Swap) -> Self { + Self::new_swap(swap.clone()) + } +} + /// Get a BlkDevReferrerKind from a FileSystem reference. impl From<&FileSystem> for BlkDevReferrerKind { fn from(fs: &FileSystem) -> Self { diff --git a/trident_api/src/config/host/storage/storage_graph/display.rs b/trident_api/src/config/host/storage/storage_graph/display.rs index bf046098f..40bd92e52 100644 --- a/trident_api/src/config/host/storage/storage_graph/display.rs +++ b/trident_api/src/config/host/storage/storage_graph/display.rs @@ -32,7 +32,6 @@ impl Display for BlkDevKind { Self::ABVolume => write!(f, "ab-volume"), Self::EncryptedVolume => write!(f, "encrypted-volume"), Self::VerityDevice => write!(f, "verity-device"), - Self::SwapDevice => write!(f, "swap-device"), } } } @@ -45,7 +44,7 @@ impl Display for BlkDevReferrerKind { Self::ABVolume => write!(f, "ab-volume"), Self::EncryptedVolume => write!(f, "encrypted-volume"), Self::VerityDevice => write!(f, "verity-device"), - Self::SwapDevice => write!(f, "swap-device"), + Self::Swap => write!(f, "swap-device"), Self::FileSystemNew => write!(f, "filesystem-new"), Self::FileSystemEsp => write!(f, "filesystem-esp"), Self::FileSystemAdopted => write!(f, "filesystem-adopted"), diff --git a/trident_api/src/config/host/storage/storage_graph/error.rs b/trident_api/src/config/host/storage/storage_graph/error.rs index c6a8fb668..aba1eaf69 100644 --- a/trident_api/src/config/host/storage/storage_graph/error.rs +++ b/trident_api/src/config/host/storage/storage_graph/error.rs @@ -22,6 +22,7 @@ fn pretty_node_id(node_identifier: &NodeIdentifier) -> String { match node_identifier { NodeIdentifier::BlockDevice(id) => format!("'{}'", id), NodeIdentifier::FileSystem(fs) => format!("filesystem [{}]", fs), + NodeIdentifier::Swap(swap) => format!("swap on '{}'", swap), } } diff --git a/trident_api/src/config/host/storage/storage_graph/graph.rs b/trident_api/src/config/host/storage/storage_graph/graph.rs index e9593e877..7616e9599 100644 --- a/trident_api/src/config/host/storage/storage_graph/graph.rs +++ b/trident_api/src/config/host/storage/storage_graph/graph.rs @@ -323,12 +323,6 @@ fn block_device_size(graph: &StoragePetgraph, idx: NodeIndex) -> Option { // For adopted partitions, we report None, as we don't know the size. HostConfigBlockDevice::AdoptedPartition(_) => None, - - // For swap devices, report the size of the backing device. - HostConfigBlockDevice::SwapDevice(_) => { - let backing_node_idx = graph.neighbors_directed(idx, Direction::Outgoing).next()?; - block_device_size(graph, backing_node_idx) - } } } diff --git a/trident_api/src/config/host/storage/storage_graph/node.rs b/trident_api/src/config/host/storage/storage_graph/node.rs index 2c5d7dfdd..2c87cca64 100644 --- a/trident_api/src/config/host/storage/storage_graph/node.rs +++ b/trident_api/src/config/host/storage/storage_graph/node.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::{config::FileSystem, BlockDeviceId}; +use crate::{ + config::{FileSystem, Swap}, + BlockDeviceId, +}; use super::{ references::{SpecialReferenceKind, StorageReference}, @@ -17,6 +20,7 @@ pub struct BlockDevice { pub enum StorageGraphNode { BlockDevice(BlockDevice), FileSystem(FileSystem), + Swap(Swap), } impl StorageGraphNode { @@ -33,11 +37,17 @@ impl StorageGraphNode { Self::FileSystem(fs) } + /// Creates a new swap device node. + pub fn new_swap(swap: Swap) -> Self { + Self::Swap(swap) + } + /// Returns a user friendly identifier of the node. pub fn identifier(&self) -> NodeIdentifier { match self { Self::BlockDevice(dev) => NodeIdentifier::from(dev), Self::FileSystem(fs) => NodeIdentifier::from(fs), + Self::Swap(swap) => NodeIdentifier::from(swap), } } @@ -54,8 +64,9 @@ impl StorageGraphNode { /// - `verity filesystem 'root'` pub fn describe(&self) -> String { match self { - Self::BlockDevice(dev) => format!("block device '{}'", dev.id), + Self::BlockDevice(dev) => format!("{} '{}'", dev.kind(), dev.id), Self::FileSystem(fs) => format!("filesystem [{}]", fs.description()), + Self::Swap(swap) => format!("swap on '{}'", swap.device_id), } } @@ -64,6 +75,7 @@ impl StorageGraphNode { match self { Self::BlockDevice(dev) => Some(&dev.id), Self::FileSystem(_) => None, + Self::Swap(_) => None, } } @@ -84,11 +96,20 @@ impl StorageGraphNode { } } + /// Returns the inner swap device, if this node is a swap device. + #[allow(dead_code)] + pub fn as_swap_device(&self) -> Option<&Swap> { + match self { + Self::Swap(swap) => Some(swap), + _ => None, + } + } + /// Returns the kind of block device this node represents. pub fn device_kind(&self) -> BlkDevKind { match self { Self::BlockDevice(dev) => dev.kind(), - Self::FileSystem(_) => BlkDevKind::None, + Self::FileSystem(_) | Self::Swap(_) => BlkDevKind::None, } } @@ -97,6 +118,7 @@ impl StorageGraphNode { match self { Self::BlockDevice(dev) => dev.host_config_ref.referrer_kind(), Self::FileSystem(fs) => (fs).into(), + Self::Swap(_) => BlkDevReferrerKind::Swap, } } @@ -133,9 +155,6 @@ impl StorageGraphNode { ), ] } - HostConfigBlockDevice::SwapDevice(swap_dev) => { - vec![StorageReference::new_regular(&swap_dev.device_id)] - } }, Self::FileSystem(fs) => fs .device_id @@ -143,6 +162,7 @@ impl StorageGraphNode { .map(StorageReference::new_regular) .into_iter() .collect(), + Self::Swap(swap) => vec![StorageReference::new_regular(&swap.device_id)], } } } @@ -152,6 +172,7 @@ impl StorageGraphNode { pub enum NodeIdentifier { BlockDevice(String), FileSystem(String), + Swap(String), } impl From<&FileSystem> for NodeIdentifier { @@ -166,6 +187,12 @@ impl From<&BlockDevice> for NodeIdentifier { } } +impl From<&Swap> for NodeIdentifier { + fn from(swap: &Swap) -> Self { + Self::Swap(swap.device_id.to_string()) + } +} + #[cfg(test)] impl NodeIdentifier { pub fn block_device(id: &str) -> Self { diff --git a/trident_api/src/config/host/storage/storage_graph/rules/mod.rs b/trident_api/src/config/host/storage/storage_graph/rules/mod.rs index 12104e14a..1a74a5742 100644 --- a/trident_api/src/config/host/storage/storage_graph/rules/mod.rs +++ b/trident_api/src/config/host/storage/storage_graph/rules/mod.rs @@ -90,7 +90,6 @@ impl HostConfigBlockDevice { Self::ABVolume(_) => (), Self::EncryptedVolume(_) => (), Self::VerityDevice(_) => (), - Self::SwapDevice(_) => (), } Ok(()) @@ -184,7 +183,7 @@ impl BlkDevReferrerKind { Self::ABVolume => ValidCardinality::new_exact(2), Self::EncryptedVolume => ValidCardinality::new_exact(1), Self::VerityDevice => ValidCardinality::new_exact(2), - Self::SwapDevice => ValidCardinality::new_exact(1), + Self::Swap => ValidCardinality::new_exact(1), Self::FileSystemNew => ValidCardinality::new_at_most(1), Self::FileSystemEsp => ValidCardinality::new_exact(1), @@ -226,7 +225,7 @@ impl BlkDevReferrerKind { Self::VerityDevice => { BlkDevKindFlag::Partition | BlkDevKindFlag::RaidArray | BlkDevKindFlag::ABVolume } - Self::SwapDevice => BlkDevKindFlag::Partition | BlkDevKindFlag::EncryptedVolume, + Self::Swap => BlkDevKindFlag::Partition | BlkDevKindFlag::EncryptedVolume, } } } @@ -265,7 +264,7 @@ impl BlkDevReferrerKind { | Self::ABVolume | Self::EncryptedVolume | Self::VerityDevice - | Self::SwapDevice + | Self::Swap | Self::FileSystemNew | Self::FileSystemEsp | Self::FileSystemAdopted @@ -288,7 +287,7 @@ impl BlkDevReferrerKind { | Self::ABVolume | Self::EncryptedVolume | Self::VerityDevice - | Self::SwapDevice => true, + | Self::Swap => true, // These only have one target, so enforcing this is meaningless. Self::FileSystemNew @@ -374,7 +373,6 @@ impl BlkDevKind { Ok(Some(blkdev.unwrap_verity_device()?.name.as_bytes())) }), )]), - Self::SwapDevice => None, } } } @@ -400,7 +398,7 @@ impl BlkDevReferrerKind { // These don't really care about partition sizes. Self::EncryptedVolume - | Self::SwapDevice + | Self::Swap | Self::FileSystemNew | Self::FileSystemEsp | Self::FileSystemAdopted @@ -426,7 +424,7 @@ impl BlkDevReferrerKind { // These care about having all underlying partitions be of the same // type. Self::EncryptedVolume - | Self::SwapDevice + | Self::Swap | Self::FileSystemNew | Self::FileSystemEsp | Self::FileSystemAdopted @@ -488,7 +486,7 @@ impl BlkDevReferrerKind { ]) } Self::FileSystemImage => AllowBlockList::Any, - Self::SwapDevice => AllowBlockList::Allow(vec![PartitionType::Swap]), + Self::Swap => AllowBlockList::Allow(vec![PartitionType::Swap]), } } } @@ -588,7 +586,7 @@ impl BlkDevReferrerKind { | Self::ABVolume | Self::EncryptedVolume | Self::VerityDevice - | Self::SwapDevice + | Self::Swap | Self::FileSystemNew | Self::FileSystemEsp | Self::FileSystemAdopted diff --git a/trident_api/src/config/host/storage/storage_graph/types.rs b/trident_api/src/config/host/storage/storage_graph/types.rs index f8fd46b1c..cedd3d55e 100644 --- a/trident_api/src/config/host/storage/storage_graph/types.rs +++ b/trident_api/src/config/host/storage/storage_graph/types.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::config::{ AbVolumePair, AdoptedPartition, Disk, EncryptedVolume, Partition, SoftwareRaidArray, - SwapDevice, VerityDevice, + VerityDevice, }; /// Enum for supported block device types @@ -44,9 +44,6 @@ pub enum BlkDevKind { /// A verity device VerityDevice, - - /// A swap partition - SwapDevice, } bitflags::bitflags! { @@ -62,7 +59,6 @@ bitflags::bitflags! { const ABVolume = 1 << 4; const EncryptedVolume = 1 << 5; const VerityDevice = 1 << 6; - const SwapDevice = 1 << 7; } } @@ -89,9 +85,6 @@ pub enum HostConfigBlockDevice { /// A verity device VerityDevice(VerityDevice), - - /// A swap partition - SwapDevice(SwapDevice), } /// Enum for referrer kinds. @@ -122,8 +115,8 @@ pub enum BlkDevReferrerKind { /// A verity device VerityDevice, - /// A swap device - SwapDevice, + /// A swap mount + Swap, /// A new filesystem FileSystemNew, @@ -186,7 +179,6 @@ impl HostConfigBlockDevice { Self::ABVolume(_) => BlkDevKind::ABVolume, Self::EncryptedVolume(_) => BlkDevKind::EncryptedVolume, Self::VerityDevice(_) => BlkDevKind::VerityDevice, - Self::SwapDevice(_) => BlkDevKind::SwapDevice, } } @@ -200,7 +192,6 @@ impl HostConfigBlockDevice { Self::ABVolume(_) => BlkDevReferrerKind::ABVolume, Self::EncryptedVolume(_) => BlkDevReferrerKind::EncryptedVolume, Self::VerityDevice(_) => BlkDevReferrerKind::VerityDevice, - Self::SwapDevice(_) => BlkDevReferrerKind::SwapDevice, } } @@ -279,7 +270,6 @@ impl BlkDevKind { Self::ABVolume => BlkDevKindFlag::ABVolume, Self::EncryptedVolume => BlkDevKindFlag::EncryptedVolume, Self::VerityDevice => BlkDevKindFlag::VerityDevice, - Self::SwapDevice => BlkDevKindFlag::SwapDevice, } } } @@ -293,7 +283,7 @@ impl BlkDevReferrerKind { Self::ABVolume => BlkDevReferrerKindFlag::ABVolume, Self::EncryptedVolume => BlkDevReferrerKindFlag::EncryptedVolume, Self::VerityDevice => BlkDevReferrerKindFlag::VerityDevice, - Self::SwapDevice => BlkDevReferrerKindFlag::SwapDevice, + Self::Swap => BlkDevReferrerKindFlag::SwapDevice, Self::FileSystemNew => BlkDevReferrerKindFlag::FileSystemNew, Self::FileSystemEsp => BlkDevReferrerKindFlag::FileSystemEsp, Self::FileSystemAdopted => BlkDevReferrerKindFlag::FileSystemAdopted, @@ -334,7 +324,6 @@ impl BitFlagsBackingEnumVec for BlkDevKindFlag { Self::ABVolume => BlkDevKind::ABVolume, Self::EncryptedVolume => BlkDevKind::EncryptedVolume, Self::VerityDevice => BlkDevKind::VerityDevice, - Self::SwapDevice => BlkDevKind::SwapDevice, _ => unreachable!("Invalid block device kind flag: {:?}", kind), }) .collect() @@ -350,7 +339,7 @@ impl BitFlagsBackingEnumVec for BlkDevReferrerKindFlag { Self::RaidArray => BlkDevReferrerKind::RaidArray, Self::ABVolume => BlkDevReferrerKind::ABVolume, Self::VerityDevice => BlkDevReferrerKind::VerityDevice, - Self::SwapDevice => BlkDevReferrerKind::SwapDevice, + Self::SwapDevice => BlkDevReferrerKind::Swap, Self::EncryptedVolume => BlkDevReferrerKind::EncryptedVolume, Self::FileSystemNew => BlkDevReferrerKind::FileSystemNew, Self::FileSystemEsp => BlkDevReferrerKind::FileSystemEsp, diff --git a/trident_api/src/config/host/storage/swap.rs b/trident_api/src/config/host/storage/swap.rs index b528867a9..5027dd063 100644 --- a/trident_api/src/config/host/storage/swap.rs +++ b/trident_api/src/config/host/storage/swap.rs @@ -10,8 +10,8 @@ use crate::BlockDeviceId; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(JsonSchema))] -pub struct SwapDevice { - /// The ID of the block device to use for this swap device. +pub struct Swap { + /// The ID of the block device to use for this swap area. #[cfg_attr( feature = "schemars", schemars(schema_with = "crate::schema_helpers::block_device_id_schema") @@ -19,18 +19,18 @@ pub struct SwapDevice { pub device_id: BlockDeviceId, } -impl FromStr for SwapDevice { +impl FromStr for Swap { type Err = String; fn from_str(s: &str) -> Result { - Ok(SwapDevice { + Ok(Swap { device_id: s.to_owned(), }) } } #[cfg(feature = "schemars")] -impl crate::primitives::shortcuts::StringOrStructMetadata for SwapDevice { +impl crate::primitives::shortcuts::StringOrStructMetadata for Swap { fn shorthand_format() -> &'static str { crate::schema_helpers::BLOCK_DEVICE_ID_FORMAT } diff --git a/trident_api/src/config/mod.rs b/trident_api/src/config/mod.rs index 64e8d9e5c..b39a3f394 100644 --- a/trident_api/src/config/mod.rs +++ b/trident_api/src/config/mod.rs @@ -21,7 +21,7 @@ pub use host::{ filesystem_types::{AdoptedFileSystemType, FileSystemType, NewFileSystemType}, partitions::{AdoptedPartition, Partition, PartitionSize, PartitionType}, raid::{Raid, RaidLevel, SoftwareRaidArray}, - swap::SwapDevice, + swap::Swap, verity::{VerityCorruptionOption, VerityDevice}, Storage, }, diff --git a/trident_api/src/samples/sample_hc.rs b/trident_api/src/samples/sample_hc.rs index db59fa702..dd2fa2c75 100644 --- a/trident_api/src/samples/sample_hc.rs +++ b/trident_api/src/samples/sample_hc.rs @@ -15,7 +15,7 @@ use crate::{ FileSystemSource, HostConfiguration, ImageSha384, MountOptions, MountPoint, NewFileSystemType, Os, OsImage, Partition, PartitionTableType, PartitionType, Raid, RaidLevel, Script, ScriptSource, Scripts, Services, ServicingTypeSelection, - SoftwareRaidArray, SshMode, Storage, SwapDevice, User, VerityDevice, + SoftwareRaidArray, SshMode, Storage, Swap, User, VerityDevice, }, constants::{self, MOUNT_OPTION_READ_ONLY, ROOT_MOUNT_POINT_PATH}, }; @@ -307,7 +307,7 @@ pub fn sample_host_configuration(name: &str) -> Result<(&'static str, HostConfig volume_b_id: "root-b".into(), }], }), - swap: vec![SwapDevice { + swap: vec![Swap { device_id: "swap".into(), }], ..Default::default() @@ -907,10 +907,10 @@ pub fn sample_host_configuration(name: &str) -> Result<(&'static str, HostConfig ..Default::default() }], swap: vec![ - SwapDevice { + Swap { device_id: "swap1".into(), }, - SwapDevice { + Swap { device_id: "swap2".into(), }, ] @@ -1049,9 +1049,9 @@ pub fn sample_host_configuration(name: &str) -> Result<(&'static str, HostConfig source: FileSystemSource::Image, }, ], - swap: vec![SwapDevice { + swap: vec![Swap { device_id: "swap1".into(), - }, SwapDevice { + }, Swap { device_id: "swap2".into(), }], ..Default::default() @@ -1194,7 +1194,7 @@ pub fn sample_host_configuration(name: &str) -> Result<(&'static str, HostConfig source: FileSystemSource::New(NewFileSystemType::Ext4), }, ], - swap: vec![SwapDevice { + swap: vec![Swap { device_id: "swap".into(), }], ..Default::default() From fc3eaa2e2b4aeaa6d6104d7aa1649b28f5a2f5b9 Mon Sep 17 00:00:00 2001 From: Nadiia Dubchak Date: Wed, 18 Jun 2025 18:08:43 +0000 Subject: [PATCH 87/99] Merged PR 23571: Re-factor get_first_backing_partition() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR removes the repeated instance of `get_first_backing_partition()` and moves the function to be a method of `EngineContext`. ---- #### AI description (iteration 1) #### PR Classification Code refactoring to centralize the logic for retrieving the first backing partition. #### PR Summary This pull request consolidates the functionality of get_first_backing_partition by removing its standalone implementations and integrating it into the EngineContext structure. The changes simplify the codebase and ensure a single source of truth for partition retrieval. - In `src/engine/storage/encryption.rs`, the standalone get_first_backing_partition function is removed and its calls are updated to use EngineContext’s method. - In `src/subsystems/storage/encryption.rs`, the duplicate function and its tests are eliminated, with calls redirected to the EngineContext method. - In `src/engine/context/mod.rs`, a new public get_first_backing_partition method is added along with corresponding tests. Related work items: #12608 --- src/engine/context/mod.rs | 110 +++++++++++++++++++++++++- src/engine/storage/encryption.rs | 113 +-------------------------- src/subsystems/storage/encryption.rs | 113 ++------------------------- 3 files changed, 117 insertions(+), 219 deletions(-) diff --git a/src/engine/context/mod.rs b/src/engine/context/mod.rs index 4ff3b20c2..8ada05cdf 100644 --- a/src/engine/context/mod.rs +++ b/src/engine/context/mod.rs @@ -3,12 +3,12 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{Context, Error}; +use anyhow::{bail, Context, Error}; use filesystem::FileSystemData; use log::{debug, trace}; use trident_api::{ - config::{HostConfiguration, VerityDevice}, + config::{HostConfiguration, Partition, VerityDevice}, constants::ROOT_MOUNT_POINT_PATH, status::{AbVolumeSelection, ServicingType}, storage_graph::graph::StorageGraph, @@ -243,6 +243,39 @@ impl EngineContext { Ok(verity_device_config) } + /// Returns the first partition that backs the given block device, or Err if the block device ID + /// does not correspond to a partition or software RAID array. + pub(crate) fn get_first_backing_partition<'a>( + &'a self, + block_device_id: &BlockDeviceId, + ) -> Result<&'a Partition, Error> { + if let Some(partition) = self.spec.storage.get_partition(block_device_id) { + Ok(partition) + } else if let Some(array) = self + .spec + .storage + .raid + .software + .iter() + .find(|r| &r.id == block_device_id) + { + let partition_id = array + .devices + .first() + .context(format!("RAID array '{}' has no partitions", array.id))?; + + self.spec + .storage + .get_partition(partition_id) + .context(format!( + "RAID array '{}' doesn't reference partition", + block_device_id + )) + } else { + bail!("Block device '{block_device_id}' is not a partition or RAID array") + } + } + /// Returns the estimated size of the block device holding the filesystem that contains the /// given path. If the path is not mounted anywhere, or if the block device size cannot be /// estimated, returns None. @@ -261,14 +294,17 @@ impl EngineContext { mod tests { use super::*; + use std::str::FromStr; + use const_format::formatcp; use maplit::btreemap; use osutils::testutils::repart::TEST_DISK_DEVICE_PATH; use trident_api::config::{ - self, AbUpdate, AbVolumePair, Disk, FileSystem, FileSystemSource, MountOptions, MountPoint, - Partition, PartitionType, Storage, + self, AbUpdate, AbVolumePair, Disk, FileSystem, FileSystemSource, HostConfiguration, + MountOptions, MountPoint, Partition, PartitionSize, PartitionType, Raid, RaidLevel, + SoftwareRaidArray, Storage, VerityDevice, }; #[test] @@ -598,4 +634,70 @@ mod tests { assert_eq!(ctx.filesystem_block_device_size("/nonexistent"), None); } + + #[test] + fn test_get_first_backing_partition() { + let ctx = EngineContext { + spec: HostConfiguration { + storage: Storage { + disks: vec![Disk { + id: "os".to_owned(), + partitions: vec![ + Partition { + id: "esp".to_owned(), + partition_type: PartitionType::Esp, + size: PartitionSize::from_str("1G").unwrap(), + }, + Partition { + id: "root".to_owned(), + partition_type: PartitionType::Root, + size: PartitionSize::from_str("8G").unwrap(), + }, + Partition { + id: "rootb".to_owned(), + partition_type: PartitionType::Root, + size: PartitionSize::from_str("8G").unwrap(), + }, + ], + ..Default::default() + }], + raid: Raid { + software: vec![SoftwareRaidArray { + id: "root-raid1".to_owned(), + devices: vec!["root".to_string(), "rootb".to_string()], + name: "raid1".to_string(), + level: RaidLevel::Raid1, + }], + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + assert_eq!( + ctx.get_first_backing_partition(&"esp".to_owned()).unwrap(), + &ctx.spec.storage.disks[0].partitions[0] + ); + assert_eq!( + ctx.get_first_backing_partition(&"root".to_owned()).unwrap(), + &ctx.spec.storage.disks[0].partitions[1] + ); + assert_eq!( + ctx.get_first_backing_partition(&"rootb".to_owned()) + .unwrap(), + &ctx.spec.storage.disks[0].partitions[2] + ); + assert_eq!( + ctx.get_first_backing_partition(&"root-raid1".to_owned()) + .unwrap(), + &ctx.spec.storage.disks[0].partitions[1] + ); + ctx.get_first_backing_partition(&"os".to_owned()) + .unwrap_err(); + ctx.get_first_backing_partition(&"non-existant".to_owned()) + .unwrap_err(); + } } diff --git a/src/engine/storage/encryption.rs b/src/engine/storage/encryption.rs index b476ad6a7..89c3af976 100644 --- a/src/engine/storage/encryption.rs +++ b/src/engine/storage/encryption.rs @@ -5,7 +5,7 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{bail, Context, Error}; +use anyhow::{Context, Error}; use enumflags2::BitFlags; use log::{debug, info}; use serde::{Deserialize, Serialize}; @@ -18,12 +18,11 @@ use osutils::{ }; use sysdefs::tpm2::Pcr; use trident_api::{ - config::{HostConfiguration, HostConfigurationStaticValidationError, Partition, PartitionSize}, + config::{HostConfiguration, HostConfigurationStaticValidationError, PartitionSize}, constants::internal_params::{ NO_CLOSE_ENCRYPTED_VOLUMES, OVERRIDE_ENCRYPTION_PCRS, REENCRYPT_ON_CLEAN_INSTALL, }, error::{InvalidInputError, ReportError, ServicingError, TridentError}, - BlockDeviceId, }; use crate::engine::EngineContext; @@ -112,7 +111,7 @@ pub(super) fn create_encrypted_devices( // Get the block device indicated by device_id if it is a partition; the first // partition of device_id if it is a RAID array; or, an error if device_id is neither // a partition nor a RAID array. - let partition = get_first_backing_partition(ctx, &ev.device_id).structured( + let partition = ctx.get_first_backing_partition(&ev.device_id).structured( InvalidInputError::from( HostConfigurationStaticValidationError::EncryptedVolumeNotPartitionOrRaid { encrypted_volume: ev.id.clone(), @@ -269,109 +268,3 @@ struct LuksDumpSegment { encryption: String, sector_size: u64, } - -/// Returns the first partition that backs the given block device, or Err if the block device ID -/// does not correspond to a partition or software RAID array. -fn get_first_backing_partition<'a>( - ctx: &'a EngineContext, - block_device_id: &BlockDeviceId, -) -> Result<&'a Partition, Error> { - if let Some(partition) = ctx.spec.storage.get_partition(block_device_id) { - Ok(partition) - } else if let Some(array) = ctx - .spec - .storage - .raid - .software - .iter() - .find(|r| &r.id == block_device_id) - { - let partition_id = array - .devices - .first() - .context(format!("RAID array '{}' has no partitions", array.id))?; - - ctx.spec - .storage - .get_partition(partition_id) - .context(format!( - "RAID array '{}' doesn't reference partition", - block_device_id - )) - } else { - bail!("Block device '{block_device_id}' is not a partition or RAID array") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use std::str::FromStr; - - use trident_api::config::{ - Disk, Partition, PartitionSize, PartitionType, Raid, RaidLevel, SoftwareRaidArray, Storage, - }; - - #[test] - fn test_get_first_backing_partition() { - let ctx = EngineContext { - spec: HostConfiguration { - storage: Storage { - disks: vec![Disk { - id: "os".to_owned(), - partitions: vec![ - Partition { - id: "esp".to_owned(), - partition_type: PartitionType::Esp, - size: PartitionSize::from_str("1G").unwrap(), - }, - Partition { - id: "root".to_owned(), - partition_type: PartitionType::Root, - size: PartitionSize::from_str("8G").unwrap(), - }, - Partition { - id: "rootb".to_owned(), - partition_type: PartitionType::Root, - size: PartitionSize::from_str("8G").unwrap(), - }, - ], - ..Default::default() - }], - raid: Raid { - software: vec![SoftwareRaidArray { - id: "root-raid1".to_owned(), - devices: vec!["root".to_string(), "rootb".to_string()], - name: "raid1".to_string(), - level: RaidLevel::Raid1, - }], - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }; - - assert_eq!( - get_first_backing_partition(&ctx, &"esp".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[0] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"root".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[1] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"rootb".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[2] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"root-raid1".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[1] - ); - get_first_backing_partition(&ctx, &"os".to_owned()).unwrap_err(); - get_first_backing_partition(&ctx, &"non-existant".to_owned()).unwrap_err(); - } -} diff --git a/src/subsystems/storage/encryption.rs b/src/subsystems/storage/encryption.rs index 337067ff4..dd95f2891 100644 --- a/src/subsystems/storage/encryption.rs +++ b/src/subsystems/storage/encryption.rs @@ -1,16 +1,14 @@ use std::{fs, os::unix::fs::PermissionsExt, path::PathBuf}; -use anyhow::{bail, Context, Error}; use log::{info, trace}; use osutils::{encryption, files}; use trident_api::{ config::{ HostConfiguration, HostConfigurationDynamicValidationError, - HostConfigurationStaticValidationError, Partition, PartitionType, + HostConfigurationStaticValidationError, PartitionType, }, error::{InvalidInputError, ReportError, ServicingError, TridentError}, - BlockDeviceId, }; use crate::engine::EngineContext; @@ -91,11 +89,12 @@ pub fn configure(ctx: &EngineContext) -> Result<(), TridentError> { for ev in encryption.volumes.iter() { let backing_partition = - get_first_backing_partition(ctx, &ev.device_id).structured(InvalidInputError::from( - HostConfigurationStaticValidationError::EncryptedVolumeNotPartitionOrRaid { - encrypted_volume: ev.id.clone(), - }, - ))?; + ctx.get_first_backing_partition(&ev.device_id) + .structured(InvalidInputError::from( + HostConfigurationStaticValidationError::EncryptedVolumeNotPartitionOrRaid { + encrypted_volume: ev.id.clone(), + }, + ))?; let device_path = &ctx.get_block_device_path(&ev.device_id).structured( ServicingError::FindEncryptedVolumeBlockDevice { device_id: ev.device_id.clone(), @@ -154,39 +153,6 @@ pub fn configure(ctx: &EngineContext) -> Result<(), TridentError> { Ok(()) } -/// Returns the first partition that backs the given block device, or Err if the block device ID -/// does not correspond to a partition or software RAID array. -fn get_first_backing_partition<'a>( - ctx: &'a EngineContext, - block_device_id: &BlockDeviceId, -) -> Result<&'a Partition, Error> { - if let Some(partition) = ctx.spec.storage.get_partition(block_device_id) { - Ok(partition) - } else if let Some(array) = ctx - .spec - .storage - .raid - .software - .iter() - .find(|r| &r.id == block_device_id) - { - let partition_id = array - .devices - .first() - .context(format!("RAID array '{}' has no partitions", array.id))?; - - ctx.spec - .storage - .get_partition(partition_id) - .context(format!( - "RAID array '{}' doesn't reference partition", - block_device_id - )) - } else { - bail!("Block device '{block_device_id}' is not a partition or RAID array") - } -} - #[cfg(test)] mod tests { use super::*; @@ -197,76 +163,13 @@ mod tests { use trident_api::{ config::{ - Disk, EncryptedVolume, Encryption, Partition, PartitionSize, PartitionType, Raid, - RaidLevel, SoftwareRaidArray, Storage, + Disk, EncryptedVolume, Encryption, Partition, PartitionSize, PartitionType, Storage, }, error::ErrorKind, }; use crate::subsystems::storage::tests as storage_tests; - #[test] - fn test_get_first_backing_partition() { - let ctx = EngineContext { - spec: HostConfiguration { - storage: Storage { - disks: vec![Disk { - id: "os".to_owned(), - partitions: vec![ - Partition { - id: "esp".to_owned(), - partition_type: PartitionType::Esp, - size: PartitionSize::from_str("1G").unwrap(), - }, - Partition { - id: "root".to_owned(), - partition_type: PartitionType::Root, - size: PartitionSize::from_str("8G").unwrap(), - }, - Partition { - id: "rootb".to_owned(), - partition_type: PartitionType::Root, - size: PartitionSize::from_str("8G").unwrap(), - }, - ], - ..Default::default() - }], - raid: Raid { - software: vec![SoftwareRaidArray { - id: "root-raid1".to_owned(), - devices: vec!["root".to_string(), "rootb".to_string()], - name: "raid1".to_string(), - level: RaidLevel::Raid1, - }], - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }; - - assert_eq!( - get_first_backing_partition(&ctx, &"esp".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[0] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"root".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[1] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"rootb".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[2] - ); - assert_eq!( - get_first_backing_partition(&ctx, &"root-raid1".to_owned()).unwrap(), - &ctx.spec.storage.disks[0].partitions[1] - ); - get_first_backing_partition(&ctx, &"os".to_owned()).unwrap_err(); - get_first_backing_partition(&ctx, &"non-existant".to_owned()).unwrap_err(); - } - fn get_storage(recovery_key_file: &tempfile::NamedTempFile) -> Storage { Storage { disks: vec![Disk { From 106bdb04691f369da7db80b9802f104d0121e390 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 18 Jun 2025 23:55:13 +0000 Subject: [PATCH 88/99] Merged PR 23584: PackageArchitecture w/ noarch for COSI OsPackages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Our prism cicd pipeline is failing since the introduction of 1.1. 1.1's OsPackages list contains 'noarch' packages. Trident's definition of OsPackages does not support noarch (it uses SystemArchitecture which only has amd64 and arm64). validation: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=840708&view=results Related work items: #12614 --- src/osimage/cosi/metadata.rs | 36 ++++++++++++++++++++++++++++++++++-- sysdefs/src/arch.rs | 11 +++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/osimage/cosi/metadata.rs b/src/osimage/cosi/metadata.rs index 94423f2d5..e89952684 100644 --- a/src/osimage/cosi/metadata.rs +++ b/src/osimage/cosi/metadata.rs @@ -7,7 +7,9 @@ use uuid::Uuid; use osutils::osrelease::OsRelease; use sysdefs::{ - arch::SystemArchitecture, osuuid::OsUuid, partition_types::DiscoverablePartitionType, + arch::{PackageArchitecture, SystemArchitecture}, + osuuid::OsUuid, + partition_types::DiscoverablePartitionType, }; use trident_api::primitives::hash::Sha384Hash; @@ -174,7 +176,7 @@ pub(crate) struct OsPackage { #[allow(dead_code)] #[serde(default)] - pub arch: Option, + pub arch: Option, } impl<'de> Deserialize<'de> for MetadataVersion { @@ -393,4 +395,34 @@ mod tests { ]; assert_eq!(metadata.get_regular_filesystems().count(), 0); } + + #[test] + fn test_noarch_os_packages() { + let noarch_os_package_json = r#" + { + "name": "package1", + "version": "1.0.0", + "arch": "noarch" + } + "#; + let _noarch_os_package: OsPackage = serde_json::from_str(noarch_os_package_json).unwrap(); + + let amd64_os_package_json = r#" + { + "name": "package1", + "version": "1.0.0", + "arch": "x86_64" + } + "#; + let _amd64_os_package: OsPackage = serde_json::from_str(amd64_os_package_json).unwrap(); + + let amd64_os_package_json = r#" + { + "name": "package1", + "version": "1.0.0", + "arch": "amd64" + } + "#; + let _amd64_os_package: OsPackage = serde_json::from_str(amd64_os_package_json).unwrap(); + } } diff --git a/sysdefs/src/arch.rs b/sysdefs/src/arch.rs index b5579891f..bfcc6a33a 100644 --- a/sysdefs/src/arch.rs +++ b/sysdefs/src/arch.rs @@ -44,3 +44,14 @@ impl<'de> Deserialize<'de> for SystemArchitecture { }) } } + +/// System architecture +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub enum PackageArchitecture { + /// NoArch + #[serde(rename = "noarch")] + NoArch, + + #[serde(untagged)] + Specific(SystemArchitecture), +} From 2e5a2613224a4fbab802a609d5174aa6d88de397 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 19 Jun 2025 19:25:28 +0000 Subject: [PATCH 89/99] Merged PR 23602: engineering: PackageArchitecture: add '(none)' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Prism is listing packages like this: { "name": "gpg-pubkey", "version": "3135ce90", "release": "5e6fda74", "arch": "(none)" }, validated with prism dev branch (https://github.com/microsoft/azure-linux-image-tools/pull/277): https://dev.azure.com/mariner-org/ECF/_build/results?buildId=841924&view=results ---- #### AI description (iteration 1) #### PR Classification This pull request introduces a new feature by extending the OS package architecture support through the addition of a '(none)' variant. #### PR Summary The changes enable the system to handle OS packages with an architecture value of "(none)" in line with the new COSI metadata requirements. - `sysdefs/src/arch.rs`: Added a new `None` variant to the `PackageArchitecture` enum with `#[serde(rename = "(none)")]`. - `src/osimage/cosi/metadata.rs`: Introduced tests to validate deserialization of OS package JSON objects containing the "(none)" architecture. Related work items: #12614 --- src/osimage/cosi/metadata.rs | 10 ++++++++++ sysdefs/src/arch.rs | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/osimage/cosi/metadata.rs b/src/osimage/cosi/metadata.rs index e89952684..57fb9412a 100644 --- a/src/osimage/cosi/metadata.rs +++ b/src/osimage/cosi/metadata.rs @@ -424,5 +424,15 @@ mod tests { } "#; let _amd64_os_package: OsPackage = serde_json::from_str(amd64_os_package_json).unwrap(); + + let none_os_package_json = r#" + { + "name": "gpg-pubkey", + "version": "3135ce90", + "release": "5e6fda74", + "arch": "(none)" + } + "#; + let _none_os_package: OsPackage = serde_json::from_str(none_os_package_json).unwrap(); } } diff --git a/sysdefs/src/arch.rs b/sysdefs/src/arch.rs index bfcc6a33a..4cfc1131b 100644 --- a/sysdefs/src/arch.rs +++ b/sysdefs/src/arch.rs @@ -49,7 +49,8 @@ impl<'de> Deserialize<'de> for SystemArchitecture { #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub enum PackageArchitecture { /// NoArch - #[serde(rename = "noarch")] + #[serde(alias = "noarch")] + #[serde(alias = "(none)")] NoArch, #[serde(untagged)] From 9b0ccb100ce8d27715b0c7fc694eb9cf3e2efcc5 Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Thu, 19 Jun 2025 22:11:50 +0000 Subject: [PATCH 90/99] Merged PR 23588: Centralize checking whether image is UKI This is preparation for being able to determine UKI vs. non-UKI images based on the COSI metadata ---- #### AI description (iteration 1) #### PR Classification This pull request is a code refactoring focused on centralizing and standardizing the UKI image detection logic. #### PR Summary The changes introduce a new centralized method, `is_uki_image`, within the engine context to replace direct checks of the UKI flag across the codebase. This refactoring enhances consistency and error handling regarding UKI image identification. - `src/engine/context/mod.rs`: Added a new `is_uki_image` function and a corresponding `is_uki` field to centralize UKI flag checks. - `src/subsystems/esp.rs`, `src/engine/boot/mod.rs`, and other subsystem files: Replaced direct internal parameter flag checks with calls to `is_uki_image()`. - `src/engine/update.rs` and `src/engine/clean_install.rs`: Updated context construction to properly propagate the UKI flag. - `osutils/src/efivar.rs`: Introduced a new helper function `current_var_set` to support related boot configuration checks. Related work items: #12161 --- osutils/src/efivar.rs | 5 +++++ src/engine/boot/grub.rs | 1 + src/engine/boot/mod.rs | 7 ++----- src/engine/clean_install.rs | 6 ++++-- src/engine/context/mod.rs | 13 +++++++++++++ src/engine/mod.rs | 4 ++-- src/engine/rollback.rs | 7 +++++-- src/engine/storage/raid.rs | 8 ++++++-- src/engine/update.rs | 7 ++++++- src/lib.rs | 1 + src/subsystems/esp.rs | 10 +++++----- src/subsystems/initrd.rs | 4 ++-- src/subsystems/osconfig/mod.rs | 15 +++++++-------- src/subsystems/selinux.rs | 6 ++---- src/subsystems/storage/mod.rs | 6 ++---- trident_api/src/config/host/mod.rs | 9 +-------- 16 files changed, 64 insertions(+), 45 deletions(-) diff --git a/osutils/src/efivar.rs b/osutils/src/efivar.rs index 909008186..d1b747d76 100644 --- a/osutils/src/efivar.rs +++ b/osutils/src/efivar.rs @@ -104,6 +104,11 @@ fn read_efi_variable(guid: &str, variable: &str) -> Result, TridentError Ok(data[4..].to_vec()) } +/// Returns whether the LoaderEntrySelected EFI variable is set. +pub fn current_var_set() -> bool { + read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_SELECTED).is_ok() +} + /// Set the LoaderEntryDefault EFI variable to the current boot entry pub fn set_default_to_current() -> Result<(), TridentError> { let current = read_efi_variable(BOOTLOADER_INTERFACE_GUID, LOADER_ENTRY_SELECTED)?; diff --git a/src/engine/boot/grub.rs b/src/engine/boot/grub.rs index 30d696feb..51295301c 100644 --- a/src/engine/boot/grub.rs +++ b/src/engine/boot/grub.rs @@ -545,6 +545,7 @@ pub(crate) mod functional_test { "root1".into() => PathBuf::from(formatcp!("{TEST_DISK_DEVICE_PATH}2")), "root2".into() => PathBuf::from(formatcp!("{TEST_DISK_DEVICE_PATH}3")), }, + is_uki: Some(false), ..Default::default() }; diff --git a/src/engine/boot/mod.rs b/src/engine/boot/mod.rs index 21b7b568b..0bacb69e1 100644 --- a/src/engine/boot/mod.rs +++ b/src/engine/boot/mod.rs @@ -4,10 +4,7 @@ use log::debug; use strum::IntoEnumIterator; use trident_api::{ - constants::{ - internal_params::ENABLE_UKI_SUPPORT, AB_VOLUME_A_NAME, AB_VOLUME_B_NAME, - AZURE_LINUX_INSTALL_ID_PREFIX, VAR_TMP_PATH, - }, + constants::{AB_VOLUME_A_NAME, AB_VOLUME_B_NAME, AZURE_LINUX_INSTALL_ID_PREFIX, VAR_TMP_PATH}, error::{ReportError, ServicingError, TridentError}, status::AbVolumeSelection, }; @@ -30,7 +27,7 @@ impl Subsystem for BootSubsystem { #[tracing::instrument(name = "boot_configuration", skip_all)] fn configure(&mut self, ctx: &EngineContext) -> Result<(), TridentError> { - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + if ctx.is_uki_image()? { debug!("Skipping grub configuration because UKI is in use"); return Ok(()); } diff --git a/src/engine/clean_install.rs b/src/engine/clean_install.rs index 03f2e4c5e..786d1c5a0 100644 --- a/src/engine/clean_install.rs +++ b/src/engine/clean_install.rs @@ -13,8 +13,8 @@ use osutils::{chroot, container, mount, mountpoint, path::join_relative}; use trident_api::{ config::{HostConfiguration, Operations}, constants::{ - internal_params::NO_TRANSITION, ESP_MOUNT_POINT_PATH, ROOT_MOUNT_POINT_PATH, - UPDATE_ROOT_PATH, + internal_params::{ENABLE_UKI_SUPPORT, NO_TRANSITION}, + ESP_MOUNT_POINT_PATH, ROOT_MOUNT_POINT_PATH, UPDATE_ROOT_PATH, }, error::{ InitializationError, InternalError, InvalidInputError, ReportError, ServicingError, @@ -200,6 +200,7 @@ fn stage_clean_install( image: Some(image), storage_graph: engine::build_storage_graph(&host_config.storage)?, // Build storage graph filesystems: Vec::new(), // Will be populated after dynamic validation + is_uki: Some(host_config.internal_params.get_flag(ENABLE_UKI_SUPPORT)), }; // Execute pre-servicing scripts @@ -301,6 +302,7 @@ pub(crate) fn finalize_clean_install( image: None, // Not used in finalize_clean_install storage_graph: engine::build_storage_graph(&state.host_status().spec.storage)?, // Build storage graph filesystems: Vec::new(), // Left empty since context does not have image + is_uki: None, }; let new_root = match new_root { diff --git a/src/engine/context/mod.rs b/src/engine/context/mod.rs index 8ada05cdf..2103e7fc6 100644 --- a/src/engine/context/mod.rs +++ b/src/engine/context/mod.rs @@ -10,6 +10,7 @@ use log::{debug, trace}; use trident_api::{ config::{HostConfiguration, Partition, VerityDevice}, constants::ROOT_MOUNT_POINT_PATH, + error::TridentError, status::{AbVolumeSelection, ServicingType}, storage_graph::graph::StorageGraph, BlockDeviceId, @@ -71,6 +72,9 @@ pub struct EngineContext { /// All of the filesystems in the system. pub filesystems: Vec, + + /// Whether the image will use a UKI or not. + pub is_uki: Option, } impl EngineContext { /// Returns the update volume selection for all A/B volume pairs. The update volume is the one @@ -288,6 +292,15 @@ impl EngineContext { self.storage_graph.block_device_size(device) } + + pub(crate) fn is_uki_image(&self) -> Result { + if let Some(is_uki) = self.is_uki { + return Ok(is_uki); + } + Err(TridentError::internal( + "is_uki() called without it being set", + )) + } } #[cfg(test)] diff --git a/src/engine/mod.rs b/src/engine/mod.rs index fa82272b0..41ae19d7e 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -12,7 +12,7 @@ use log::{debug, error, info, warn}; use osutils::{dependencies::Dependency, path::join_relative}; use trident_api::{ config::Storage, - constants::{self, internal_params::ENABLE_UKI_SUPPORT}, + constants, error::{InternalError, ReportError, ServicingError, TridentError, TridentResultExt}, status::{ServicingState, ServicingType}, storage_graph::graph::StorageGraph, @@ -292,7 +292,7 @@ fn configure( let use_overlay = (ctx.servicing_type == ServicingType::CleanInstall || ctx.servicing_type == ServicingType::AbUpdate) && ctx.storage_graph.root_fs_is_verity() - && !ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT); + && !ctx.is_uki_image()?; info!("Starting step 'Configure'"); for subsystem in subsystems { diff --git a/src/engine/rollback.rs b/src/engine/rollback.rs index 2f2bd405a..ef44ba6bb 100644 --- a/src/engine/rollback.rs +++ b/src/engine/rollback.rs @@ -5,7 +5,7 @@ use log::{debug, info, trace, warn}; use osutils::{block_devices, efivar, lsblk, veritysetup}; use trident_api::{ - constants::internal_params::{ENABLE_UKI_SUPPORT, VIRTDEPLOY_BOOT_ORDER_WORKAROUND}, + constants::internal_params::VIRTDEPLOY_BOOT_ORDER_WORKAROUND, error::{InternalError, ReportError, ServicingError, TridentError, TridentResultExt}, status::{AbVolumeSelection, ServicingState, ServicingType}, BlockDeviceId, @@ -37,6 +37,7 @@ pub fn validate_boot(datastore: &mut DataStore) -> Result<(), TridentError> { image: None, // Not used for boot validation logic storage_graph: engine::build_storage_graph(&datastore.host_status().spec.storage)?, // Build storage graph filesystems: Vec::new(), // Left empty since context does not have image + is_uki: None, }; // Get the block device path of the current root @@ -68,7 +69,9 @@ pub fn validate_boot(datastore: &mut DataStore) -> Result<(), TridentError> { .message("Failed to persist boot order after reboot")?; } - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + // If the bootloader set the LoaderEntrySelected variable, then make its value the default + // boot entry. Systemd-boot sets this variable, but GRUB does not. + if efivar::current_var_set() { efivar::set_default_to_current() .message("Failed to set default boot entry to current")?; } diff --git a/src/engine/storage/raid.rs b/src/engine/storage/raid.rs index d53a3a5f4..9b0718124 100644 --- a/src/engine/storage/raid.rs +++ b/src/engine/storage/raid.rs @@ -14,7 +14,8 @@ use strum_macros::{Display, EnumString}; use osutils::{block_devices, dependencies::Dependency, mdadm, udevadm}; use trident_api::{ config::{HostConfiguration, SoftwareRaidArray}, - constants::{internal_params::ENABLE_UKI_SUPPORT, MDSTAT_PATH}, + constants::MDSTAT_PATH, + error::TridentResultExt, BlockDeviceId, }; @@ -43,7 +44,7 @@ fn create(config: SoftwareRaidArray, ctx: &EngineContext) -> Result<(), Error> { info!("Initializing '{}': creating RAID array", config.id); - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + if ctx.is_uki_image().unstructured("UKI setting unknown")? { // If UKI support is enabled, we need to create the RAID array with the // homehost=any option to ensure that the RAID array can be opened by the // runtime OS. @@ -568,6 +569,7 @@ mod functional_test { }, ..Default::default() }, + is_uki: Some(false), ..Default::default() }; @@ -618,6 +620,7 @@ mod functional_test { }, ..Default::default() }, + is_uki: Some(false), ..Default::default() }; @@ -667,6 +670,7 @@ mod functional_test { }, ..Default::default() }, + is_uki: Some(false), ..Default::default() }; diff --git a/src/engine/update.rs b/src/engine/update.rs index b2fdaf7e1..7bd0f38f6 100644 --- a/src/engine/update.rs +++ b/src/engine/update.rs @@ -7,7 +7,10 @@ use tokio::sync::mpsc; use osutils::{chroot, container, path::join_relative}; use trident_api::{ config::{HostConfiguration, Operations}, - constants::{internal_params::NO_TRANSITION, ESP_MOUNT_POINT_PATH}, + constants::{ + internal_params::{ENABLE_UKI_SUPPORT, NO_TRANSITION}, + ESP_MOUNT_POINT_PATH, + }, error::{ InternalError, InvalidInputError, ReportError, ServicingError, TridentError, TridentResultExt, @@ -64,6 +67,7 @@ pub(crate) fn update( image: Some(image), storage_graph: engine::build_storage_graph(&host_config.storage)?, // Build storage graph filesystems: Vec::new(), // Will be populated after dynamic validation + is_uki: Some(host_config.internal_params.get_flag(ENABLE_UKI_SUPPORT)), }; // Before starting an update servicing, need to validate that the active volume is set @@ -321,6 +325,7 @@ pub(crate) fn finalize_update( image: None, // Not used in finalize_update storage_graph: engine::build_storage_graph(&state.host_status().spec.storage)?, // Build storage graph filesystems: Vec::new(), // Left empty since context does not have image + is_uki: None, }; let esp_path = if container::is_running_in_container() diff --git a/src/lib.rs b/src/lib.rs index 51ddadb3a..20949e7e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -436,6 +436,7 @@ impl Trident { image: None, storage_graph: engine::build_storage_graph(&host_config.storage)?, // Build storage graph filesystems: Vec::new(), // Left empty since context does not have image + is_uki: None, }; if ctx.ab_active_volume.is_none() { diff --git a/src/subsystems/esp.rs b/src/subsystems/esp.rs index b604380d6..a2d0d4bb6 100644 --- a/src/subsystems/esp.rs +++ b/src/subsystems/esp.rs @@ -19,11 +19,11 @@ use osutils::{ }; use trident_api::{ constants::{ - internal_params::{DISABLE_GRUB_NOPREFIX_CHECK, ENABLE_UKI_SUPPORT}, - EFI_DEFAULT_BIN_RELATIVE_PATH, ESP_EFI_DIRECTORY, ESP_RELATIVE_MOUNT_POINT_PATH, - GRUB2_CONFIG_FILENAME, GRUB2_CONFIG_RELATIVE_PATH, + internal_params::DISABLE_GRUB_NOPREFIX_CHECK, EFI_DEFAULT_BIN_RELATIVE_PATH, + ESP_EFI_DIRECTORY, ESP_RELATIVE_MOUNT_POINT_PATH, GRUB2_CONFIG_FILENAME, + GRUB2_CONFIG_RELATIVE_PATH, }, - error::{ReportError, ServicingError, TridentError}, + error::{ReportError, ServicingError, TridentError, TridentResultExt}, }; use crate::engine::{ @@ -194,7 +194,7 @@ fn copy_file_artifacts( esp_dir_path.display() ))?; - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + if ctx.is_uki_image().unstructured("UKI setting unknown")? { // Prepare ESP directory structure for UKI boot uki::prepare_esp_for_uki(mount_point)?; diff --git a/src/subsystems/initrd.rs b/src/subsystems/initrd.rs index e893b1798..4cddf8bd1 100644 --- a/src/subsystems/initrd.rs +++ b/src/subsystems/initrd.rs @@ -1,7 +1,7 @@ use log::{debug, info}; use osutils::mkinitrd; -use trident_api::{constants::internal_params::ENABLE_UKI_SUPPORT, error::TridentError}; +use trident_api::error::TridentError; use crate::engine::{EngineContext, Subsystem}; @@ -18,7 +18,7 @@ impl Subsystem for InitrdSubsystem { #[tracing::instrument(name = "initrd_regeneration", skip_all)] fn configure(&mut self, ctx: &EngineContext) -> Result<(), TridentError> { - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) { + if ctx.is_uki_image()? { debug!("Skipping initrd regeneration because UKI is in use"); return Ok(()); } diff --git a/src/subsystems/osconfig/mod.rs b/src/subsystems/osconfig/mod.rs index 1ef85271c..8bece1f3d 100644 --- a/src/subsystems/osconfig/mod.rs +++ b/src/subsystems/osconfig/mod.rs @@ -6,7 +6,7 @@ use log::{debug, error, info, warn}; use osutils::{osmodifier::OSModifierConfig, path}; use trident_api::{ config::{ManagementOs, SshMode}, - constants::internal_params::{DISABLE_HOSTNAME_CARRY_OVER, ENABLE_UKI_SUPPORT}, + constants::internal_params::DISABLE_HOSTNAME_CARRY_OVER, error::{ExecutionEnvironmentMisconfigurationError, ReportError, ServicingError, TridentError}, status::ServicingType, }; @@ -123,9 +123,7 @@ impl Subsystem for OsConfigSubsystem { self.name() ); return Ok(()); - } else if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) - && ctx.storage_graph.root_fs_is_verity() - { + } else if ctx.is_uki_image()? && ctx.storage_graph.root_fs_is_verity() { error!("Skipping OS configuration changes requested in Host Configuration because UKI root verity is in use."); return Ok(()); } @@ -176,10 +174,9 @@ impl Subsystem for OsConfigSubsystem { os_modifier_config.kernel_command_line = Some(ctx.spec.os.kernel_command_line.clone()); } - // If UKI support is enabled, update SELinux mode here - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) - && ctx.spec.os.selinux.mode.is_some() - { + // If we have a UKI image, update SELinux mode here since it cannot be set via kernel + // command line. + if ctx.is_uki_image()? && ctx.spec.os.selinux.mode.is_some() { debug!("Updating SELinux config"); os_modifier_config.selinux = Some(ctx.spec.os.selinux.clone()); } @@ -398,6 +395,7 @@ mod functional_test { }, ..Default::default() }, + is_uki: Some(false), ..Default::default() }; assert!(os_config_requires_os_modifier(&ctx)); @@ -438,6 +436,7 @@ mod functional_test { // Create EngineContext with no hostname specified let ctx = EngineContext { servicing_type: ServicingType::AbUpdate, + is_uki: Some(false), ..Default::default() }; assert!(os_config_requires_os_modifier(&ctx)); diff --git a/src/subsystems/selinux.rs b/src/subsystems/selinux.rs index cd983a143..8e60ac3de 100644 --- a/src/subsystems/selinux.rs +++ b/src/subsystems/selinux.rs @@ -13,7 +13,7 @@ use osutils::dependencies::{Dependency, DependencyResultExt}; use sysdefs::filesystems::{KernelFilesystemType, RealFilesystemType}; use trident_api::{ config::{HostConfigurationDynamicValidationError, SelinuxMode}, - constants::{internal_params::ENABLE_UKI_SUPPORT, SELINUX_CONFIG}, + constants::SELINUX_CONFIG, error::{InvalidInputError, ReportError, ServicingError, TridentError}, status::ServicingType, }; @@ -124,9 +124,7 @@ impl Subsystem for SelinuxSubsystem { // If a verity filesystem is mounted at root, ensure that SELinux is not // in enforcing mode and warn if it is in permissive mode - if ctx.storage_graph.root_fs_is_verity() - && !ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) - { + if ctx.storage_graph.root_fs_is_verity() && !ctx.is_uki_image()? { match final_selinux_mode { SelinuxMode::Enforcing => { return Err(TridentError::new(InvalidInputError::from( diff --git a/src/subsystems/storage/mod.rs b/src/subsystems/storage/mod.rs index af2d4478b..646aa1157 100644 --- a/src/subsystems/storage/mod.rs +++ b/src/subsystems/storage/mod.rs @@ -8,7 +8,7 @@ use log::{debug, error, warn}; use osutils::lsblk; use trident_api::{ config::HostConfigurationDynamicValidationError, - constants::internal_params::{ENABLE_UKI_SUPPORT, RELAXED_COSI_VALIDATION}, + constants::internal_params::RELAXED_COSI_VALIDATION, error::{ InvalidInputError, ReportError, ServicingError, TridentError, TridentResultExt, UnsupportedConfigurationError, @@ -179,9 +179,7 @@ impl Subsystem for StorageSubsystem { #[tracing::instrument(name = "storage_configuration", skip_all)] fn configure(&mut self, ctx: &EngineContext) -> Result<(), TridentError> { - if ctx.spec.internal_params.get_flag(ENABLE_UKI_SUPPORT) - && ctx.storage_graph.root_fs_is_verity() - { + if ctx.is_uki_image()? && ctx.storage_graph.root_fs_is_verity() { debug!("Skipping storage configuration because UKI root verity is in use"); return Ok(()); } diff --git a/trident_api/src/config/host/mod.rs b/trident_api/src/config/host/mod.rs index 953cc776c..dc47bfdf6 100644 --- a/trident_api/src/config/host/mod.rs +++ b/trident_api/src/config/host/mod.rs @@ -4,9 +4,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "schemars")] use schemars::JsonSchema; -use crate::{ - constants::internal_params::ENABLE_UKI_SUPPORT, is_default, storage_graph::graph::StorageGraph, -}; +use crate::{is_default, storage_graph::graph::StorageGraph}; pub(crate) mod error; pub(crate) mod harpoon; @@ -83,11 +81,6 @@ impl HostConfiguration { self.validate_datastore_location()?; - // Gate usr-verity support behind the UKI support parameter. - if graph.usr_fs_is_verity() && !self.internal_params.get_flag(ENABLE_UKI_SUPPORT) { - return Err(HostConfigurationStaticValidationError::UsrVerityRequiresUkiSupport); - } - Ok(()) } From b67c2c99b5cb2ec1790c9904a6a92e297cb2eafe Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Fri, 20 Jun 2025 01:47:17 +0000 Subject: [PATCH 91/99] Merged PR 23612: engineering: Publish RPMs from PR pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description What is this PR about? feature/doc/engineering/bug? Publish Trident RPMs to a dev feed for easier consumption from Steamboat. Copy of this PR (https://dev.azure.com/mariner-org/ECF/_git/trident/pullrequest/23590), but created a new one since the last had several files that were only altering white space. ---- #### AI description (iteration 1) #### PR Classification This PR introduces a new pipeline feature to automatically publish RPM packages from the PR pipeline. #### PR Summary The pull request adds a new YAML template for RPM publishing and updates existing pipeline configurations to enable conditional publishing during PR builds. - `/.pipelines/templates/stages/trident_rpms/publish-dev.yml`: Added a new template that checks if an RPM version already exists in the feed and, if not, copies RPMs to a staging directory and publishes them. - `/.pipelines/templates/stages/trident_rpms/build-source.yml`: Introduced the `publishToDevFeed` parameter and conditionally includes the RPM publishing template. - `/.pipelines/templates/stages/trident_rpms/trident-stage.yml`: Configured the pipeline to set `publishToDevFeed` to true for PR stages, enabling RPM publishing. Related work items: #12637 --- .../stages/trident_rpms/build-source.yml | 6 +++ .../stages/trident_rpms/publish-dev.yml | 47 +++++++++++++++++++ .../stages/trident_rpms/trident-stage.yml | 2 + 3 files changed, 55 insertions(+) create mode 100644 .pipelines/templates/stages/trident_rpms/publish-dev.yml diff --git a/.pipelines/templates/stages/trident_rpms/build-source.yml b/.pipelines/templates/stages/trident_rpms/build-source.yml index 8d5873f81..da3d990ea 100644 --- a/.pipelines/templates/stages/trident_rpms/build-source.yml +++ b/.pipelines/templates/stages/trident_rpms/build-source.yml @@ -1,4 +1,8 @@ parameters: + - name: publishToDevFeed + type: boolean + default: false + - name: tridentArtifactName type: string @@ -84,6 +88,8 @@ stages: baseimgBuildType: ${{ parameters.baseimgBuildType }} baseImagePipelineBuildId: ${{ parameters.baseImagePipelineBuildId }} previewContainerPipeline: "[AMD64-6-OneBranch]-Prod-BuildImages" + - ${{ if eq(parameters.publishToDevFeed, true) }}: + - template: publish-dev.yml - job: BuildTridentARM64 displayName: Build Trident 3.0 RPMs for ARM64 diff --git a/.pipelines/templates/stages/trident_rpms/publish-dev.yml b/.pipelines/templates/stages/trident_rpms/publish-dev.yml new file mode 100644 index 000000000..4ba7d3c26 --- /dev/null +++ b/.pipelines/templates/stages/trident_rpms/publish-dev.yml @@ -0,0 +1,47 @@ +parameters: + - name: "feed" + type: string + default: TridentDev + values: [TridentDev] + - name: "packageName" + type: string + default: rpms-pr + values: [rpms-pr] + +steps: + - script: echo $(Build.BuildNumber) + displayName: "Print Trident Version" + + - script: | + set -eux + + alreadyPublished=$(./scripts/get-packages.py --debug \ + --feed '${{ parameters.feed }}' \ + --package '${{ parameters.packageName }}' \ + --version '$(Build.BuildNumber)' \ + --action=exists) + + # Save variable to know if package needs to be published + set +x + echo "##vso[task.setvariable variable=isVersionInFeed;]$alreadyPublished" + displayName: "Check if package version has already been published in the feed." + workingDirectory: $(Build.SourcesDirectory) + env: + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + + - script: | + mkdir -p $(Build.SourcesDirectory)/staging_dir + cp $(ob_outputDirectory)/*.rpm $(Build.SourcesDirectory)/staging_dir + ls $(Build.SourcesDirectory)/staging_dir + displayName: Copy RPMs into Staging Directory + + - task: UniversalPackages@0 + displayName: "Publish RPMs Universal Package" + condition: and(succeeded(), eq(variables.isVersionInFeed, false)) + inputs: + command: publish + vstsFeedPublish: "ECF/${{ parameters.feed }}" + vstsFeedPackagePublish: "${{ parameters.packageName }}" + publishDirectory: "$(Build.SourcesDirectory)/staging_dir" + versionPublish: $(Build.BuildNumber) + versionOption: custom diff --git a/.pipelines/templates/stages/trident_rpms/trident-stage.yml b/.pipelines/templates/stages/trident_rpms/trident-stage.yml index 0de43cd8a..9f8bc562d 100644 --- a/.pipelines/templates/stages/trident_rpms/trident-stage.yml +++ b/.pipelines/templates/stages/trident_rpms/trident-stage.yml @@ -43,6 +43,8 @@ stages: - ${{ if or(parameters.forceTridentRebuild, eq(parameters.stageType, 'pr'), eq(parameters.stageType, 'pr-e2e'), eq(parameters.stageType, 'ci'), eq(parameters.stageType, 'pr-e2e-azure')) }}: - template: build-source.yml parameters: + ${{ if eq(parameters.stageType, 'pr') }}: + publishToDevFeed: true tridentArtifactName: ${{ parameters.tridentArtifactName }} codeCoverage: ${{ parameters.codeCoverage }} baseimgBuildType: ${{ parameters.baseimgBuildType }} From ca60e18e4474d9ebd5e36caf1112280e84715755 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 20 Jun 2025 20:20:31 +0000 Subject: [PATCH 92/99] Merged PR 23621: engineering: azure servicing tests: unbound variable stopping cleanup from running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description The servicing tests has a task for cleaning up azure resources in case the tests fail/timeout. But that task is erroring out because a variable used to determine if cleanup is needed is unbound (https://dev.azure.com/mariner-org/ECF/_build/results?buildId=838833&view=logs&j=004ade76-5a0e-5a2d-af0c-1845bd9783cc&t=6db98abd-232b-510f-d24d-4e378b1d9353). ---- #### AI description (iteration 1) #### PR Classification Bug fix. #### PR Summary This pull request fixes an issue in the Azure servicing tests where an unbound variable error was preventing the cleanup process from running. - `.pipelines/templates/stages/testing_servicing/testing-template.yml`: modified the bash command by removing the `-u` flag (changed from `set -eux` to `set -ex`) to prevent the script from failing on unbound variables. Related work items: #12644 --- .../templates/stages/testing_servicing/testing-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/templates/stages/testing_servicing/testing-template.yml b/.pipelines/templates/stages/testing_servicing/testing-template.yml index 83c40354f..463019545 100644 --- a/.pipelines/templates/stages/testing_servicing/testing-template.yml +++ b/.pipelines/templates/stages/testing_servicing/testing-template.yml @@ -187,7 +187,7 @@ jobs: - ${{ if eq(parameters.platform, 'azure') }}: - bash: | - set -eux + set -ex # If platform is azure AND the test failed to finish, run cleanup to # ensure there are no azure resources left behind From cb4d1046d743d4887030df93bbf4be95f5d0c3c6 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 20 Jun 2025 20:20:38 +0000 Subject: [PATCH 93/99] Merged PR 23625: engineering: loop-update.sh task did not previously have a timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description The scale tests are timing out because of a 20 minute task timeout I added when migrating the servicing tests to storm. Removing the timeout here. ---- #### AI description (iteration 1) #### PR Classification This pull request is an engineering configuration update that removes an unnecessary timeout setting. #### PR Summary The change eliminates the explicit 20-minute timeout in the pipeline template for the loop-update.sh task, ensuring that the task runs without an imposed time limit. - `/.pipelines/templates/stages/testing_servicing/testing-template.yml`: Removed the `timeoutInMinutes: 20` line from the servicing test step. Related work items: #12643 --- .../templates/stages/testing_servicing/testing-template.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pipelines/templates/stages/testing_servicing/testing-template.yml b/.pipelines/templates/stages/testing_servicing/testing-template.yml index 463019545..85a1ec801 100644 --- a/.pipelines/templates/stages/testing_servicing/testing-template.yml +++ b/.pipelines/templates/stages/testing_servicing/testing-template.yml @@ -166,7 +166,6 @@ jobs: echo "##vso[task.setvariable variable=STORM_SCENARIO_FINISHED;]true" displayName: "Servicing test" - timeoutInMinutes: 20 env: AZCOPY_AUTO_LOGIN_TYPE: "MSI" From 51e4fae454ce93777ec3e9f4b6ea0747027137b5 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Mon, 23 Jun 2025 18:03:57 +0000 Subject: [PATCH 94/99] Merged PR 23583: engineering: Changes to Trident SELinux policy for Steamboat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description This PR makes certain permissions optional, since Steamboat does not install all of the same SELinux modules that Trident does. (For example, some RAID permissions are made optional). In addition, this PR adds some permissions such that Steamboat can run Trident successfully from the `ci_unconfined_t` domain. Steamboat validation: https://dev.azure.com/mariner-org/mariner/_build/results?buildId=845278&view=results pr-e2e test: https://dev.azure.com/mariner-org/ECF/_build/results?buildId=845239&view=results ![image.png](https://dev.azure.com/mariner-org/2311650c-e79e-4301-b4d2-96543fdd84ff/_apis/git/repositories/895b6b3d-5077-488a-8001-ab6b5a14c1a3/pullRequests/23583/attachments/image.png) # 🤔 Rationale Why is this PR needed? # 📝 Checks - [ ] Check [dev-docs/manual-validation.md](/dev-docs/manual-validation.md) # 📌 Follow-ups TODO: - #0000 # đŸ—’ī¸ Notes make optional ---- #### AI description (iteration 1) #### PR Classification This pull request implements an engineering change to make the `typeattribute` optional in the SELinux policy. #### PR Summary The changes refactor the SELinux policy in `selinux-policy-trident/trident.te` by wrapping the `typeattribute` rule within an `optional_policy` block and removing its redundant declaration from the require block. - `selinux-policy-trident/trident.te`: Replaces the direct `typeattribute` rule with an `optional_policy` block that includes the required type and attribute declarations. - `selinux-policy-trident/trident.te`: Deletes the redundant `attribute can_change_object_identity;` line from the require section. Related work items: #12599 --- selinux-policy-trident/trident.te | 130 +++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 30 deletions(-) diff --git a/selinux-policy-trident/trident.te b/selinux-policy-trident/trident.te index 79b9c3984..4df4d2777 100644 --- a/selinux-policy-trident/trident.te +++ b/selinux-policy-trident/trident.te @@ -27,7 +27,6 @@ require { type auditctl_exec_t; type auditd_exec_t; type auditd_unit_t; - type bluetooth_unit_t; type boot_t; type bpf_t; type bootloader_t; @@ -41,8 +40,8 @@ require { type cloud_init_exec_t; type cloud_init_state_t; type cloud_init_t; - type colord_var_lib_t; type container_unit_t; + type container_var_lib_t; type crack_db_t; type crack_exec_t; type cron_spool_t; @@ -54,7 +53,6 @@ require { type devpts_t; type dhcpc_exec_t; type dhcpc_state_t; - type dhcpd_unit_t; type dmesg_exec_t; type dosfs_t; type efivarfs_t; @@ -82,8 +80,6 @@ require { type ld_so_t; type ldconfig_cache_t; type lib_t; - type loadkeys_exec_t; - type loadkeys_t; type load_policy_t; type locale_t; type locate_exec_t; @@ -94,8 +90,6 @@ require { type lvm_t; type lvm_unit_t; type mail_spool_t; - type mdadm_exec_t; - type mdadm_unit_t; type memory_pressure_t; type mnt_t; type modules_conf_t; @@ -127,6 +121,7 @@ require { type ssh_agent_exec_t; type ssh_exec_t; type ssh_home_t; + type ssh_port_t; type sshd_t; type sshd_keygen_unit_t; type sshd_unit_t; @@ -139,6 +134,7 @@ require { type sysfs_t; type syslog_conf_t; type syslogd_exec_t; + type syslogd_runtime_t; type syslogd_unit_t; type systemd_analyze_exec_t; type systemd_backlight_exec_t; @@ -195,9 +191,10 @@ require { type unreserved_port_t; type updpwd_exec_t; type useradd_exec_t; + type user_tmp_t; + type user_devpts_t; type usr_t; type uuidd_exec_t; - type unconfined_t; type var_run_t; # Define object classes that SELinux can protect @@ -215,7 +212,6 @@ require { class udp_socket { create ioctl }; attribute can_change_object_identity; - attribute domain; role unconfined_r; role system_r; @@ -227,6 +223,21 @@ typeattribute trident_t can_change_object_identity; # Defines transition from trident_t to fsadm_t domain when Trident executes fsadm tool (i.e. mkfs) type_transition trident_t fsadm_exec_t:process fsadm_t; +# Defines transition from ci_unconfined_t to trident_t when Steamboat executes Trident, as well as +# specific permissions necessary for Steamboat testing +optional_policy(` + require { + type ci_unconfined_t; + role ci_unconfined_r; + } + type_transition ci_unconfined_t trident_exec_t:process trident_t; + allow ci_unconfined_t trident_exec_t:file { getattr open read execute }; + allow trident_t trident_exec_t:file entrypoint; + role ci_unconfined_r types trident_t; + + allow trident_t ci_unconfined_t:fd use; +') + # Allow transition between unconfined_t and trident_t domains; necessary for an interactive run optional_policy(` unconfined_run_to(trident_t, trident_exec_t) @@ -248,6 +259,7 @@ allow trident_t self:netlink_route_socket { bind create getattr nlmsg_read read allow trident_t self:process { getsched setsched getcap setpgid signull getattr signal }; allow trident_t self:tcp_socket { connect create getattr getopt read setopt shutdown write }; allow trident_t self:unix_dgram_socket { connect create write }; +allow trident_t self:udp_socket { connect create getattr }; allow trident_t self:key { search write }; allow trident_t self:sem { associate create destroy read unix_read unix_write write }; @@ -270,7 +282,6 @@ allow trident_t auditd_unit_t:file getattr; allow trident_t admin_passwd_exec_t:file getattr; allow trident_t anacron_exec_t:file getattr; allow trident_t audisp_remote_exec_t:file getattr; -allow trident_t bluetooth_unit_t:file getattr; allow trident_t boot_t:dir { mounton create relabelto }; allow trident_t boot_t:file relabelto; allow trident_t bpf_t:dir search; @@ -285,8 +296,8 @@ allow trident_t cloud_init_exec_t:file getattr; allow trident_t cloud_init_state_t:dir { list_dir_perms relabelto }; allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms; allow trident_t cloud_init_state_t:file getattr; -allow trident_t colord_var_lib_t:dir { getattr open read relabelto }; allow trident_t container_unit_t:file getattr; +allow trident_t container_var_lib_t:file getattr; allow trident_t crack_db_t:dir { getattr open search read }; allow trident_t crack_db_t:file getattr; allow trident_t crack_db_t:lnk_file getattr; @@ -303,13 +314,12 @@ allow trident_t devpts_t:chr_file { read write ioctl getattr }; allow trident_t devlog_t:sock_file { getattr write }; allow trident_t dhcpc_exec_t:file getattr; allow trident_t dhcpc_state_t:dir { getattr open read relabelto }; -allow trident_t dhcpd_unit_t:file getattr; allow trident_t dmesg_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t dosfs_t:filesystem { getattr mount unmount }; allow trident_t efivarfs_t:filesystem getattr; allow trident_t efivarfs_t:dir search; allow trident_t etc_runtime_t:file { getattr open read relabelto relabelfrom setattr unlink }; -allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write }; +allow trident_t etc_t:file { create execute execute_no_trans link relabelfrom relabelto rename setattr unlink write append }; allow trident_t etc_t:dir { mounton relabelfrom }; allow trident_t faillog_t:file relabelto; allow trident_t fs_t:filesystem { mount unmount }; @@ -345,7 +355,6 @@ allow trident_t ld_so_t:file { execute_no_trans relabelto setattr unlink write } allow trident_t ldconfig_cache_t:dir { getattr open read search relabelto }; allow trident_t ldconfig_cache_t:file { getattr relabelto }; allow trident_t lib_t:file { create relabelto rename setattr unlink write }; -allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; allow trident_t locale_t:dir { add_name relabelto remove_name rmdir setattr write }; allow trident_t locale_t:file { link relabelto rename setattr unlink write }; allow trident_t locate_exec_t:file getattr; @@ -355,9 +364,6 @@ allow trident_t lost_found_t:dir { getattr open read relabelto }; allow trident_t lvm_metadata_t:dir { getattr open read }; allow trident_t lvm_unit_t:file getattr; allow trident_t mail_spool_t:dir { list_dir_perms relabelto }; -allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; -allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; -allow trident_t mdadm_runtime_t:dir { add_name remove_name search write }; allow trident_t memory_pressure_t:file { read open getattr setattr }; allow trident_t mnt_t:dir { add_name create getattr mounton open read search write }; allow trident_t modules_conf_t:file { create relabelto setattr unlink write }; @@ -394,15 +400,16 @@ allow trident_t semanage_store_t:dir relabelto; allow trident_t semanage_store_t:file relabelto; allow trident_t semanage_trans_lock_t:file relabelto; allow trident_t setfiles_exec_t:file entrypoint; -allow trident_t shadow_t:file { getattr open read relabelto setattr link unlink write relabelfrom }; +allow trident_t shadow_t:file { getattr open read relabelto setattr link unlink write append relabelfrom }; allow trident_t shadow_lock_t:file { create getattr lock open read link unlink write setattr relabelfrom }; allow trident_t shell_exec_t:file { execute execute_no_trans getattr map open read relabelto setattr unlink write }; allow trident_t ssh_agent_exec_t:file getattr; allow trident_t ssh_exec_t:file { execute getattr }; allow trident_t ssh_home_t:dir { setattr relabelto }; allow trident_t ssh_home_t:file relabelto; +allow trident_t ssh_port_t:tcp_socket name_connect; allow trident_t sshd_t:fd use; -allow trident_t sshd_t:fifo_file { read write getattr }; # Allow Trident to read/write stdin/stdout/stderr +allow trident_t sshd_t:fifo_file { read write getattr ioctl }; # Allow Trident to read/write stdin/stdout/stderr allow trident_t sshd_keygen_unit_t:file getattr; allow trident_t sshd_unit_t:file getattr; allow trident_t sulogin_exec_t:file { execute getattr map open read relabelto setattr unlink write }; @@ -470,7 +477,6 @@ allow trident_t systemd_user_manager_unit_t:file getattr; allow trident_t systemd_user_runtime_dir_exec_t:file getattr; allow trident_t systemd_userdbd_exec_t:file getattr; allow trident_t systemd_userdbd_unit_t:file getattr; -allow trident_t tcsd_var_lib_t:dir relabelto; allow trident_t tmp_t:chr_file { create getattr unlink }; allow trident_t tmp_t:dir { add_name create getattr mounton open read relabelfrom remove_name rmdir search setattr write relabelto }; allow trident_t tmp_t:file { append create getattr ioctl open read relabelfrom rename setattr unlink write map execute link }; @@ -486,6 +492,8 @@ allow trident_t unlabeled_t:lnk_file { create getattr read relabelfrom rename un allow trident_t unreserved_port_t:tcp_socket name_connect; allow trident_t updpwd_exec_t:file getattr; allow trident_t useradd_exec_t:file { execute execute_no_trans getattr map open read }; +allow trident_t user_tmp_t:file { getattr open read }; +allow trident_t user_devpts_t:chr_file { ioctl read write }; allow trident_t usr_t:dir { add_name create read relabelto remove_name rmdir setattr write relabelto mounton }; allow trident_t usr_t:file { create execute execute_no_trans getattr ioctl link open read relabelto rename setattr unlink write }; allow trident_t uuidd_exec_t:file getattr; @@ -502,6 +510,48 @@ allow trident_t var_run_t:lnk_file relabelto; allow trident_t var_spool_t:dir relabelto; allow trident_t wtmp_t:file relabelto; +# Policies below must be optional for Steamboat +optional_policy(` + require { + type trident_t; + type bluetooth_unit_t; + } + allow trident_t bluetooth_unit_t:file getattr; +') +optional_policy(` + require { + type trident_t; + type colord_var_lib_t; + } + allow trident_t colord_var_lib_t:dir { getattr open read relabelto }; +') +optional_policy(` + require { + type trident_t; + type dhcpd_unit_t; + } + allow trident_t dhcpd_unit_t:file getattr; +') +optional_policy(` + require { + type trident_t; + type loadkeys_exec_t; + } + allow trident_t loadkeys_exec_t:file { execute getattr map open read relabelto setattr unlink write }; +') +optional_policy(` + require { + type trident_t; + type mdadm_exec_t; + type mdadm_unit_t; + type mdadm_runtime_t; + } + allow trident_t mdadm_exec_t:file { open read getattr map relabelto setattr unlink write execute execute_no_trans }; + allow trident_t mdadm_unit_t:file { getattr open read relabelto setattr unlink }; + allow trident_t mdadm_runtime_t:dir { add_name remove_name search write }; + raid_manage_mdadm_runtime_files(trident_t) +') + #============= interfaces ============== ########################################### # Authentication and User Management @@ -693,7 +743,6 @@ dev_rw_vhost(trident_t) dev_search_sysfs(trident_t) dev_write_sysfs(trident_t) dev_write_urand(trident_t) -raid_manage_mdadm_runtime_files(trident_t) term_getattr_ptmx(trident_t) term_use_virtio_console(trident_t) term_getattr_pty_fs(trident_t) @@ -716,8 +765,14 @@ kerberos_read_config(trident_t) libs_exec_ldconfig(trident_t) libs_manage_lib_dirs(trident_t) modutils_read_module_config(trident_t) -tcsd_manage_lib_dirs(trident_t) uuidd_manage_lib_dirs(trident_t) +optional_policy(` + require { + type tcsd_var_lib_t; + } + allow trident_t tcsd_var_lib_t:dir relabelto; + tcsd_manage_lib_dirs(trident_t) +') ########################################### # Logging and Monitoring @@ -733,12 +788,21 @@ logging_manage_generic_logs(trident_t) ########################################### # Miscellaneous ########################################### -miscfiles_read_generic_tls_privkey(trident_t) -miscfiles_read_man_pages(trident_t) -miscfiles_read_localization(trident_t) -miscfiles_read_generic_certs(trident_t) -xserver_read_xkb_libs(trident_t) - +optional_policy(` + miscfiles_read_generic_tls_privkey(trident_t) +') +optional_policy(` + miscfiles_read_man_pages(trident_t) +') +optional_policy(` + miscfiles_read_localization(trident_t) +') +optional_policy(` + miscfiles_read_generic_certs(trident_t) +') +optional_policy(` + xserver_read_xkb_libs(trident_t) +') #################### # @@ -771,8 +835,14 @@ fs_manage_tmpfs_dirs(fsadm_t) fs_manage_tmpfs_files(fsadm_t) #============= loadkeys_t ============== -files_read_default_symlinks(loadkeys_t) -fs_search_tmpfs(loadkeys_t) +optional_policy(` + require { + type trident_t; + type loadkeys_t; + } + files_read_default_symlinks(loadkeys_t) + fs_search_tmpfs(loadkeys_t) +') #============= lvm_t ============== # This is necessary for Trident to create encrypted volumes From ad69c547143d0133cc3246742eaa5eba57279a60 Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Mon, 23 Jun 2025 19:12:41 +0000 Subject: [PATCH 95/99] Merged PR 23536: engineering: Update SELinux and verity error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description SELinux can be in `enforcing` mode with usr-verity so clarify error message to apply only to root-verity. Related work items: #12579 --- src/subsystems/selinux.rs | 2 +- trident_api/src/config/host/error.rs | 16 +++++-------- trident_api/src/config/host/mod.rs | 36 ++++++++++------------------ 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/subsystems/selinux.rs b/src/subsystems/selinux.rs index 8e60ac3de..b058c8bc6 100644 --- a/src/subsystems/selinux.rs +++ b/src/subsystems/selinux.rs @@ -128,7 +128,7 @@ impl Subsystem for SelinuxSubsystem { match final_selinux_mode { SelinuxMode::Enforcing => { return Err(TridentError::new(InvalidInputError::from( - HostConfigurationDynamicValidationError::VerityAndSelinuxUnsupported { + HostConfigurationDynamicValidationError::RootVerityAndSelinuxUnsupported { selinux_mode: final_selinux_mode.to_string(), }, ))); diff --git a/trident_api/src/config/host/error.rs b/trident_api/src/config/host/error.rs index 9be381f5b..6b3ec343e 100644 --- a/trident_api/src/config/host/error.rs +++ b/trident_api/src/config/host/error.rs @@ -123,11 +123,6 @@ pub enum HostConfigurationStaticValidationError { #[error("In order to use usr-verity, UKI support must be enabled")] UsrVerityRequiresUkiSupport, - #[error( - "SELinux mode '{selinux_mode}' is not supported with verity, must be set to 'disabled'" - )] - VerityAndSelinuxUnsupported { selinux_mode: String }, - #[error("Verity device '{device_name}' must define a mount point.")] VerityFilesystemWithoutMountPoint { device_name: String }, @@ -212,11 +207,12 @@ pub enum HostConfigurationDynamicValidationError { #[error("Failed to load script '{name}' at '{path}'")] LoadScript { name: String, path: String }, - #[error("Cannot modify storage configuration during update")] - StorageConfigurationChanged, - #[error( - "SELinux mode '{selinux_mode}' is not supported with verity, must be set to 'disabled'" + "SELinux is not support with root-verity and grub. SELinux is set to '{selinux_mode}', \ + but should be set to 'disabled'." )] - VerityAndSelinuxUnsupported { selinux_mode: String }, + RootVerityAndSelinuxUnsupported { selinux_mode: String }, + + #[error("Cannot modify storage configuration during update")] + StorageConfigurationChanged, } diff --git a/trident_api/src/config/host/mod.rs b/trident_api/src/config/host/mod.rs index dc47bfdf6..fa2b2bdb0 100644 --- a/trident_api/src/config/host/mod.rs +++ b/trident_api/src/config/host/mod.rs @@ -107,20 +107,15 @@ impl HostConfiguration { return Err(HostConfigurationStaticValidationError::SelfUpgradeOnReadOnlyRootVerityFs); } - // If SELinux is in `enforcing` mode, produce an error. Warn if SELinux - // is in `permissive` mode. - match self.os.selinux.mode { - Some(SelinuxMode::Enforcing) => { - return Err( - HostConfigurationStaticValidationError::VerityAndSelinuxUnsupported { - selinux_mode: SelinuxMode::Enforcing.to_string(), - }, + // Warn if SELinux is not `disbled. + if let Some(selinux_mode) = self.os.selinux.mode { + if selinux_mode != SelinuxMode::Disabled { + warn!( + "The use of SELinux with verity and grub is not supported. SELinux mode is \ + currently set to '{}', but should be 'disabled'.", + selinux_mode.to_string() ); } - Some(SelinuxMode::Permissive) => { - warn!("The use of SELinux with verity is not supported. SELinux mode is currently set to '{}', but should be 'disabled'.", SelinuxMode::Permissive.to_string()); - } - _ => {} } Ok(()) @@ -412,23 +407,16 @@ mod tests { let graph = host_config.storage.build_graph().unwrap(); - // Check that 'enforcing' mode returns an error - host_config.os.selinux.mode = Some(SelinuxMode::Enforcing); + // Check that if 'selfUpgrade' is set, we return an error + host_config.trident.self_upgrade = true; let validation_error = host_config.validate_root_verity_config(&graph).unwrap_err(); assert_eq!( validation_error, - HostConfigurationStaticValidationError::VerityAndSelinuxUnsupported { - selinux_mode: SelinuxMode::Enforcing.to_string() - }, - "{validation_error}" + HostConfigurationStaticValidationError::SelfUpgradeOnReadOnlyRootVerityFs ); - // Check that 'permissive' mode does not return an error - host_config.os.selinux.mode = Some(SelinuxMode::Permissive); - host_config.validate_root_verity_config(&graph).unwrap(); - - // Check that 'disabled' mode does not return an error - host_config.os.selinux.mode = Some(SelinuxMode::Disabled); + // Check that if 'selfUpgrade' is not set, no error is returned + host_config.trident.self_upgrade = false; host_config.validate_root_verity_config(&graph).unwrap(); } } From 96488973a1f82acbdb0c48bcda37306fdd4264cb Mon Sep 17 00:00:00 2001 From: Ayana Yaegashi Date: Mon, 23 Jun 2025 20:20:40 +0000 Subject: [PATCH 96/99] Merged PR 23645: engineering: Updating warning for root-verity + grub + SELinux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description Explain that root-verity + SELinux only words with UKI image Related work items: #12579 --- trident_api/src/config/host/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/trident_api/src/config/host/mod.rs b/trident_api/src/config/host/mod.rs index fa2b2bdb0..5258b210f 100644 --- a/trident_api/src/config/host/mod.rs +++ b/trident_api/src/config/host/mod.rs @@ -111,9 +111,8 @@ impl HostConfiguration { if let Some(selinux_mode) = self.os.selinux.mode { if selinux_mode != SelinuxMode::Disabled { warn!( - "The use of SELinux with verity and grub is not supported. SELinux mode is \ - currently set to '{}', but should be 'disabled'.", - selinux_mode.to_string() + "The use of SELinux with root-verity and grub is not supported. This \ + configuration will only work with a UKI-based image." ); } } From c58c1920b9a2324c6f071cb0c26bc9332c8c2188 Mon Sep 17 00:00:00 2001 From: Paco Huelsz Prince Date: Mon, 23 Jun 2025 23:47:25 +0000 Subject: [PATCH 97/99] Merged PR 23614: engineering: Steamboat helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description make helpers to work with steamboat --- Dockerfile.full | 5 ++++- Makefile | 56 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/Dockerfile.full b/Dockerfile.full index a7fcd3a96..37109074d 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -34,7 +34,10 @@ ARG RPM_REL=1 # This entry needs to exist in the config.toml file to allow cargo to use the # token from the environment variable. -# RUN printf '[registry]\nglobal-credential-providers = ["cargo:token"]\n' >> ./.cargo/config.toml +ARG CARGO_REGISTRIES_FROM_ENV=false +RUN if [ "$CARGO_REGISTRIES_FROM_ENV" = "true" ]; then \ + printf '[registry]\nglobal-credential-providers = ["cargo:token"]\n' >> ./.cargo/config.toml; \ +fi RUN --mount=type=secret,id=registry_token \ export CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN=$(cat /run/secrets/registry_token) && \ diff --git a/Makefile b/Makefile index ffde6ce12..0efb851a6 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ NETLAUNCH_CONFIG ?= input/netlaunch.yaml OVERRIDE_RUST_FEED ?= true .PHONY: all -all: format check test build-api-docs bin/trident-rpms-azl3.tar.gz docker-build build-functional-test coverage validate-configs generate-mermaid-diagrams +all: format check test build-api-docs bin/trident-rpms.tar.gz docker-build build-functional-test coverage validate-configs generate-mermaid-diagrams .PHONY: check check: @@ -62,10 +62,16 @@ check-sh: fi @echo "NOTICE: Created local .cargo/config file." -.PHONY: build -build: .cargo/config +.PHONY: version-vars +version-vars: $(eval TRIDENT_CARGO_VERSION := $(shell python3 ./scripts/get-version.py "$(shell date +%Y%m%d).99")) $(eval GIT_COMMIT := $(shell git rev-parse --short HEAD)$(shell git diff --quiet || echo '.dirty')) + $(eval LOCAL_BUILD_TRIDENT_VERSION=$(TRIDENT_CARGO_VERSION)-dev.$(GIT_COMMIT)) + @echo "TRIDENT_CARGO_VERSION=$(TRIDENT_CARGO_VERSION)" + @echo "GIT_COMMIT=$(GIT_COMMIT)" + +.PHONY: build +build: .cargo/config version-vars @OPENSSL_STATIC=1 \ OPENSSL_LIB_DIR=$(shell dirname `whereis libssl.a | cut -d" " -f2`) \ OPENSSL_INCLUDE_DIR=/usr/include/openssl \ @@ -117,9 +123,30 @@ bin/trident: build @mkdir -p bin @cp -u target/release/trident bin/ -bin/trident-rpms-azl3.tar.gz: Dockerfile.azl3 systemd/*.service trident.spec artifacts/osmodifier bin/trident selinux-policy-trident/* +# This will do a proper build on azl3, exactly as the pipelines would, with the custom registry and all. +bin/trident-rpms-azl3.tar.gz: Dockerfile.full systemd/*.service trident.spec artifacts/osmodifier selinux-policy-trident/* version-vars + $(eval CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN := $(shell az account get-access-token --query "join(' ', ['Bearer', accessToken])" --output tsv)) + + @export CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN="$(CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN)" &&\ + docker build -t trident/trident-build:latest \ + --secret id=registry_token,env=CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN \ + --build-arg CARGO_REGISTRIES_FROM_ENV="true" \ + --build-arg TRIDENT_VERSION="$(LOCAL_BUILD_TRIDENT_VERSION)" \ + --build-arg RPM_VER="$(TRIDENT_CARGO_VERSION)" \ + --build-arg RPM_REL="dev.$(GIT_COMMIT)" \ + -f Dockerfile.full \ + . + @mkdir -p bin/ + @id=$$(docker create trident/trident-build:latest) && \ + docker cp -q $$id:/work/trident-rpms.tar.gz $@ || \ + docker rm -v $$id + @rm -rf bin/RPMS/ + @tar xf $@ -C bin/ + +# This one does a fast trick-build where we build locally and inject the binary into the container to add it to the RPM. +bin/trident-rpms.tar.gz: Dockerfile.azl3 systemd/*.service trident.spec artifacts/osmodifier bin/trident selinux-policy-trident/* @docker build -t trident/trident-build:latest \ - --build-arg TRIDENT_VERSION="$(TRIDENT_CARGO_VERSION)-dev.$(GIT_COMMIT)" \ + --build-arg TRIDENT_VERSION="$(LOCAL_BUILD_TRIDENT_VERSION)" \ --build-arg RPM_VER="$(TRIDENT_CARGO_VERSION)" \ --build-arg RPM_REL="dev.$(GIT_COMMIT)" \ -f Dockerfile.azl3 \ @@ -128,14 +155,25 @@ bin/trident-rpms-azl3.tar.gz: Dockerfile.azl3 systemd/*.service trident.spec art @id=$$(docker create trident/trident-build:latest) && \ docker cp -q $$id:/work/trident-rpms.tar.gz $@ || \ docker rm -v $$id - @rm -rf bin/RPMS/x86_64 + @rm -rf bin/RPMS/ @tar xf $@ -C bin/ -bin/trident-rpms.tar.gz: bin/trident-rpms-azl3.tar.gz - cp $< $@ +STEAMBOAT_RPMS_DIR ?= /tmp/mariner/uki/out/RPMS + +.PHONY: copy-rpms-to-steamboat +copy-rpms-to-steamboat: bin/trident-rpms-azl3.tar.gz + @echo "Cleaning up old Trident RPMs in Steamboat..." + @rm -f $(STEAMBOAT_RPMS_DIR)/trident-* + @echo "Copying Trident RPMs to Steamboat..." + @mkdir -p $(STEAMBOAT_RPMS_DIR) + @find bin/RPMS -type f -name 'trident-*.rpm' -exec cp {} $(STEAMBOAT_RPMS_DIR) \; + @echo "Trident RPMs copied to Steamboat directory: $(STEAMBOAT_RPMS_DIR)" + @ls -alh $(STEAMBOAT_RPMS_DIR)/trident-*.rpm + +# Grabs bin/trident-rpms.tar.gz from the local build directory and builds a Docker image with it. .PHONY: docker-build -docker-build: Dockerfile.runtime bin/trident-rpms-azl3.tar.gz +docker-build: Dockerfile.runtime bin/trident-rpms.tar.gz @docker build --quiet -f Dockerfile.runtime -t trident/trident:latest . artifacts/test-image/trident-container.tar.gz: docker-build From 520d3863d8d9d998998a7d394c28a08ac0497d20 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Wed, 25 Jun 2025 18:45:43 +0000 Subject: [PATCH 98/99] Merged PR 23676: enginerring: ensure publish depends on barmetal container tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🔍 Description We are publishing before bm container tests run. Make publish depend on bm container tests. ---- #### AI description (iteration 1) #### PR Classification This pull request is a pipeline configuration update to add a dependency on baremetal container tests for the publish stage. #### PR Summary The changes update the pipeline definition to ensure that publishing now depends on baremetal container tests. - `/.pipelines/templates/stages/publishing/publish.yml`: Added the `BaremetalDeploymentTesting_container` stage to the publish workflow. Related work items: #12676 --- .pipelines/templates/stages/publishing/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pipelines/templates/stages/publishing/publish.yml b/.pipelines/templates/stages/publishing/publish.yml index 5ecf9d894..1899d3303 100644 --- a/.pipelines/templates/stages/publishing/publish.yml +++ b/.pipelines/templates/stages/publishing/publish.yml @@ -26,6 +26,7 @@ stages: - DeploymentTesting_container - FunctionalTesting - BaremetalDeploymentTesting_host + - BaremetalDeploymentTesting_container - ServicingTesting condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main') ) From 185110ab75b75ed0e0e86de297800688ebc07037 Mon Sep 17 00:00:00 2001 From: Brian Telfer Date: Fri, 6 Jun 2025 16:49:31 -0700 Subject: [PATCH 99/99] Create codeql.yml --- .github/workflows/codeql.yml | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..b86ceb8ba --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '43 9 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: go + build-mode: autobuild + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}"