mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-30 07:46:36 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b14c18131 | |||
| 8430cadb03 | |||
| cef0c61532 | |||
| fc325bcc56 | |||
| 4a46f5ff3d | |||
| e0a973bf4e | |||
| 671238458c | |||
| 151103a609 | |||
| cc7d5e3f5f | |||
| d556d4dc94 | |||
| 6300fbe74f | |||
| 6ebc78d115 | |||
| 5f4d5e8fe8 | |||
| e4520f70a1 | |||
| f54945a4c4 | |||
| 4beac12439 | |||
| 6523ab8d9f | |||
| 0609e7ce14 | |||
| afb9ddabb4 | |||
| 2e014d25b3 | |||
| 719000460b | |||
| 528a368144 | |||
| d791d7b761 | |||
| 1a251de4f3 | |||
| e634df8611 | |||
| f5fbb5f4e1 | |||
| 5d2d539423 | |||
| acebc9649d | |||
| e33542b0c2 | |||
| 2ee0ad1704 | |||
| 4c5fc3c991 | |||
| 6d2b1c629e | |||
| de484b93e0 | |||
| 4f61b007da | |||
| 1dd354233c | |||
| 6eb14a11d0 | |||
| 3667eea893 | |||
| e67bbd7bd3 | |||
| 8d033c85d8 | |||
| b7420fe5f2 | |||
| 5cb1c9e8cb | |||
| 20356ab709 | |||
| 2c5c43b0e0 | |||
| cf096b3c22 | |||
| 82024907c5 | |||
| e2785846d0 | |||
| 31ac243865 |
@@ -1 +1 @@
|
||||
sha256=548a58d74f3cb3b5a1ab9dcd4d3f8625b9ddebe847a2e14de1d08c3b2e6292eb
|
||||
sha256=5de6852b2e606eca4ee0d86dbb3b725a3eb8923143fd00df96c549acc36040bd
|
||||
|
||||
@@ -132,10 +132,11 @@ test-group = 'ecstore-serial-flaky'
|
||||
filter = 'package(rustfs) & (binary(/^embedded.*_test$/) | binary(admin_diagnostic_capability_e2e))'
|
||||
test-group = 'embedded-test-ports'
|
||||
|
||||
# The real object probe includes bucket creation and cleanup in its short
|
||||
# measurement budget. Keep competing storage fixtures outside that budget.
|
||||
# Inventory delivery and real drive/object probes include durable filesystem IO
|
||||
# in short deadlines. Reserve capacity so unrelated storage fixtures cannot
|
||||
# exhaust those budgets. Keep the deadlines and assertions unchanged.
|
||||
[[profile.default.overrides]]
|
||||
filter = 'package(rustfs) & binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)'
|
||||
filter = 'package(rustfs) & (binary(connect_inventory) | binary(connect_perf_drive) | (binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)))'
|
||||
threads-required = "num-test-threads"
|
||||
|
||||
# Serialize the durable manual-transition checkpoint test across nextest's
|
||||
@@ -334,7 +335,7 @@ filter = 'package(rustfs) & (binary(/^embedded.*_test$/) | binary(admin_diagnost
|
||||
test-group = 'embedded-test-ports'
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = 'package(rustfs) & binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)'
|
||||
filter = 'package(rustfs) & (binary(connect_inventory) | binary(connect_perf_drive) | (binary(connect_perf_object) & test(=real_rustfs_endpoint_and_production_cli_support_bounded_get_and_put)))'
|
||||
threads-required = "num-test-threads"
|
||||
|
||||
# Serialize the durable manual-transition checkpoint test under the ci profile
|
||||
|
||||
+109
-74
@@ -27,7 +27,8 @@
|
||||
# schedule and manual dispatch build the full platform matrix.
|
||||
#
|
||||
# Manual Parameters:
|
||||
# - build_docker: Build and push Docker images (default: true)
|
||||
# - publish: Publish binaries/releases for manual runs (default: false)
|
||||
# - build_docker: Advisory only; manual runs do not trigger Docker publishing
|
||||
# - platforms: Comma-separated platform IDs or 'all' (default: all)
|
||||
|
||||
name: Build and Release
|
||||
@@ -55,12 +56,16 @@ on:
|
||||
- cron: "13 1 * * 0" # Weekly on Sunday 01:13 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: "Publish to download channels and GitHub Releases (false keeps only workflow artifacts)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_docker:
|
||||
# Advisory only. docker.yml triggers on workflow_run and its job-level
|
||||
# condition requires the triggering event to be a tag push, so a manual
|
||||
# dispatch of this workflow never produces images regardless of this
|
||||
# value. Kept because the summary step reports it; wiring it up would
|
||||
# mean teaching docker.yml's version parser a second event shape.
|
||||
# value. Publishing remains controlled by the explicit publish input.
|
||||
description: "Build and push Docker images after binary build (ignored: dispatch runs never reach docker.yml)"
|
||||
required: false
|
||||
default: true
|
||||
@@ -92,6 +97,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
should_publish: ${{ github.event_name != 'workflow_dispatch' || inputs.publish == true }}
|
||||
build_type: ${{ steps.check.outputs.build_type }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
short_sha: ${{ steps.check.outputs.short_sha }}
|
||||
@@ -208,9 +214,9 @@ jobs:
|
||||
|
||||
all='{"include":[
|
||||
{"target_id":"linux-x86_64-musl","os":"sm-standard-4","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":""},
|
||||
{"target_id":"linux-aarch64-musl","os":"sm-standard-4","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":"","features":""},
|
||||
{"target_id":"linux-aarch64-musl","os":"sm-standard-4-arm","target":"aarch64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":""},
|
||||
{"target_id":"linux-x86_64-gnu","os":"sm-standard-4","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":"","features":"pyroscope"},
|
||||
{"target_id":"linux-aarch64-gnu","os":"sm-standard-4","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope"},
|
||||
{"target_id":"linux-aarch64-gnu","os":"sm-standard-4-arm","target":"aarch64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":"","features":"pyroscope"},
|
||||
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":"","features":"pyroscope"},
|
||||
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":"","features":""}
|
||||
]}'
|
||||
@@ -402,6 +408,8 @@ jobs:
|
||||
|
||||
- name: Build RustFS
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
# Force rebuild by touching build.rs
|
||||
touch rustfs/build.rs
|
||||
@@ -418,14 +426,37 @@ jobs:
|
||||
# Build one binary at a time so release LTO links cannot exhaust a
|
||||
# self-hosted runner by running concurrently.
|
||||
for binary in "${BINARY_NAMES[@]}"; do
|
||||
PROFILE_ARGS=()
|
||||
if [[ "$binary" == "rustfs" && "$BUILD_TARGET" == *-linux-gnu && "${{ matrix.features }}" == "pyroscope" ]]; then
|
||||
# The in-process profiler resolves names from the final executable.
|
||||
# Retain function symbols without enabling DWARF source information.
|
||||
PROFILE_ARGS=(--config 'profile.release.strip="none"')
|
||||
fi
|
||||
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
||||
# All cross targets in the matrix are Linux; zigbuild handles them.
|
||||
cargo zigbuild --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" -p rustfs --bin "$binary"
|
||||
cargo zigbuild --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" "${PROFILE_ARGS[@]}" -p rustfs --bin "$binary"
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" -p rustfs --bin "$binary"
|
||||
cargo build --release --target ${{ matrix.target }} "${FEATURE_ARGS[@]}" "${PROFILE_ARGS[@]}" -p rustfs --bin "$binary"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Generate the CPU symbol catalog
|
||||
if: endsWith(matrix.target, '-linux-gnu') && matrix.features == 'pyroscope'
|
||||
shell: bash
|
||||
run: |
|
||||
cargo metadata --locked --format-version 1 --filter-platform '${{ matrix.target }}' \
|
||||
--features rustfs/pyroscope > "$RUNNER_TEMP/cpu-catalog-metadata.json"
|
||||
features=$(jq -ce '
|
||||
(.packages[] | select(.name == "rustfs") | .id) as $id |
|
||||
.resolve.nodes[] | select(.id == $id) | .features | map(select(. != "default")) | sort
|
||||
' "$RUNNER_TEMP/cpu-catalog-metadata.json")
|
||||
release_dir="target/${{ matrix.target }}/release"
|
||||
# These task-owned outputs may remain in a restored target cache.
|
||||
rm -f "$release_dir/rustfs.cpu-symbol-catalog.json" "$release_dir/rustfs.cpu-symbol-catalog.sha256"
|
||||
cargo run --locked -p rustfs-connect-symbol-catalog -- \
|
||||
"$release_dir/rustfs" "$(git rev-parse HEAD)" '${{ matrix.target }}' "$features" \
|
||||
"$release_dir/rustfs.cpu-symbol-catalog.json" > "$release_dir/rustfs.cpu-symbol-catalog.sha256"
|
||||
|
||||
- name: Create release package
|
||||
id: package
|
||||
shell: bash
|
||||
@@ -516,6 +547,10 @@ jobs:
|
||||
BINARY_FILES+=("rustfs-cli")
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" == *-linux-gnu && "${{ matrix.features }}" == "pyroscope" ]]; then
|
||||
BINARY_FILES+=(rustfs.cpu-symbol-catalog.json rustfs.cpu-symbol-catalog.sha256)
|
||||
fi
|
||||
|
||||
# Verify the binaries exist before packaging
|
||||
for binary in "${BINARY_FILES[@]}"; do
|
||||
if [[ ! -f "$binary" ]]; then
|
||||
@@ -625,7 +660,7 @@ jobs:
|
||||
echo "🔧 Build type: ${BUILD_TYPE}"
|
||||
echo "📊 Version: ${VERSION}"
|
||||
|
||||
- name: Verify packaged Linux CLI
|
||||
- name: Verify packaged Linux binaries
|
||||
if: matrix.platform == 'linux'
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -634,21 +669,27 @@ jobs:
|
||||
package_file="${{ steps.package.outputs.package_file }}"
|
||||
archive_members="$(unzip -Z1 "$package_file" | sort)"
|
||||
expected_members=$'rustfs\nrustfs-cli'
|
||||
target="${{ matrix.target }}"
|
||||
features="${{ matrix.features }}"
|
||||
if [[ "$target" == *-linux-gnu && "$features" == "pyroscope" ]]; then
|
||||
expected_members+=$'\nrustfs.cpu-symbol-catalog.json\nrustfs.cpu-symbol-catalog.sha256'
|
||||
fi
|
||||
if [[ "$archive_members" != "$expected_members" ]]; then
|
||||
echo "Linux package must contain exactly rustfs and rustfs-cli" >&2
|
||||
echo "Linux package does not contain the expected files" >&2
|
||||
printf '%s\n' "$archive_members" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${{ matrix.target }}" == "x86_64-unknown-linux-gnu" ]]; then
|
||||
cli_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$cli_dir"' EXIT
|
||||
unzip -q "$package_file" rustfs-cli -d "$cli_dir"
|
||||
"$cli_dir/rustfs-cli" --help >/dev/null
|
||||
fi
|
||||
# Every Linux target builds natively, so executing the binaries here
|
||||
# proves each artifact matches its runner architecture.
|
||||
cli_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$cli_dir"' EXIT
|
||||
unzip -q "$package_file" rustfs rustfs-cli -d "$cli_dir"
|
||||
"$cli_dir/rustfs" --version
|
||||
"$cli_dir/rustfs-cli" --help >/dev/null
|
||||
|
||||
- name: Verify packaged console
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -709,7 +750,7 @@ jobs:
|
||||
retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 30 || 7 }}
|
||||
|
||||
- name: Upload to Cloudflare R2
|
||||
if: env.R2_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development')
|
||||
if: needs.build-check.outputs.should_publish == 'true' && env.R2_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development')
|
||||
env:
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
@@ -726,12 +767,33 @@ jobs:
|
||||
fi
|
||||
|
||||
# The self-hosted Linux runners do not ship the aws CLI (GitHub-hosted
|
||||
# images did). Install it on demand so R2 uploads survive a fresh runner
|
||||
# instead of hard-failing here.
|
||||
# images did). Install the official CLI bundle on demand so R2 uploads
|
||||
# survive a fresh runner without relying on a distro package.
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
echo "aws CLI not found on runner; installing..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update && sudo apt-get install -y awscli
|
||||
aws_arch="$(uname -m)"
|
||||
case "$aws_arch" in
|
||||
x86_64) aws_arch="x86_64" ;;
|
||||
aarch64|arm64) aws_arch="aarch64" ;;
|
||||
*)
|
||||
echo "❌ Unsupported Linux architecture for AWS CLI: $aws_arch"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
aws_install_root="${RUNNER_TEMP:-$PWD}/aws-cli"
|
||||
rm -rf "$aws_install_root"
|
||||
mkdir -p "$aws_install_root"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://awscli.amazonaws.com/awscli-exe-linux-${aws_arch}.zip" \
|
||||
--output "$aws_install_root/awscliv2.zip"
|
||||
unzip -q "$aws_install_root/awscliv2.zip" -d "$aws_install_root"
|
||||
"$aws_install_root/aws/install" \
|
||||
--install-dir "$aws_install_root/aws-cli" \
|
||||
--bin-dir "$aws_install_root/bin"
|
||||
export PATH="$aws_install_root/bin:$PATH"
|
||||
aws --version
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
brew install awscli
|
||||
else
|
||||
@@ -776,62 +838,35 @@ jobs:
|
||||
- name: Build completion summary
|
||||
shell: bash
|
||||
env:
|
||||
# dispatch input via env: free-form text must not be pasted into the
|
||||
# script for bash to evaluate.
|
||||
INPUT_BUILD_DOCKER: ${{ github.event.inputs.build_docker }}
|
||||
SHOULD_PUBLISH: ${{ needs.build-check.outputs.should_publish }}
|
||||
BUILD_TYPE: ${{ needs.build-check.outputs.build_type }}
|
||||
VERSION: ${{ needs.build-check.outputs.version }}
|
||||
BUILD_STATUS: ${{ needs.build-rustfs.result }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||
VERSION="${{ needs.build-check.outputs.version }}"
|
||||
|
||||
echo "🎉 Build completed successfully!"
|
||||
echo "📦 Build type: $BUILD_TYPE"
|
||||
echo "🔢 Version: $VERSION"
|
||||
echo ""
|
||||
|
||||
# Check build status
|
||||
BUILD_STATUS="${{ needs.build-rustfs.result }}"
|
||||
|
||||
echo "📊 Build Results:"
|
||||
echo " 📦 All platforms: $BUILD_STATUS"
|
||||
echo ""
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
"preview")
|
||||
echo "🔍 Preview artifacts are published in a GitHub prerelease"
|
||||
echo "⏭️ Preview releases do not update latest channels"
|
||||
;;
|
||||
"development")
|
||||
echo "🛠️ Development build artifacts have been uploaded to OSS dev directory"
|
||||
echo "⚠️ This is a development build - not suitable for production use"
|
||||
;;
|
||||
"release")
|
||||
echo "🚀 Release build artifacts have been uploaded to OSS release directory"
|
||||
echo "✅ This build is ready for production use"
|
||||
echo "🏷️ GitHub Release will be created in this workflow"
|
||||
;;
|
||||
"prerelease")
|
||||
echo "🧪 Prerelease build artifacts have been uploaded to OSS release directory"
|
||||
echo "⚠️ This is a prerelease build - use with caution"
|
||||
echo "🏷️ GitHub Release will be created in this workflow"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🐳 Docker Images:"
|
||||
if [[ "$INPUT_BUILD_DOCKER" == "false" ]]; then
|
||||
echo "⏭️ Docker image build was skipped (binary only build)"
|
||||
echo "Build type: $BUILD_TYPE"
|
||||
echo "Version: $VERSION"
|
||||
echo "Build result: $BUILD_STATUS"
|
||||
if [[ "$SHOULD_PUBLISH" != "true" ]]; then
|
||||
echo "Artifact-only run: no download channels or GitHub Releases are updated."
|
||||
echo "Successful platform packages are available as workflow artifacts."
|
||||
elif [[ "$BUILD_STATUS" == "success" ]]; then
|
||||
echo "🔄 Docker images will be built and pushed automatically via workflow_run event"
|
||||
echo "Publishing enabled; check the upload and release jobs for their results."
|
||||
else
|
||||
echo "❌ Docker image build will be skipped due to build failure"
|
||||
echo "Build incomplete; check individual platform jobs for any completed uploads."
|
||||
fi
|
||||
if [[ "$EVENT_NAME" == "push" && "$GITHUB_REF" == refs/tags/* && "$BUILD_STATUS" == "success" ]]; then
|
||||
echo "Docker publishing is eligible through the downstream workflow."
|
||||
else
|
||||
echo "This run does not trigger Docker publishing."
|
||||
fi
|
||||
|
||||
# Create GitHub Release for every valid release tag, including previews
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
needs: [ build-check, build-rustfs ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: sm-standard-2
|
||||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -891,8 +926,8 @@ jobs:
|
||||
upload-release-assets:
|
||||
name: Upload Release Assets
|
||||
needs: [ build-check, build-rustfs, create-release ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: sm-standard-2
|
||||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -985,7 +1020,7 @@ jobs:
|
||||
update-latest-version:
|
||||
name: Update Latest Version
|
||||
needs: [ build-check, publish-release ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: sm-standard-2
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -1045,8 +1080,8 @@ jobs:
|
||||
publish-release:
|
||||
name: Publish Release
|
||||
needs: [ build-check, create-release, upload-release-assets ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: sm-standard-2
|
||||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'preview' || needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -1083,8 +1118,8 @@ jobs:
|
||||
cleanup-preview-releases:
|
||||
name: Cleanup Preview Releases
|
||||
needs: [ build-check, publish-release ]
|
||||
if: startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: sm-standard-2
|
||||
if: needs.build-check.outputs.should_publish == 'true' && startsWith(github.ref, 'refs/tags/') && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease')
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# Copyright 2026 RustFS Team
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Connect bounded health service acceptance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: Successful main-branch Build and Release run ID
|
||||
required: true
|
||||
type: string
|
||||
artifact_id:
|
||||
description: Linux x86_64 GNU artifact from that run
|
||||
required: true
|
||||
type: string
|
||||
source_sha:
|
||||
description: Exact RustFS source commit
|
||||
required: true
|
||||
type: string
|
||||
artifact_digest:
|
||||
description: GitHub artifact digest including sha256 prefix
|
||||
required: true
|
||||
type: string
|
||||
binary_sha256:
|
||||
description: Independently verified rustfs binary SHA-256
|
||||
required: true
|
||||
type: string
|
||||
connect_sha:
|
||||
description: Exact Connect health service acceptance harness commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
health-service:
|
||||
name: Verify bounded health service over mTLS
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.connect_sha }}
|
||||
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Ensure GitHub CLI
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
gh --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh_version="2.101.0"
|
||||
case "$(uname -m)" in
|
||||
x86_64) gh_arch="amd64" ;;
|
||||
aarch64|arm64) gh_arch="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported Linux architecture for GitHub CLI: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
gh_install_root="${RUNNER_TEMP:-$PWD}/gh-cli"
|
||||
rm -rf "$gh_install_root"
|
||||
mkdir -p "$gh_install_root/bin"
|
||||
archive="$gh_install_root/gh.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_linux_${gh_arch}.tar.gz" \
|
||||
--output "$archive"
|
||||
tar -xzf "$archive" -C "$gh_install_root"
|
||||
install -m 0755 \
|
||||
"$gh_install_root/gh_${gh_version}_linux_${gh_arch}/bin/gh" \
|
||||
"$gh_install_root/bin/gh"
|
||||
echo "$gh_install_root/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$gh_install_root/bin:$PATH"
|
||||
gh --version
|
||||
|
||||
- name: Verify official source and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$GITHUB_REPOSITORY" == rustfs/rustfs ]]
|
||||
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ && "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ && "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
|
||||
[[ "$BINARY_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
|
||||
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
|
||||
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
|
||||
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
|
||||
run=$(gh api "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}")
|
||||
jq -e --arg source "$SOURCE_SHA" '
|
||||
.head_sha == $source and .head_branch == "main"
|
||||
and .head_repository.full_name == "rustfs/rustfs"
|
||||
and .name == "Build and Release" and .path == ".github/workflows/build.yml"
|
||||
and .status == "completed" and .conclusion == "success"
|
||||
' <<<"$run" >/dev/null
|
||||
jobs=$(gh api --paginate --slurp "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e '
|
||||
[.[].jobs[] | select(.name | test("^Build RustFS \\(linux-x86_64-gnu, [a-z0-9-]+, x86_64-unknown-linux-gnu, false, linux, pyroscope\\)$"))] as $matches
|
||||
| ($matches | length) == 1 and $matches[0].conclusion == "success"
|
||||
' <<<"$jobs" >/dev/null
|
||||
artifact=$(gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}")
|
||||
jq -e --argjson run "$BUILD_RUN_ID" --arg source "$SOURCE_SHA" --arg digest "$ARTIFACT_DIGEST" '
|
||||
.workflow_run.id == $run and .workflow_run.head_sha == $source
|
||||
and .name == ("rustfs-linux-x86_64-gnu-dev-" + $source[0:7])
|
||||
and .digest == $digest and .expired == false
|
||||
and (.expires_at | fromdateiso8601) > now
|
||||
and .size_in_bytes > 0 and .size_in_bytes <= 2147483648
|
||||
' <<<"$artifact" >/dev/null
|
||||
jq -r '.expires_at' <<<"$artifact" > artifact-expires-at
|
||||
gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}/zip" > official-artifact.zip
|
||||
[[ $(stat --format=%s official-artifact.zip) == $(jq -r '.size_in_bytes' <<<"$artifact") ]]
|
||||
printf '%s official-artifact.zip\n' "${ARTIFACT_DIGEST#sha256:}" | sha256sum --check --strict
|
||||
|
||||
- name: Extract only the verified official service binary
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import zipfile
|
||||
|
||||
def regular(entry):
|
||||
mode = entry.external_attr >> 16
|
||||
return not entry.is_dir() and stat.S_IFMT(mode) in (0, stat.S_IFREG)
|
||||
|
||||
package = f"rustfs-linux-x86_64-gnu-dev-{os.environ['SOURCE_SHA'][:7]}.zip"
|
||||
with zipfile.ZipFile('official-artifact.zip') as outer:
|
||||
entries = outer.infolist()
|
||||
assert 1 <= len(entries) <= 16
|
||||
assert len({entry.filename for entry in entries}) == len(entries)
|
||||
assert all(regular(entry) and pathlib.PurePosixPath(entry.filename).name == entry.filename for entry in entries)
|
||||
assert sum(entry.file_size for entry in entries) <= 2147483648
|
||||
assert outer.testzip() is None
|
||||
with zipfile.ZipFile(io.BytesIO(outer.read(package))) as inner:
|
||||
entries = inner.infolist()
|
||||
assert {entry.filename for entry in entries} == {'rustfs', 'rustfs-cli'} and len(entries) == 2
|
||||
assert all(regular(entry) and 0 < entry.file_size <= 1073741824 for entry in entries)
|
||||
assert inner.testzip() is None
|
||||
pathlib.Path('binary').mkdir()
|
||||
with inner.open('rustfs') as source, open('binary/rustfs', 'xb') as destination:
|
||||
header = source.read(20)
|
||||
assert header[:7] == b'\x7fELF\x02\x01\x01' and header[18:20] == b'\x3e\x00', 'Expected a little-endian ELF64 x86-64 binary'
|
||||
destination.write(header)
|
||||
shutil.copyfileobj(source, destination)
|
||||
PY
|
||||
chmod 0755 binary/rustfs
|
||||
printf '%s binary/rustfs\n' "$BINARY_SHA256" | sha256sum --check --strict
|
||||
|
||||
- name: Run the real health service job
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git -C connect-harness diff --exit-code
|
||||
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
|
||||
changed=$(git -C connect-harness diff --name-only)
|
||||
[[ -z "$changed" || "$changed" == tests/e2e/connected/rustfs-ref ]]
|
||||
npm --prefix connect-harness/web ci
|
||||
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
|
||||
make -C connect-harness e2e-connected-dispatch-check
|
||||
(cd connect-harness && ./tests/connectivity/health-service-evidence.test.sh)
|
||||
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
|
||||
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
|
||||
CONNECT_E2E_HEALTH_SERVICE_EVIDENCE="$GITHUB_WORKSPACE/health-service-evidence.json" \
|
||||
make -C connect-harness e2e-connected E2E_SCENARIO=health-service
|
||||
|
||||
- name: Bind and validate sanitized evidence
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq -e --arg connect "$CONNECT_SHA" --arg source "$SOURCE_SHA" --arg binary "$BINARY_SHA256" '
|
||||
.provenance.connectSha == $connect and .provenance.sourceSha == $source
|
||||
and .provenance.binarySha256 == $binary
|
||||
' health-service-evidence.json >/dev/null
|
||||
jq --arg run "$GITHUB_RUN_ID" --arg build "$BUILD_RUN_ID" \
|
||||
--arg artifact "$ARTIFACT_ID" --arg digest "$ARTIFACT_DIGEST" \
|
||||
--arg expiry "$(cat artifact-expires-at)" '
|
||||
.provenance += {workflowRunId:$run,buildRunId:$build,artifactId:$artifact,artifactDigest:$digest,artifactExpiresAt:$expiry}
|
||||
' health-service-evidence.json > health-service-evidence.bound.json
|
||||
mv health-service-evidence.bound.json health-service-evidence.json
|
||||
node connect-harness/tests/connectivity/health-service-evidence.mjs health-service-evidence.json --bound
|
||||
|
||||
- name: Upload only verified sanitized evidence
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: connect-health-service-evidence-${{ github.run_id }}
|
||||
path: health-service-evidence.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
|
||||
@@ -37,6 +37,10 @@ on:
|
||||
description: Expected rustfs binary SHA-256
|
||||
required: true
|
||||
type: string
|
||||
catalog_sha256:
|
||||
description: Independently reviewed release CPU catalog SHA-256
|
||||
required: true
|
||||
type: string
|
||||
connect_sha:
|
||||
description: Exact 40-character RustFS Connect harness commit
|
||||
required: true
|
||||
@@ -49,7 +53,7 @@ permissions:
|
||||
jobs:
|
||||
profile-cpu:
|
||||
name: Verify native Linux x86_64 profile.cpu service job
|
||||
runs-on: sm-standard-2
|
||||
runs-on: dind-sm-standard-2
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
@@ -76,7 +80,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -101,20 +105,27 @@ jobs:
|
||||
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
|
||||
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
|
||||
|
||||
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
|
||||
github_api() {
|
||||
curl --fail --silent --show-error \
|
||||
--header "Authorization: Bearer $GH_TOKEN" \
|
||||
--header 'Accept: application/vnd.github+json' \
|
||||
"https://api.github.com/$1"
|
||||
}
|
||||
|
||||
run=$(github_api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
|
||||
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
|
||||
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(github_api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
[.jobs[] | select(.name == $name)] as $matches
|
||||
| (($matches | length) == 1 and $matches[0].conclusion == "success")
|
||||
' <<<"$jobs" >/dev/null
|
||||
|
||||
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
|
||||
artifact=$(github_api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
|
||||
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
|
||||
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
|
||||
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
|
||||
@@ -129,24 +140,32 @@ jobs:
|
||||
run-id: ${{ inputs.build_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Extract exact RustFS binary
|
||||
- name: Extract exact RustFS binary and reviewed catalog
|
||||
shell: bash
|
||||
env:
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CATALOG_SHA256: ${{ inputs.catalog_sha256 }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$BINARY_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ "$CATALOG_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
short_sha=${SOURCE_SHA:0:7}
|
||||
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
|
||||
[[ -n "$package" ]]
|
||||
mkdir -p binary
|
||||
unzip -qq "$package" rustfs -d binary
|
||||
unzip -qq "$package" rustfs rustfs.cpu-symbol-catalog.json rustfs.cpu-symbol-catalog.sha256 -d binary
|
||||
chmod +x binary/rustfs
|
||||
printf '%s %s\n' "$BINARY_SHA256" binary/rustfs | sha256sum --check --strict
|
||||
# The packaged digest is corroboration, never the independent trust input.
|
||||
[[ $(cat binary/rustfs.cpu-symbol-catalog.sha256) == "$CATALOG_SHA256" ]]
|
||||
printf '%s %s\n' "$CATALOG_SHA256" binary/rustfs.cpu-symbol-catalog.json | sha256sum --check --strict
|
||||
chmod 0444 binary/rustfs.cpu-symbol-catalog.json
|
||||
|
||||
- name: Run signed service job acceptance
|
||||
shell: bash
|
||||
env:
|
||||
CATALOG_SHA256: ${{ inputs.catalog_sha256 }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -156,6 +175,8 @@ jobs:
|
||||
make -C connect-harness e2e-connected-dispatch-check
|
||||
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
|
||||
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
|
||||
CONNECT_E2E_CPU_CATALOG="$GITHUB_WORKSPACE/binary/rustfs.cpu-symbol-catalog.json" \
|
||||
CONNECT_E2E_CPU_CATALOG_SHA256="$CATALOG_SHA256" \
|
||||
CONNECT_E2E_PROFILE_EVIDENCE="$GITHUB_WORKSPACE/profile-service-job-evidence.json" \
|
||||
make -C connect-harness e2e-connected E2E_SCENARIO=profile
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ permissions:
|
||||
jobs:
|
||||
profile-memory:
|
||||
name: Verify signed service memory profile over mTLS
|
||||
runs-on: sm-standard-2
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout exact RustFS source
|
||||
@@ -60,10 +60,43 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Ensure GitHub CLI
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
gh --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh_version="2.101.0"
|
||||
case "$(uname -m)" in
|
||||
x86_64) gh_arch="amd64" ;;
|
||||
aarch64|arm64) gh_arch="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported Linux architecture for GitHub CLI: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
gh_install_root="${RUNNER_TEMP:-$PWD}/gh-cli"
|
||||
rm -rf "$gh_install_root"
|
||||
mkdir -p "$gh_install_root/bin"
|
||||
archive="$gh_install_root/gh.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_linux_${gh_arch}.tar.gz" \
|
||||
--output "$archive"
|
||||
tar -xzf "$archive" -C "$gh_install_root"
|
||||
install -m 0755 \
|
||||
"$gh_install_root/gh_${gh_version}_linux_${gh_arch}/bin/gh" \
|
||||
"$gh_install_root/bin/gh"
|
||||
echo "$gh_install_root/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$gh_install_root/bin:$PATH"
|
||||
gh --version
|
||||
|
||||
- name: Verify official source and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
@@ -139,7 +172,8 @@ jobs:
|
||||
assert outer.testzip() is None
|
||||
with zipfile.ZipFile(io.BytesIO(outer.read(package))) as inner:
|
||||
entries = inner.infolist()
|
||||
assert {entry.filename for entry in entries} == {'rustfs', 'rustfs-cli'} and len(entries) == 2
|
||||
expected = {'rustfs', 'rustfs-cli', 'rustfs.cpu-symbol-catalog.json', 'rustfs.cpu-symbol-catalog.sha256'}
|
||||
assert {entry.filename for entry in entries} == expected and len(entries) == len(expected)
|
||||
assert all(regular(entry) and 0 < entry.file_size <= 1073741824 for entry in entries)
|
||||
assert inner.testzip() is None
|
||||
pathlib.Path('binary').mkdir()
|
||||
|
||||
@@ -17,6 +17,14 @@ name: Connect profile.threads service-job acceptance
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
thread_scope:
|
||||
description: Thread profile scope
|
||||
required: true
|
||||
type: choice
|
||||
default: native
|
||||
options:
|
||||
- native
|
||||
- runtime
|
||||
build_run_id:
|
||||
description: Main-branch Build and Release workflow run ID
|
||||
required: true
|
||||
@@ -48,8 +56,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
profile-threads:
|
||||
name: Verify native Linux x86_64 profile.threads service job
|
||||
runs-on: sm-standard-2
|
||||
name: Verify Linux x86_64 profile.threads service job
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
@@ -76,10 +84,43 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Ensure GitHub CLI
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
gh --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh_version="2.101.0"
|
||||
case "$(uname -m)" in
|
||||
x86_64) gh_arch="amd64" ;;
|
||||
aarch64|arm64) gh_arch="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported Linux architecture for GitHub CLI: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
gh_install_root="${RUNNER_TEMP:-$PWD}/gh-cli"
|
||||
rm -rf "$gh_install_root"
|
||||
mkdir -p "$gh_install_root/bin"
|
||||
archive="$gh_install_root/gh.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_linux_${gh_arch}.tar.gz" \
|
||||
--output "$archive"
|
||||
tar -xzf "$archive" -C "$gh_install_root"
|
||||
install -m 0755 \
|
||||
"$gh_install_root/gh_${gh_version}_linux_${gh_arch}/bin/gh" \
|
||||
"$gh_install_root/bin/gh"
|
||||
echo "$gh_install_root/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$gh_install_root/bin:$PATH"
|
||||
gh --version
|
||||
|
||||
- name: Verify source run and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
@@ -107,7 +148,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
@@ -148,8 +189,14 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
THREAD_SCOPE: ${{ inputs.thread_scope }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$THREAD_SCOPE" in
|
||||
native) export CONNECT_E2E_THREAD_SCOPE=NATIVE_THREADS ;;
|
||||
runtime) export CONNECT_E2E_THREAD_SCOPE=TOKIO_RUNTIME ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
|
||||
npm --prefix connect-harness/web ci
|
||||
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
|
||||
@@ -168,6 +215,7 @@ jobs:
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
THREAD_SCOPE: ${{ inputs.thread_scope }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq \
|
||||
@@ -177,7 +225,8 @@ jobs:
|
||||
--arg artifactDigest "$ARTIFACT_DIGEST" \
|
||||
--arg connectSha "$CONNECT_SHA" \
|
||||
--arg sourceSha "$SOURCE_SHA" \
|
||||
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha}' \
|
||||
--arg requestedThreadScope "$THREAD_SCOPE" \
|
||||
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest, connectSha: $connectSha, sourceSha: $sourceSha, requestedThreadScope: $requestedThreadScope}' \
|
||||
profile-threads-service-job-evidence.json >profile-threads-service-job-evidence.bound.json
|
||||
mv profile-threads-service-job-evidence.bound.json profile-threads-service-job-evidence.json
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: Connect top.disk artifact acceptance
|
||||
name: Connect top.disk proxy service-job acceptance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: Successful main-branch Build and Release workflow run ID
|
||||
description: Successful main-branch Build and Release run ID
|
||||
required: true
|
||||
type: string
|
||||
artifact_id:
|
||||
description: Linux x86_64 GNU artifact ID from that run
|
||||
description: Linux x86_64 GNU artifact from that run
|
||||
required: true
|
||||
type: string
|
||||
source_sha:
|
||||
description: Exact 40-character source commit
|
||||
description: Exact RustFS source commit
|
||||
required: true
|
||||
type: string
|
||||
artifact_digest:
|
||||
@@ -34,7 +34,11 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
binary_sha256:
|
||||
description: Expected rustfs binary SHA-256
|
||||
description: Independently verified rustfs binary SHA-256
|
||||
required: true
|
||||
type: string
|
||||
connect_sha:
|
||||
description: Exact Connect top.disk proxy service-job acceptance harness commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -44,16 +48,67 @@ permissions:
|
||||
|
||||
jobs:
|
||||
top-disk:
|
||||
name: Verify native Linux x86_64 top.disk artifact
|
||||
runs-on: sm-standard-2
|
||||
timeout-minutes: 15
|
||||
name: Verify native top.disk service job through authenticated proxy
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout acceptance harness
|
||||
- name: Checkout exact RustFS source
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
path: rustfs-source
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Verify source run and artifact identity
|
||||
- name: Checkout exact Connect harness
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
repository: rustfs/connect
|
||||
path: connect-harness
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.connect_sha }}
|
||||
token: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
- name: Ensure GitHub CLI
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
gh --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh_version="2.101.0"
|
||||
case "$(uname -m)" in
|
||||
x86_64) gh_arch="amd64" ;;
|
||||
aarch64|arm64) gh_arch="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported Linux architecture for GitHub CLI: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
gh_install_root="${RUNNER_TEMP:-$PWD}/gh-cli"
|
||||
rm -rf "$gh_install_root"
|
||||
mkdir -p "$gh_install_root/bin"
|
||||
archive="$gh_install_root/gh.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_linux_${gh_arch}.tar.gz" \
|
||||
--output "$archive"
|
||||
tar -xzf "$archive" -C "$gh_install_root"
|
||||
install -m 0755 \
|
||||
"$gh_install_root/gh_${gh_version}_linux_${gh_arch}/bin/gh" \
|
||||
"$gh_install_root/bin/gh"
|
||||
echo "$gh_install_root/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$gh_install_root/bin:$PATH"
|
||||
gh --version
|
||||
|
||||
- name: Verify official source and artifact identity
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -61,82 +116,150 @@ jobs:
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$BUILD_RUN_ID" =~ ^[0-9]+$ ]]
|
||||
[[ "$ARTIFACT_ID" =~ ^[0-9]+$ ]]
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$GITHUB_REPOSITORY" == rustfs/rustfs ]]
|
||||
[[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ && "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ && "$CONNECT_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$ARTIFACT_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
|
||||
[[ "$BINARY_SHA256" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ $(git -C rustfs-source rev-parse HEAD) == "$SOURCE_SHA" ]]
|
||||
[[ $(git -C connect-harness rev-parse HEAD) == "$CONNECT_SHA" ]]
|
||||
[[ $(git -C rustfs-source remote get-url origin) == https://github.com/rustfs/rustfs ]]
|
||||
[[ $(git -C connect-harness remote get-url origin) == https://github.com/rustfs/connect ]]
|
||||
run=$(gh api "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}")
|
||||
jq -e --arg source "$SOURCE_SHA" '
|
||||
.head_sha == $source and .head_branch == "main"
|
||||
and .head_repository.full_name == "rustfs/rustfs"
|
||||
and .name == "Build and Release" and .path == ".github/workflows/build.yml"
|
||||
and .status == "completed" and .conclusion == "success"
|
||||
' <<<"$run" >/dev/null
|
||||
jobs=$(gh api --paginate --slurp "repos/rustfs/rustfs/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e '
|
||||
[.[].jobs[] | select(.name | test("^Build RustFS \\(linux-x86_64-gnu, [a-z0-9-]+, x86_64-unknown-linux-gnu, false, linux, pyroscope\\)$"))] as $matches
|
||||
| ($matches | length) == 1 and $matches[0].conclusion == "success"
|
||||
' <<<"$jobs" >/dev/null
|
||||
artifact=$(gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}")
|
||||
jq -e --argjson run "$BUILD_RUN_ID" --arg source "$SOURCE_SHA" --arg digest "$ARTIFACT_DIGEST" '
|
||||
.workflow_run.id == $run and .workflow_run.head_sha == $source
|
||||
and .name == ("rustfs-linux-x86_64-gnu-dev-" + $source[0:7])
|
||||
and .digest == $digest and .expired == false
|
||||
and (.expires_at | fromdateiso8601) > now
|
||||
and .size_in_bytes > 0 and .size_in_bytes <= 2147483648
|
||||
' <<<"$artifact" >/dev/null
|
||||
jq -r '.expires_at' <<<"$artifact" > artifact-expires-at
|
||||
gh api "repos/rustfs/rustfs/actions/artifacts/${ARTIFACT_ID}/zip" > official-artifact.zip
|
||||
[[ $(stat --format=%s official-artifact.zip) == $(jq -r '.size_in_bytes' <<<"$artifact") ]]
|
||||
printf '%s official-artifact.zip\n' "${ARTIFACT_DIGEST#sha256:}" | sha256sum --check --strict
|
||||
|
||||
run=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}")
|
||||
[[ $(jq -r '.conclusion' <<<"$run") == success ]]
|
||||
[[ $(jq -r '.head_sha' <<<"$run") == "$SOURCE_SHA" ]]
|
||||
[[ $(jq -r '.head_branch' <<<"$run") == main ]]
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
artifact=$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")
|
||||
[[ $(jq -r '.workflow_run.id' <<<"$artifact") == "$BUILD_RUN_ID" ]]
|
||||
[[ $(jq -r '.workflow_run.head_sha' <<<"$artifact") == "$SOURCE_SHA" ]]
|
||||
[[ $(jq -r '.name' <<<"$artifact") == rustfs-linux-x86_64-gnu-* ]]
|
||||
[[ $(jq -r '.digest' <<<"$artifact") == "$ARTIFACT_DIGEST" ]]
|
||||
[[ $(jq -r '.expired' <<<"$artifact") == false ]]
|
||||
|
||||
- name: Download exact build artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs.artifact_id }}
|
||||
path: artifact
|
||||
run-id: ${{ inputs.build_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Extract exact RustFS binary
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short_sha=${SOURCE_SHA:0:7}
|
||||
package=$(find artifact -type f -name "rustfs-linux-x86_64-gnu-dev-${short_sha}.zip" -print -quit)
|
||||
[[ -n "$package" ]]
|
||||
mkdir -p binary
|
||||
unzip -qq "$package" rustfs -d binary
|
||||
chmod +x binary/rustfs
|
||||
|
||||
- name: Run top.disk acceptance
|
||||
- name: Extract only the verified official service binary
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/ci/check_connect_top_disk_artifact.sh \
|
||||
binary/rustfs "$SOURCE_SHA" "$BINARY_SHA256" \
|
||||
top-disk-runtime-evidence.json top-disk.zip
|
||||
python3 - <<'PY'
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import zipfile
|
||||
|
||||
- name: Bind workflow and artifact provenance
|
||||
def regular(entry):
|
||||
mode = entry.external_attr >> 16
|
||||
return not entry.is_dir() and stat.S_IFMT(mode) in (0, stat.S_IFREG)
|
||||
|
||||
package = f"rustfs-linux-x86_64-gnu-dev-{os.environ['SOURCE_SHA'][:7]}.zip"
|
||||
with zipfile.ZipFile('official-artifact.zip') as outer:
|
||||
entries = outer.infolist()
|
||||
assert 1 <= len(entries) <= 16
|
||||
assert len({entry.filename for entry in entries}) == len(entries)
|
||||
assert all(regular(entry) and pathlib.PurePosixPath(entry.filename).name == entry.filename for entry in entries)
|
||||
assert sum(entry.file_size for entry in entries) <= 2147483648
|
||||
assert outer.testzip() is None
|
||||
with zipfile.ZipFile(io.BytesIO(outer.read(package))) as inner:
|
||||
entries = inner.infolist()
|
||||
expected = {'rustfs', 'rustfs-cli', 'rustfs.cpu-symbol-catalog.json', 'rustfs.cpu-symbol-catalog.sha256'}
|
||||
assert {entry.filename for entry in entries} == expected and len(entries) == len(expected)
|
||||
assert all(regular(entry) and 0 < entry.file_size <= 1073741824 for entry in entries)
|
||||
assert inner.testzip() is None
|
||||
pathlib.Path('binary').mkdir()
|
||||
with inner.open('rustfs') as source, open('binary/rustfs', 'xb') as destination:
|
||||
header = source.read(20)
|
||||
assert header[:7] == b'\x7fELF\x02\x01\x01' and header[18:20] == b'\x3e\x00', 'Expected a little-endian ELF64 x86-64 binary'
|
||||
destination.write(header)
|
||||
shutil.copyfileobj(source, destination)
|
||||
PY
|
||||
chmod 0755 binary/rustfs
|
||||
printf '%s binary/rustfs\n' "$BINARY_SHA256" | sha256sum --check --strict
|
||||
|
||||
- name: Run the real top.disk service job through proxy
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
CONNECT_E2E_PRODUCER_SOURCE: ${{ inputs.source_sha }}
|
||||
CONNECT_E2E_PRODUCER_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_E2E_PRODUCER_ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git -C connect-harness diff --exit-code
|
||||
printf '%s\n' "$SOURCE_SHA" >connect-harness/tests/e2e/connected/rustfs-ref
|
||||
changed=$(git -C connect-harness diff --name-only)
|
||||
[[ -z "$changed" || "$changed" == tests/e2e/connected/rustfs-ref ]]
|
||||
npm --prefix connect-harness/web ci
|
||||
connect-harness/web/node_modules/.bin/playwright install --with-deps chromium
|
||||
make -C connect-harness e2e-connected-dispatch-check
|
||||
RUSTFS_BINARY="$GITHUB_WORKSPACE/binary/rustfs" \
|
||||
RUSTFS_WORKTREE="$GITHUB_WORKSPACE/rustfs-source" \
|
||||
CONNECT_E2E_TOP_DISK_PROXY_EVIDENCE="$GITHUB_WORKSPACE/top-disk-proxy-service-job-evidence.json" \
|
||||
make -C connect-harness e2e-connected E2E_SCENARIO=top-disk-proxy
|
||||
|
||||
- name: Bind and validate sanitized evidence
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_RUN_ID: ${{ inputs.build_run_id }}
|
||||
ARTIFACT_ID: ${{ inputs.artifact_id }}
|
||||
ARTIFACT_DIGEST: ${{ inputs.artifact_digest }}
|
||||
BINARY_SHA256: ${{ inputs.binary_sha256 }}
|
||||
CONNECT_SHA: ${{ inputs.connect_sha }}
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq \
|
||||
--arg workflowRunId "$GITHUB_RUN_ID" \
|
||||
--arg buildRunId "$BUILD_RUN_ID" \
|
||||
--arg artifactId "$ARTIFACT_ID" \
|
||||
--arg artifactDigest "$ARTIFACT_DIGEST" \
|
||||
'. + {workflowRunId: $workflowRunId, buildRunId: $buildRunId, artifactId: $artifactId, artifactDigest: $artifactDigest}' \
|
||||
top-disk-runtime-evidence.json >top-disk-runtime-evidence.bound.json
|
||||
mv top-disk-runtime-evidence.bound.json top-disk-runtime-evidence.json
|
||||
jq -e --arg source "$SOURCE_SHA" --arg binary "$BINARY_SHA256" --arg artifact "$ARTIFACT_ID" '
|
||||
.scenario == "top-disk-proxy" and .topDisk.status == "PASS"
|
||||
and .topDisk.officialArtifactId == $artifact and .topDisk.producerSource == $source
|
||||
and .topDisk.producerSha256 == $binary
|
||||
and .topDisk.result.provenance.sourceCommit == $source
|
||||
and .topDisk.result.provenance.executableSha256 == $binary
|
||||
and .topology.agentsInternalOnly == true and .topology.proxyOnlyBridge == true
|
||||
and .topology.proxyCredentialsFromFiles == true
|
||||
and .probes.direct.connectStatus == "000"
|
||||
and .probes.noAuthentication.connectStatus == "407"
|
||||
and .probes.deniedTarget.connectStatus == "403"
|
||||
and .probes.wrongCa.connectStatus == "200" and .probes.wrongCa.exitCode == 60
|
||||
and .probes.approved.connectStatus == "200" and .probes.approved.exitCode == 0
|
||||
and .proxyLogCounts.approvedTunnels >= 3
|
||||
and .ingress.jobFetchObserved == true
|
||||
and .ingress.bundleReservationObserved == true
|
||||
and .ingress.boundBundleCompletionObserved == true
|
||||
and .ingress.jobResultObserved == true
|
||||
' top-disk-proxy-service-job-evidence.json >/dev/null
|
||||
jq --arg run "$GITHUB_RUN_ID" --arg build "$BUILD_RUN_ID" \
|
||||
--arg artifact "$ARTIFACT_ID" --arg digest "$ARTIFACT_DIGEST" \
|
||||
--arg connect "$CONNECT_SHA" --arg source "$SOURCE_SHA" --arg binary "$BINARY_SHA256" \
|
||||
--arg expiry "$(cat artifact-expires-at)" '
|
||||
. + {workflowRunId:$run,buildRunId:$build,artifactId:$artifact,artifactDigest:$digest,artifactExpiresAt:$expiry,connectSha:$connect,sourceSha:$source,binarySha256:$binary}
|
||||
' top-disk-proxy-service-job-evidence.json > top-disk-proxy-service-job-evidence.bound.json
|
||||
mv top-disk-proxy-service-job-evidence.bound.json top-disk-proxy-service-job-evidence.json
|
||||
|
||||
- name: Upload acceptance evidence
|
||||
- name: Upload only verified sanitized evidence
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: connect-top-disk-evidence-${{ github.run_id }}
|
||||
path: |
|
||||
top-disk-runtime-evidence.json
|
||||
top-disk.zip
|
||||
name: connect-top-disk-proxy-service-job-evidence-${{ github.run_id }}
|
||||
path: top-disk-proxy-service-job-evidence.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22.22.2
|
||||
node-version: 25.8.1
|
||||
cache: npm
|
||||
cache-dependency-path: connect-harness/web/package-lock.json
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
[[ $(jq -r '.head_repository.full_name' <<<"$run") == "$GITHUB_REPOSITORY" ]]
|
||||
[[ $(jq -r '.name' <<<"$run") == "Build and Release" ]]
|
||||
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-2, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
expected_job='Build RustFS (linux-x86_64-gnu, sm-standard-4, x86_64-unknown-linux-gnu, false, linux, pyroscope)'
|
||||
jobs=$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${BUILD_RUN_ID}/jobs?per_page=100")
|
||||
jq -e --arg name "$expected_job" '
|
||||
[.[].jobs[] | select(.name == $name)] as $matches
|
||||
|
||||
@@ -11,12 +11,44 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
collect:
|
||||
runs-on: sm-standard-2
|
||||
runs-on: smoke-testing
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Ensure GitHub CLI
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v gh >/dev/null 2>&1; then
|
||||
gh --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh_version="2.101.0"
|
||||
case "$(uname -m)" in
|
||||
x86_64) gh_arch="amd64" ;;
|
||||
aarch64|arm64) gh_arch="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported Linux architecture for GitHub CLI: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
gh_install_root="${RUNNER_TEMP:-$PWD}/gh-cli"
|
||||
rm -rf "$gh_install_root"
|
||||
mkdir -p "$gh_install_root/bin"
|
||||
archive="$gh_install_root/gh.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/cli/cli/releases/download/v${gh_version}/gh_${gh_version}_linux_${gh_arch}.tar.gz" \
|
||||
--output "$archive"
|
||||
tar -xzf "$archive" -C "$gh_install_root"
|
||||
install -m 0755 \
|
||||
"$gh_install_root/gh_${gh_version}_linux_${gh_arch}/bin/gh" \
|
||||
"$gh_install_root/bin/gh"
|
||||
echo "$gh_install_root/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$gh_install_root/bin:$PATH"
|
||||
gh --version
|
||||
- name: Collect and publish verified chain health
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch != 'main')
|
||||
runs-on: sm-standard-2
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
package:
|
||||
name: Package (${{ matrix.arch }}-${{ matrix.libc }})
|
||||
needs: resolve
|
||||
runs-on: sm-standard-2
|
||||
runs-on: ubuntu-latest # gh CLI required; the sm-standard-2 fleet does not ship it
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -42,7 +42,7 @@ concurrency:
|
||||
jobs:
|
||||
prepare:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'schedule' }}
|
||||
runs-on: sm-standard-2
|
||||
runs-on: smoke-testing
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
manifest: ${{ steps.candidate.outputs.manifest }}
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
complete-chain:
|
||||
needs: [prepare, upgrade, s3, kms, tier, storage, heal, pool, security, replication, fault-tolerance, table, performance]
|
||||
if: ${{ always() && needs.prepare.result != 'skipped' }}
|
||||
runs-on: sm-standard-2
|
||||
runs-on: smoke-testing
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
|
||||
@@ -7,6 +7,11 @@ on:
|
||||
description: Verified candidate and chain attempt from the chain driver
|
||||
type: string
|
||||
required: true
|
||||
pool_timeout_minutes:
|
||||
description: Wall-clock budget (minutes) for the pool run step
|
||||
type: string
|
||||
required: false
|
||||
default: '240'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rustfs_version:
|
||||
@@ -39,6 +44,10 @@ on:
|
||||
description: 'Run the pool decommission step (3-pool topology only)'
|
||||
type: boolean
|
||||
default: true
|
||||
pool_timeout_minutes:
|
||||
description: 'Wall-clock budget (minutes) for the whole pool run: fill + rebalance wait + decommission'
|
||||
required: false
|
||||
default: '240'
|
||||
cleanup_before:
|
||||
description: 'Reset the nodes before the test (DESTROYS existing data/config)'
|
||||
type: boolean
|
||||
@@ -82,7 +91,8 @@ jobs:
|
||||
pool-expansion-test:
|
||||
name: Pool expansion / decommission test
|
||||
runs-on: smoke-testing
|
||||
timeout-minutes: 60
|
||||
# Leave setup and finalizer headroom beyond the default 240-minute test.
|
||||
timeout-minutes: 360
|
||||
if: ${{ inputs.chain_manifest != '' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch' }}
|
||||
env:
|
||||
RUSTFS_POOL_ADMIN_ENDPOINT: ${{ secrets.RUSTFS_POOL_ADMIN_ENDPOINT || vars.RUSTFS_POOL_ADMIN_ENDPOINT || 'http://rustfs-node1:9000' }}
|
||||
@@ -251,7 +261,12 @@ jobs:
|
||||
|
||||
- name: Run pool expansion & decommission test
|
||||
id: pool_test
|
||||
timeout-minutes: 45
|
||||
# The suite budgets up to 24h each for rebalance and decommission
|
||||
# (REBALANCE_TIMEOUT / DECOMMISSION_TIMEOUT in rustfs_pool_expand.sh);
|
||||
# the previous 45m step cap killed four consecutive runs mid-rebalance
|
||||
# with steps 1-7 green. 240m covers the observed pace (rebalance 1/4
|
||||
# in ~21m) plus one decommission pass; override per dispatch.
|
||||
timeout-minutes: ${{ inputs.pool_timeout_minutes || 240 }}
|
||||
# Case failures keep the run green: the report and the backlog
|
||||
# issue manager carry the product signal.
|
||||
continue-on-error: true
|
||||
@@ -568,6 +583,25 @@ jobs:
|
||||
echo "pool test script reported a failure (step FAIL verdict or die() abort)" >&2
|
||||
failed=1
|
||||
fi
|
||||
# A runner-level kill (step timeout) truncates the suite without a
|
||||
# FAIL verdict or an [ERROR] line, so require every expected
|
||||
# [POOL-STEP] marker to be present with PASS - missing markers mean
|
||||
# the run was truncated, not green. This also closes the gap for
|
||||
# standalone dispatches, where the chain driver's evidence
|
||||
# validation does not run.
|
||||
last_step=6
|
||||
if [ "${{ inputs.pools || '3' }}" = "3" ]; then
|
||||
last_step=8
|
||||
if [ "${{ inputs.run_decommission != 'false' }}" = "true" ]; then
|
||||
last_step=9
|
||||
fi
|
||||
fi
|
||||
for n in $(seq 1 "${last_step}"); do
|
||||
grep -q "^\[POOL-STEP\] ${n} .* PASS" "${POOL_ARTIFACT_DIR}/pool-test.log" 2>/dev/null || {
|
||||
echo "pool test log is missing [POOL-STEP] ${n} PASS (expected through step ${last_step}); the run was truncated" >&2
|
||||
failed=1
|
||||
}
|
||||
done
|
||||
[ "${failed}" -eq 0 ] || exit 1
|
||||
|
||||
- name: Upload functional report to dashboard
|
||||
|
||||
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Presigned URLs honour only signed headers** (GHSA-g8w9-qw9q-fghr): a SigV4 presigned request that carries an `x-amz-*` request header not listed in `X-Amz-SignedHeaders` is now rejected with `403 AccessDenied` ("There were headers present in the request which were not signed"), matching AWS S3. Previously the holder of a presigned `PutObject` URL could add unsigned `x-amz-tagging`, `x-amz-storage-class`, `x-amz-website-redirect-location`, ACL, metadata, Object Lock or SSE headers and have them applied. Presigners that intend a property must set it before signing so the SDK lists the header in `SignedHeaders`; `x-amz-cf-id` (CloudFront) remains tolerated unsigned. Header-signed SigV4 and SigV2 requests are unchanged.
|
||||
|
||||
### Fixed
|
||||
- **Two admin handlers reported a missing policy or user as `500 InternalError`**: `GET /rustfs/admin/v3/info-canned-policy` and `DELETE /rustfs/admin/v3/remove-user` wrapped every IAM error as an internal fault instead of classifying it, so a lookup for something that does not exist answered `500 InternalError` where the rest of the admin API answers `404 NoSuchResource` — `GET /rustfs/admin/v3/user-info` already did, and `iam_error_to_s3_error` already mapped `NoSuchPolicy`, `NoSuchUser`, `NoSuchServiceAccount` and `NoSuchTempAccount` to `NoSuchResource`. A client could not tell "does not exist" from a real server failure, and a controller that checks existence before creating (a Kubernetes operator reconciling policies or users) could never create the object: the missing-policy lookup failed with a `500` and it retried forever. Both handlers now map their IAM errors through the shared `iam_error_to_s3_error` helper, so a missing policy or user is `404 NoSuchResource` and the `error` is preserved as the S3 error's source; the three IAM calls in the removal path (temporary-user lookup, service-account lookup, delete) are all reclassified, and every other IAM error stays `500 InternalError`. Fixes #8117.
|
||||
- **Vault backends accepted key names that escape the key prefix**: only the Local backend refused a key identifier containing a path separator, so on Vault KV2 `POST /rustfs/admin/v3/kms/keys` with a name such as `bad/name` succeeded and created a nested KV2 path that the key listing then reported as a directory rather than a key, and an identifier containing `..` addressed a record outside the configured key prefix once the HTTP client normalised the URL. Both Vault backends now apply the Local backend's containment rule at the point where the identifier becomes a path or a Transit key name: an empty identifier, one containing `/`, `\\` or NUL, or the dot segments `.` and `..` is refused with `400` (`InvalidKey`) before any request reaches Vault. The AWS backend is unaffected, since it addresses keys by ARN and alias. A Vault deployment that created such a key on an earlier build must re-create it under a plain name; objects encrypted under the old name stay readable only until that key is refused, so migrate them first. Refs rustfs/backlog#2474.
|
||||
- **Fresh multi-pool bootstrap with distinct format creators**: a new deployment whose pools have their first endpoint on different nodes (for example two single-node pools) could never publish its initial `pool.bin`: each node held fresh-bootstrap proof only for the pool it formatted, the deployment-wide proof collapsed to none, and every node died with `pool metadata recovery required: no durable bootstrap identity or pool.bin replica is available` after the startup retry budget. The first pool's creator now mints the pending cluster identity on its own pool, every other creator copies that nonce-bound identity onto the pool it formatted first-hand, and the elected writer publishes `pool.bin` once every pool replica carries the same pending identity. Corrupt or disagreeing replicas, pools that merely have a format, expansion pools joining an initialized deployment, and restarts without first-hand proof still fail closed. Non-elected nodes that start before `pool.bin` exists, and the elected writer while it waits for the other creators, no longer latch their pool-metadata write gate for the life of the process. Refs rustfs/backlog#2338, rustfs/backlog#2375.
|
||||
- **Lock RPC timeout storms** (#7363): the remote lock client no longer evicts and re-dials the shared internode HTTP/2 channel on every request deadline. A timeout evicts only when the peer has not completed any lock RPC for two deadlines, evictions and transport-failure re-dials are rate limited per peer (`RUSTFS_OBJECT_LOCK_RPC_EVICTION_COOLDOWN_MS`, default 5 s), and a timed-out request is left running instead of being reset (bounded per peer by `RUSTFS_OBJECT_LOCK_RPC_DETACHED_LIMIT`, default 256), so a slow lock endpoint can no longer drive the `RST_STREAM`/`GOAWAY too_many_resets`/reconnect loop. A lock granted after its caller timed out is released immediately, and unlocks that fail the quick retries continue on a deferred 1/2/4/8/16 s schedule before the server lease reclaims them. New `rustfs_remote_lock_*` metrics cover timeouts, evictions, suppressed evictions, detached streams, late completions and late releases per peer. Operator guide at `docs/operations/lock-rpc-storm-protection.md`.
|
||||
|
||||
Generated
+15
@@ -7221,7 +7221,10 @@ version = "0.39.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"hashbrown 0.17.1",
|
||||
"indexmap 2.14.2",
|
||||
"memchr",
|
||||
"ruzstd",
|
||||
]
|
||||
@@ -9697,6 +9700,18 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-connect-symbol-catalog"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"hex-simd",
|
||||
"object 0.39.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"symbolic-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-credentials"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -20,6 +20,7 @@ members = [
|
||||
"crates/common", # Shared utilities and data structures
|
||||
"crates/data-usage", # Shared data usage models and algorithms
|
||||
"crates/config", # Configuration management
|
||||
"crates/connect-symbol-catalog", # Release-only CPU profile symbol catalog generator
|
||||
"crates/credentials", # Credential management system
|
||||
"crates/crypto", # Cryptography and security features
|
||||
"crates/ecstore", # Erasure coding storage implementation
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "rustfs-connect-symbol-catalog"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
hex-simd.workspace = true
|
||||
object = { version = "=0.39.1", default-features = false, features = ["read", "std", "elf"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
# Match pyroscope's pprof-rs frame normalization exactly.
|
||||
symbolic-demangle = { version = "=13.9.0", default-features = false, features = ["rust"] }
|
||||
|
||||
[dev-dependencies]
|
||||
object = { version = "=0.39.1", default-features = false, features = ["write", "elf"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Release tooling only: derive reviewed CPU symbol candidates from the final ELF.
|
||||
|
||||
use object::{Object, ObjectSymbol};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
const MAX_BINARY_BYTES: u64 = 1_073_741_824;
|
||||
const MAX_CATALOG_BYTES: usize = 32 * 1024 * 1024;
|
||||
const MAX_SYMBOLS: usize = 200_000;
|
||||
const NORMALIZATION: &str = "pyroscope-2.1.1-symbolic-demangle-13.9.0";
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Catalog<'a> {
|
||||
schema_version: u8,
|
||||
normalization: &'static str,
|
||||
source_commit: &'a str,
|
||||
executable_sha256: String,
|
||||
os_family: &'static str,
|
||||
architecture: &'static str,
|
||||
build_features: Vec<String>,
|
||||
symbols: BTreeSet<String>,
|
||||
}
|
||||
|
||||
fn invalid(message: &'static str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
}
|
||||
|
||||
fn symbol_name(raw: &[u8]) -> Option<String> {
|
||||
// This is the same lossy UTF-8 conversion and demangler used by pprof-rs.
|
||||
let name = symbolic_demangle::demangle(&String::from_utf8_lossy(raw)).into_owned();
|
||||
if name.is_empty()
|
||||
|| name.len() > 4096
|
||||
|| matches!(name.as_str(), "Unknown" | "<unresolved>")
|
||||
|| name.bytes().any(|b| b < 32 || b == 127)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(name)
|
||||
}
|
||||
|
||||
fn catalog(binary: &[u8], source: &str, target: &str, features: Vec<String>) -> io::Result<Vec<u8>> {
|
||||
if source.len() != 40 || !source.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
|
||||
return Err(invalid("source commit must be a lowercase full SHA"));
|
||||
}
|
||||
let expected_arch = match target {
|
||||
"x86_64-unknown-linux-gnu" => object::Architecture::X86_64,
|
||||
"aarch64-unknown-linux-gnu" => object::Architecture::Aarch64,
|
||||
_ => return Err(invalid("only supported Linux GNU CPU profiling targets have catalogs")),
|
||||
};
|
||||
if features.is_empty()
|
||||
|| features.len() > 64
|
||||
|| !features.iter().any(|f| f == "pyroscope")
|
||||
|| features.iter().any(|f| {
|
||||
f.is_empty()
|
||||
|| f.len() > 64
|
||||
|| !f.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
|
||||
|| !f
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
|
||||
})
|
||||
|| features.windows(2).any(|pair| pair[0] >= pair[1])
|
||||
{
|
||||
return Err(invalid("build features must be sorted, unique and include pyroscope"));
|
||||
}
|
||||
let file = object::File::parse(binary).map_err(|_| invalid("cannot parse executable"))?;
|
||||
if file.format() != object::BinaryFormat::Elf
|
||||
|| file.architecture() != expected_arch
|
||||
|| !matches!(file.kind(), object::ObjectKind::Executable | object::ObjectKind::Dynamic)
|
||||
{
|
||||
return Err(invalid("the executable does not match the Linux target"));
|
||||
}
|
||||
if file.symbol_table().is_none() {
|
||||
return Err(invalid("the executable is stripped; retain its function symbol table"));
|
||||
}
|
||||
let mut symbols = BTreeSet::new();
|
||||
let mut symbol_bytes = 0;
|
||||
for symbol in file.symbols().chain(file.dynamic_symbols()) {
|
||||
if symbol.kind() != object::SymbolKind::Text || symbol.is_undefined() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = symbol.name_bytes().ok().and_then(symbol_name)
|
||||
&& !symbols.contains(&name)
|
||||
{
|
||||
symbol_bytes += name.len() + 3;
|
||||
if symbol_bytes > MAX_CATALOG_BYTES {
|
||||
return Err(invalid("symbol catalog exceeds its byte limit"));
|
||||
}
|
||||
symbols.insert(name);
|
||||
}
|
||||
if symbols.len() > MAX_SYMBOLS {
|
||||
return Err(invalid("symbol catalog exceeds its reviewed symbol limit"));
|
||||
}
|
||||
}
|
||||
if symbols.is_empty() {
|
||||
return Err(invalid("the executable has no usable function symbols"));
|
||||
}
|
||||
let catalog = Catalog {
|
||||
schema_version: 1,
|
||||
normalization: NORMALIZATION,
|
||||
source_commit: source,
|
||||
executable_sha256: hex_simd::encode_to_string(Sha256::digest(binary), hex_simd::AsciiCase::Lower),
|
||||
os_family: "LINUX",
|
||||
architecture: if expected_arch == object::Architecture::X86_64 {
|
||||
"x86_64"
|
||||
} else {
|
||||
"aarch64"
|
||||
},
|
||||
build_features: features,
|
||||
symbols,
|
||||
};
|
||||
let json = serde_json::to_vec(&catalog).map_err(io::Error::other)?;
|
||||
if json.len() > MAX_CATALOG_BYTES {
|
||||
return Err(invalid("symbol catalog exceeds its byte limit"));
|
||||
}
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let [binary_path, source, target, features_json, output] = args.as_slice() else {
|
||||
return Err(invalid(
|
||||
"usage: rustfs-connect-symbol-catalog BINARY SOURCE_SHA TARGET FEATURES_JSON OUTPUT",
|
||||
));
|
||||
};
|
||||
let input = fs::File::open(binary_path)?;
|
||||
if !input.metadata()?.is_file() || input.metadata()?.len() > MAX_BINARY_BYTES {
|
||||
return Err(invalid("input must be a bounded regular executable"));
|
||||
}
|
||||
let mut binary = Vec::new();
|
||||
input.take(MAX_BINARY_BYTES + 1).read_to_end(&mut binary)?;
|
||||
if u64::try_from(binary.len()).map_err(io::Error::other)? > MAX_BINARY_BYTES {
|
||||
return Err(invalid("executable grew beyond its byte limit"));
|
||||
}
|
||||
let features = serde_json::from_str(features_json).map_err(io::Error::other)?;
|
||||
let json = catalog(&binary, source, target, features)?;
|
||||
// Never overwrite another build's reviewed artifact.
|
||||
let mut file = OpenOptions::new().write(true).create_new(true).open(Path::new(output))?;
|
||||
file.write_all(&json)?;
|
||||
file.sync_all()?;
|
||||
println!("{}", hex_simd::encode_to_string(Sha256::digest(&json), hex_simd::AsciiCase::Lower));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn executable() -> Vec<u8> {
|
||||
let mut object =
|
||||
object::write::Object::new(object::BinaryFormat::Elf, object::Architecture::X86_64, object::Endianness::Little);
|
||||
let section = object.section_id(object::write::StandardSection::Text);
|
||||
object.append_section_data(section, &[0xc3], 1);
|
||||
object.add_symbol(object::write::Symbol {
|
||||
name: b"_ZN4test4main17h0123456789abcdefE".to_vec(),
|
||||
value: 0,
|
||||
size: 1,
|
||||
kind: object::SymbolKind::Text,
|
||||
scope: object::SymbolScope::Linkage,
|
||||
weak: false,
|
||||
section: object::write::SymbolSection::Section(section),
|
||||
flags: object::SymbolFlags::None,
|
||||
});
|
||||
let mut bytes = object.write().expect("write ELF symbol fixture");
|
||||
// The object writer emits ET_REL; this fixture models the final ET_EXEC header.
|
||||
bytes[16..18].copy_from_slice(&2_u16.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn bulk_executable(count: usize, suffix_len: usize) -> Vec<u8> {
|
||||
let mut object =
|
||||
object::write::Object::new(object::BinaryFormat::Elf, object::Architecture::X86_64, object::Endianness::Little);
|
||||
let section = object.section_id(object::write::StandardSection::Text);
|
||||
object.append_section_data(section, &[0xc3], 1);
|
||||
let suffix = "x".repeat(suffix_len);
|
||||
for index in 0..count {
|
||||
object.add_symbol(object::write::Symbol {
|
||||
name: format!("rustfs::profile::function_{index:06}_{suffix}").into_bytes(),
|
||||
value: 0,
|
||||
size: 1,
|
||||
kind: object::SymbolKind::Text,
|
||||
scope: object::SymbolScope::Linkage,
|
||||
weak: false,
|
||||
section: object::write::SymbolSection::Section(section),
|
||||
flags: object::SymbolFlags::None,
|
||||
});
|
||||
}
|
||||
let mut binary = object.write().expect("write bulk ELF fixture");
|
||||
binary[16..18].copy_from_slice(&2_u16.to_le_bytes());
|
||||
binary
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_binds_the_exact_executable_and_normalizes_function_names() {
|
||||
let binary = executable();
|
||||
let encoded =
|
||||
catalog(&binary, &"a".repeat(40), "x86_64-unknown-linux-gnu", vec!["pyroscope".into()]).expect("generate catalog");
|
||||
let value: serde_json::Value = serde_json::from_slice(&encoded).expect("catalog JSON");
|
||||
assert_eq!(value["symbols"], serde_json::json!(["test::main"]));
|
||||
assert_eq!(
|
||||
value["executableSha256"],
|
||||
hex_simd::encode_to_string(Sha256::digest(&binary), hex_simd::AsciiCase::Lower)
|
||||
);
|
||||
assert_eq!(value["normalization"], NORMALIZATION);
|
||||
assert!(catalog(&binary, &"a".repeat(40), "aarch64-unknown-linux-gnu", vec!["pyroscope".into()]).is_err());
|
||||
assert!(catalog(&binary, "bad", "x86_64-unknown-linux-gnu", vec!["pyroscope".into()]).is_err());
|
||||
assert!(catalog(&binary, &"a".repeat(40), "x86_64-unknown-linux-gnu", vec!["pyroscope".into(); 2]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stripped_executables_and_wrong_abis_are_refused() {
|
||||
let mut binary = executable();
|
||||
// Removing section headers models a fully stripped executable without dynsym.
|
||||
binary[40..48].fill(0);
|
||||
binary[60..64].fill(0);
|
||||
let error = catalog(&binary, &"a".repeat(40), "x86_64-unknown-linux-gnu", vec!["pyroscope".into()])
|
||||
.expect_err("stripped binary must not yield an empty but trusted catalog");
|
||||
assert!(error.to_string().contains("stripped"));
|
||||
assert!(catalog(&executable(), &"a".repeat(40), "x86_64-unknown-linux-musl", vec!["pyroscope".into()]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_hash_matches_the_connect_nonce_vector() {
|
||||
let nonce: Vec<u8> = (0..32).collect();
|
||||
let name = symbol_name(b"core::hint::black_box").expect("reviewed function name");
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(b"rustfs-connect-cpu-symbol-v1\0");
|
||||
digest.update(nonce);
|
||||
digest.update(name.as_bytes());
|
||||
assert_eq!(
|
||||
hex_simd::encode_to_string(digest.finalize(), hex_simd::AsciiCase::Lower),
|
||||
"5d5b472a0dd09124bfedf0bd243f88d5e61952b8da15d7463da019137320d77c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_and_non_function_material_cannot_become_catalog_entries() {
|
||||
assert!(symbol_name(b"Unknown").is_none());
|
||||
assert!(symbol_name(b"<unresolved>").is_none());
|
||||
assert!(symbol_name(b"secret\nname").is_none());
|
||||
assert!(symbol_name(&vec![b'x'; 4097]).is_none());
|
||||
assert!(catalog(b"not ELF", &"a".repeat(40), "x86_64-unknown-linux-gnu", vec!["pyroscope".into()]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_sized_catalog_retains_all_function_names() {
|
||||
let binary = bulk_executable(110_000, 160);
|
||||
let encoded = catalog(&binary, &"a".repeat(40), "x86_64-unknown-linux-gnu", vec!["pyroscope".into()])
|
||||
.expect("retain complete bounded symbol catalog");
|
||||
assert!(encoded.len() > 16 * 1024 * 1024);
|
||||
assert!(encoded.len() <= MAX_CATALOG_BYTES);
|
||||
let value: serde_json::Value = serde_json::from_slice(&encoded).expect("catalog JSON");
|
||||
assert_eq!(value["symbols"].as_array().expect("symbol array").len(), 110_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalogs_over_either_resource_limit_are_refused() {
|
||||
let source = "a".repeat(40);
|
||||
let too_many = catalog(
|
||||
&bulk_executable(MAX_SYMBOLS + 1, 0),
|
||||
&source,
|
||||
"x86_64-unknown-linux-gnu",
|
||||
vec!["pyroscope".into()],
|
||||
)
|
||||
.expect_err("too many function names must be refused");
|
||||
assert!(too_many.to_string().contains("reviewed symbol limit"));
|
||||
|
||||
let too_large = catalog(
|
||||
&bulk_executable(10_000, 3_500),
|
||||
&source,
|
||||
"x86_64-unknown-linux-gnu",
|
||||
vec!["pyroscope".into()],
|
||||
)
|
||||
.expect_err("oversized catalog must be refused");
|
||||
assert!(too_large.to_string().contains("byte limit"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||
use tracing::info;
|
||||
|
||||
const PARTS_COUNT_BUCKET: &str = "parts-count-test-bucket";
|
||||
const MULTIPART_KEY: &str = "multipart-object.bin";
|
||||
const SINGLE_PUT_KEY: &str = "single-put-object.bin";
|
||||
|
||||
/// s3's minimum size for every multipart part but the last.
|
||||
const PART_SIZE: usize = 5 * 1024 * 1024;
|
||||
const LAST_PART_SIZE: usize = 1024;
|
||||
const EXPECTED_PARTS: i32 = 3;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_object_reports_parts_count_for_multipart_upload() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
init_logging();
|
||||
info!("Starting GetObject x-amz-mp-parts-count regression test");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(Vec::new()).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
env.create_test_bucket(PARTS_COUNT_BUCKET).await?;
|
||||
|
||||
// A genuine multipart upload of three parts.
|
||||
let created = client
|
||||
.create_multipart_upload()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
let upload_id = created.upload_id().expect("upload id");
|
||||
|
||||
let mut completed_parts = Vec::new();
|
||||
for part_number in 1..=EXPECTED_PARTS {
|
||||
let size = if part_number == EXPECTED_PARTS {
|
||||
LAST_PART_SIZE
|
||||
} else {
|
||||
PART_SIZE
|
||||
};
|
||||
let uploaded = client
|
||||
.upload_part()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.upload_id(upload_id)
|
||||
.part_number(part_number)
|
||||
.body(ByteStream::from(vec![b'a' + (part_number as u8); size]))
|
||||
.send()
|
||||
.await?;
|
||||
completed_parts.push(
|
||||
CompletedPart::builder()
|
||||
.part_number(part_number)
|
||||
.e_tag(uploaded.e_tag().expect("part etag"))
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.complete_multipart_upload()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.upload_id(upload_id)
|
||||
.multipart_upload(CompletedMultipartUpload::builder().set_parts(Some(completed_parts)).build())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let total_len = (PART_SIZE * 2 + LAST_PART_SIZE) as i64;
|
||||
|
||||
// The header the fix is about.
|
||||
let first_part = client
|
||||
.get_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.part_number(1)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
first_part.parts_count(),
|
||||
Some(EXPECTED_PARTS),
|
||||
"GetObject with partNumber must report the object's part count"
|
||||
);
|
||||
assert_eq!(
|
||||
first_part.content_length(),
|
||||
Some(PART_SIZE as i64),
|
||||
"the response body must be part 1, not the whole object"
|
||||
);
|
||||
let expected_range = format!("bytes 0-{}/{}", PART_SIZE - 1, total_len);
|
||||
assert_eq!(
|
||||
first_part.content_range(),
|
||||
Some(expected_range.as_str()),
|
||||
"Content-Range must state part 1's extent and the whole object's size"
|
||||
);
|
||||
|
||||
// Without partNumber s3 omits the header, so a plain GET is unchanged.
|
||||
let whole = client
|
||||
.get_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(whole.parts_count(), None, "a GET that does not name a part is owed no part count");
|
||||
assert_eq!(whole.content_length(), Some(total_len), "a plain GET returns the whole object");
|
||||
|
||||
// A single PutObject is not a multipart upload, so it is owed no count
|
||||
// even when a part is named.
|
||||
client
|
||||
.put_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(SINGLE_PUT_KEY)
|
||||
.body(ByteStream::from_static(b"0123456789abcdef"))
|
||||
.send()
|
||||
.await?;
|
||||
let single = client
|
||||
.get_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(SINGLE_PUT_KEY)
|
||||
.part_number(1)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(single.parts_count(), None, "an object stored by a single PutObject is not multipart");
|
||||
|
||||
client
|
||||
.delete_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(MULTIPART_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
client
|
||||
.delete_object()
|
||||
.bucket(PARTS_COUNT_BUCKET)
|
||||
.key(SINGLE_PUT_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
env.delete_test_bucket(PARTS_COUNT_BUCKET).await?;
|
||||
env.stop_server();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -232,6 +232,10 @@ mod tier_stats_cluster_test;
|
||||
#[cfg(test)]
|
||||
mod checksum_upload_test;
|
||||
|
||||
// GetObject with partNumber reporting x-amz-mp-parts-count
|
||||
#[cfg(test)]
|
||||
mod get_object_parts_count_test;
|
||||
|
||||
// Group deletion tests
|
||||
#[cfg(test)]
|
||||
mod group_delete_test;
|
||||
@@ -272,6 +276,8 @@ mod storage_class_capability_test;
|
||||
// S3 dummy-compat bucket API tests
|
||||
#[cfg(test)]
|
||||
mod bucket_logging_test;
|
||||
#[cfg(test)]
|
||||
mod website_hosting_test;
|
||||
|
||||
// Multipart control API auth regression tests
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -311,4 +311,233 @@ mod tests {
|
||||
|
||||
env.stop_server();
|
||||
}
|
||||
|
||||
async fn collect_prefix_pages(
|
||||
client: &Client,
|
||||
bucket: &str,
|
||||
prefix: &str,
|
||||
delimiter: Option<&str>,
|
||||
max_keys: i32,
|
||||
start_after: Option<&str>,
|
||||
) -> (Vec<aws_sdk_s3::types::Object>, Vec<String>) {
|
||||
let mut objects = Vec::new();
|
||||
let mut prefixes = Vec::new();
|
||||
let mut token = None;
|
||||
let mut seen_tokens = std::collections::HashSet::new();
|
||||
for _ in 0..64 {
|
||||
let page = client
|
||||
.list_objects_v2()
|
||||
.bucket(bucket)
|
||||
.prefix(prefix)
|
||||
.set_delimiter(delimiter.map(ToOwned::to_owned))
|
||||
.max_keys(max_keys)
|
||||
.set_start_after(start_after.map(ToOwned::to_owned))
|
||||
.set_continuation_token(token)
|
||||
.send()
|
||||
.await
|
||||
.expect("prefix pagination request should succeed");
|
||||
let count = page.contents().len() + page.common_prefixes().len();
|
||||
assert!(count <= usize::try_from(max_keys).expect("positive page size"));
|
||||
assert_eq!(page.key_count(), Some(i32::try_from(count).expect("small page")));
|
||||
objects.extend_from_slice(page.contents());
|
||||
prefixes.extend(
|
||||
page.common_prefixes()
|
||||
.iter()
|
||||
.map(|item| item.prefix().expect("CommonPrefix must contain a prefix").to_owned()),
|
||||
);
|
||||
if page.is_truncated() == Some(false) {
|
||||
assert!(page.next_continuation_token().is_none(), "final page must not have a next token");
|
||||
return (objects, prefixes);
|
||||
}
|
||||
assert_eq!(page.is_truncated(), Some(true));
|
||||
let next = page.next_continuation_token().expect("truncated page requires a token");
|
||||
assert!(!next.is_empty());
|
||||
assert!(seen_tokens.insert(next.to_owned()), "continuation token must advance");
|
||||
token = Some(next.to_owned());
|
||||
}
|
||||
panic!("prefix pagination exceeded its finite page budget");
|
||||
}
|
||||
|
||||
fn object_keys(objects: &[aws_sdk_s3::types::Object]) -> Vec<&str> {
|
||||
objects
|
||||
.iter()
|
||||
.map(|object| object.key().expect("listed object must have a key"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Issue #8175: the prefix object must occur once, including across page boundaries.
|
||||
#[tokio::test]
|
||||
async fn test_list_objects_v2_prefix_marker_pagination() {
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await.expect("create test environment");
|
||||
env.start_rustfs_server(vec![]).await.expect("start RustFS");
|
||||
let client = create_s3_client(&env);
|
||||
let bucket = "test-prefix-marker-pagination";
|
||||
create_bucket(&client, bucket).await.expect("create bucket");
|
||||
let mut expected = vec!["content/".to_owned()];
|
||||
expected.extend((0..25).map(|index| format!("content/0123456789abcdef0123456789abcdef/{index:03}")));
|
||||
for key in &expected {
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.body(ByteStream::from_static(if key == "content/" { b"" } else { b"x" }))
|
||||
.send()
|
||||
.await
|
||||
.expect("create prefix fixture");
|
||||
}
|
||||
for max_keys in [1000, 1, 2] {
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "content/", None, max_keys, None).await;
|
||||
assert_eq!(object_keys(&objects), expected, "all keys must appear once at MaxKeys={max_keys}");
|
||||
assert!(prefixes.is_empty());
|
||||
}
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "content/", None, 1, Some("content/")).await;
|
||||
assert_eq!(object_keys(&objects), expected[1..]);
|
||||
assert!(prefixes.is_empty());
|
||||
|
||||
// V1 shares the storage listing path but resumes with a key marker.
|
||||
let mut marker = Some("content/".to_owned());
|
||||
let mut v1_keys = Vec::new();
|
||||
let mut finished = false;
|
||||
for _ in 0..32 {
|
||||
let page = client
|
||||
.list_objects()
|
||||
.bucket(bucket)
|
||||
.prefix("content/")
|
||||
.max_keys(1)
|
||||
.set_marker(marker.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("list V1 continuation page");
|
||||
assert!(page.contents().len() <= 1);
|
||||
let keys = object_keys(page.contents());
|
||||
v1_keys.extend(keys.iter().map(|key| (*key).to_owned()));
|
||||
if page.is_truncated() == Some(false) {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
assert_eq!(page.is_truncated(), Some(true));
|
||||
let next = page
|
||||
.next_marker()
|
||||
.or_else(|| keys.last().copied())
|
||||
.expect("V1 page must advance");
|
||||
assert!(marker.as_deref().is_none_or(|previous| next > previous));
|
||||
marker = Some(next.to_owned());
|
||||
}
|
||||
assert!(finished, "V1 pagination must terminate");
|
||||
assert_eq!(v1_keys, expected[1..]);
|
||||
env.stop_server();
|
||||
}
|
||||
|
||||
/// An exact prefix match does not establish EOF, including for ordinary keys.
|
||||
#[tokio::test]
|
||||
async fn test_list_objects_v2_exact_prefix_pagination_boundaries() {
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await.expect("create test environment");
|
||||
env.start_rustfs_server(vec![]).await.expect("start RustFS");
|
||||
let client = create_s3_client(&env);
|
||||
let bucket = "test-exact-prefix-boundaries";
|
||||
create_bucket(&client, bucket).await.expect("create bucket");
|
||||
for key in [
|
||||
"a",
|
||||
"ab",
|
||||
"solo/",
|
||||
"marker/",
|
||||
"marker/file",
|
||||
"marker/subdir/",
|
||||
"marker/subdir/file",
|
||||
] {
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.body(ByteStream::from_static(b""))
|
||||
.send()
|
||||
.await
|
||||
.expect("create boundary fixture");
|
||||
}
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "a", None, 1, None).await;
|
||||
assert_eq!(object_keys(&objects), vec!["a", "ab"]);
|
||||
assert!(prefixes.is_empty());
|
||||
let first = client
|
||||
.list_objects()
|
||||
.bucket(bucket)
|
||||
.prefix("a")
|
||||
.max_keys(1)
|
||||
.send()
|
||||
.await
|
||||
.expect("list V1 exact-prefix first page");
|
||||
assert_eq!(object_keys(first.contents()), vec!["a"]);
|
||||
assert_eq!(first.is_truncated(), Some(true));
|
||||
let last = client
|
||||
.list_objects()
|
||||
.bucket(bucket)
|
||||
.prefix("a")
|
||||
.max_keys(1)
|
||||
.marker(first.next_marker().unwrap_or("a"))
|
||||
.send()
|
||||
.await
|
||||
.expect("list V1 exact-prefix final page");
|
||||
assert_eq!(object_keys(last.contents()), vec!["ab"]);
|
||||
assert_eq!(last.is_truncated(), Some(false));
|
||||
|
||||
let solo = client
|
||||
.list_objects_v2()
|
||||
.bucket(bucket)
|
||||
.prefix("solo/")
|
||||
.max_keys(1)
|
||||
.send()
|
||||
.await
|
||||
.expect("list isolated marker");
|
||||
assert_eq!(object_keys(solo.contents()), vec!["solo/"]);
|
||||
assert_eq!(solo.is_truncated(), Some(false));
|
||||
assert!(solo.next_continuation_token().is_none());
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "marker/", Some("/"), 1, None).await;
|
||||
assert_eq!(object_keys(&objects), vec!["marker/", "marker/file"]);
|
||||
assert_eq!(prefixes, vec!["marker/subdir/"]);
|
||||
env.stop_server();
|
||||
}
|
||||
|
||||
/// A directory marker must retain its own metadata when a same-named object exists.
|
||||
#[tokio::test]
|
||||
async fn test_list_objects_v2_prefix_marker_preserves_metadata() {
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await.expect("create test environment");
|
||||
env.start_rustfs_server(vec![]).await.expect("start RustFS");
|
||||
let client = create_s3_client(&env);
|
||||
let bucket = "test-prefix-marker-metadata";
|
||||
create_bucket(&client, bucket).await.expect("create bucket");
|
||||
let plain = client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key("content")
|
||||
.body(ByteStream::from_static(b"plain object body"))
|
||||
.send()
|
||||
.await
|
||||
.expect("put plain object");
|
||||
let directory = client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key("content/")
|
||||
.body(ByteStream::from_static(b""))
|
||||
.send()
|
||||
.await
|
||||
.expect("put directory marker");
|
||||
assert_ne!(plain.e_tag(), directory.e_tag(), "fixture ETags must distinguish the objects");
|
||||
for max_keys in [1, 1000] {
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "content/", None, max_keys, None).await;
|
||||
assert_eq!(object_keys(&objects), vec!["content/"]);
|
||||
assert!(prefixes.is_empty());
|
||||
assert_eq!(objects[0].size(), Some(0));
|
||||
assert_eq!(objects[0].e_tag(), directory.e_tag());
|
||||
let (objects, prefixes) = collect_prefix_pages(&client, bucket, "content", None, max_keys, None).await;
|
||||
assert_eq!(object_keys(&objects), vec!["content", "content/"]);
|
||||
assert!(prefixes.is_empty());
|
||||
assert_eq!(objects[0].size(), Some(17));
|
||||
assert_eq!(objects[0].e_tag(), plain.e_tag());
|
||||
assert_eq!(objects[1].size(), Some(0));
|
||||
assert_eq!(objects[1].e_tag(), directory.e_tag());
|
||||
}
|
||||
env.stop_server();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,10 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
}
|
||||
let write_ready = survivors >= 3;
|
||||
let read_quorum = survivors >= 2;
|
||||
let expected_status = if write_ready { 200 } else { 503 };
|
||||
// `/health/ready` keeps a node that can still serve reads in the Service.
|
||||
let node_ready = read_quorum;
|
||||
let node_status = if node_ready { 200 } else { 503 };
|
||||
let cluster_status = if write_ready { 200 } else { 503 };
|
||||
for (idx, client) in clients.iter().enumerate().take(survivors) {
|
||||
let url = &cluster.nodes[idx].url;
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
@@ -472,27 +475,27 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
let response = http.get(format!("{url}/health/ready")).send().await?;
|
||||
let status = response.status().as_u16();
|
||||
let payload: serde_json::Value = response.json().await?;
|
||||
if status == expected_status
|
||||
&& payload["ready"] == write_ready
|
||||
&& payload["details"]["storage"]["ready"] == write_ready
|
||||
if status == node_status
|
||||
&& payload["ready"] == node_ready
|
||||
&& payload["details"]["storage"]["ready"] == node_ready
|
||||
&& payload["details"]["storage"]["readQuorum"] == read_quorum
|
||||
&& payload["details"]["storage"]["writeQuorum"] == write_ready
|
||||
&& payload["details"]["poolMetadata"]["ready"] == true
|
||||
&& payload["details"]["iam"]["ready"] == true
|
||||
&& payload["details"]["lock"]["ready"] == write_ready
|
||||
&& payload["details"]["lock"]["ready"] == read_quorum
|
||||
{
|
||||
break payload;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "node {idx}, survivors={survivors}: HTTP {status}, {payload}");
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
};
|
||||
assert_eq!(payload["details"]["storage"]["readinessScope"], "write_quorum_and_pool_metadata");
|
||||
assert_eq!(payload["details"]["storage"]["readinessScope"], "read_quorum");
|
||||
assert_eq!(payload["details"]["storage"]["source"], "local_runtime");
|
||||
assert_eq!(
|
||||
payload["details"]["storage"]["status"],
|
||||
if write_ready { "connected" } else { "disconnected" }
|
||||
if node_ready { "connected" } else { "disconnected" }
|
||||
);
|
||||
if !write_ready {
|
||||
if !node_ready {
|
||||
assert!(
|
||||
payload["degradedReasons"]
|
||||
.as_array()
|
||||
@@ -504,10 +507,10 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
|
||||
for path in ["/health/ready", "/minio/health/ready"] {
|
||||
let head = http.head(format!("{url}{path}")).send().await?;
|
||||
assert_eq!(head.status().as_u16(), expected_status, "HEAD {path}, survivors={survivors}");
|
||||
assert_eq!(head.status().as_u16(), node_status, "HEAD {path}, survivors={survivors}");
|
||||
assert!(head.bytes().await?.is_empty());
|
||||
let response = http.get(format!("{url}{path}")).send().await?;
|
||||
assert_eq!(response.status().as_u16(), expected_status);
|
||||
assert_eq!(response.status().as_u16(), node_status);
|
||||
let body: serde_json::Value = response.json().await?;
|
||||
assert_eq!(body["details"]["storage"], payload["details"]["storage"]);
|
||||
assert_eq!(body["details"]["poolMetadata"], payload["details"]["poolMetadata"]);
|
||||
@@ -526,7 +529,7 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
let response = http.get(format!("{url}{path}")).send().await?;
|
||||
let status = response.status().as_u16();
|
||||
let body: serde_json::Value = response.json().await?;
|
||||
if status == expected_status
|
||||
if status == cluster_status
|
||||
&& body["details"]["storage"]["ready"] == storage_ready
|
||||
&& body["details"]["lock"]["ready"] == write_ready
|
||||
{
|
||||
@@ -559,11 +562,12 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
let get = client.get_object().bucket(BUCKET).key(seed_key).send().await;
|
||||
let get_status = match get {
|
||||
Ok(object) => {
|
||||
assert!(read_quorum, "GET must fail once read quorum is lost");
|
||||
assert_eq!(object.body.collect().await?.into_bytes().as_ref(), seed_body);
|
||||
200
|
||||
}
|
||||
Err(error) => {
|
||||
assert!(!write_ready, "GET must succeed on a ready cluster: {error:?}");
|
||||
assert!(!read_quorum, "GET must succeed while read quorum holds: {error:?}");
|
||||
let status = error
|
||||
.raw_response()
|
||||
.expect("GET should have an HTTP response")
|
||||
@@ -576,11 +580,12 @@ async fn assert_node_readiness_tracks_quorum(cluster: &mut RustFSTestClusterEnvi
|
||||
let list = client.list_objects_v2().bucket(BUCKET).send().await;
|
||||
let list_status = match list {
|
||||
Ok(result) => {
|
||||
assert!(read_quorum, "listing must fail once read quorum is lost");
|
||||
assert!(result.contents().iter().any(|object| object.key() == Some(seed_key)));
|
||||
200
|
||||
}
|
||||
Err(error) => {
|
||||
assert!(!write_ready, "listing must succeed on a ready cluster: {error:?}");
|
||||
assert!(!read_quorum, "listing must succeed while read quorum holds: {error:?}");
|
||||
let status = error
|
||||
.raw_response()
|
||||
.expect("LIST should have an HTTP response")
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||
use aws_sdk_s3::presigning::PresigningConfig;
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{
|
||||
BucketVersioningStatus, Condition, ErrorDocument, IndexDocument, Redirect, RedirectAllRequestsTo, RoutingRule,
|
||||
VersioningConfiguration, WebsiteConfiguration,
|
||||
};
|
||||
use reqwest::header::HOST;
|
||||
use std::time::Duration;
|
||||
|
||||
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
#[tokio::test]
|
||||
async fn website_host_serves_pages_without_changing_s3_reads() -> TestResult {
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server_with_env(vec![], &[("RUSTFS_WEBSITE_DOMAINS", "website.test")])
|
||||
.await?;
|
||||
let s3 = env.create_s3_client();
|
||||
let bucket = "website-hosting";
|
||||
s3.create_bucket().bucket(bucket).send().await?;
|
||||
for (key, body) in [
|
||||
("index.html", "home"),
|
||||
("404.html", "error page"),
|
||||
("docs", "ordinary object"),
|
||||
("section/index.html", "section page"),
|
||||
("private.txt", "private"),
|
||||
] {
|
||||
s3.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.body(ByteStream::from(body.as_bytes().to_vec()))
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
s3.put_object()
|
||||
.bucket(bucket)
|
||||
.key("move")
|
||||
.website_redirect_location("/new-page")
|
||||
.body(ByteStream::from_static(b"ordinary redirect object"))
|
||||
.send()
|
||||
.await?;
|
||||
s3.put_bucket_website()
|
||||
.bucket(bucket)
|
||||
.website_configuration(
|
||||
WebsiteConfiguration::builder()
|
||||
.index_document(IndexDocument::builder().suffix("index.html").build()?)
|
||||
.error_document(ErrorDocument::builder().key("404.html").build()?)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let policy = serde_json::json!({
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":format!("arn:aws:s3:::{bucket}/*")},
|
||||
{"Effect":"Deny","Principal":"*","Action":"s3:GetObject","Resource":format!("arn:aws:s3:::{bucket}/private.txt")}
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
s3.put_bucket_policy().bucket(bucket).policy(policy).send().await?;
|
||||
|
||||
let http = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
let website = |path: &str| format!("{}{path}", env.url);
|
||||
let host = format!("{bucket}.website.test");
|
||||
let base_domain = http
|
||||
.get(website("/website-hosting/index.html"))
|
||||
.header(HOST, "website.test")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(base_domain.status(), 404);
|
||||
assert!(!base_domain.text().await?.contains("<Error>"));
|
||||
let home = http.get(website("/")).header(HOST, &host).send().await?;
|
||||
assert_eq!(home.status(), 200);
|
||||
assert_eq!(home.text().await?, "home");
|
||||
let head = http.head(website("/")).header(HOST, &host).send().await?;
|
||||
assert_eq!(head.status(), 200);
|
||||
assert_eq!(head.text().await?, "");
|
||||
let missing = http.get(website("/missing")).header(HOST, &host).send().await?;
|
||||
assert_eq!(missing.status(), 404);
|
||||
assert_eq!(missing.text().await?, "error page");
|
||||
let denied = http.get(website("/private.txt")).header(HOST, &host).send().await?;
|
||||
assert_eq!(denied.status(), 403);
|
||||
assert_eq!(denied.text().await?, "error page");
|
||||
let collision = http.get(website("/docs")).header(HOST, &host).send().await?;
|
||||
assert_eq!(collision.status(), 200);
|
||||
assert_eq!(collision.text().await?, "ordinary object");
|
||||
let directory = http.get(website("/section")).header(HOST, &host).send().await?;
|
||||
assert_eq!(directory.status(), 302);
|
||||
assert_eq!(directory.headers()[reqwest::header::LOCATION], "/section/");
|
||||
let section = http.get(website("/section/")).header(HOST, &host).send().await?;
|
||||
assert_eq!(section.status(), 200);
|
||||
assert_eq!(section.text().await?, "section page");
|
||||
let redirect = http.get(website("/move")).header(HOST, &host).send().await?;
|
||||
assert_eq!(redirect.status(), 301);
|
||||
assert_eq!(redirect.headers()[reqwest::header::LOCATION], "/new-page");
|
||||
let redirect_with_condition = http
|
||||
.get(website("/move"))
|
||||
.header(HOST, &host)
|
||||
.header(reqwest::header::IF_NONE_MATCH, "*")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(redirect_with_condition.status(), 301);
|
||||
let redirect_with_range = http
|
||||
.get(website("/move"))
|
||||
.header(HOST, &host)
|
||||
.header(reqwest::header::RANGE, "bytes=999999-")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(redirect_with_range.status(), 301);
|
||||
let plain_range_error = http
|
||||
.get(website("/index.html"))
|
||||
.header(HOST, &host)
|
||||
.header(reqwest::header::RANGE, "bytes=999999-")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(plain_range_error.status(), 416);
|
||||
assert!(!plain_range_error.text().await?.contains("<Error>"));
|
||||
|
||||
let ordinary = s3.get_object().bucket(bucket).key("move").send().await?;
|
||||
assert_eq!(ordinary.body.collect().await?.into_bytes(), "ordinary redirect object");
|
||||
let ranged = s3.get_object().bucket(bucket).key("move").range("bytes=0-7").send().await?;
|
||||
assert_eq!(ranged.body.collect().await?.into_bytes(), "ordinary");
|
||||
let head_object = s3.head_object().bucket(bucket).key("move").send().await?;
|
||||
let etag = head_object.e_tag().expect("object ETag");
|
||||
let unchanged = s3.get_object().bucket(bucket).key("move").if_none_match(etag).send().await;
|
||||
assert!(unchanged.is_err(), "ordinary S3 conditional GET should return NotModified");
|
||||
let presigned = s3
|
||||
.get_object()
|
||||
.bucket(bucket)
|
||||
.key("move")
|
||||
.presigned(PresigningConfig::expires_in(Duration::from_secs(300))?)
|
||||
.await?;
|
||||
let presigned_read = http.get(presigned.uri().to_string()).send().await?;
|
||||
assert_eq!(presigned_read.status(), 200);
|
||||
assert_eq!(presigned_read.text().await?, "ordinary redirect object");
|
||||
let listed = s3.list_objects_v2().bucket(bucket).send().await?;
|
||||
assert!(listed.contents().iter().any(|object| object.key() == Some("index.html")));
|
||||
let s3_missing = http.get(format!("{}/{bucket}/missing", env.url)).send().await?;
|
||||
assert!(s3_missing.status().is_client_error());
|
||||
assert!(s3_missing.text().await?.contains("<Error>"));
|
||||
s3.put_bucket_versioning()
|
||||
.bucket(bucket)
|
||||
.versioning_configuration(
|
||||
VersioningConfiguration::builder()
|
||||
.status(BucketVersioningStatus::Enabled)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let first = s3
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key("versioned.txt")
|
||||
.body(ByteStream::from_static(b"first"))
|
||||
.send()
|
||||
.await?;
|
||||
s3.put_object()
|
||||
.bucket(bucket)
|
||||
.key("versioned.txt")
|
||||
.body(ByteStream::from_static(b"second"))
|
||||
.send()
|
||||
.await?;
|
||||
let versioned = s3
|
||||
.get_object()
|
||||
.bucket(bucket)
|
||||
.key("versioned.txt")
|
||||
.version_id(first.version_id().expect("first object version"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(versioned.body.collect().await?.into_bytes(), "first");
|
||||
|
||||
s3.put_bucket_website()
|
||||
.bucket(bucket)
|
||||
.website_configuration(
|
||||
WebsiteConfiguration::builder()
|
||||
.index_document(IndexDocument::builder().suffix("index.html").build()?)
|
||||
.routing_rules(
|
||||
RoutingRule::builder()
|
||||
.condition(Condition::builder().http_error_code_returned_equals("404").build())
|
||||
.redirect(Redirect::builder().replace_key_with("missing-page").build())
|
||||
.build(),
|
||||
)
|
||||
.routing_rules(
|
||||
RoutingRule::builder()
|
||||
.condition(Condition::builder().key_prefix_equals("docs/").build())
|
||||
.redirect(Redirect::builder().replace_key_with("docs-page").build())
|
||||
.build(),
|
||||
)
|
||||
.routing_rules(
|
||||
RoutingRule::builder()
|
||||
.condition(Condition::builder().http_error_code_returned_equals("500").build())
|
||||
.redirect(Redirect::builder().replace_key_with("wrong-status").build())
|
||||
.build(),
|
||||
)
|
||||
.routing_rules(
|
||||
RoutingRule::builder()
|
||||
.condition(Condition::builder().http_error_code_returned_equals("416").build())
|
||||
.redirect(Redirect::builder().replace_key_with("range-error").build())
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let rule = http.get(website("/docs/missing")).header(HOST, &host).send().await?;
|
||||
assert_eq!(rule.status(), 301);
|
||||
assert_eq!(
|
||||
rule.headers()[reqwest::header::LOCATION],
|
||||
"http://website-hosting.website.test/missing-page"
|
||||
);
|
||||
let ranged_error = http
|
||||
.get(website("/index.html"))
|
||||
.header(HOST, &host)
|
||||
.header(reqwest::header::RANGE, "bytes=999999-")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(ranged_error.status(), 301);
|
||||
assert_eq!(
|
||||
ranged_error.headers()[reqwest::header::LOCATION],
|
||||
"http://website-hosting.website.test/range-error"
|
||||
);
|
||||
|
||||
s3.put_bucket_website()
|
||||
.bucket(bucket)
|
||||
.website_configuration(
|
||||
WebsiteConfiguration::builder()
|
||||
.redirect_all_requests_to(RedirectAllRequestsTo::builder().host_name("destination.test").build()?)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let all = http.get(website("/a%20b?x=1")).header(HOST, &host).send().await?;
|
||||
assert_eq!(all.status(), 301);
|
||||
assert_eq!(all.headers()[reqwest::header::LOCATION], "http://destination.test/a%20b?x=1");
|
||||
|
||||
let dotted = "my.website.bucket";
|
||||
s3.create_bucket().bucket(dotted).send().await?;
|
||||
s3.put_bucket_website()
|
||||
.bucket(dotted)
|
||||
.website_configuration(
|
||||
WebsiteConfiguration::builder()
|
||||
.redirect_all_requests_to(RedirectAllRequestsTo::builder().host_name("destination.test").build()?)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let dotted_host = format!("{dotted}.website.test");
|
||||
let dotted_response = http.get(website("/key")).header(HOST, dotted_host).send().await?;
|
||||
assert_eq!(dotted_response.status(), 301);
|
||||
assert_eq!(dotted_response.headers()[reqwest::header::LOCATION], "http://destination.test/key");
|
||||
env.stop_server();
|
||||
Ok(())
|
||||
}
|
||||
@@ -409,6 +409,11 @@ pub struct BucketTargetSys {
|
||||
/// lock (never held across an await) so the replication worker can record
|
||||
/// a verdict from inside its synchronous PUT-response audit.
|
||||
version_identity_map: Arc<std::sync::RwLock<HashMap<String, VersionIdentityCapability>>>,
|
||||
/// Target ARNs whose remote rejected a PUT / CreateMultipartUpload
|
||||
/// `versionId` query with `InvalidArgument`; later requests to the same
|
||||
/// ARN omit the query instead of failing again. Reset alongside
|
||||
/// `version_identity_map` — same lifetime, same reasoning.
|
||||
put_version_id_query_rejected: Arc<std::sync::RwLock<HashSet<String>>>,
|
||||
pub targets_map: Arc<RwLock<HashMap<String, Vec<BucketTarget>>>>,
|
||||
/// Buckets whose persisted `bucket-targets.json` exists but cannot be
|
||||
/// decoded (rustfs/backlog#2282). Written under the bucket's update mutex
|
||||
@@ -458,6 +463,7 @@ impl BucketTargetSys {
|
||||
arn_remotes_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
ssec_passthrough_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
version_identity_map: Arc::new(std::sync::RwLock::new(HashMap::new())),
|
||||
put_version_id_query_rejected: Arc::new(std::sync::RwLock::new(HashSet::new())),
|
||||
targets_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
unreadable_targets: Arc::new(RwLock::new(HashSet::new())),
|
||||
h_mutex: Arc::new(RwLock::new(HashMap::new())),
|
||||
@@ -782,10 +788,36 @@ impl BucketTargetSys {
|
||||
health_map.remove(&target.arn);
|
||||
ssec_map.remove(&target.arn);
|
||||
self.forget_version_identity_capability(&target.arn);
|
||||
self.forget_put_version_id_query_rejected(&target.arn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this target ARN is already known to reject a `versionId`
|
||||
/// query on PUT / CreateMultipartUpload.
|
||||
pub fn put_version_id_query_rejected(&self, arn: &str) -> bool {
|
||||
self.put_version_id_query_rejected
|
||||
.read()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.contains(arn)
|
||||
}
|
||||
|
||||
/// Record that this target ARN rejected a `versionId` query on PUT /
|
||||
/// CreateMultipartUpload, so later requests to it omit the query.
|
||||
pub fn record_put_version_id_query_rejected(&self, arn: &str) {
|
||||
self.put_version_id_query_rejected
|
||||
.write()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.insert(arn.to_string());
|
||||
}
|
||||
|
||||
fn forget_put_version_id_query_rejected(&self, arn: &str) {
|
||||
self.put_version_id_query_rejected
|
||||
.write()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.remove(arn);
|
||||
}
|
||||
|
||||
/// Cached version-identity verdict for a target ARN; `Unknown` until a
|
||||
/// replication write or a replication-check VersionFidelity probe judged
|
||||
/// it since the target was built.
|
||||
@@ -1252,6 +1284,7 @@ impl BucketTargetSys {
|
||||
.is_none_or(|edited| !same_replication_service(edited, &target))
|
||||
{
|
||||
self.forget_version_identity_capability(&target.arn);
|
||||
self.forget_put_version_id_query_rejected(&target.arn);
|
||||
}
|
||||
self.update_bandwidth_limit(bucket, &target.arn, 0);
|
||||
}
|
||||
@@ -1449,8 +1482,13 @@ pub fn resolve_delete_api_version_id(version_id: Option<String>, opts: &RemoveOb
|
||||
/// "null" (the delete path already maps it via `target_delete_version_id`),
|
||||
/// and an empty id means the source object carries no version: send no query
|
||||
/// so an unversioned target stays valid.
|
||||
fn resolve_put_api_version_id(source_version_id: &str) -> Option<&str> {
|
||||
if source_version_id.is_empty() {
|
||||
///
|
||||
/// A standard S3 target (AWS S3 included) instead rejects this query on PUT /
|
||||
/// CreateMultipartUpload outright with `InvalidArgument: This operation does
|
||||
/// not accept a version-id`. Once a target ARN has been observed to reject
|
||||
/// it (`arn_rejected_query`), it is never sent to that ARN again.
|
||||
fn resolve_put_api_version_id(source_version_id: &str, arn_rejected_query: bool) -> Option<&str> {
|
||||
if arn_rejected_query || source_version_id.is_empty() {
|
||||
None
|
||||
} else if Uuid::parse_str(source_version_id).is_ok_and(|uuid| uuid.is_nil()) {
|
||||
Some(rustfs_filemeta::NULL_VERSION_ID)
|
||||
@@ -1459,6 +1497,13 @@ fn resolve_put_api_version_id(source_version_id: &str) -> Option<&str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// True when a PUT / CreateMultipartUpload service error is the standard S3
|
||||
/// rejection of a `versionId` query on those operations (AWS S3 and other
|
||||
/// strict S3-compatible targets; MinIO/RustFS peers accept the query).
|
||||
fn is_put_version_id_query_rejected(code: Option<&str>, message: Option<&str>) -> bool {
|
||||
code == Some("InvalidArgument") && message.is_some_and(|m| m.to_ascii_lowercase().contains("version-id"))
|
||||
}
|
||||
|
||||
/// Resolve the S3 `versionId` for a proxied read against a remote target.
|
||||
/// RustFS represents the null version internally as the nil UUID while the S3
|
||||
/// API addresses it as the literal "null" (same mapping as
|
||||
@@ -2251,7 +2296,9 @@ impl TargetClient {
|
||||
if !version_id.is_empty() {
|
||||
insert_header(&mut headers, SUFFIX_SOURCE_VERSION_ID, &version_id);
|
||||
}
|
||||
let api_version_id = resolve_put_api_version_id(&version_id).map(ToOwned::to_owned);
|
||||
let api_version_id =
|
||||
resolve_put_api_version_id(&version_id, BucketTargetSys::get().put_version_id_query_rejected(&self.arn))
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
// A PUT carrying Object Lock parameters must also carry Content-MD5 or
|
||||
// an x-amz-checksum-* header on AWS-compatible targets (rustfs#7082).
|
||||
@@ -2333,6 +2380,9 @@ impl TargetClient {
|
||||
SdkError::ServiceError(service_err) => {
|
||||
let err = service_err.into_err();
|
||||
let meta = err.meta();
|
||||
if is_put_version_id_query_rejected(meta.code(), meta.message()) {
|
||||
BucketTargetSys::get().record_put_version_id_query_rejected(&self.arn);
|
||||
}
|
||||
let error = match (meta.code(), meta.message()) {
|
||||
(Some(code), Some(message)) => format!("put_object failed: {code}: {message}"),
|
||||
(Some(code), None) => format!("put_object failed: {code}"),
|
||||
@@ -2369,7 +2419,9 @@ impl TargetClient {
|
||||
let version_id = opts.internal.source_version_id.clone();
|
||||
// The remote version of a multipart replication is decided at initiate
|
||||
// time; CompleteMultipartUpload does not read a versionId.
|
||||
let api_version_id = resolve_put_api_version_id(&version_id).map(ToOwned::to_owned);
|
||||
let api_version_id =
|
||||
resolve_put_api_version_id(&version_id, BucketTargetSys::get().put_version_id_query_rejected(&self.arn))
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
match self
|
||||
.client
|
||||
@@ -2395,7 +2447,15 @@ impl TargetClient {
|
||||
.await
|
||||
{
|
||||
Ok(res) => Ok(res.upload_id.unwrap_or_default()),
|
||||
Err(e) => Err(e.into()),
|
||||
Err(e) => {
|
||||
if let SdkError::ServiceError(service_err) = &e {
|
||||
let meta = service_err.err().meta();
|
||||
if is_put_version_id_query_rejected(meta.code(), meta.message()) {
|
||||
BucketTargetSys::get().record_put_version_id_query_rejected(&self.arn);
|
||||
}
|
||||
}
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3597,6 +3657,87 @@ mod tests {
|
||||
assert_eq!(sys.version_identity_capability(arn), VersionIdentityCapability::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_version_id_query_rejection_is_per_arn_and_forgotten_with_the_target() {
|
||||
let sys = BucketTargetSys::default();
|
||||
let arn = "arn:rustfs:replication:us-east-1:bucket:versionid";
|
||||
assert!(!sys.put_version_id_query_rejected(arn));
|
||||
sys.record_put_version_id_query_rejected(arn);
|
||||
assert!(sys.put_version_id_query_rejected(arn));
|
||||
assert!(!sys.put_version_id_query_rejected("other"));
|
||||
// A rebuilt target may point at a different service.
|
||||
sys.forget_put_version_id_query_rejected(arn);
|
||||
assert!(!sys.put_version_id_query_rejected(arn));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_all_targets_forgets_put_version_id_rejection_when_endpoint_changes() {
|
||||
// Editing a target's ARN to point at a different endpoint must not
|
||||
// carry over a stale "this remote rejects versionId" verdict from the
|
||||
// service it used to point at (rustfs review follow-up on the
|
||||
// versionId-on-PUT fix): otherwise a peer that legitimately needs the
|
||||
// query silently loses version-id fidelity until the process restarts.
|
||||
let sys = BucketTargetSys::default();
|
||||
let arn = "arn:rustfs:replication:us-east-1:bucket:versionid";
|
||||
let target = |endpoint: &str| BucketTarget {
|
||||
arn: arn.to_string(),
|
||||
endpoint: endpoint.to_string(),
|
||||
target_bucket: "target-bucket".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
credentials: Some(Credentials {
|
||||
access_key: "access".to_string(),
|
||||
secret_key: "secret".to_string(),
|
||||
session_token: None,
|
||||
expiration: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
sys.update_all_targets(
|
||||
"bucket",
|
||||
Some(&BucketTargets {
|
||||
targets: vec![target("aws-s3.example:9000")],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
sys.record_put_version_id_query_rejected(arn);
|
||||
assert!(sys.put_version_id_query_rejected(arn));
|
||||
|
||||
// Same ARN, different endpoint: the update path (not `delete`) is the
|
||||
// only thing that runs here, so the reset must come from
|
||||
// `update_all_targets_locked` itself.
|
||||
sys.update_all_targets(
|
||||
"bucket",
|
||||
Some(&BucketTargets {
|
||||
targets: vec![target("minio-peer.example:9000")],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!sys.put_version_id_query_rejected(arn),
|
||||
"editing the target to a different endpoint must forget the stale rejection verdict"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_put_api_version_id_omits_query_once_rejected() {
|
||||
let version_id = Uuid::new_v4().to_string();
|
||||
assert_eq!(resolve_put_api_version_id(&version_id, false), Some(version_id.as_str()));
|
||||
assert_eq!(resolve_put_api_version_id(&version_id, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_put_version_id_query_rejected_matches_aws_invalid_argument() {
|
||||
assert!(is_put_version_id_query_rejected(
|
||||
Some("InvalidArgument"),
|
||||
Some("This operation does not accept a version-id.")
|
||||
));
|
||||
assert!(!is_put_version_id_query_rejected(Some("AccessDenied"), Some("access denied")));
|
||||
assert!(!is_put_version_id_query_rejected(Some("InvalidArgument"), Some("bucket name is invalid")));
|
||||
assert!(!is_put_version_id_query_rejected(None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_health_key_preserves_explicit_port() {
|
||||
let url = Url::parse("https://remote.example:9443").expect("url should parse");
|
||||
|
||||
@@ -14,13 +14,18 @@
|
||||
|
||||
//! Bounded, authenticated inter-node network probes.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use rustfs_protos::ChannelClass;
|
||||
use rustfs_protos::models::{PingBody, PingBodyBuilder};
|
||||
use rustfs_protos::proto_gen::node_service::{PingRequest, PingResponse};
|
||||
use thiserror::Error;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tonic::{Code, Request};
|
||||
|
||||
@@ -33,6 +38,40 @@ pub const MAX_NETWORK_PROBE_DURATION: Duration = Duration::from_secs(30);
|
||||
const PING_PROTOCOL_VERSION: u64 = 1;
|
||||
const LATENCY_PAYLOAD: &[u8] = b"network-probe-latency-v1";
|
||||
const EXPECTED_RESPONSE_PAYLOAD: &[u8] = b"hello, caller";
|
||||
const DIAGNOSTIC_BYTES_PER_SECOND: u64 = 1_048_576;
|
||||
const DIAGNOSTIC_CHUNK_BYTES: u64 = 16_384;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiagnosticPacing {
|
||||
started: Instant,
|
||||
deadline: Instant,
|
||||
charged_bytes: AtomicU64,
|
||||
}
|
||||
|
||||
impl DiagnosticPacing {
|
||||
fn reserve(&self, bytes: u64) -> Result<Instant, NetworkPeerProbeError> {
|
||||
if bytes == 0 {
|
||||
return Err(NetworkPeerProbeError::LimitExceeded);
|
||||
}
|
||||
// Reservation is never refunded: a failed or dropped RPC may have sent some payload.
|
||||
let previous = self
|
||||
.charged_bytes
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |charged| {
|
||||
charged.checked_add(bytes).filter(|total| *total <= MAX_NETWORK_PROBE_BYTES)
|
||||
})
|
||||
.map_err(|_| NetworkPeerProbeError::LimitExceeded)?;
|
||||
let charged = previous.checked_add(bytes).ok_or(NetworkPeerProbeError::LimitExceeded)?;
|
||||
let millis = u128::from(charged)
|
||||
.checked_mul(1_000)
|
||||
.ok_or(NetworkPeerProbeError::LimitExceeded)?
|
||||
.div_ceil(u128::from(DIAGNOSTIC_BYTES_PER_SECOND));
|
||||
self.started
|
||||
.checked_add(Duration::from_millis(
|
||||
u64::try_from(millis).map_err(|_| NetworkPeerProbeError::LimitExceeded)?,
|
||||
))
|
||||
.ok_or(NetworkPeerProbeError::LimitExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NetworkPeerTarget {
|
||||
@@ -66,6 +105,7 @@ pub enum NetworkPeerProbeError {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NetworkPeerProbeClient {
|
||||
targets: Vec<NetworkPeerTarget>,
|
||||
diagnostic_pacing: Option<Arc<DiagnosticPacing>>,
|
||||
}
|
||||
|
||||
impl NetworkPeerProbeClient {
|
||||
@@ -80,7 +120,25 @@ impl NetworkPeerProbeClient {
|
||||
address,
|
||||
})
|
||||
.collect();
|
||||
Self { targets }
|
||||
Self {
|
||||
targets,
|
||||
diagnostic_pacing: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opt in one diagnostic run; clones share its charged payload and absolute deadline.
|
||||
/// Default/admin probes retain their existing unpaced behavior.
|
||||
pub fn with_diagnostic_pacing(mut self, started: Instant, duration: Duration) -> Result<Self, NetworkPeerProbeError> {
|
||||
if self.diagnostic_pacing.is_some() || duration.is_zero() || duration > MAX_NETWORK_PROBE_DURATION {
|
||||
return Err(NetworkPeerProbeError::LimitExceeded);
|
||||
}
|
||||
let deadline = started.checked_add(duration).ok_or(NetworkPeerProbeError::LimitExceeded)?;
|
||||
self.diagnostic_pacing = Some(Arc::new(DiagnosticPacing {
|
||||
started,
|
||||
deadline,
|
||||
charged_bytes: AtomicU64::new(0),
|
||||
}));
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn targets(&self) -> Vec<NetworkPeerTarget> {
|
||||
@@ -110,7 +168,19 @@ impl NetworkPeerProbeClient {
|
||||
.find(|target| target.alias == peer_alias)
|
||||
.map(|target| target.address.as_str())
|
||||
.ok_or(NetworkPeerProbeError::UnknownPeer)?;
|
||||
let probe = probe_peer(address, traffic_bytes);
|
||||
let probe = probe_peer(address, traffic_bytes, self.diagnostic_pacing.as_deref(), cancel);
|
||||
if let Some(pacing) = &self.diagnostic_pacing {
|
||||
let deadline = Instant::now()
|
||||
.checked_add(max_duration)
|
||||
.ok_or(NetworkPeerProbeError::LimitExceeded)?
|
||||
.min(pacing.deadline);
|
||||
return tokio::select! {
|
||||
biased;
|
||||
() = cancel.cancelled() => Err(NetworkPeerProbeError::Cancelled),
|
||||
() = tokio::time::sleep_until(deadline) => Err(NetworkPeerProbeError::TimedOut),
|
||||
result = probe => result,
|
||||
};
|
||||
}
|
||||
tokio::select! {
|
||||
() = cancel.cancelled() => Err(NetworkPeerProbeError::Cancelled),
|
||||
result = tokio::time::timeout(max_duration, probe) => {
|
||||
@@ -120,7 +190,12 @@ impl NetworkPeerProbeClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe_peer(address: &str, traffic_bytes: u64) -> Result<NetworkPeerProbeMeasurement, NetworkPeerProbeError> {
|
||||
async fn probe_peer(
|
||||
address: &str,
|
||||
traffic_bytes: u64,
|
||||
pacing: Option<&DiagnosticPacing>,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<NetworkPeerProbeMeasurement, NetworkPeerProbeError> {
|
||||
let started = Instant::now();
|
||||
let mut control = client(address, ChannelClass::Control).await?;
|
||||
let latency_started = Instant::now();
|
||||
@@ -132,15 +207,18 @@ async fn probe_peer(address: &str, traffic_bytes: u64) -> Result<NetworkPeerProb
|
||||
validate_ping_response(&latency_response)?;
|
||||
let latency = latency_started.elapsed();
|
||||
|
||||
let payload_len = usize::try_from(traffic_bytes).map_err(|_| NetworkPeerProbeError::LimitExceeded)?;
|
||||
let payload = vec![0x5a; payload_len];
|
||||
let mut bulk = client(address, ChannelClass::Bulk).await?;
|
||||
let response = bulk
|
||||
.ping(Request::new(ping_request(&payload)))
|
||||
.await
|
||||
.map_err(map_status)?
|
||||
.into_inner();
|
||||
validate_ping_response(&response)?;
|
||||
send_payload(traffic_bytes, pacing, cancel, &mut bulk, |bulk, payload| {
|
||||
Box::pin(async move {
|
||||
let response = bulk
|
||||
.ping(Request::new(ping_request(&payload)))
|
||||
.await
|
||||
.map_err(map_status)?
|
||||
.into_inner();
|
||||
validate_ping_response(&response)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(NetworkPeerProbeMeasurement {
|
||||
transferred_bytes: traffic_bytes,
|
||||
@@ -149,6 +227,59 @@ async fn probe_peer(address: &str, traffic_bytes: u64) -> Result<NetworkPeerProb
|
||||
})
|
||||
}
|
||||
|
||||
type PayloadSendFuture<'a> = Pin<Box<dyn Future<Output = Result<(), NetworkPeerProbeError>> + Send + 'a>>;
|
||||
|
||||
async fn send_payload<T, F>(
|
||||
traffic_bytes: u64,
|
||||
pacing: Option<&DiagnosticPacing>,
|
||||
cancel: &CancellationToken,
|
||||
sender: &mut T,
|
||||
mut send: F,
|
||||
) -> Result<(), NetworkPeerProbeError>
|
||||
where
|
||||
T: Send,
|
||||
F: for<'a> FnMut(&'a mut T, Vec<u8>) -> PayloadSendFuture<'a> + Send,
|
||||
{
|
||||
let mut remaining = traffic_bytes;
|
||||
while remaining > 0 {
|
||||
let bytes = if pacing.is_some() {
|
||||
remaining.min(DIAGNOSTIC_CHUNK_BYTES)
|
||||
} else {
|
||||
remaining
|
||||
};
|
||||
if let Some(pacing) = pacing {
|
||||
if cancel.is_cancelled() {
|
||||
return Err(NetworkPeerProbeError::Cancelled);
|
||||
}
|
||||
if Instant::now() >= pacing.deadline {
|
||||
return Err(NetworkPeerProbeError::TimedOut);
|
||||
}
|
||||
let due = pacing.reserve(bytes)?;
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = cancel.cancelled() => return Err(NetworkPeerProbeError::Cancelled),
|
||||
() = tokio::time::sleep_until(pacing.deadline) => return Err(NetworkPeerProbeError::TimedOut),
|
||||
() = tokio::time::sleep_until(due) => {},
|
||||
}
|
||||
}
|
||||
let payload_len = usize::try_from(bytes).map_err(|_| NetworkPeerProbeError::LimitExceeded)?;
|
||||
let payload = vec![0x5a; payload_len];
|
||||
if let Some(pacing) = pacing {
|
||||
// Check before polling the send future, including a cancellation/deadline tie.
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = cancel.cancelled() => return Err(NetworkPeerProbeError::Cancelled),
|
||||
() = tokio::time::sleep_until(pacing.deadline) => return Err(NetworkPeerProbeError::TimedOut),
|
||||
result = send(sender, payload) => result?,
|
||||
}
|
||||
} else {
|
||||
send(sender, payload).await?;
|
||||
}
|
||||
remaining -= bytes;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn client(
|
||||
address: &str,
|
||||
class: ChannelClass,
|
||||
@@ -208,6 +339,205 @@ mod tests {
|
||||
use crate::layout::endpoint::Endpoint;
|
||||
use crate::layout::endpoints::{Endpoints, PoolEndpoints};
|
||||
|
||||
fn pacing(duration: Duration) -> Arc<DiagnosticPacing> {
|
||||
NetworkPeerProbeClient::from_endpoint_pools(&topology())
|
||||
.with_diagnostic_pacing(Instant::now(), duration)
|
||||
.expect("bounded diagnostic pacing window")
|
||||
.diagnostic_pacing
|
||||
.expect("opt-in pacing is present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pacing_uses_exact_millisecond_ceiling_at_n_and_n_plus_one() {
|
||||
for duration in [Duration::ZERO, MAX_NETWORK_PROBE_DURATION + Duration::from_nanos(1)] {
|
||||
assert!(matches!(
|
||||
NetworkPeerProbeClient::from_endpoint_pools(&topology()).with_diagnostic_pacing(Instant::now(), duration),
|
||||
Err(NetworkPeerProbeError::LimitExceeded)
|
||||
));
|
||||
}
|
||||
for (bytes, millis) in [(1, 1), (1_047_527, 999), (1_047_528, 1_000), (1_048_576, 1_000)] {
|
||||
let pacing = pacing(Duration::from_secs(2));
|
||||
assert_eq!(
|
||||
pacing.reserve(bytes).expect("bounded reservation") - pacing.started,
|
||||
Duration::from_millis(millis)
|
||||
);
|
||||
}
|
||||
for bytes in [0, MAX_NETWORK_PROBE_BYTES + 1, u64::MAX] {
|
||||
let pacing = pacing(Duration::from_secs(2));
|
||||
assert_eq!(pacing.reserve(bytes), Err(NetworkPeerProbeError::LimitExceeded));
|
||||
assert_eq!(pacing.charged_bytes.load(Ordering::Acquire), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_diagnostic_clients_share_one_atomic_budget_without_reset() {
|
||||
let client = NetworkPeerProbeClient::from_endpoint_pools(&topology())
|
||||
.with_diagnostic_pacing(Instant::now(), Duration::from_secs(2))
|
||||
.expect("pacing");
|
||||
let cloned = client.clone();
|
||||
let first = client.diagnostic_pacing.as_ref().expect("first budget");
|
||||
let second = cloned.diagnostic_pacing.as_ref().expect("cloned budget");
|
||||
assert!(Arc::ptr_eq(first, second));
|
||||
let mut due = std::thread::scope(|scope| {
|
||||
let first = scope.spawn(|| first.reserve(MAX_NETWORK_PROBE_BYTES / 2));
|
||||
let second = scope.spawn(|| second.reserve(MAX_NETWORK_PROBE_BYTES / 2));
|
||||
[
|
||||
first.join().expect("first reservation thread").expect("first half"),
|
||||
second.join().expect("second reservation thread").expect("second half"),
|
||||
]
|
||||
});
|
||||
due.sort();
|
||||
assert_eq!(
|
||||
due,
|
||||
[
|
||||
first.started + Duration::from_millis(500),
|
||||
first.started + Duration::from_secs(1)
|
||||
]
|
||||
);
|
||||
assert_eq!(first.reserve(1), Err(NetworkPeerProbeError::LimitExceeded));
|
||||
assert_eq!(first.charged_bytes.load(Ordering::Acquire), MAX_NETWORK_PROBE_BYTES);
|
||||
assert!(matches!(
|
||||
cloned.with_diagnostic_pacing(Instant::now(), Duration::from_secs(2)),
|
||||
Err(NetworkPeerProbeError::LimitExceeded)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn failed_peer_payload_stays_charged_before_the_next_peer_send() {
|
||||
let pacing = pacing(Duration::from_secs(2));
|
||||
let cancel = CancellationToken::new();
|
||||
let mut observed_partial_bytes = 0;
|
||||
let failed = send_payload(32_768, Some(&pacing), &cancel, &mut observed_partial_bytes, |partial, payload| {
|
||||
Box::pin(async move {
|
||||
assert_eq!(payload.len(), 16_384);
|
||||
// Model failure after a partial write: the sender cannot safely refund the remainder.
|
||||
*partial += 8_192;
|
||||
Err(NetworkPeerProbeError::Unreachable)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
assert_eq!(failed, Err(NetworkPeerProbeError::Unreachable));
|
||||
assert_eq!(observed_partial_bytes, 8_192);
|
||||
assert_eq!(pacing.charged_bytes.load(Ordering::Acquire), 16_384);
|
||||
let started = pacing.started;
|
||||
let mut second_bytes = 0;
|
||||
send_payload(16_384, Some(&pacing), &cancel, &mut second_bytes, move |bytes, payload| {
|
||||
Box::pin(async move {
|
||||
assert!(started.elapsed() >= Duration::from_millis(32));
|
||||
*bytes += payload.len();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("second peer uses remaining shared budget");
|
||||
assert_eq!(second_bytes, 16_384);
|
||||
assert_eq!(pacing.charged_bytes.load(Ordering::Acquire), 32_768);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn cancellation_and_deadline_take_priority_over_an_earned_send() {
|
||||
for cancelled in [false, true] {
|
||||
let pacing = pacing(Duration::from_millis(16));
|
||||
let cancel = CancellationToken::new();
|
||||
let mut sends = 0;
|
||||
{
|
||||
let mut sending =
|
||||
std::pin::pin!(send_payload(8_192, Some(&pacing), &cancel, &mut sends, |sends, _| Box::pin(async move {
|
||||
*sends += 1;
|
||||
Ok(())
|
||||
})));
|
||||
assert!(futures::poll!(&mut sending).is_pending());
|
||||
tokio::time::advance(Duration::from_millis(16)).await;
|
||||
if cancelled {
|
||||
cancel.cancel();
|
||||
}
|
||||
let expected = if cancelled {
|
||||
NetworkPeerProbeError::Cancelled
|
||||
} else {
|
||||
NetworkPeerProbeError::TimedOut
|
||||
};
|
||||
assert_eq!(sending.await, Err(expected));
|
||||
}
|
||||
assert_eq!(sends, 0);
|
||||
assert_eq!(pacing.charged_bytes.load(Ordering::Acquire), 8_192);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn cancellation_during_a_payload_rpc_retains_its_attempted_charge() {
|
||||
let pacing = pacing(Duration::from_secs(2));
|
||||
let cancel = CancellationToken::new();
|
||||
let mut sends = 0;
|
||||
{
|
||||
let mut sending = std::pin::pin!(send_payload(16_384, Some(&pacing), &cancel, &mut sends, |sends, _| Box::pin(
|
||||
async move {
|
||||
*sends += 1;
|
||||
std::future::pending().await
|
||||
}
|
||||
)));
|
||||
assert!(futures::poll!(&mut sending).is_pending());
|
||||
tokio::time::advance(Duration::from_millis(16)).await;
|
||||
assert!(futures::poll!(&mut sending).is_pending());
|
||||
cancel.cancel();
|
||||
assert_eq!(sending.await, Err(NetworkPeerProbeError::Cancelled));
|
||||
}
|
||||
assert_eq!(sends, 1);
|
||||
assert_eq!(pacing.charged_bytes.load(Ordering::Acquire), 16_384);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn default_payload_send_remains_one_unpaced_rpc() {
|
||||
let started = Instant::now();
|
||||
let mut sends = 0;
|
||||
send_payload(32_768, None, &CancellationToken::new(), &mut sends, move |sends, payload| {
|
||||
Box::pin(async move {
|
||||
assert_eq!(started.elapsed(), Duration::ZERO);
|
||||
assert_eq!(payload.len(), 32_768);
|
||||
*sends += 1;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("legacy admin send");
|
||||
assert_eq!(sends, 1);
|
||||
assert!(
|
||||
NetworkPeerProbeClient::from_endpoint_pools(&topology())
|
||||
.diagnostic_pacing
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn diagnostic_payload_is_charged_before_each_actual_send_poll() {
|
||||
let started = tokio::time::Instant::now();
|
||||
let pacing = DiagnosticPacing {
|
||||
started,
|
||||
deadline: started + Duration::from_secs(2),
|
||||
charged_bytes: AtomicU64::new(0),
|
||||
};
|
||||
let mut attempted = 0_u64;
|
||||
send_payload(
|
||||
32_768,
|
||||
Some(&pacing),
|
||||
&CancellationToken::new(),
|
||||
&mut attempted,
|
||||
move |attempted, payload| {
|
||||
Box::pin(async move {
|
||||
*attempted += u64::try_from(payload.len()).expect("bounded payload length");
|
||||
let available = u128::from(MAX_NETWORK_PROBE_BYTES) * started.elapsed().as_millis() / 1_000;
|
||||
assert!(
|
||||
u128::from(*attempted) <= available,
|
||||
"send boundary emitted {attempted} bytes with only {available} bytes earned"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("bounded diagnostic payload");
|
||||
assert_eq!(attempted, 32_768);
|
||||
}
|
||||
|
||||
fn endpoint(value: &str, is_local: bool) -> Endpoint {
|
||||
let mut endpoint = Endpoint::try_from(value).expect("test endpoint should parse");
|
||||
endpoint.is_local = is_local;
|
||||
@@ -230,6 +560,14 @@ mod tests {
|
||||
}])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_future_remains_send() {
|
||||
fn require_send<T: Send>(_: T) {}
|
||||
let client = NetworkPeerProbeClient::from_endpoint_pools(&topology());
|
||||
let cancel = CancellationToken::new();
|
||||
require_send(client.probe("peer-1", 1, Duration::from_secs(1), &cancel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn targets_are_unique_sorted_remote_nodes_with_stable_aliases() {
|
||||
let client = NetworkPeerProbeClient::from_endpoint_pools(&topology());
|
||||
|
||||
@@ -993,7 +993,19 @@ impl RemoteDisk {
|
||||
// internode transport failure classified as retryable can be safely re-dialed. The
|
||||
// classifier is direction-agnostic — it inspects the InternodeHttpError kind — so it is
|
||||
// reused here from the write path.
|
||||
err.is_retryable_internode_write_failure()
|
||||
if err.is_retryable_internode_write_failure() {
|
||||
return true;
|
||||
}
|
||||
// Header wait uses the body stall budget. That error is `BodyStalled`, not an
|
||||
// `InternodeHttpError`, and the open has not consumed a shard byte yet.
|
||||
matches!(
|
||||
err,
|
||||
DiskError::Io(error)
|
||||
if error
|
||||
.get_ref()
|
||||
.and_then(|source| source.downcast_ref::<rustfs_rio::BodyStalled>())
|
||||
.is_some()
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn new(ep: &Endpoint, opt: &DiskOption, data_transport: Arc<dyn InternodeDataTransport>) -> Result<Self> {
|
||||
@@ -1102,7 +1114,14 @@ impl RemoteDisk {
|
||||
let mut attempt = 1;
|
||||
let mut last_retry_classification = None;
|
||||
loop {
|
||||
match self.data_transport.open_read(request.clone()).await {
|
||||
// The second attempt bypasses the pool. The first failure is often a
|
||||
// stale kept-alive connection to a peer that just restarted.
|
||||
let opened = if attempt == 1 {
|
||||
self.data_transport.open_read(request.clone()).await
|
||||
} else {
|
||||
self.data_transport.open_read_fresh(request.clone()).await
|
||||
};
|
||||
match opened {
|
||||
Ok(reader) => {
|
||||
if attempt > 1
|
||||
&& let Some(classification) = last_retry_classification
|
||||
@@ -1138,7 +1157,12 @@ impl RemoteDisk {
|
||||
let mut attempt = 1;
|
||||
let mut last_retry_classification = None;
|
||||
loop {
|
||||
match self.data_transport.open_read_chunks(request.clone()).await {
|
||||
let opened = if attempt == 1 {
|
||||
self.data_transport.open_read_chunks(request.clone()).await
|
||||
} else {
|
||||
self.data_transport.open_read_chunks_fresh(request.clone()).await
|
||||
};
|
||||
match opened {
|
||||
Ok(reader) => {
|
||||
if attempt > 1
|
||||
&& let Some(classification) = last_retry_classification
|
||||
@@ -1220,13 +1244,14 @@ impl RemoteDisk {
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Own the flag before spawning: shutdown can drop the task without polling it.
|
||||
let lease = RecoveryMonitorLease {
|
||||
active: Arc::clone(&active),
|
||||
};
|
||||
let span = Self::recovery_monitor_span(&addr, &endpoint, handle_id);
|
||||
super::spawn_background_monitor(span, async move {
|
||||
#[cfg(test)]
|
||||
test_state.start_count.fetch_add(1, Ordering::AcqRel);
|
||||
let lease = RecoveryMonitorLease {
|
||||
active: Arc::clone(&active),
|
||||
};
|
||||
Self::monitor_remote_disk_recovery(addr.clone(), endpoint.clone(), Arc::clone(&health), cancel_token.clone()).await;
|
||||
#[cfg(test)]
|
||||
if let Some(hook) = test_state.teardown_hook.lock().await.take() {
|
||||
@@ -1532,7 +1557,8 @@ impl RemoteDisk {
|
||||
};
|
||||
|
||||
if evict_cached_connection {
|
||||
evict_failed_connection(addr).await;
|
||||
// Cache contention must not hold the recovery lease indefinitely.
|
||||
let _ = timeout(get_drive_active_check_timeout(), evict_failed_connection(addr)).await;
|
||||
}
|
||||
|
||||
result
|
||||
@@ -1691,7 +1717,7 @@ impl RemoteDisk {
|
||||
if timeout_duration == Duration::ZERO {
|
||||
let operation_result = operation().await;
|
||||
if operation_result.is_ok() {
|
||||
self.health.log_success();
|
||||
self.health.record_operation_success(&self.endpoint, "operation_success");
|
||||
}
|
||||
self.handle_network_like_error(op, timeout_duration, &operation_result, failure_health_action)
|
||||
.await;
|
||||
@@ -1705,7 +1731,7 @@ impl RemoteDisk {
|
||||
Ok(operation_result) => {
|
||||
// Log success; the waiting guard balances every exit path.
|
||||
if operation_result.is_ok() {
|
||||
self.health.log_success();
|
||||
self.health.record_operation_success(&self.endpoint, "operation_success");
|
||||
}
|
||||
self.handle_network_like_error(op, timeout_duration, &operation_result, failure_health_action)
|
||||
.await;
|
||||
@@ -7729,6 +7755,25 @@ mod tests {
|
||||
assert_eq!(transport.calls().len(), 2, "read_file_stream should retry exactly once");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remote_disk_read_file_stream_retries_header_stall_on_fresh_connection() {
|
||||
let stall = rustfs_rio::BodyStalled {
|
||||
timeout: Duration::from_millis(50),
|
||||
};
|
||||
let transport = RetryingOpenReadInternodeDataTransport::with_steps(vec![
|
||||
OpenWriteTestStep::Error(DiskError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, stall))),
|
||||
OpenWriteTestStep::Success,
|
||||
]);
|
||||
let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
|
||||
|
||||
let _reader = remote_disk
|
||||
.read_file_stream("bucket", "object/part.1", 0, 4096)
|
||||
.await
|
||||
.expect("a header stall must be retried once before the shard is failed");
|
||||
|
||||
assert_eq!(transport.calls().len(), 2, "header stall should re-dial exactly once");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remote_disk_read_file_stream_does_not_retry_non_retryable_open_read_error() {
|
||||
let transport = RetryingOpenReadInternodeDataTransport::with_steps(vec![OpenWriteTestStep::Error(DiskError::from(
|
||||
@@ -8161,6 +8206,97 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn successful_operation_recovers_suspect_remote_disk_without_monitor() {
|
||||
let endpoint = Endpoint {
|
||||
url: url::Url::parse("http://remote-recovery:9000/data").expect("valid endpoint"),
|
||||
is_local: false,
|
||||
pool_idx: 0,
|
||||
set_idx: 0,
|
||||
disk_idx: 0,
|
||||
};
|
||||
let disk = RemoteDisk::new(
|
||||
&endpoint,
|
||||
&DiskOption {
|
||||
cleanup: false,
|
||||
health_check: false,
|
||||
},
|
||||
Arc::new(TcpHttpInternodeDataTransport),
|
||||
)
|
||||
.await
|
||||
.expect("create remote disk");
|
||||
|
||||
for duration in [Duration::ZERO, Duration::from_secs(1)] {
|
||||
disk.health.mark_failure(&endpoint, "read_operation_deadline");
|
||||
assert_eq!(disk.runtime_state(), RuntimeDriveHealthState::Suspect);
|
||||
assert!(!disk.recovery_monitor_is_active());
|
||||
let result = disk
|
||||
.execute_with_timeout(|| async { Err::<(), _>(DiskError::FileNotFound) }, duration)
|
||||
.await;
|
||||
assert!(matches!(result, Err(DiskError::FileNotFound)));
|
||||
assert_eq!(
|
||||
disk.runtime_state(),
|
||||
RuntimeDriveHealthState::Suspect,
|
||||
"a failed operation must not restore readiness"
|
||||
);
|
||||
|
||||
disk.execute_with_timeout(|| async { Ok(()) }, duration)
|
||||
.await
|
||||
.expect("successful disk RPC");
|
||||
assert_eq!(
|
||||
disk.runtime_state(),
|
||||
RuntimeDriveHealthState::Online,
|
||||
"successful traffic must restore readiness without a recovery monitor"
|
||||
);
|
||||
assert!(disk.offline_duration_secs().is_none());
|
||||
assert_eq!(disk.health.waiting.load(Ordering::Acquire), 0);
|
||||
}
|
||||
|
||||
disk.health.mark_offline(&endpoint, "test_offline");
|
||||
let result = disk.execute_with_timeout(|| async { Ok(()) }, Duration::from_secs(1)).await;
|
||||
assert!(
|
||||
matches!(result, Err(DiskError::FaultyDisk)),
|
||||
"offline handles still require recovery probes before data I/O"
|
||||
);
|
||||
assert_eq!(disk.runtime_state(), RuntimeDriveHealthState::Offline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_monitor_releases_lease_when_dropped_before_first_poll() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("create test runtime");
|
||||
let endpoint = Endpoint {
|
||||
url: url::Url::parse("http://remote-unpolled:9000/data").expect("valid endpoint"),
|
||||
is_local: false,
|
||||
pool_idx: 0,
|
||||
set_idx: 0,
|
||||
disk_idx: 0,
|
||||
};
|
||||
let active = Arc::new(AtomicBool::new(false));
|
||||
let start_count = Arc::new(AtomicU32::new(0));
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
RemoteDisk::schedule_recovery_monitor(
|
||||
"http://remote-unpolled:9000".to_string(),
|
||||
endpoint,
|
||||
Uuid::new_v4(),
|
||||
Arc::new(DiskHealthTracker::new()),
|
||||
CancellationToken::new(),
|
||||
Arc::clone(&active),
|
||||
RecoveryMonitorTestState {
|
||||
start_count: Arc::clone(&start_count),
|
||||
teardown_hook: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
},
|
||||
);
|
||||
assert!(active.load(Ordering::Acquire));
|
||||
}
|
||||
drop(runtime);
|
||||
assert_eq!(start_count.load(Ordering::Acquire), 0, "task must never be polled");
|
||||
assert!(!active.load(Ordering::Acquire), "dropping an unpolled monitor must release its lease");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_with_timeout_marks_remote_disk_faulty() {
|
||||
let url = url::Url::parse("http://remote-timeout:9000").expect("operation should succeed");
|
||||
|
||||
@@ -1236,6 +1236,17 @@ impl DiskHealthTracker {
|
||||
record_drive_recovery_class(classify_drive_recovery(duration));
|
||||
}
|
||||
self.offline_since_unix_secs.store(0, Ordering::Release);
|
||||
info!(
|
||||
event = EVENT_DISK_RECOVERY_PROBE_STATE,
|
||||
component = LOG_COMPONENT_ECSTORE,
|
||||
subsystem = LOG_SUBSYSTEM_DISK,
|
||||
endpoint = %endpoint,
|
||||
state = "recovered",
|
||||
previous_state = current.as_str(),
|
||||
runtime_state = next.as_str(),
|
||||
reason,
|
||||
"Disk recovered"
|
||||
);
|
||||
} else if let Some(duration) = self.offline_duration() {
|
||||
record_drive_offline_duration(endpoint, duration);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,12 @@ use uuid::Uuid;
|
||||
// Bound outstanding filesystem jobs and metadata buffers per disk request.
|
||||
const PART_METADATA_READ_CONCURRENCY: usize = 8;
|
||||
|
||||
/// The fresh health path uses a synchronous capacity syscall. Keep the permit
|
||||
/// inside the blocking closure so cancelling its async caller cannot admit a
|
||||
/// second probe while the first syscall is still running.
|
||||
static FRESH_CAPACITY_PROBE_PERMIT: std::sync::LazyLock<Arc<Semaphore>> =
|
||||
std::sync::LazyLock::new(|| Arc::new(Semaphore::new(1)));
|
||||
|
||||
const DELETED_OBJECTS_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 5);
|
||||
const STALE_TMP_OBJECT_EXPIRY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
@@ -10441,28 +10447,30 @@ impl DiskAPI for LocalDisk {
|
||||
};
|
||||
write_metacache_obj(&mut out, &meta).await?;
|
||||
objs_returned += 1;
|
||||
} else {
|
||||
let fpath = self
|
||||
.io_get_object_path(&opts.bucket, path_join_buf(&[opts.base_dir.as_str(), STORAGE_FORMAT_FILE]).as_str())?;
|
||||
}
|
||||
|
||||
if let Ok(meta) = with_walk_stall_deadline(stall, tokio::fs::metadata(&fpath)).await?
|
||||
&& meta.is_file()
|
||||
// A plain object shares this directory with its children even when
|
||||
// an explicit directory marker exists in the sibling encoded path.
|
||||
let fpath =
|
||||
self.io_get_object_path(&opts.bucket, path_join_buf(&[opts.base_dir.as_str(), STORAGE_FORMAT_FILE]).as_str())?;
|
||||
|
||||
if let Ok(meta) = with_walk_stall_deadline(stall, tokio::fs::metadata(&fpath)).await?
|
||||
&& meta.is_file()
|
||||
{
|
||||
skip_current_dir_object = true;
|
||||
if let Ok(meta_bytes) = with_walk_stall_deadline(
|
||||
stall,
|
||||
self.read_metadata(
|
||||
opts.bucket.as_str(),
|
||||
path_join_buf(&[opts.base_dir.as_str(), STORAGE_FORMAT_FILE]).as_str(),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
&& let Ok(file_meta) = FileMeta::load(&meta_bytes)
|
||||
&& let Ok(data_dirs) = file_meta.get_data_dirs()
|
||||
{
|
||||
skip_current_dir_object = true;
|
||||
if let Ok(meta_bytes) = with_walk_stall_deadline(
|
||||
stall,
|
||||
self.read_metadata(
|
||||
opts.bucket.as_str(),
|
||||
path_join_buf(&[opts.base_dir.as_str(), STORAGE_FORMAT_FILE]).as_str(),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
&& let Ok(file_meta) = FileMeta::load(&meta_bytes)
|
||||
&& let Ok(data_dirs) = file_meta.get_data_dirs()
|
||||
{
|
||||
for data_dir in data_dirs.iter().flatten() {
|
||||
multipart_dir_to_skip.insert(data_dir.to_string());
|
||||
}
|
||||
for data_dir in data_dirs.iter().flatten() {
|
||||
multipart_dir_to_skip.insert(data_dir.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11123,7 +11131,34 @@ impl DiskAPI for LocalDisk {
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn disk_info(&self, _: &DiskInfoOptions) -> Result<DiskInfo> {
|
||||
async fn disk_info(&self, opts: &DiskInfoOptions) -> Result<DiskInfo> {
|
||||
if opts.fresh_capacity {
|
||||
let permit = FRESH_CAPACITY_PROBE_PERMIT
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| DiskError::other("fresh capacity probe unavailable"))?;
|
||||
let root = self.root.clone();
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
let _permit = permit;
|
||||
let drive_path = root.to_string_lossy().to_string();
|
||||
check_path_length(&drive_path)?;
|
||||
get_info(&drive_path).map_err(DiskError::from)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| DiskError::other("fresh capacity probe failed"))??;
|
||||
let healing = fs::try_exists(self.root.join(RUSTFS_META_BUCKET).join(super::HEALING_MARKER_PATH))
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
return Ok(DiskInfo {
|
||||
total: info.total,
|
||||
free: info.free,
|
||||
used: info.used,
|
||||
healing,
|
||||
fresh_capacity: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let mut info = Cache::get(self.disk_info_cache.clone()).await?;
|
||||
info.nr_requests = self.nrrequests;
|
||||
info.rotational = self.rotational;
|
||||
@@ -18674,6 +18709,89 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
async fn assert_walk_dir_prefix_marker_entries(with_plain_object: bool) {
|
||||
use rustfs_filemeta::MetacacheReader;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn write_metadata(root: &Path, name: &str, size: i64, data_dir: Option<Uuid>) -> Vec<u8> {
|
||||
let object_dir = root.join(encode_dir_object(name));
|
||||
fs::create_dir_all(&object_dir)
|
||||
.await
|
||||
.expect("object directory should be created");
|
||||
let mut file_info = FileInfo::new(name, 1, 1);
|
||||
file_info.size = size;
|
||||
file_info.data_dir = data_dir;
|
||||
file_info.mod_time = Some(OffsetDateTime::now_utc());
|
||||
let mut metadata = FileMeta::default();
|
||||
metadata.add_version(file_info).expect("object metadata should be valid");
|
||||
let bytes = metadata.marshal_msg().expect("object metadata should encode");
|
||||
fs::write(object_dir.join(STORAGE_FORMAT_FILE), &bytes)
|
||||
.await
|
||||
.expect("object metadata should be written");
|
||||
bytes
|
||||
}
|
||||
|
||||
let dir = tempdir().expect("temporary disk should be created");
|
||||
let bucket = "test-bucket";
|
||||
let bucket_dir = dir.path().join(bucket);
|
||||
let marker_metadata = write_metadata(&bucket_dir, "content/", 0, None).await;
|
||||
let child_metadata = write_metadata(&bucket_dir, "content/child", 7, None).await;
|
||||
let data_dir = Uuid::parse_str("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb").expect("data directory UUID should parse");
|
||||
if with_plain_object {
|
||||
write_metadata(&bucket_dir, "content", 19, Some(data_dir)).await;
|
||||
// A storage data directory can contain metadata-bearing subdirectories.
|
||||
// The entire directory must be skipped rather than exposed as objects.
|
||||
write_metadata(&bucket_dir, &format!("content/{data_dir}/segment"), 31, None).await;
|
||||
fs::write(bucket_dir.join("content").join(data_dir.to_string()).join("part.1"), b"part")
|
||||
.await
|
||||
.expect("object part should be written");
|
||||
}
|
||||
|
||||
let endpoint =
|
||||
Endpoint::try_from(dir.path().to_str().expect("disk path should be UTF-8")).expect("disk endpoint should parse");
|
||||
let disk = LocalDisk::new(&endpoint, false).await.expect("local disk should initialize");
|
||||
let (reader, mut writer) = tokio::io::duplex(65536);
|
||||
disk.walk_dir(
|
||||
WalkDirOptions {
|
||||
bucket: bucket.to_owned(),
|
||||
base_dir: "content/".to_owned(),
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
&mut writer,
|
||||
)
|
||||
.await
|
||||
.expect("prefix walk should succeed");
|
||||
let entries = MetacacheReader::new(reader)
|
||||
.read_all()
|
||||
.await
|
||||
.expect("walk output should decode");
|
||||
assert!(
|
||||
entries
|
||||
.iter()
|
||||
.all(|entry| !entry.name.starts_with(&format!("content/{data_dir}"))),
|
||||
"plain object storage directories must not appear in the walk"
|
||||
);
|
||||
let objects: Vec<_> = entries.into_iter().filter(MetaCacheEntry::is_object).collect();
|
||||
assert_eq!(
|
||||
objects.iter().map(|entry| entry.name.as_str()).collect::<Vec<_>>(),
|
||||
vec!["content/", "content/child"],
|
||||
"the marker and child must each appear exactly once"
|
||||
);
|
||||
assert_eq!(objects[0].metadata, marker_metadata, "the marker must retain its own metadata");
|
||||
assert_eq!(objects[1].metadata, child_metadata, "the child must retain its own metadata");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_walk_dir_prefix_marker_with_children_is_unique() {
|
||||
assert_walk_dir_prefix_marker_entries(false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_walk_dir_prefix_marker_skips_plain_object_and_parts() {
|
||||
assert_walk_dir_prefix_marker_entries(true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scan_dir_reports_base_dir_object_metadata() {
|
||||
use rustfs_filemeta::MetacacheReader;
|
||||
@@ -21108,6 +21226,7 @@ mod test {
|
||||
disk_id: "test-disk".to_string(),
|
||||
metrics: true,
|
||||
noop: false,
|
||||
fresh_capacity: false,
|
||||
};
|
||||
|
||||
let disk_info = disk.disk_info(&disk_info_opts).await.expect("operation should succeed");
|
||||
@@ -21125,6 +21244,26 @@ mod test {
|
||||
assert!(!disk_info.mount_path.is_empty());
|
||||
assert!(!disk_info.endpoint.is_empty());
|
||||
|
||||
let fresh = disk
|
||||
.disk_info(&DiskInfoOptions {
|
||||
fresh_capacity: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("fresh capacity should be available");
|
||||
assert!(fresh.fresh_capacity);
|
||||
assert!(fresh.total > 0);
|
||||
assert!(fresh.used <= fresh.total);
|
||||
assert!(fresh.endpoint.is_empty());
|
||||
assert!(fresh.mount_path.is_empty());
|
||||
assert!(fresh.physical_device_ids.is_empty());
|
||||
assert!(fresh.id.is_none());
|
||||
assert!(fresh.fs_type.is_empty());
|
||||
assert_eq!(fresh.major, 0);
|
||||
assert_eq!(fresh.minor, 0);
|
||||
assert_eq!(fresh.used_inodes, 0);
|
||||
assert_eq!(fresh.free_inodes, 0);
|
||||
|
||||
// Clean up the test directory
|
||||
let _ = fs::remove_dir_all(&test_dir).await;
|
||||
}
|
||||
|
||||
@@ -1365,6 +1365,10 @@ pub struct DiskInfoOptions {
|
||||
pub disk_id: String,
|
||||
pub metrics: bool,
|
||||
pub noop: bool,
|
||||
/// Bypass the ordinary one-second admin cache and return a positive
|
||||
/// acknowledgement only after a fresh capacity syscall completes.
|
||||
#[serde(default)]
|
||||
pub fresh_capacity: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -1389,6 +1393,10 @@ pub struct DiskInfo {
|
||||
pub rotational: bool,
|
||||
pub metrics: DiskMetrics,
|
||||
pub error: String,
|
||||
/// Older peers omit this field. Callers must treat an absent/false value as
|
||||
/// cached or otherwise unproven capacity evidence.
|
||||
#[serde(default)]
|
||||
pub fresh_capacity: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -1912,11 +1920,40 @@ mod tests {
|
||||
disk_id: "test-disk-id".to_string(),
|
||||
metrics: true,
|
||||
noop: false,
|
||||
fresh_capacity: true,
|
||||
};
|
||||
|
||||
assert_eq!(opts.disk_id, "test-disk-id");
|
||||
assert!(opts.metrics);
|
||||
assert!(!opts.noop);
|
||||
assert!(opts.fresh_capacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disk_info_fresh_capacity_ack_is_additive_for_old_peers() {
|
||||
let old_options: DiskInfoOptions =
|
||||
serde_json::from_str(r#"{"disk_id":"disk","metrics":false,"noop":false}"#).expect("old options");
|
||||
assert!(!old_options.fresh_capacity);
|
||||
|
||||
let mut old_response_json = serde_json::to_value(DiskInfo {
|
||||
total: 10,
|
||||
free: 4,
|
||||
used: 6,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("response fixture");
|
||||
old_response_json
|
||||
.as_object_mut()
|
||||
.expect("response object")
|
||||
.remove("fresh_capacity");
|
||||
let old_response: DiskInfo = serde_json::from_value(old_response_json).expect("old response");
|
||||
assert!(!old_response.fresh_capacity);
|
||||
|
||||
let requested = DiskInfoOptions {
|
||||
fresh_capacity: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(serde_json::to_value(requested).expect("new options")["fresh_capacity"], true);
|
||||
}
|
||||
|
||||
/// Test ReadMultipleReq structure
|
||||
|
||||
@@ -5267,6 +5267,71 @@ mod tests {
|
||||
assert_eq!(DATA_SHARDS + 1, bufs.iter().filter(|buf| buf.is_some()).count());
|
||||
}
|
||||
|
||||
/// A peer that sends part of a shard and then stops must not pin the stripe
|
||||
/// for the full read timeout. The other EC 2+2 shards already hold a
|
||||
/// decode-plus-verification quorum, so the lockstep hedge retires the
|
||||
/// stalled reader and the stripe completes.
|
||||
#[tokio::test]
|
||||
async fn test_lockstep_hedges_shard_that_stops_mid_read() {
|
||||
const NUM_SHARDS: usize = 1;
|
||||
const BLOCK_SIZE: usize = 64;
|
||||
const DATA_SHARDS: usize = 2;
|
||||
const PARITY_SHARDS: usize = 2;
|
||||
const SHARD_SIZE: usize = BLOCK_SIZE / DATA_SHARDS;
|
||||
|
||||
let hash_algo = HashAlgorithm::None;
|
||||
let readers = vec![
|
||||
Some(BitrotReader::new(
|
||||
TestShardReader::PartialThenPending {
|
||||
data: vec![0xab],
|
||||
emitted: false,
|
||||
},
|
||||
SHARD_SIZE,
|
||||
hash_algo.clone(),
|
||||
false,
|
||||
)),
|
||||
Some(BitrotReader::new(
|
||||
TestShardReader::Ready(Cursor::new(vec![1_u8; SHARD_SIZE * NUM_SHARDS])),
|
||||
SHARD_SIZE,
|
||||
hash_algo.clone(),
|
||||
false,
|
||||
)),
|
||||
Some(BitrotReader::new(
|
||||
TestShardReader::Ready(Cursor::new(vec![2_u8; SHARD_SIZE * NUM_SHARDS])),
|
||||
SHARD_SIZE,
|
||||
hash_algo.clone(),
|
||||
false,
|
||||
)),
|
||||
Some(BitrotReader::new(
|
||||
TestShardReader::Ready(Cursor::new(vec![3_u8; SHARD_SIZE * NUM_SHARDS])),
|
||||
SHARD_SIZE,
|
||||
hash_algo,
|
||||
false,
|
||||
)),
|
||||
];
|
||||
|
||||
let erasure = Erasure::new(DATA_SHARDS, PARITY_SHARDS, BLOCK_SIZE);
|
||||
let mut parallel_reader = ParallelReader::new_with_metrics_path_read_costs_timeout_and_reconstruction_verification(
|
||||
readers,
|
||||
erasure,
|
||||
0,
|
||||
NUM_SHARDS * BLOCK_SIZE,
|
||||
None,
|
||||
vec![ShardReadCost::Unknown; 4],
|
||||
Duration::from_secs(60),
|
||||
true,
|
||||
);
|
||||
|
||||
let (bufs, errs) = tokio::time::timeout(Duration::from_millis(500), parallel_reader.read())
|
||||
.await
|
||||
.expect("a shard that stops mid-read must be hedged instead of waiting out the 60s read timeout");
|
||||
|
||||
assert!(matches!(&errs[0], Some(DiskError::Io(err)) if err.kind() == ErrorKind::TimedOut));
|
||||
assert!(parallel_reader.readers[0].is_none());
|
||||
assert!(bufs[0].is_none());
|
||||
assert_eq!(DATA_SHARDS + 1, bufs.iter().filter(|buf| buf.is_some()).count());
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn lockstep_reopens_hedged_shard_after_later_peer_loss() {
|
||||
const DATA_SHARDS: usize = 2;
|
||||
|
||||
@@ -25,6 +25,7 @@ use bytes::{Bytes, BytesMut};
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use rustfs_utils::HashAlgorithm;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::vec;
|
||||
@@ -325,6 +326,80 @@ impl Default for WriteProgressPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// After write quorum is already in hand, how long a slower shard may still
|
||||
/// finish before it is dropped. The per-shard stall budget (30s by default)
|
||||
/// is what a black-hole peer is allowed to consume *before* quorum exists.
|
||||
/// Once quorum exists, waiting out that whole budget pins the caller for as
|
||||
/// long as the client request timeout, so the client gives up on a write the
|
||||
/// remaining disks have already accepted.
|
||||
const WRITE_QUORUM_STRAGGLER_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
|
||||
fn post_quorum_straggler_grace(stall_timeout: Option<std::time::Duration>) -> Option<std::time::Duration> {
|
||||
stall_timeout.map(|stall| stall.min(WRITE_QUORUM_STRAGGLER_GRACE))
|
||||
}
|
||||
|
||||
/// Drive shard ops until write quorum has succeeded and stragglers have had
|
||||
/// [`post_quorum_straggler_grace`] to finish. Returns `true` for each slot
|
||||
/// whose future completed; `false` means the caller must fail that slot.
|
||||
async fn await_write_quorum<F>(
|
||||
write_quorum: usize,
|
||||
stall_timeout: Option<std::time::Duration>,
|
||||
slot_count: usize,
|
||||
mut futures: FuturesUnordered<F>,
|
||||
) -> Vec<bool>
|
||||
where
|
||||
F: std::future::Future<Output = (usize, bool)>,
|
||||
{
|
||||
let mut saw = vec![false; slot_count];
|
||||
let mut successes = 0usize;
|
||||
let mut grace: Option<Pin<Box<tokio::time::Sleep>>> = None;
|
||||
loop {
|
||||
if futures.is_empty() {
|
||||
break;
|
||||
}
|
||||
let completed = match grace.as_mut() {
|
||||
Some(sleep) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
item = futures.next() => item,
|
||||
() = sleep.as_mut() => break,
|
||||
}
|
||||
}
|
||||
None => futures.next().await,
|
||||
};
|
||||
let Some((index, succeeded)) = completed else {
|
||||
break;
|
||||
};
|
||||
if let Some(slot) = saw.get_mut(index) {
|
||||
*slot = true;
|
||||
}
|
||||
if succeeded {
|
||||
successes = successes.saturating_add(1);
|
||||
if successes >= write_quorum
|
||||
&& grace.is_none()
|
||||
&& let Some(delay) = post_quorum_straggler_grace(stall_timeout)
|
||||
{
|
||||
grace = Some(Box::pin(tokio::time::sleep(delay)));
|
||||
}
|
||||
}
|
||||
}
|
||||
saw
|
||||
}
|
||||
|
||||
fn abandon_unfinished_writers(writers: &mut [Option<BitrotWriterWrapper>], errs: &mut [Option<Error>], saw: &[bool]) {
|
||||
for (index, seen) in saw.iter().enumerate() {
|
||||
if *seen || errs.get(index).and_then(Option::as_ref).is_some() {
|
||||
continue;
|
||||
}
|
||||
if let Some(err) = errs.get_mut(index) {
|
||||
*err = Some(Error::Timeout);
|
||||
}
|
||||
if let Some(writer) = writers.get_mut(index) {
|
||||
*writer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MultiWriter<'a> {
|
||||
writers: &'a mut [Option<BitrotWriterWrapper>],
|
||||
integrity: Option<&'a mut IntegrityBuilder>,
|
||||
@@ -418,9 +493,12 @@ impl<'a> MultiWriter<'a> {
|
||||
assert_eq!(shards.len(), self.writers.len());
|
||||
|
||||
let budget = self.next_progress_budget();
|
||||
{
|
||||
let mut futures = FuturesUnordered::new();
|
||||
for ((writer_opt, err), shard) in self.writers.iter_mut().zip(self.errs.iter_mut()).zip(shards) {
|
||||
let stall_timeout = self.policy.stall_timeout;
|
||||
let write_quorum = self.write_quorum;
|
||||
let slot_count = self.writers.len();
|
||||
let saw = {
|
||||
let futures = FuturesUnordered::new();
|
||||
for (index, ((writer_opt, err), shard)) in self.writers.iter_mut().zip(self.errs.iter_mut()).zip(shards).enumerate() {
|
||||
if err.is_some() {
|
||||
continue; // Skip if we already have an error for this writer
|
||||
}
|
||||
@@ -428,7 +506,9 @@ impl<'a> MultiWriter<'a> {
|
||||
// failed and its disk dropped, so a stalled peer cannot pin an
|
||||
// otherwise-healthy write quorum (rustfs/backlog#1319). `budget`
|
||||
// is recomputed per block, so it bounds a stall — not the total
|
||||
// transfer time — and a slow-but-honest writer is never killed.
|
||||
// transfer time — and a slow-but-honest writer is never killed
|
||||
// while quorum is still open. Once quorum has succeeded, a
|
||||
// straggler only gets `WRITE_QUORUM_STRAGGLER_GRACE` more time.
|
||||
futures.push(async move {
|
||||
match budget {
|
||||
Some(budget) => match tokio::time::timeout(budget, Self::write_shard(writer_opt, err, shard)).await {
|
||||
@@ -440,10 +520,13 @@ impl<'a> MultiWriter<'a> {
|
||||
},
|
||||
None => Self::write_shard(writer_opt, err, shard).await,
|
||||
}
|
||||
let succeeded = err.is_none() && writer_opt.is_some();
|
||||
(index, succeeded)
|
||||
});
|
||||
}
|
||||
while let Some(()) = futures.next().await {}
|
||||
}
|
||||
await_write_quorum(write_quorum, stall_timeout, slot_count, futures).await
|
||||
};
|
||||
abandon_unfinished_writers(self.writers, &mut self.errs, &saw);
|
||||
|
||||
let nil_count = self.errs.iter().filter(|&e| e.is_none()).count();
|
||||
if nil_count >= self.write_quorum {
|
||||
@@ -490,9 +573,12 @@ impl<'a> MultiWriter<'a> {
|
||||
pub async fn shutdown(&mut self) -> std::io::Result<()> {
|
||||
crate::hp_guard!("MultiWriter::shutdown");
|
||||
let budget = self.next_progress_budget();
|
||||
{
|
||||
let mut futures = FuturesUnordered::new();
|
||||
for (writer_opt, err) in self.writers.iter_mut().zip(self.errs.iter_mut()) {
|
||||
let stall_timeout = self.policy.stall_timeout;
|
||||
let write_quorum = self.write_quorum;
|
||||
let slot_count = self.writers.len();
|
||||
let saw = {
|
||||
let futures = FuturesUnordered::new();
|
||||
for (index, (writer_opt, err)) in self.writers.iter_mut().zip(self.errs.iter_mut()).enumerate() {
|
||||
if err.is_some() {
|
||||
continue;
|
||||
}
|
||||
@@ -501,6 +587,8 @@ impl<'a> MultiWriter<'a> {
|
||||
// here forever for a small object whose bytes were fully buffered
|
||||
// (so `write` never blocked). Bound it with the same progress
|
||||
// budget and drop the stalled writer before the quorum check.
|
||||
// After quorum has shut down, do not keep waiting out the rest of
|
||||
// that budget for a peer that has stopped answering.
|
||||
futures.push(async move {
|
||||
match budget {
|
||||
Some(budget) => match tokio::time::timeout(budget, Self::shutdown_writer(writer_opt, err)).await {
|
||||
@@ -512,10 +600,13 @@ impl<'a> MultiWriter<'a> {
|
||||
},
|
||||
None => Self::shutdown_writer(writer_opt, err).await,
|
||||
}
|
||||
let succeeded = err.is_none() && writer_opt.is_some();
|
||||
(index, succeeded)
|
||||
});
|
||||
}
|
||||
while let Some(()) = futures.next().await {}
|
||||
}
|
||||
await_write_quorum(write_quorum, stall_timeout, slot_count, futures).await
|
||||
};
|
||||
abandon_unfinished_writers(self.writers, &mut self.errs, &saw);
|
||||
|
||||
let nil_count = self.errs.iter().filter(|&e| e.is_none()).count();
|
||||
if nil_count >= self.write_quorum {
|
||||
@@ -2134,6 +2225,68 @@ mod tests {
|
||||
assert!(writers[1].is_some() && writers[2].is_some() && writers[3].is_some());
|
||||
}
|
||||
|
||||
/// Quorum is met by the three healthy writers at t=0. The black-hole peer
|
||||
/// must be abandoned after the post-quorum grace, not after the full 5s
|
||||
/// stall budget. Waiting out the stall makes a 30s default stall consume
|
||||
/// the whole client request budget.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn multi_writer_quorum_abandons_black_hole_after_straggler_grace() {
|
||||
let committed: Vec<Arc<Mutex<Vec<u8>>>> = (0..3).map(|_| Arc::new(Mutex::new(Vec::new()))).collect();
|
||||
let mut writers = vec![
|
||||
Some(bitrot_writer_plain(StallingWriter::stalls_on_write(0), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[0].clone()), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[1].clone()), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[2].clone()), 64)),
|
||||
];
|
||||
|
||||
let started = tokio::time::Instant::now();
|
||||
{
|
||||
let policy = WriteProgressPolicy::new(Duration::from_secs(5), Duration::ZERO);
|
||||
let mut mw = MultiWriter::with_policy(&mut writers, 3, policy);
|
||||
mw.write(four_shards())
|
||||
.await
|
||||
.expect("quorum must succeed without waiting out the black-hole stall");
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
assert!(
|
||||
elapsed < Duration::from_secs(2),
|
||||
"post-quorum grace must cut the black-hole wait short of the 5s stall, elapsed {elapsed:?}"
|
||||
);
|
||||
assert!(
|
||||
elapsed >= WRITE_QUORUM_STRAGGLER_GRACE,
|
||||
"the healthy quorum must still give the straggler its grace window, elapsed {elapsed:?}"
|
||||
);
|
||||
assert!(writers[0].is_none(), "the black-hole writer must be dropped once grace expires");
|
||||
assert!(writers[1].is_some() && writers[2].is_some() && writers[3].is_some());
|
||||
}
|
||||
|
||||
/// A slow-but-honest shard that finishes inside the post-quorum grace keeps
|
||||
/// its writer. The grace exists to drop peers that have stopped, not to
|
||||
/// discard a disk that is merely behind the fastest quorum.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn multi_writer_quorum_keeps_straggler_that_finishes_within_grace() {
|
||||
let committed: Vec<Arc<Mutex<Vec<u8>>>> = (0..4).map(|_| Arc::new(Mutex::new(Vec::new()))).collect();
|
||||
let mut writers = vec![
|
||||
Some(bitrot_writer_plain(SlowWriter::new(Duration::from_millis(200)), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[1].clone()), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[2].clone()), 64)),
|
||||
Some(bitrot_writer_plain(DeferredCommitWriter::new(committed[3].clone()), 64)),
|
||||
];
|
||||
|
||||
{
|
||||
let policy = WriteProgressPolicy::new(Duration::from_secs(5), Duration::ZERO);
|
||||
let mut mw = MultiWriter::with_policy(&mut writers, 3, policy);
|
||||
mw.write(four_shards())
|
||||
.await
|
||||
.expect("a straggler inside the grace window must still satisfy the write");
|
||||
}
|
||||
|
||||
assert!(
|
||||
writers.iter().all(Option::is_some),
|
||||
"a shard that finishes within the grace window must not be dropped"
|
||||
);
|
||||
}
|
||||
|
||||
// Two black-hole writers drop the healthy count to 2/4, below the quorum of
|
||||
// 3, so the write must fail cleanly (not hang).
|
||||
#[tokio::test(start_paused = true)]
|
||||
|
||||
@@ -2430,6 +2430,99 @@ mod tests {
|
||||
restore_set_disks(&store, set, offline).await;
|
||||
}
|
||||
|
||||
/// EC 2+2 with two disks offline still has object read quorum and has lost
|
||||
/// write quorum. A node that has not cached the bucket must still serve
|
||||
/// HEAD bucket, GET, HEAD object, and List; a write must fail.
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn cold_bucket_reads_succeed_below_write_quorum() {
|
||||
let (_temp_dir, store) = setup_bucket_quorum_test_env(&[4], Some(2)).await;
|
||||
metadata_sys::init_bucket_metadata_sys(store.clone(), Vec::new()).await;
|
||||
let bucket = format!("cold-read-quorum-{}", Uuid::new_v4().simple());
|
||||
let object = "seed-object";
|
||||
let body = b"cold bucket reads must survive lost write quorum".to_vec();
|
||||
store
|
||||
.make_bucket(&bucket, &MakeBucketOptions::default())
|
||||
.await
|
||||
.expect("healthy namespace should accept bucket creation");
|
||||
store
|
||||
.put_object(&bucket, object, &mut PutObjReader::from_vec(body.clone()), &ObjectOptions::default())
|
||||
.await
|
||||
.expect("healthy erasure set should accept the seed object");
|
||||
|
||||
metadata_sys::init_bucket_metadata_sys(store.clone(), Vec::new()).await;
|
||||
assert!(
|
||||
metadata_sys::get_in(&store.ctx, &bucket).await.is_err(),
|
||||
"the replacement metadata system must start without this bucket"
|
||||
);
|
||||
let set = &store.pools[0].disk_set[0];
|
||||
let lock = set
|
||||
.new_ns_lock(&bucket, object)
|
||||
.await
|
||||
.expect("seed namespace lock should resolve");
|
||||
drop(
|
||||
lock.get_write_lock(Duration::from_secs(30))
|
||||
.await
|
||||
.expect("seed physical fanout must finish before taking disks offline"),
|
||||
);
|
||||
let offline = take_set_disks_offline(&store, set, &[0, 1]).await;
|
||||
|
||||
let info = store
|
||||
.get_bucket_info(&bucket, &BucketOptions::default())
|
||||
.await
|
||||
.expect("HEAD bucket must succeed at read quorum before the bucket is cached");
|
||||
assert_eq!(info.name, bucket);
|
||||
|
||||
let mut reader = store
|
||||
.get_object_reader(&bucket, object, None, Default::default(), &ObjectOptions::default())
|
||||
.await
|
||||
.expect("GET must succeed for a bucket that was not loaded at startup");
|
||||
let mut restored = Vec::new();
|
||||
reader
|
||||
.stream
|
||||
.read_to_end(&mut restored)
|
||||
.await
|
||||
.expect("quorum read should reconstruct the body");
|
||||
assert_eq!(restored, body);
|
||||
drop(reader);
|
||||
|
||||
let object_info = store
|
||||
.get_object_info(&bucket, object, &ObjectOptions::default())
|
||||
.await
|
||||
.expect("HEAD object must succeed at read quorum");
|
||||
assert_eq!(object_info.name, object);
|
||||
assert_eq!(object_info.size, i64::try_from(body.len()).expect("body length fits i64"));
|
||||
|
||||
let listed = store
|
||||
.clone()
|
||||
.list_objects_v2(&bucket, "", None, None, 100, false, None, false)
|
||||
.await
|
||||
.expect("List must succeed at read quorum");
|
||||
assert!(
|
||||
listed.objects.iter().any(|item| item.name == object),
|
||||
"list should include the seed object, got {:?}",
|
||||
listed.objects.iter().map(|item| item.name.clone()).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(
|
||||
metadata_sys::get_in(&store.ctx, &bucket).await.is_ok(),
|
||||
"the cold load should publish authoritative metadata"
|
||||
);
|
||||
|
||||
let write_error = store
|
||||
.put_object(&bucket, "rejected-object", &mut PutObjReader::from_vec(body), &ObjectOptions::default())
|
||||
.await
|
||||
.expect_err("writes must still fail without write quorum");
|
||||
assert!(
|
||||
matches!(
|
||||
write_error,
|
||||
StorageError::InsufficientWriteQuorum(_, _) | StorageError::ErasureWriteQuorum
|
||||
),
|
||||
"writes must fail with a write-quorum error, got {write_error}"
|
||||
);
|
||||
|
||||
restore_set_disks(&store, set, offline).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn lazy_metadata_load_rejects_below_read_quorum() {
|
||||
|
||||
@@ -33,8 +33,7 @@ use crate::core::sets::Sets;
|
||||
use crate::disk::error::DiskError;
|
||||
use crate::disk::{DiskAPI, DiskInfo, DiskStore, RUSTFS_META_BUCKET, WalkDirOptions};
|
||||
use crate::error::{
|
||||
Error, Result, StorageError, is_all_disk_not_found, is_all_not_found, is_all_volume_not_found, is_err_bucket_not_found,
|
||||
to_object_err,
|
||||
Error, Result, StorageError, is_all_disk_not_found, is_all_not_found, is_all_volume_not_found, to_object_err,
|
||||
};
|
||||
use crate::object_api::{ObjectInfo, ObjectOptions};
|
||||
use crate::set_disk::SetDisks;
|
||||
@@ -4023,32 +4022,6 @@ impl ECStore {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Optimization: use get for single object lookup with exact prefix
|
||||
if !opts.prefix.is_empty() && max_keys == 1 && opts.marker.is_none() && !incl_deleted {
|
||||
match self
|
||||
.get_object_info(
|
||||
&opts.bucket,
|
||||
&opts.prefix,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) if !res.delete_marker && res.version_purge_status.is_empty() => {
|
||||
return Ok(ListObjectsInfo {
|
||||
objects: vec![res],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Err(err) if is_err_bucket_not_found(&err) => {
|
||||
return Err(err);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
};
|
||||
|
||||
let mut list_result = self
|
||||
.clone()
|
||||
.list_path(&opts)
|
||||
@@ -5534,31 +5507,6 @@ impl Sets {
|
||||
// (notably `forward_past`) — see backlog#1047.
|
||||
opts.parse_marker();
|
||||
|
||||
if !opts.prefix.is_empty() && max_keys == 1 && opts.marker.is_none() && !incl_deleted {
|
||||
match self
|
||||
.get_object_info(
|
||||
&opts.bucket,
|
||||
&opts.prefix,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) if !res.delete_marker && res.version_purge_status.is_empty() => {
|
||||
return Ok(ListObjectsInfo {
|
||||
objects: vec![res],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Err(err) if is_err_bucket_not_found(&err) => {
|
||||
return Err(err);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let mut list_result = self
|
||||
.list_path(&opts)
|
||||
.await
|
||||
@@ -6448,33 +6396,6 @@ impl SetDisks {
|
||||
// (notably `forward_past`) — see backlog#1047.
|
||||
opts.parse_marker();
|
||||
|
||||
if !opts.prefix.is_empty() && max_keys == 1 && opts.marker.is_none() {
|
||||
match self
|
||||
.get_object_info(
|
||||
&opts.bucket,
|
||||
&opts.prefix,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) if !res.delete_marker && res.version_purge_status.is_empty() => {
|
||||
return Ok(ListObjectsInfo {
|
||||
objects: vec![res],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
if is_err_bucket_not_found(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut list_result = self
|
||||
.list_path_result(&opts)
|
||||
.await
|
||||
@@ -9637,6 +9558,88 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_objects_exact_prefix_paginates_across_storage_layers() {
|
||||
use crate::bucket::metadata_sys::{init_bucket_metadata_sys, test_support::isolated_store_over_temp_disks};
|
||||
use crate::object_api::{ObjectOptions, PutObjReader};
|
||||
use crate::storage_api_contracts::bucket::{BucketOperations as _, MakeBucketOptions};
|
||||
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations as _};
|
||||
|
||||
let (_dirs, store) = isolated_store_over_temp_disks().await;
|
||||
let bucket = "exact-prefix-pagination-bucket";
|
||||
init_bucket_metadata_sys(store.clone(), Vec::new()).await;
|
||||
store
|
||||
.make_bucket(bucket, &MakeBucketOptions::default())
|
||||
.await
|
||||
.expect("exact-prefix pagination bucket should be created");
|
||||
for name in ["a", "ab"] {
|
||||
store.pools[0]
|
||||
.put_object(
|
||||
bucket,
|
||||
name,
|
||||
&mut PutObjReader::from_vec(b"pagination fixture".to_vec()),
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("pagination fixture object should be written");
|
||||
let info = store
|
||||
.get_object_info(
|
||||
bucket,
|
||||
name,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("exact-prefix fixture must be readable by object lookup");
|
||||
assert!(!info.delete_marker && info.version_purge_status.is_empty());
|
||||
}
|
||||
|
||||
for layer in 0..3 {
|
||||
for (prefix, expected) in [("a", vec!["a", "ab"]), ("ab", vec!["ab"]), ("missing", vec![])] {
|
||||
let mut marker = None;
|
||||
for page in 0..expected.len().max(1) {
|
||||
let result = match layer {
|
||||
0 => {
|
||||
store
|
||||
.clone()
|
||||
.list_objects_generic(bucket, prefix, marker.clone(), None, 1, false)
|
||||
.await
|
||||
}
|
||||
1 => {
|
||||
store.pools[0]
|
||||
.clone()
|
||||
.list_objects_generic(bucket, prefix, marker.clone(), None, 1, false)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
store.pools[0].disk_set[0]
|
||||
.clone()
|
||||
.list_objects_generic(bucket, prefix, marker.clone(), None, 1, false)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.expect("exact-prefix page should list successfully");
|
||||
let names: Vec<_> = result.objects.iter().map(|object| object.name.as_str()).collect();
|
||||
let expected_page: Vec<_> = expected.get(page).copied().into_iter().collect();
|
||||
assert_eq!(names, expected_page, "layer {layer}, prefix {prefix}, page {page}");
|
||||
assert!(result.prefixes.is_empty(), "recursive listing should not return common prefixes");
|
||||
let has_more = page + 1 < expected.len();
|
||||
assert_eq!(result.is_truncated, has_more, "layer {layer}, prefix {prefix}, page {page}");
|
||||
assert_eq!(result.next_marker.is_some(), has_more, "only non-final pages should carry a marker");
|
||||
if has_more {
|
||||
assert_ne!(result.next_marker, marker, "pagination marker must advance");
|
||||
}
|
||||
marker = result.next_marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_objects_hides_pending_version_purge_across_walk_and_exact_prefix() {
|
||||
use crate::bucket::metadata_sys::{init_bucket_metadata_sys, test_support::isolated_store_over_temp_disks};
|
||||
@@ -9685,7 +9688,7 @@ mod test {
|
||||
.expect("exact-prefix listing should succeed");
|
||||
assert!(
|
||||
exact.objects.is_empty(),
|
||||
"exact-prefix max_keys=1 shortcut should hide pending version-purge entries"
|
||||
"exact-prefix max_keys=1 listing should hide pending version-purge entries"
|
||||
);
|
||||
assert!(exact.prefixes.is_empty());
|
||||
}
|
||||
|
||||
@@ -1065,7 +1065,26 @@ impl HttpReader {
|
||||
}
|
||||
|
||||
let request_started = Instant::now();
|
||||
let resp = request.send().await.map_err(|e| {
|
||||
// `send()` resolves at the response headers, before the body stall timer
|
||||
// in `poll_read` can run. A restarted peer (or a pooled connection left
|
||||
// half-open when its pod network namespace disappeared) accepts the TCP
|
||||
// connection and then never sends headers. Without this bound the shard
|
||||
// open waits out kernel retransmits, long after the client has given up
|
||||
// on the GET. The body stall budget is the same deadline: a header
|
||||
// black hole is the same failure as a body that stops mid-shard.
|
||||
let send_result = match stall_timeout {
|
||||
Some(stall_timeout) => match time::timeout(stall_timeout, request.send()).await {
|
||||
Ok(result) => result,
|
||||
Err(_elapsed) => {
|
||||
record_internode_operation_duration(track_internode_metrics, internode_operation, request_started.elapsed());
|
||||
record_internode_stall_timeout(track_internode_metrics, internode_operation);
|
||||
record_internode_error(track_internode_metrics, internode_operation);
|
||||
return Err(body_stalled_error(stall_timeout));
|
||||
}
|
||||
},
|
||||
None => request.send().await,
|
||||
};
|
||||
let resp = send_result.map_err(|e| {
|
||||
record_internode_operation_duration(track_internode_metrics, internode_operation, request_started.elapsed());
|
||||
record_internode_error(track_internode_metrics, internode_operation);
|
||||
record_internode_classified_error(track_internode_metrics, internode_operation, classify_reqwest_error(&e));
|
||||
@@ -2672,6 +2691,52 @@ mod tests {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
/// A peer that accepts the connection and then never sends response headers.
|
||||
/// This is the restarted-pod case: the pooled TCP connection stays open, so
|
||||
/// connect timeout does not fire, and the body stall timer has not started
|
||||
/// because `send()` has not returned. The open itself must fail as
|
||||
/// `BodyStalled` inside the stall budget.
|
||||
#[tokio::test]
|
||||
async fn http_reader_header_stall_fails_open_within_stall_budget() {
|
||||
let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
|
||||
Ok(listener) => listener,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return,
|
||||
Err(err) => panic!("test listener should bind: {err}"),
|
||||
};
|
||||
let addr = listener.local_addr().expect("listener local address should be available");
|
||||
let app = Router::new().route(
|
||||
"/hang-headers",
|
||||
axum::routing::get(|| async {
|
||||
std::future::pending::<()>().await;
|
||||
StatusCode::OK
|
||||
}),
|
||||
);
|
||||
let server_handle = tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
let url = format!("http://{addr}/hang-headers");
|
||||
let stall = Duration::from_millis(50);
|
||||
let opened = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
HttpReader::new_with_stall_timeout(url, Method::GET, HeaderMap::new(), None, Some(stall)),
|
||||
)
|
||||
.await
|
||||
.expect("header stall must fail the open instead of hanging until the test deadline");
|
||||
let err = match opened {
|
||||
Ok(_reader) => panic!("a peer that never sends headers must fail the reader open"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
|
||||
let stalled = err
|
||||
.get_ref()
|
||||
.and_then(|source| source.downcast_ref::<BodyStalled>())
|
||||
.expect("header stall should retain the typed body-stalled source");
|
||||
assert_eq!(stalled.timeout, stall);
|
||||
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_chunk_reader_stall_timeout_retains_typed_source() {
|
||||
let state = TestState::default();
|
||||
|
||||
@@ -16,6 +16,14 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use thiserror::Error;
|
||||
|
||||
mod writer;
|
||||
pub(crate) use writer::RawEnumerationPageWriter;
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
pub(crate) static RAW_PAGE_DIGEST_ENTRIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
const RAW_PAGE_INDEX_VERSION: u16 = 1;
|
||||
const RAW_PAGE_ENTRY_MAX_BYTES: usize = 16 * 1024;
|
||||
|
||||
@@ -145,6 +153,13 @@ impl RawEnumerationPageIndex {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parent(&self) -> Option<&str> {
|
||||
match &self.state {
|
||||
RawEnumerationPageIndexState::Unsupported => None,
|
||||
RawEnumerationPageIndexState::Supported(inner) => Some(&inner.parent),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> RawEnumerationPageOwnerStatus {
|
||||
match &self.state {
|
||||
RawEnumerationPageIndexState::Unsupported => RawEnumerationPageOwnerStatus::Unsupported,
|
||||
@@ -295,25 +310,7 @@ impl RawEnumerationPageIndex {
|
||||
return Err(RawEnumerationPageIndexError::StaleGeneration);
|
||||
}
|
||||
let entries_start = inner.validated_committed_entries()?.len();
|
||||
let Some(building) = inner.building.as_ref() else {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
};
|
||||
if building.entries.is_empty() {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
}
|
||||
building.validate(
|
||||
u64::try_from(inner.pages.len()).unwrap_or(u64::MAX),
|
||||
u64::try_from(entries_start).unwrap_or(u64::MAX),
|
||||
inner.page_entry_limit,
|
||||
)?;
|
||||
let Some(building) = inner.building.take() else {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
};
|
||||
let page = RawEnumerationPage::new(&inner.parent, building);
|
||||
inner.complete = page.terminal;
|
||||
inner.pages.push(page.clone());
|
||||
inner.generation = inner.generation.saturating_add(1);
|
||||
Ok(page)
|
||||
inner.commit_building_page(entries_start)
|
||||
}
|
||||
|
||||
pub fn page(&self, page_index: u64, expected_digest: [u8; 32]) -> Result<&RawEnumerationPage, RawEnumerationPageIndexError> {
|
||||
@@ -349,6 +346,30 @@ impl RawEnumerationPageIndex {
|
||||
}
|
||||
|
||||
impl RawEnumerationPageIndexInner {
|
||||
// Callers validate the committed prefix at the restore/API boundary. The
|
||||
// live writer maintains its entry count without revisiting that prefix.
|
||||
fn commit_building_page(&mut self, entries_start: usize) -> Result<RawEnumerationPage, RawEnumerationPageIndexError> {
|
||||
let Some(building) = self.building.as_ref() else {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
};
|
||||
if building.entries.is_empty() {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
}
|
||||
building.validate(
|
||||
u64::try_from(self.pages.len()).unwrap_or(u64::MAX),
|
||||
u64::try_from(entries_start).unwrap_or(u64::MAX),
|
||||
self.page_entry_limit,
|
||||
)?;
|
||||
let Some(building) = self.building.take() else {
|
||||
return Err(RawEnumerationPageIndexError::EmptyCommit);
|
||||
};
|
||||
let page = RawEnumerationPage::new(&self.parent, building);
|
||||
self.complete = page.terminal;
|
||||
self.pages.push(page.clone());
|
||||
self.generation = self.generation.saturating_add(1);
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
fn status(&self) -> RawEnumerationPageOwnerStatus {
|
||||
let indexed_entries = u64::try_from(self.indexed_entries()).unwrap_or(u64::MAX);
|
||||
if let Some(building) = &self.building {
|
||||
@@ -528,6 +549,8 @@ fn entry_sets_match(left: &[String], right: &[String]) -> bool {
|
||||
}
|
||||
|
||||
fn raw_page_digest(parent: &str, building: &RawEnumerationPageBuilder) -> [u8; 32] {
|
||||
#[cfg(test)]
|
||||
RAW_PAGE_DIGEST_ENTRIES.with(|count| count.set(count.get().saturating_add(building.entries.len())));
|
||||
let mut digest = Sha256::new();
|
||||
update_digest(&mut digest, b"version", &RAW_PAGE_INDEX_VERSION.to_le_bytes());
|
||||
update_digest(&mut digest, b"parent", parent.as_bytes());
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{
|
||||
RawEnumerationPageBuilder, RawEnumerationPageIndex, RawEnumerationPageIndexError, RawEnumerationPageIndexInner,
|
||||
RawEnumerationPageIndexState, owner_entry_is_valid,
|
||||
};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
/// Exclusive, validated owner of an index during one directory enumeration.
|
||||
/// The auxiliary sets are never serialized: restored indexes must cross the
|
||||
/// validation boundary again before any pages can be trusted or extended.
|
||||
pub(crate) struct RawEnumerationPageWriter {
|
||||
inner: RawEnumerationPageIndexInner,
|
||||
indexed: HashSet<String>,
|
||||
pending: BTreeSet<String>,
|
||||
observed_complete: HashSet<String>,
|
||||
complete_source_changed: bool,
|
||||
observations: usize,
|
||||
revalidate_after_entries: usize,
|
||||
committed_entries: usize,
|
||||
}
|
||||
|
||||
impl RawEnumerationPageWriter {
|
||||
pub(crate) fn new(index: RawEnumerationPageIndex) -> Result<Self, RawEnumerationPageIndexError> {
|
||||
let RawEnumerationPageIndexState::Supported(inner) = index.state else {
|
||||
return Err(RawEnumerationPageIndexError::Unsupported);
|
||||
};
|
||||
if inner.parent.is_empty() || inner.page_entry_limit == 0 {
|
||||
return Err(RawEnumerationPageIndexError::CorruptIndex);
|
||||
}
|
||||
let entries = inner.validated_indexed_entries()?;
|
||||
let entry_count = entries.len();
|
||||
let indexed: HashSet<_> = entries.into_iter().collect();
|
||||
if indexed.len() != entry_count
|
||||
|| inner.pages.iter().any(|page| page.entries.len() > inner.page_entry_limit)
|
||||
|| (inner.complete && inner.building.is_some())
|
||||
|| inner.pages.last().is_some_and(|page| page.terminal != inner.complete)
|
||||
{
|
||||
return Err(RawEnumerationPageIndexError::CorruptIndex);
|
||||
}
|
||||
let committed_entries = entry_count.saturating_sub(inner.building.as_ref().map_or(0, |page| page.entries.len()));
|
||||
Ok(Self {
|
||||
inner,
|
||||
indexed,
|
||||
pending: BTreeSet::new(),
|
||||
observed_complete: HashSet::new(),
|
||||
complete_source_changed: false,
|
||||
observations: 0,
|
||||
revalidate_after_entries: entry_count,
|
||||
committed_entries,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn indexed_entry_count(&self) -> usize {
|
||||
self.indexed.len()
|
||||
}
|
||||
|
||||
pub(crate) fn record_entry(&mut self, entry: &str) -> Result<(), RawEnumerationPageIndexError> {
|
||||
if !owner_entry_is_valid(entry) {
|
||||
return Err(RawEnumerationPageIndexError::InvalidEntry);
|
||||
}
|
||||
self.observations = self.observations.saturating_add(1);
|
||||
if self.inner.complete || self.inner.building.as_ref().is_some_and(|page| page.terminal) {
|
||||
if self.indexed.contains(entry) {
|
||||
self.observed_complete.insert(entry.to_owned());
|
||||
} else {
|
||||
self.complete_source_changed = true;
|
||||
}
|
||||
}
|
||||
if self.inner.complete {
|
||||
if self.observations >= self.revalidate_after_entries
|
||||
&& (self.observed_complete.len() != self.indexed.len() || self.complete_source_changed)
|
||||
{
|
||||
return Err(RawEnumerationPageIndexError::IdentityMismatch);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if !self.indexed.contains(entry) {
|
||||
self.pending.insert(entry.to_owned());
|
||||
}
|
||||
// A restored directory can be enumerated in a different order. Keep
|
||||
// the old observation floor and append at most one new entry per call,
|
||||
// in the same order as normalization of the cumulative source.
|
||||
if self.observations < self.revalidate_after_entries {
|
||||
return Ok(());
|
||||
}
|
||||
if self
|
||||
.inner
|
||||
.building
|
||||
.as_ref()
|
||||
.is_some_and(|page| page.entries.len() >= self.inner.page_entry_limit)
|
||||
{
|
||||
return self.commit_page();
|
||||
}
|
||||
if let Some(entry) = self.pending.pop_first() {
|
||||
let building = self.inner.building.get_or_insert_with(|| RawEnumerationPageBuilder {
|
||||
page_index: u64::try_from(self.inner.pages.len()).unwrap_or(u64::MAX),
|
||||
entries_start: u64::try_from(self.committed_entries).unwrap_or(u64::MAX),
|
||||
entries: Vec::new(),
|
||||
terminal: false,
|
||||
});
|
||||
self.indexed.insert(entry.clone());
|
||||
building.entries.push(entry);
|
||||
building.entries.sort();
|
||||
building.terminal = false;
|
||||
self.inner.generation = self.inner.generation.saturating_add(1);
|
||||
}
|
||||
if self
|
||||
.inner
|
||||
.building
|
||||
.as_ref()
|
||||
.is_some_and(|page| page.terminal || page.entries.len() >= self.inner.page_entry_limit)
|
||||
{
|
||||
self.commit_page()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit_page(&mut self) -> Result<(), RawEnumerationPageIndexError> {
|
||||
self.inner.commit_building_page(self.committed_entries)?;
|
||||
self.committed_entries = self.indexed.len();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn checkpoint(&self) -> Result<RawEnumerationPageIndex, RawEnumerationPageIndexError> {
|
||||
let mut inner = self.inner.clone();
|
||||
if inner.building.is_some() {
|
||||
inner.commit_building_page(self.committed_entries)?;
|
||||
}
|
||||
Ok(RawEnumerationPageIndex {
|
||||
state: RawEnumerationPageIndexState::Supported(inner),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::raw_page_index::RAW_PAGE_DIGEST_ENTRIES;
|
||||
|
||||
fn checkpoint(mut index: RawEnumerationPageIndex) -> RawEnumerationPageIndex {
|
||||
if let RawEnumerationPageIndexState::Supported(inner) = &index.state
|
||||
&& inner.building.is_some()
|
||||
{
|
||||
index
|
||||
.commit_building_page(index.generation().expect("supported index generation"))
|
||||
.expect("reference checkpoint must commit its partial page");
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
fn compare_with_cumulative_ingestion(initial: RawEnumerationPageIndex, source: &[&str]) {
|
||||
let mut reference = initial.clone();
|
||||
let floor = reference.indexed_entries().expect("reference entries").len();
|
||||
let mut writer = RawEnumerationPageWriter::new(initial).expect("validated writer");
|
||||
let mut observed = Vec::new();
|
||||
for entry in source {
|
||||
observed.push((*entry).to_owned());
|
||||
let expected = if observed.len() < floor {
|
||||
Ok(())
|
||||
} else {
|
||||
reference
|
||||
.ingest_partial_owner_entries(observed.clone(), 1, reference.generation().expect("reference generation"))
|
||||
.and_then(|outcome| {
|
||||
if outcome.ready_to_commit {
|
||||
reference.commit_building_page(reference.generation().expect("commit generation"))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
let actual = writer.record_entry(entry);
|
||||
assert_eq!(actual, expected, "observation {observed:?}");
|
||||
if actual.is_err() {
|
||||
return;
|
||||
}
|
||||
let expected = checkpoint(reference.clone());
|
||||
let actual = writer.checkpoint().expect("incremental checkpoint");
|
||||
assert_eq!(actual, expected, "page identity and generation at {observed:?}");
|
||||
assert_eq!(
|
||||
rmp_serde::to_vec(&actual).expect("encode incremental index"),
|
||||
rmp_serde::to_vec(&expected).expect("encode reference index"),
|
||||
"persisted format must remain unchanged"
|
||||
);
|
||||
assert_eq!(writer.indexed_entry_count(), actual.indexed_entries().expect("validate output").len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_matches_cumulative_pages_with_duplicates_and_restarts() {
|
||||
for limit in [1, 2, 3, 128] {
|
||||
let source = ["c", "a", "b", "b", "f", "d", "e", "g"];
|
||||
let empty = RawEnumerationPageIndex::new("bucket/metadata", limit).expect("empty index");
|
||||
compare_with_cumulative_ingestion(empty.clone(), &source);
|
||||
let mut writer = RawEnumerationPageWriter::new(empty).expect("writer");
|
||||
for entry in source {
|
||||
writer.record_entry(entry).expect("entry before interruption");
|
||||
let saved = writer.checkpoint().expect("save interrupted enumeration");
|
||||
let encoded = rmp_serde::to_vec(&saved).expect("encode checkpoint");
|
||||
let restored = rmp_serde::from_slice(&encoded).expect("restore checkpoint");
|
||||
compare_with_cumulative_ingestion(restored, &["h", "g", "f", "e", "d", "c", "b", "a", "i", "j"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_preserves_restored_building_and_terminal_pages() {
|
||||
for complete in [false, true] {
|
||||
for budget in [1, 2] {
|
||||
let mut index = RawEnumerationPageIndex::new("bucket", 2).expect("index");
|
||||
let source = ["a".to_owned(), "b".to_owned()];
|
||||
if complete {
|
||||
index.ingest_owner_entries(source, budget, 0).expect("complete source");
|
||||
} else {
|
||||
index.ingest_partial_owner_entries(source, budget, 0).expect("partial source");
|
||||
}
|
||||
compare_with_cumulative_ingestion(index.clone(), &["b", "a", "a", "c", "d"]);
|
||||
compare_with_cumulative_ingestion(checkpoint(index), &["b", "a", "a", "c", "d"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_keeps_identity_check_when_terminal_builder_commits() {
|
||||
for limit in [2, 3] {
|
||||
let mut index = RawEnumerationPageIndex::new("bucket", limit).expect("index");
|
||||
index
|
||||
.ingest_owner_entries(["a".to_owned(), "b".to_owned()], 2, 0)
|
||||
.expect("terminal building page");
|
||||
compare_with_cumulative_ingestion(index.clone(), &["b", "a", "b", "c"]);
|
||||
compare_with_cumulative_ingestion(index, &["foreign", "a", "b", "c"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_rejects_complete_source_drift_after_observation_floor() {
|
||||
let mut index = RawEnumerationPageIndex::new("bucket", 2).expect("index");
|
||||
index
|
||||
.ingest_owner_entries(["a".to_owned(), "b".to_owned()], 2, 0)
|
||||
.expect("complete source");
|
||||
let index = checkpoint(index);
|
||||
for source in [["foreign", "a"], ["a", "foreign"], ["a", "a"], ["b", "a"]] {
|
||||
compare_with_cumulative_ingestion(index.clone(), &source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_rejects_corruption_at_restore() {
|
||||
let mut writer = RawEnumerationPageWriter::new(RawEnumerationPageIndex::new("bucket", 2).expect("index")).expect("writer");
|
||||
for entry in ["a", "b", "c"] {
|
||||
writer.record_entry(entry).expect("valid entry");
|
||||
}
|
||||
let valid = writer.checkpoint().expect("checkpoint");
|
||||
for case in 0..7 {
|
||||
let mut corrupt = valid.clone();
|
||||
let RawEnumerationPageIndexState::Supported(inner) = &mut corrupt.state else {
|
||||
panic!("supported checkpoint");
|
||||
};
|
||||
match case {
|
||||
0 => inner.pages[0].digest[0] ^= 1,
|
||||
1 => inner.pages[1].entries_start = 0,
|
||||
2 => inner.pages[1] = inner.pages[0].clone(),
|
||||
3 => inner.page_entry_limit = 0,
|
||||
4 => inner.complete = true,
|
||||
5 => inner.parent.clear(),
|
||||
6 => {
|
||||
inner.building = Some(RawEnumerationPageBuilder {
|
||||
page_index: 2,
|
||||
entries_start: 3,
|
||||
entries: vec!["a".to_owned()],
|
||||
terminal: false,
|
||||
});
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let bytes = rmp_serde::to_vec(&corrupt).expect("encode damaged checkpoint");
|
||||
let restored = rmp_serde::from_slice(&bytes).expect("decode untrusted checkpoint");
|
||||
assert!(
|
||||
matches!(RawEnumerationPageWriter::new(restored), Err(RawEnumerationPageIndexError::CorruptIndex)),
|
||||
"corruption case {case} must not become trusted runtime state"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_rejects_invalid_names_without_publishing_them() {
|
||||
for entry in ["", ".", "..", "nested/name", &"x".repeat(16 * 1024 + 1)] {
|
||||
let mut writer =
|
||||
RawEnumerationPageWriter::new(RawEnumerationPageIndex::new("bucket", 128).expect("index")).expect("writer");
|
||||
let before = writer.checkpoint().expect("empty checkpoint");
|
||||
assert_eq!(writer.record_entry(entry), Err(RawEnumerationPageIndexError::InvalidEntry));
|
||||
assert_eq!(writer.checkpoint().expect("checkpoint after rejection"), before);
|
||||
assert_eq!(writer.indexed_entry_count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_hashes_only_new_pages_after_restore() {
|
||||
let count = 4096;
|
||||
let mut writer = RawEnumerationPageWriter::new(RawEnumerationPageIndex::new("bucket", 128).expect("index")).expect("writer");
|
||||
RAW_PAGE_DIGEST_ENTRIES.set(0);
|
||||
for i in (0..count).rev() {
|
||||
writer.record_entry(&format!("entry-{i:06}")).expect("append entry");
|
||||
}
|
||||
assert_eq!(
|
||||
RAW_PAGE_DIGEST_ENTRIES.get(),
|
||||
count,
|
||||
"each committed entry is hashed once during enumeration"
|
||||
);
|
||||
let saved = writer.checkpoint().expect("checkpoint");
|
||||
assert_eq!(saved.indexed_entries().expect("validated checkpoint").len(), count);
|
||||
let mut resumed = RawEnumerationPageWriter::new(saved).expect("restore large checkpoint");
|
||||
RAW_PAGE_DIGEST_ENTRIES.set(0);
|
||||
for i in 0..count * 2 {
|
||||
resumed.record_entry(&format!("entry-{i:06}")).expect("resume and append");
|
||||
}
|
||||
assert_eq!(resumed.indexed_entry_count(), count * 2);
|
||||
assert_eq!(
|
||||
RAW_PAGE_DIGEST_ENTRIES.get(),
|
||||
count,
|
||||
"restored pages must not be rehashed for each entry or commit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_writer_keeps_snapshot_independent_of_later_appends() {
|
||||
let mut writer = RawEnumerationPageWriter::new(RawEnumerationPageIndex::new("bucket", 128).expect("index")).expect("writer");
|
||||
for i in 0..127 {
|
||||
writer.record_entry(&format!("entry-{i:03}")).expect("append before boundary");
|
||||
}
|
||||
let partial = writer.checkpoint().expect("partial page checkpoint");
|
||||
writer.record_entry("entry-127").expect("full page");
|
||||
let full = writer.checkpoint().expect("full page checkpoint");
|
||||
writer.record_entry("entry-128").expect("next page");
|
||||
assert_eq!(partial.indexed_entries().expect("old partial snapshot").len(), 127);
|
||||
assert_eq!(full.indexed_entries().expect("old full snapshot").len(), 128);
|
||||
assert_eq!(
|
||||
writer
|
||||
.checkpoint()
|
||||
.expect("new snapshot")
|
||||
.indexed_entries()
|
||||
.expect("new entries")
|
||||
.len(),
|
||||
129
|
||||
);
|
||||
assert_ne!(partial, full);
|
||||
}
|
||||
@@ -2257,7 +2257,7 @@ where
|
||||
emit_scan_cycle_deferred(cycle_start.elapsed());
|
||||
ScannerCycleOutcome::Deferred(ScannerCycleDeferReason::DataMovement)
|
||||
} else {
|
||||
ScannerCycleOutcome::Failed
|
||||
ScannerCycleOutcome::StatePersistenceFailed
|
||||
};
|
||||
}
|
||||
Some(ScannerCyclePreCommitOutcome::Deferred(reason)) => {
|
||||
@@ -2336,7 +2336,7 @@ where
|
||||
emit_scan_cycle_deferred(cycle_start.elapsed());
|
||||
ScannerCycleOutcome::Deferred(ScannerCycleDeferReason::DataMovement)
|
||||
} else {
|
||||
ScannerCycleOutcome::Failed
|
||||
ScannerCycleOutcome::StatePersistenceFailed
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2371,7 +2371,7 @@ where
|
||||
};
|
||||
let pending_maintenance_work = scanner_pending_maintenance_work || unresolved_heal_work || remote_dirty_usage_pending;
|
||||
match completion_outcome {
|
||||
ScannerCycleOutcome::Failed => {
|
||||
ScannerCycleOutcome::Failed | ScannerCycleOutcome::StatePersistenceFailed => {
|
||||
error!(
|
||||
target: "rustfs::scanner",
|
||||
event = EVENT_SCANNER_PERSIST_STATE,
|
||||
@@ -2430,7 +2430,7 @@ where
|
||||
emit_scan_cycle_deferred(cycle_start.elapsed());
|
||||
ScannerCycleOutcome::Deferred(ScannerCycleDeferReason::DataMovement)
|
||||
} else {
|
||||
ScannerCycleOutcome::Failed
|
||||
ScannerCycleOutcome::StatePersistenceFailed
|
||||
};
|
||||
}
|
||||
ScannerCycleOutcome::Deferred(reason) => {
|
||||
@@ -2483,7 +2483,7 @@ where
|
||||
return ScannerCycleOutcome::Deferred(ScannerCycleDeferReason::DataMovement);
|
||||
}
|
||||
emit_scan_cycle_complete(false, cycle_start.elapsed());
|
||||
return ScannerCycleOutcome::Failed;
|
||||
return ScannerCycleOutcome::StatePersistenceFailed;
|
||||
}
|
||||
ScannerCycleOutcome::Completed | ScannerCycleOutcome::CompletedWithPendingMaintenance => {}
|
||||
}
|
||||
@@ -2531,7 +2531,7 @@ where
|
||||
}
|
||||
cycle_metrics_guard.finish(cycle_info.clone()).await;
|
||||
emit_scan_cycle_complete(false, cycle_start.elapsed());
|
||||
return ScannerCycleOutcome::Failed;
|
||||
return ScannerCycleOutcome::StatePersistenceFailed;
|
||||
}
|
||||
cycle_budget.mark_cycle_state_persisted();
|
||||
|
||||
@@ -3045,6 +3045,12 @@ where
|
||||
}
|
||||
};
|
||||
finish_scanner_pause_backlog_cycle(&mut pause_backlog, &storeapi, initial_pause_backlog_attempt, initial_outcome).await;
|
||||
if initial_outcome == ScannerCycleOutcome::StatePersistenceFailed {
|
||||
global_metrics().set_cycle(None).await;
|
||||
let error = "scanner cycle state persistence failed; retrying from durable state".to_string();
|
||||
finish_scanner_leader_iteration(false, "state_persist_failed", error.clone()).await;
|
||||
return Err(ScannerError::Other(error));
|
||||
}
|
||||
if usage_bootstrap_rebuild.record_cycle(initial_outcome) {
|
||||
clean_idle_backoff.reset();
|
||||
}
|
||||
@@ -3362,6 +3368,12 @@ where
|
||||
}
|
||||
};
|
||||
finish_scanner_pause_backlog_cycle(&mut pause_backlog, &storeapi, pause_backlog_attempt, outcome).await;
|
||||
if outcome == ScannerCycleOutcome::StatePersistenceFailed {
|
||||
global_metrics().set_cycle(None).await;
|
||||
let error = "scanner cycle state persistence failed; retrying from durable state".to_string();
|
||||
finish_scanner_leader_iteration(false, "state_persist_failed", error.clone()).await;
|
||||
return Err(ScannerError::Other(error));
|
||||
}
|
||||
if usage_bootstrap_rebuild.record_cycle(outcome) {
|
||||
clean_idle_backoff.reset();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub(crate) enum ScannerCycleOutcome {
|
||||
Superseded,
|
||||
Deferred(ScannerCycleDeferReason),
|
||||
Failed,
|
||||
// The write may have committed; reload and reclaim before dispatching again.
|
||||
StatePersistenceFailed,
|
||||
}
|
||||
|
||||
pub(crate) fn scanner_cycle_outcome_with_pending_maintenance(
|
||||
|
||||
@@ -639,9 +639,10 @@ impl From<ScannerCycleOutcome> for ScannerPauseBacklogCycleOutcome {
|
||||
ScannerCycleOutcome::CompletedWithPendingMaintenance => Self::PendingMaintenance,
|
||||
ScannerCycleOutcome::Partial => Self::Progressed,
|
||||
ScannerCycleOutcome::Deferred(super::ScannerCycleDeferReason::DataMovement) => Self::DataMovementDeferred,
|
||||
ScannerCycleOutcome::Superseded | ScannerCycleOutcome::Deferred(_) | ScannerCycleOutcome::Failed => {
|
||||
Self::RetryableFailure
|
||||
}
|
||||
ScannerCycleOutcome::Superseded
|
||||
| ScannerCycleOutcome::Deferred(_)
|
||||
| ScannerCycleOutcome::Failed
|
||||
| ScannerCycleOutcome::StatePersistenceFailed => Self::RetryableFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3835,8 +3835,29 @@ pub(super) async fn persist_scanner_cycle_state_for_epoch(
|
||||
);
|
||||
return false;
|
||||
};
|
||||
save_config_with_preconditions(storeapi.clone(), &DATA_USAGE_BLOOM_NAME_PATH, buf.clone(), revision.preconditions())
|
||||
.await
|
||||
let save = save_config_with_preconditions(
|
||||
storeapi.clone(),
|
||||
&DATA_USAGE_BLOOM_NAME_PATH,
|
||||
buf.clone(),
|
||||
revision.preconditions(),
|
||||
);
|
||||
#[cfg(test)]
|
||||
let injected_failure = tests::cycle_persist_failure::take_failure(leader_epoch);
|
||||
#[cfg(test)]
|
||||
let save = async {
|
||||
if injected_failure == Some(false) {
|
||||
return Err(EcstoreError::other("injected scanner state write failure"));
|
||||
}
|
||||
save.await
|
||||
};
|
||||
let result = save.await;
|
||||
#[cfg(test)]
|
||||
let result = if injected_failure == Some(true) && result.is_ok() {
|
||||
Err(EcstoreError::other("injected scanner state post-commit failure"))
|
||||
} else {
|
||||
result
|
||||
};
|
||||
result
|
||||
};
|
||||
match save_result {
|
||||
Ok(object_info) => {
|
||||
|
||||
@@ -37,6 +37,7 @@ use tokio::time::{Duration, advance};
|
||||
|
||||
const TEST_DEFAULT_SCANNER_CYCLE_SECS: u64 = 24 * 60 * 60;
|
||||
|
||||
pub(super) mod cycle_persist_failure;
|
||||
mod quota_reset_preservation;
|
||||
|
||||
mod recovery_control;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Failure {
|
||||
epoch: u64,
|
||||
successful_saves: usize,
|
||||
after_commit: bool,
|
||||
}
|
||||
|
||||
static FAILURE: StdMutex<Option<Failure>> = StdMutex::new(None);
|
||||
|
||||
struct FailureGuard;
|
||||
|
||||
impl Drop for FailureGuard {
|
||||
fn drop(&mut self) {
|
||||
*FAILURE.lock().expect("failure injection lock") = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::scanner) fn take_failure(epoch: u64) -> Option<bool> {
|
||||
let mut slot = FAILURE.lock().expect("failure injection lock");
|
||||
let failure = slot.as_mut().filter(|failure| failure.epoch == epoch)?;
|
||||
if failure.successful_saves > 0 {
|
||||
failure.successful_saves -= 1;
|
||||
return None;
|
||||
}
|
||||
slot.take().map(|failure| failure.after_commit)
|
||||
}
|
||||
|
||||
async fn assert_leader_reloads_after_failed_save(successful_saves: usize, after_commit: bool) {
|
||||
temp_env::async_with_vars([(ENV_SCANNER_CYCLE, Some("1")), (ENV_SCANNER_START_DELAY_SECS, Some("0"))], async {
|
||||
crate::runtime_config::refresh_scanner_runtime_config_for_tests();
|
||||
crate::scanner_io::clear_dirty_usage_buckets_for_tests();
|
||||
let (_temp_dir, store) = setup_scanner_cycle_store().await;
|
||||
let ctx = CancellationToken::new();
|
||||
let mut cycle = CurrentCycle {
|
||||
next: 12,
|
||||
..Default::default()
|
||||
};
|
||||
let mut revision = DataUsageCacheRevision::Missing;
|
||||
assert!(persist_scanner_cycle_state(&ctx, store.clone(), &mut cycle, &mut revision, 42).await);
|
||||
*FAILURE.lock().expect("failure injection lock") = Some(Failure {
|
||||
epoch: 43,
|
||||
successful_saves,
|
||||
after_commit,
|
||||
});
|
||||
let failure_guard = FailureGuard;
|
||||
|
||||
let result = tokio::time::timeout(Duration::from_secs(20), run_data_scanner(ctx.clone(), store.clone()))
|
||||
.await
|
||||
.expect("a failed cycle-state save must stop this leader iteration");
|
||||
assert!(
|
||||
result
|
||||
.expect_err("state persistence failure must be retryable")
|
||||
.to_string()
|
||||
.contains("retrying from durable state")
|
||||
);
|
||||
assert!(
|
||||
FAILURE.lock().expect("failure injection lock").is_none(),
|
||||
"the injected failure must fire"
|
||||
);
|
||||
drop(failure_guard);
|
||||
assert!(!ctx.is_cancelled(), "recovery must not stop the scanner supervisor");
|
||||
assert!(!global_metrics().report().await.current_cycle_active);
|
||||
|
||||
let (durable, epoch) = decode_scanner_cycle_state(
|
||||
&read_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH)
|
||||
.await
|
||||
.expect("durable cycle state"),
|
||||
)
|
||||
.expect("valid durable cycle state");
|
||||
assert_eq!(epoch, 43);
|
||||
let expected_next = 12 + u64::try_from(successful_saves).expect("test count fits") + u64::from(after_commit);
|
||||
assert_eq!(durable.next, expected_next);
|
||||
|
||||
let resumed_ctx = ctx.child_token();
|
||||
let _cancel_on_drop = resumed_ctx.clone().drop_guard();
|
||||
let mut resumed = tokio::spawn({
|
||||
let ctx = resumed_ctx.clone();
|
||||
let store = store.clone();
|
||||
async move { run_data_scanner(ctx, store).await }
|
||||
});
|
||||
tokio::time::timeout(Duration::from_secs(20), async {
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = &mut resumed => panic!("resumed scanner stopped before making progress: {result:?}"),
|
||||
_ = tokio::time::sleep(Duration::from_millis(10)) => {}
|
||||
}
|
||||
let state = read_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH)
|
||||
.await
|
||||
.expect("resumed state");
|
||||
let (cycle, epoch) = decode_scanner_cycle_state(&state).expect("resumed cycle state");
|
||||
if epoch == 44 && cycle.next > expected_next && !global_metrics().report().await.current_cycle_active {
|
||||
crate::remote_scanner::validate_remote_scanner_request_fence_with_store(cycle.next, epoch, store.clone())
|
||||
.await
|
||||
.expect("the recovered cycle must pass the peer fence");
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("the same process must resume durable scanner progress");
|
||||
resumed_ctx.cancel();
|
||||
resumed
|
||||
.await
|
||||
.expect("resumed scanner task should not panic")
|
||||
.expect("resumed scanner should stop cleanly");
|
||||
global_metrics().set_cycle(None).await;
|
||||
crate::scanner_io::clear_dirty_usage_buckets_for_tests();
|
||||
})
|
||||
.await;
|
||||
crate::runtime_config::refresh_scanner_runtime_config_for_tests();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn initial_cycle_save_failure_reloads_durable_state() {
|
||||
assert_leader_reloads_after_failed_save(0, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn later_cycle_save_failure_reloads_durable_state() {
|
||||
assert_leader_reloads_after_failed_save(1, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn initial_cycle_post_commit_failure_reloads_durable_state() {
|
||||
assert_leader_reloads_after_failed_save(0, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn later_cycle_post_commit_failure_reloads_durable_state() {
|
||||
assert_leader_reloads_after_failed_save(1, true).await;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ use crate::data_usage_define::{
|
||||
PendingScannerHealKind, ScannerSizeSummaryExt, SizeReconciliationEntry, SizeSummary, hash_path,
|
||||
};
|
||||
use crate::error::ScannerError;
|
||||
use crate::raw_page_index::{RawEnumerationPageIndex, RawEnumerationPageIndexError};
|
||||
use crate::raw_page_index::{RawEnumerationPageIndex, RawEnumerationPageWriter};
|
||||
use crate::runtime_config::{
|
||||
scanner_alert_excess_folders, scanner_alert_excess_version_size, scanner_alert_excess_versions, scanner_yield_every_n_objects,
|
||||
};
|
||||
@@ -95,7 +95,6 @@ const SCANNER_CHECKPOINT_OBJECT_INTERVAL: u64 = 1024;
|
||||
const SCANNER_CHECKPOINT_MIN_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const SCANNER_CHECKPOINT_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const SCANNER_RAW_ENUMERATION_PAGE_ENTRY_LIMIT: usize = 128;
|
||||
const SCANNER_RAW_ENUMERATION_PAGE_BUILD_BUDGET: usize = 1;
|
||||
// Erasure data directories contain direct part.N files; keep namespace probes bounded.
|
||||
const ERASURE_DATA_DIR_PROBE_ENTRY_LIMIT: usize = 64;
|
||||
const DEFAULT_HEAL_OBJECT_SELECT_PROB: u32 = 1024;
|
||||
@@ -761,33 +760,21 @@ struct RawEnumerationProgress {
|
||||
last_entry: Option<String>,
|
||||
entries_seen: u64,
|
||||
digest: Sha256,
|
||||
observed_entries: Vec<String>,
|
||||
revalidate_after_entries: usize,
|
||||
page_index: Option<RawEnumerationPageIndex>,
|
||||
page_index: Option<RawEnumerationPageWriter>,
|
||||
}
|
||||
|
||||
impl RawEnumerationProgress {
|
||||
fn new(parent: &str, page_index: Option<RawEnumerationPageIndex>) -> Self {
|
||||
let mut digest = Sha256::new();
|
||||
update_raw_enumeration_digest(&mut digest, b"parent", parent.as_bytes());
|
||||
let mut revalidate_after_entries = 0;
|
||||
let page_index = match page_index {
|
||||
Some(index) => match index.indexed_entries() {
|
||||
Ok(entries) => {
|
||||
revalidate_after_entries = entries.len();
|
||||
Some(index)
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
None => RawEnumerationPageIndex::new(parent, SCANNER_RAW_ENUMERATION_PAGE_ENTRY_LIMIT).ok(),
|
||||
};
|
||||
let page_index = page_index
|
||||
.or_else(|| RawEnumerationPageIndex::new(parent, SCANNER_RAW_ENUMERATION_PAGE_ENTRY_LIMIT).ok())
|
||||
.and_then(|index| RawEnumerationPageWriter::new(index).ok());
|
||||
Self {
|
||||
parent: parent.to_string(),
|
||||
last_entry: None,
|
||||
entries_seen: 0,
|
||||
digest,
|
||||
observed_entries: Vec::new(),
|
||||
revalidate_after_entries,
|
||||
page_index,
|
||||
}
|
||||
}
|
||||
@@ -796,34 +783,10 @@ impl RawEnumerationProgress {
|
||||
update_raw_enumeration_digest(&mut self.digest, b"entry", entry.as_bytes());
|
||||
self.last_entry = Some(entry.to_string());
|
||||
self.entries_seen = self.entries_seen.saturating_add(1);
|
||||
self.observed_entries.push(entry.to_string());
|
||||
if let Some(index) = &mut self.page_index {
|
||||
if self.observed_entries.len() < self.revalidate_after_entries {
|
||||
return;
|
||||
}
|
||||
let result = index
|
||||
.generation()
|
||||
.ok_or(RawEnumerationPageIndexError::Unsupported)
|
||||
.and_then(|generation| {
|
||||
index.ingest_partial_owner_entries(
|
||||
self.observed_entries.clone(),
|
||||
SCANNER_RAW_ENUMERATION_PAGE_BUILD_BUDGET,
|
||||
generation,
|
||||
)
|
||||
});
|
||||
match result {
|
||||
Ok(outcome) if outcome.ready_to_commit => {
|
||||
if let Some(generation) = index.generation()
|
||||
&& index.commit_building_page(generation).is_err()
|
||||
{
|
||||
self.page_index = None;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
self.page_index = None;
|
||||
}
|
||||
}
|
||||
if let Some(index) = &mut self.page_index
|
||||
&& index.record_entry(entry).is_err()
|
||||
{
|
||||
self.page_index = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,28 +803,20 @@ impl RawEnumerationProgress {
|
||||
}
|
||||
|
||||
fn page_index(&self) -> Option<RawEnumerationPageIndex> {
|
||||
self.page_index.clone().and_then(|mut index| {
|
||||
if let Some(generation) = index.generation()
|
||||
&& matches!(index.status(), crate::raw_page_index::RawEnumerationPageOwnerStatus::Building { .. })
|
||||
&& index.commit_building_page(generation).is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
match index.indexed_entries() {
|
||||
Ok(entries) if !entries.is_empty() => Some(index),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
self.page_index
|
||||
.as_ref()
|
||||
.filter(|index| index.indexed_entry_count() > 0)
|
||||
.and_then(|index| index.checkpoint().ok())
|
||||
}
|
||||
|
||||
fn has_checkpointable_page_index(&self) -> bool {
|
||||
self.page_index().is_some()
|
||||
self.checkpointable_entry_count() > 0
|
||||
}
|
||||
|
||||
fn checkpointable_entry_count(&self) -> usize {
|
||||
self.page_index()
|
||||
.and_then(|index| index.indexed_entries().ok())
|
||||
.map_or(0, |entries| entries.len())
|
||||
self.page_index
|
||||
.as_ref()
|
||||
.map_or(0, RawEnumerationPageWriter::indexed_entry_count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1130,19 +1085,6 @@ impl FolderScanner {
|
||||
if self.old_cache.info.scan_progress.is_none() {
|
||||
return;
|
||||
}
|
||||
let page_index = self
|
||||
.old_cache
|
||||
.validated_raw_enumeration_page_index()
|
||||
.filter(|index| match index.status() {
|
||||
crate::raw_page_index::RawEnumerationPageOwnerStatus::Building {
|
||||
parent: index_parent, ..
|
||||
}
|
||||
| crate::raw_page_index::RawEnumerationPageOwnerStatus::Ready {
|
||||
parent: index_parent, ..
|
||||
} => index_parent == parent,
|
||||
crate::raw_page_index::RawEnumerationPageOwnerStatus::Unsupported => false,
|
||||
})
|
||||
.cloned();
|
||||
if let Some(position) = self
|
||||
.raw_enumeration_progress
|
||||
.iter()
|
||||
@@ -1150,6 +1092,7 @@ impl FolderScanner {
|
||||
{
|
||||
self.raw_enumeration_progress.truncate(position + 1);
|
||||
} else {
|
||||
let page_index = self.raw_enumeration_page_index_for_parent(parent).cloned();
|
||||
self.raw_enumeration_progress
|
||||
.push(RawEnumerationProgress::new(parent, page_index));
|
||||
}
|
||||
@@ -1158,8 +1101,17 @@ impl FolderScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_enumeration_page_index_for_parent(&self, parent: &str) -> Option<&RawEnumerationPageIndex> {
|
||||
self.old_cache
|
||||
.info
|
||||
.scan_raw_enumeration_page_index
|
||||
.as_ref()
|
||||
.filter(|index| index.parent() == Some(parent))?;
|
||||
self.old_cache.validated_raw_enumeration_page_index()
|
||||
}
|
||||
|
||||
fn raw_enumeration_committed_entry_oracle(&self, parent: &str) -> HashSet<String> {
|
||||
let Some(index) = self.old_cache.validated_raw_enumeration_page_index() else {
|
||||
let Some(index) = self.raw_enumeration_page_index_for_parent(parent) else {
|
||||
return HashSet::new();
|
||||
};
|
||||
let generation_matches_parent = match index.status() {
|
||||
|
||||
@@ -28,6 +28,7 @@ use std::sync::Mutex;
|
||||
|
||||
mod checkpoint_fixture;
|
||||
pub(super) mod enumeration_restart;
|
||||
mod incremental_enumeration;
|
||||
|
||||
/// Reset the process-global alert cooldown map; test-only.
|
||||
fn reset_alert_cooldowns() {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::raw_page_index::RAW_PAGE_DIGEST_ENTRIES;
|
||||
|
||||
#[tokio::test]
|
||||
async fn raw_enumeration_reuses_validated_parent_and_skips_unrelated_index() {
|
||||
let (mut scanner, temp_dir) = build_test_scanner().await;
|
||||
let _guard = TestGuard {
|
||||
temp_dir: Some(temp_dir),
|
||||
};
|
||||
scanner.old_cache.info.name = "bucket".to_owned();
|
||||
scanner.old_cache.info.scan_progress = Some(crate::DataUsageScanProgress {
|
||||
started_plan: crate::DataUsageScanPlanDigest([1; 32]),
|
||||
requested_plan: crate::DataUsageScanPlanDigest([1; 32]),
|
||||
});
|
||||
scanner.old_cache.info.source = Some(crate::DataUsageCacheSource::new(0, 0));
|
||||
scanner.old_cache.info.scan_identity = Some(crate::DataUsageScanIdentity {
|
||||
version: 1,
|
||||
bucket_incarnation: Uuid::from_u128(1),
|
||||
set_layout: crate::DataUsageScanPlanDigest([2; 32]),
|
||||
publication_epoch: 1,
|
||||
tier_registry_generation: 0,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
});
|
||||
let mut saved =
|
||||
RawEnumerationPageWriter::new(RawEnumerationPageIndex::new("bucket/metadata", 128).expect("index")).expect("writer");
|
||||
for i in 0..1024 {
|
||||
saved.record_entry(&format!("entry-{i:04}")).expect("initial entry");
|
||||
}
|
||||
scanner.old_cache.info.scan_raw_enumeration_page_index = Some(saved.checkpoint().expect("saved index"));
|
||||
scanner.record_raw_enumeration_entry("bucket/metadata", "entry-0000");
|
||||
RAW_PAGE_DIGEST_ENTRIES.set(0);
|
||||
for i in 1..1024 {
|
||||
scanner.record_raw_enumeration_entry("bucket/metadata", &format!("entry-{i:04}"));
|
||||
}
|
||||
assert_eq!(
|
||||
RAW_PAGE_DIGEST_ENTRIES.get(),
|
||||
0,
|
||||
"the same parent must not revalidate its restored index per entry"
|
||||
);
|
||||
let progress = scanner.raw_enumeration_progress.last().expect("active progress");
|
||||
assert_eq!(progress.checkpointable_entry_count(), 1024);
|
||||
assert!(progress.has_checkpointable_page_index());
|
||||
assert_eq!(
|
||||
RAW_PAGE_DIGEST_ENTRIES.get(),
|
||||
0,
|
||||
"progress selection must use cached counts, not snapshot/validation"
|
||||
);
|
||||
for i in 0..32 {
|
||||
let parent = format!("bucket/other-{i}");
|
||||
assert!(scanner.raw_enumeration_committed_entry_oracle(&parent).is_empty());
|
||||
scanner.record_raw_enumeration_entry(&parent, "object");
|
||||
scanner.finish_raw_enumeration_parent(&parent);
|
||||
}
|
||||
assert_eq!(
|
||||
RAW_PAGE_DIGEST_ENTRIES.get(),
|
||||
0,
|
||||
"unrelated directories must not validate the saved metadata index"
|
||||
);
|
||||
let (_, checkpoint) = scanner.take_raw_enumeration_resume_state();
|
||||
assert_eq!(
|
||||
checkpoint
|
||||
.expect("metadata checkpoint")
|
||||
.indexed_entries()
|
||||
.expect("valid restored output")
|
||||
.len(),
|
||||
1024
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_enumeration_progress_invalid_entry_discards_index_but_retains_diagnostic_cursor() {
|
||||
let mut progress = RawEnumerationProgress::new("bucket", None);
|
||||
progress.record_entry("valid");
|
||||
progress.record_entry("invalid/name");
|
||||
progress.record_entry("later");
|
||||
assert_eq!(progress.checkpointable_entry_count(), 0);
|
||||
assert!(progress.page_index().is_none());
|
||||
assert_eq!(progress.cursor().expect("diagnostic cursor").entries_seen, 3);
|
||||
}
|
||||
@@ -39,20 +39,21 @@ an unknown or unsupported peer-health snapshot degrades readiness with
|
||||
|
||||
- Liveness reports process availability and must not depend on storage, IAM,
|
||||
lock quorum, or peer health.
|
||||
- Node readiness reports the node's observed storage write quorum, local pool metadata write gate, IAM, and lock quorum. Runtime storage diagnostics do not change the existing startup `FullReady` publication gate or S3 request admission.
|
||||
- A blocked pool metadata writer degrades node and cluster-write readiness with
|
||||
`pool_meta_write_blocked`. Metadata save-gate inspection is bounded to 100 ms;
|
||||
contention reports `pool_metadata_check_timeout` without installing a block.
|
||||
Node readiness keeps the last confirmed writable observation for the normal
|
||||
readiness-cache TTL during a transient inspection timeout, while preserving a
|
||||
confirmed block and failing closed when no fresh observation exists.
|
||||
- Node readiness (`/health/ready`, `/minio/health/ready`) reports whether this process can serve reads: storage read quorum, IAM, and shared-lock quorum. It is the Kubernetes Service membership signal. Losing write quorum, the pool-metadata writer, or exclusive-lock quorum does not remove a node that can still read. The public health handler still returns 503 until startup publishes `FullReady`, so a process that has never opened S3 admission does not join the Service. Runtime storage diagnostics do not change that startup gate.
|
||||
- A blocked pool metadata writer degrades cluster-write readiness with
|
||||
`pool_meta_write_blocked`. It does not withdraw `/health/ready`, and the node
|
||||
probe omits that reason from `degradedReasons`. Metadata save-gate inspection
|
||||
is bounded to 100 ms; contention reports `pool_metadata_check_timeout` on the
|
||||
cluster-write probe without installing a block. The node probe keeps the last
|
||||
confirmed writable observation for `details.poolMetadata` during a transient
|
||||
inspection timeout, while preserving a confirmed block and failing that
|
||||
detail closed when no fresh observation exists.
|
||||
- The authenticated cluster snapshot extends its existing node-local metadata
|
||||
gate inspection with safe reason, failure phase, and original block time. It
|
||||
distinguishes timeout from a block and changes no admission or recovery
|
||||
decision. Runtime readiness and gate status are separate bounded observations.
|
||||
- Cluster write readiness requires write quorum and the runtime dependency
|
||||
readiness used by `FullReady`.
|
||||
- Cluster read readiness uses the storage read-quorum path and cluster-health timeout behavior. Its lock dependency still uses the per-set majority/write-lock health check. Actual shared namespace locks require `ceil(lock_clients / 2)`, so the cluster read probe is conservative: a four-client set can still admit some reads with two clients while the probe returns 503. This diagnostic change does not lower that probe's lock threshold.
|
||||
- Cluster write readiness (`/minio/health/cluster`) requires storage write quorum, the pool-metadata write gate, and exclusive-lock quorum. Use it when a caller must know that new writes can commit. It is not the Service membership probe.
|
||||
- Cluster read readiness uses the storage read-quorum path and cluster-health timeout behavior. Its lock dependency still uses the per-set majority/write-lock health check. Actual shared namespace locks require `n - n/2`, so the cluster read probe is conservative: a four-client set can still admit reads with two clients while that probe returns 503. Node `/health/ready` uses the shared-lock threshold instead, because that is what object reads acquire.
|
||||
- `HEAD` health probes keep header/status semantics and do not require response
|
||||
bodies.
|
||||
|
||||
@@ -62,22 +63,26 @@ The existing `details.storage.ready` boolean and `connected` / `disconnected` st
|
||||
|
||||
| Probe | `readinessScope` | `source` |
|
||||
| --- | --- | --- |
|
||||
| `/health/ready`, `/minio/health/ready` | `write_quorum_and_pool_metadata` | `local_runtime` |
|
||||
| `/health/ready`, `/minio/health/ready` | `read_quorum` | `local_runtime` |
|
||||
| `/minio/health/cluster` | `write_quorum_and_pool_metadata` | `storage_inventory` |
|
||||
| `/minio/health/cluster/read` | `read_quorum` | `storage_inventory` |
|
||||
|
||||
Node readiness additionally reports `details.storage.readQuorum`, `details.storage.writeQuorum`, and `details.poolMetadata.ready`. The metadata component's status is `writable` or `unavailable`; existing typed degradation reasons distinguish a write block from an inspection timeout. A healthy metadata writer alone no longer makes the storage component ready.
|
||||
Node readiness additionally reports `details.storage.readQuorum`, `details.storage.writeQuorum`, and `details.poolMetadata.ready`. `details.storage.ready` follows read quorum. `details.lock.ready` on this probe follows shared-lock quorum; on `/minio/health/cluster` it follows exclusive-lock quorum. The metadata component's status is `writable` or `unavailable`. A blocked metadata writer is visible there and on the cluster-write probe; it does not by itself make `/health/ready` return 503.
|
||||
|
||||
`details.storage.unavailableDrives` lists inventoried handles excluded from the node's quorum snapshot. Each entry has zero-based `poolIndex`, `setIndex`, and `diskIndex`, its `runtimeState`, and `hostOnline` from the lock reachability observation (always true for local drives). A `suspect` drive with `hostOnline: true` is reachable but not counted; an `online` drive with `hostOnline: false` is excluded because its host did not answer. Internal addresses and filesystem paths are omitted. This list does not enumerate missing inventory slots, and an empty list does not override a failed inventory or quorum check. It is omitted with the other details in minimal responses; HEAD responses have no body. Admin storage info still reports each owner's disk view, which can differ from these node-local handle states.
|
||||
|
||||
Node storage quorum uses configured drives per set, all configured pools/sets, and their Standard storage-class data/parity layout. Missing, duplicate, unreachable, or unhealthy disk observations cannot supply extra quorum votes. The read quorum is the data-drive count; the write quorum is that count plus one when data and parity counts are equal. These are observations of available storage slots, not guarantees that a particular object's metadata, shards, or required locks are available.
|
||||
|
||||
For a healthy IAM and metadata writer in a four-node, one-drive-per-node EC 2+2 set:
|
||||
For a healthy IAM and metadata writer in a four-node, one-drive-per-node EC 2+2 set, after startup has published `FullReady`:
|
||||
|
||||
| Surviving nodes | Storage read quorum | Storage write quorum | Pool metadata ready | Node HTTP / top-level ready |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 4 or 3 | true | true | true | 200 / true |
|
||||
| 2 | true | false | true | 503 / false |
|
||||
| 1 | false | false | true | 503 / false |
|
||||
| All restored | true | true | true | 200 / true |
|
||||
| Surviving nodes | Storage read quorum | Storage write quorum | Shared locks | Exclusive locks | Node `/health/ready` | `/minio/health/cluster` |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 4 or 3 | true | true | true | true | 200 | 200 |
|
||||
| 2 | true | false | true | false | 200 | 503 |
|
||||
| 1 | false | false | false | false | 503 | 503 |
|
||||
| All restored | true | true | true | true | 200 | 200 |
|
||||
|
||||
With two of four nodes up, GET, HEAD, and List of a bucket that this process has not yet cached still succeed. PUT and other mutations fail because write quorum and exclusive locks are gone.
|
||||
|
||||
The node path reads local disk-handle health and reuses the same reachable-host observation as its lock dependency, including the existing `RUSTFS_HEALTH_READINESS_CACHE_TTL_MS` cache. Only `Online` drives count; a reachable host with a `Returning` drive does not yet prove data I/O has recovered. It does not call cluster `storage_info`, local `disk_info`, or add disk-info RPCs. The entire storage inventory snapshot has a separate 100 ms wait budget; expiry reports `storage_readiness_check_timeout` and fails closed. Pool metadata inspection retains its own 100 ms budget; a timeout is counted by `rustfs_pool_metadata_check_timeouts_total` and uses the last confirmed node-local gate state only within the same cache TTL. These observations are not an atomic cluster snapshot and do not bypass the existing lock-probe timing or cache policy.
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ When the whole cluster (or several nodes) went down and nodes are brought back o
|
||||
| `startup_finalization` | Last startup steps are being published. |
|
||||
|
||||
2. Logs say what the node waits for. The IAM recovery loop retries with backoff and logs `event="iam_bootstrap_retry_failed"` with an actionable `hint` field (for example, "storage read quorum not met yet; waiting for enough cluster nodes/disks to come online"). After repeated failures the level escalates from WARN to ERROR; this still does not kill the process.
|
||||
3. Recovery is automatic. Once storage read quorum is available, pending nodes can finish IAM bootstrap on the next retry. `/health/ready` returns `200` when storage write quorum, the metadata write gate, IAM, and lock readiness are satisfied. Restarting pending nodes does not speed this up.
|
||||
4. Check readiness detail while waiting. `/health/ready` (and `/minio/health/ready`) separate `storage` / `poolMetadata` / `iam` / `lock` readiness. `storage.ready` summarizes write quorum plus the metadata write gate; `storage.readQuorum` and `storage.writeQuorum` show the separate quorum observations. A healthy `poolMetadata.ready` does not imply storage quorum. `degradedReasons` lists machine-readable causes such as `storage_quorum_unavailable`, `storage_and_lock_unavailable`, or `pool_metadata_check_timeout`. See the [storage detail contract](../architecture/readiness-matrix.md#storage-detail-contract) for probe scopes and sampling limits:
|
||||
3. Recovery is automatic. Once storage read quorum is available, pending nodes can finish IAM bootstrap on the next retry. `/health/ready` returns `200` when startup has published `FullReady` and the node can still serve reads (storage read quorum, IAM, and shared-lock quorum). Write quorum is reported separately and is required by `/minio/health/cluster`, not by Service membership. Restarting pending nodes does not speed this up.
|
||||
4. Check readiness detail while waiting. `/health/ready` (and `/minio/health/ready`) separate `storage` / `poolMetadata` / `iam` / `lock` readiness. `storage.ready` follows read quorum (`readinessScope` is `read_quorum`); `storage.readQuorum` and `storage.writeQuorum` show the two observations. `poolMetadata.ready` is the metadata writer and does not by itself withdraw `/health/ready`. `degradedReasons` on this probe names read-path causes such as `storage_quorum_unavailable` and `storage_and_lock_unavailable`. `pool_meta_write_blocked` and `pool_metadata_check_timeout` stay on `/minio/health/cluster`. See the [storage detail contract](../architecture/readiness-matrix.md#storage-detail-contract) for probe scopes and sampling limits:
|
||||
|
||||
```bash
|
||||
curl -s http://<node>:9000/health/ready | jq
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Static website hosting
|
||||
|
||||
Set `RUSTFS_WEBSITE_DOMAINS` to one or more comma-separated base domains on the S3 listener. For example, `RUSTFS_WEBSITE_DOMAINS=website.example.com` serves bucket `photos` at `photos.website.example.com`. Configure wildcard DNS and a TLS certificate covering the bucket hostnames. The website domain must not overlap `RUSTFS_SERVER_DOMAINS`; startup rejects overlapping values.
|
||||
|
||||
Website routing is enabled only when the environment variable is set. Keep the public website hostname in the reverse proxy's `Host` header. The S3 API hostname and path-style endpoints continue to serve ordinary S3 requests, including object GET/HEAD, listing, version selection, and XML errors. A website hostname accepts GET and HEAD only.
|
||||
|
||||
Configure the bucket with `PutBucketWebsite`. An index document is required unless `RedirectAllRequestsTo` is configured. A trailing slash resolves to the index document; a missing slash redirects to the trailing-slash URL when an index exists. The website endpoint uses the configured error document for 403 and 404 responses and evaluates routing rules in configuration order. Object metadata `x-amz-website-redirect-location` redirects only on the website endpoint.
|
||||
|
||||
Website requests are anonymous. Grant `s3:GetObject` through the bucket policy for every page to be served, including index and error documents. Explicit deny and public-access restrictions still apply. A private error document is not exposed as a fallback. Use the S3 endpoint for authenticated reads and writes.
|
||||
|
||||
For HTTPS deployments, set the `Protocol` field explicitly in website redirect rules and `RedirectAllRequestsTo` when the proxy sends HTTP to RustFS. Otherwise, redirects use the protocol visible to the S3 listener.
|
||||
@@ -22,13 +22,13 @@ The final job checks all twelve expected suite results and all twelve proof file
|
||||
|
||||
## Stalled suites and time limits
|
||||
|
||||
The nine non-performance reusable suite jobs, and the standalone table suite, have a **60-minute job limit**. The primary test step has a **45-minute limit** so a stalled test can fail before the hard job cancellation. Cleanup steps are limited to five minutes; report generation, dashboard/backlog operations and ordinary artifact uploads are limited to two minutes each. Separate installer and preflight steps are limited to five minutes. Performance is explicitly excluded from these limits.
|
||||
Non-performance suites other than pool expansion have a **60-minute job limit** and a **45-minute primary test limit** so a stalled test can fail before the hard job cancellation. Pool expansion has a **360-minute job limit** and a **240-minute default test limit** to allow rebalance and decommission to finish. Its `pool_timeout_minutes` input overrides the test limit for manual and reusable calls; repository dispatch uses the default. Overrides must leave room within the fixed job limit for setup and finalizers. Cleanup steps are limited to five minutes; report generation, dashboard/backlog operations and ordinary artifact uploads are limited to two minutes each. Separate installer and preflight steps are limited to five minutes. Performance is explicitly excluded from these limits.
|
||||
|
||||
The limits use GitHub Actions native timeouts. They are wall-clock limits, not log-idle detection: a process printing progress forever is still stopped. A step timeout is not a passing result. Existing `always()` finalizers attempt reporting and cleanup, and the chain driver's `always()` plus successful-prepare condition allows the next suite after a failed or cancelled suite. Missing or partial evidence still fails the complete-success gate; it cannot authorize closing historical issues.
|
||||
|
||||
The 15-minute difference between test and job limits is **headroom, not a reserved cleanup window**: checkout and setup also consume the 60-minute job budget, and several failing finalizers may exhaust it. Reaching the hard limit, losing a runner, or terminating an SSH connection does not guarantee remote processes have stopped or cleanup has completed. The next suite must retain its pre-test cleanup. Inspect the runner and remote VMs after a hard timeout before trusting subsequent results; do not interpret contaminated-environment failures as independent product regressions.
|
||||
The difference between test and job limits (15 minutes for ordinary suites, 120 minutes for the default pool run) is **headroom, not a reserved cleanup window**: checkout and setup also consume the job budget, and several failing finalizers may exhaust it. Reaching the hard limit, losing a runner, or terminating an SSH connection does not guarantee remote processes have stopped or cleanup has completed. The next suite must retain its pre-test cleanup. Inspect the runner and remote VMs after a hard timeout before trusting subsequent results; do not interpret contaminated-environment failures as independent product regressions.
|
||||
|
||||
The performance workflow remains unchanged: its job limit is 900 minutes, the default duration is five minutes per round, and the external script retains its default sixty-second pauses. Its methods, sizes, manual overrides and step limits are not modified by the functional timeout policy. A long performance run can therefore still occupy the final lane and delay chain completion; it is not covered by the one-hour guarantee for non-performance suites.
|
||||
The performance workflow remains unchanged: its job limit is 900 minutes, the default duration is five minutes per round, and the external script retains its default sixty-second pauses. Its methods, sizes, manual overrides and step limits are not modified by the functional timeout policy. A long performance run can therefore still occupy the final lane and delay chain completion; like pool expansion, it is not covered by the one-hour limit for ordinary functional suites.
|
||||
|
||||
Job timeouts start when execution starts; they do **not** bound runner or concurrency queue time. Runner preflight checks detect an already-offline runner but are not reservations. The reusable chain continues after a job timeout without cancelling the whole Actions run. The legacy `repository_dispatch` path relies on an in-job handoff and cannot guarantee continuation after hard cancellation; use the reusable driver for bounded nightly chains. A queue watchdog would need an external dispatcher and environment recovery, not cancellation of the whole parent run (which would also cancel the remaining suites).
|
||||
|
||||
|
||||
+17
-2
@@ -277,7 +277,8 @@ uer. `ClusterIssuer` or `Issuer`. |
|
||||
| gatewayApi.listeners.tls.name | string | `tls` | Gateway API TLS passthrough listener name. |
|
||||
| gatewayApi.listeners.tls.port | int | `443` | Gateway API TLS passthrough listener port. |
|
||||
| gatewayApi.listeners.tls.backendPort | int | `null` | Backend service port that terminates TLS; defaults to the console port. |
|
||||
| gatewayApi.hostname | string | Hostname to access RustFS via gateway api. |
|
||||
| gatewayApi.hostname | string | `example.rustfs.com` | Console hostname for Gateway API. |
|
||||
| gatewayApi.endpointHostname | string | `""` | Optional separate S3 endpoint hostname; empty disables the S3 HTTPRoute. |
|
||||
| gatewayApi.secretName | string | Secret tls to via RustFS using HTTPS. |
|
||||
| gatewayApi.existingGateway.name | string | `""` | The existing gateway name, instead of creating a new one. |
|
||||
| gatewayApi.existingGateway.namespace | string | `""` | The namespace of the existing gateway, if not the local namespace. |
|
||||
@@ -449,7 +450,21 @@ NAME HOSTNAMES AGE
|
||||
rustfs-route ["example.rustfs.com"] 172m
|
||||
```
|
||||
|
||||
Then, via RustFS instance via `https://example.rustfs.com` or `http://example.rustfs.com`.
|
||||
Access the console at `https://example.rustfs.com`; HTTP redirects to HTTPS by default. HTTPRoutes reference the chart's Kubernetes Service directly and do not require the TraefikService CRD or Traefik's Kubernetes CRD provider. The former TraefikService cookie stickiness is no longer configured by this chart.
|
||||
|
||||
To expose the S3 endpoint alongside the console, set a different `gatewayApi.endpointHostname`:
|
||||
|
||||
```yaml
|
||||
gatewayApi:
|
||||
enabled: true
|
||||
hostname: console.example.com
|
||||
endpointHostname: s3.example.com
|
||||
existingGateway:
|
||||
name: shared-gateway
|
||||
namespace: gateway-system
|
||||
```
|
||||
|
||||
The console route uses `service.console.port` (9001 by default), and the optional S3 route uses `service.endpoint.port` (9000 by default). Both attach to the configured HTTPS listener and preserve request paths. Configure DNS and the Gateway's TLS certificate for both hostnames. When using a Gateway in another namespace, its listener must allow routes from the RustFS namespace. Leaving `endpointHostname` empty preserves the console-only behavior.
|
||||
|
||||
For end-to-end encryption, set `gatewayApi.listeners.tls.enabled` to `true`. The chart then adds a `TLS` listener with `tls.mode: Passthrough` to the `Gateway` and generates a `TLSRoute` that forwards the encrypted stream to the RustFS service, where TLS is terminated on the backend side. Note that backend TLS termination must be configured on RustFS itself (for example `RUSTFS_TLS_PATH` pointing to server certificates), and the installed Gateway API CRDs must include `TLSRoute`.
|
||||
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
{{- if .Values.gatewayApi.enabled }}
|
||||
{{- $routes := list (dict "name" "route" "hostname" .Values.gatewayApi.hostname "port" .Values.service.console.port) }}
|
||||
{{- with .Values.gatewayApi.endpointHostname }}
|
||||
{{- if eq . $.Values.gatewayApi.hostname }}
|
||||
{{- fail "gatewayApi.endpointHostname must differ from gatewayApi.hostname" }}
|
||||
{{- end }}
|
||||
{{- $routes = append $routes (dict "name" "endpoint-route" "hostname" . "port" $.Values.service.endpoint.port) }}
|
||||
{{- end }}
|
||||
{{- range $routes }}
|
||||
{{- if eq .name "endpoint-route" }}
|
||||
---
|
||||
{{- end }}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "rustfs.fullname" . }}-route
|
||||
namespace: {{ .Release.Namespace }}
|
||||
name: {{ include "rustfs.fullname" $ }}-{{ .name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
annotations:
|
||||
{{- if eq .Values.gatewayApi.gatewayClass "contour" }}
|
||||
{{- if and (eq $.Values.gatewayApi.gatewayClass "contour") (eq .name "route") }}
|
||||
projectcontour.io/upstream-hash-policy: "cookie"
|
||||
{{- end }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- if .Values.gatewayApi.existingGateway.name }}
|
||||
- name: {{ .Values.gatewayApi.existingGateway.name }}
|
||||
{{- if .Values.gatewayApi.existingGateway.namespace }}
|
||||
namespace: {{ .Values.gatewayApi.existingGateway.namespace }}
|
||||
{{- if $.Values.gatewayApi.existingGateway.name }}
|
||||
- name: {{ $.Values.gatewayApi.existingGateway.name }}
|
||||
{{- if $.Values.gatewayApi.existingGateway.namespace }}
|
||||
namespace: {{ $.Values.gatewayApi.existingGateway.namespace }}
|
||||
{{- end }}
|
||||
sectionName: {{ .Values.gatewayApi.listeners.https.name | default "websecure"}}
|
||||
sectionName: {{ $.Values.gatewayApi.listeners.https.name | default "websecure"}}
|
||||
{{- else }}
|
||||
- name: {{ include "rustfs.fullname" $ }}-gateway
|
||||
sectionName: {{ .Values.gatewayApi.listeners.https.name | default "websecure"}}
|
||||
sectionName: {{ $.Values.gatewayApi.listeners.https.name | default "websecure"}}
|
||||
{{- end }}
|
||||
hostnames:
|
||||
- {{ .Values.gatewayApi.hostname }}
|
||||
- {{ .hostname | quote }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
{{- if eq .Values.gatewayApi.gatewayClass "traefik" }}
|
||||
- name: {{ include "rustfs.fullname" . }}-sticky-svc
|
||||
port: {{ .Values.service.console.port }}
|
||||
kind: TraefikService
|
||||
group: traefik.io
|
||||
{{- else }}
|
||||
- name: {{ include "rustfs.fullname" $ }}-svc
|
||||
port: {{ .Values.service.console.port }}
|
||||
{{- end }}
|
||||
port: {{ .port }}
|
||||
{{- end }}
|
||||
{{- if .Values.gatewayApi.httpToHttpsRedirect }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- if and .Values.gatewayApi.enabled (eq .Values.gatewayApi.gatewayClass "traefik") }}
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: TraefikService
|
||||
metadata:
|
||||
name: {{ include "rustfs.fullname" . }}-sticky-svc
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
weighted:
|
||||
services:
|
||||
- name: {{ include "rustfs.fullname" . }}-svc
|
||||
namespace: {{ .Release.Namespace }}
|
||||
port: {{ .Values.service.console.port }}
|
||||
weight: 1
|
||||
sticky:
|
||||
cookie:
|
||||
name: rustfs
|
||||
httpOnly: true
|
||||
secure: {{ .Values.ingress.tls.enabled | default false }}
|
||||
sameSite: {{ if .Values.ingress.tls.enabled }}none{{ else }}lax{{ end }}
|
||||
{{- end }}
|
||||
@@ -381,7 +381,8 @@ gatewayApi:
|
||||
port: 443
|
||||
# Service port that terminates TLS on the backend; defaults to the console port.
|
||||
backendPort: null
|
||||
hostname: example.rustfs.com
|
||||
hostname: example.rustfs.com # Console hostname.
|
||||
endpointHostname: "" # Optional separate hostname for the S3 endpoint.
|
||||
httpToHttpsRedirect: true
|
||||
existingGateway:
|
||||
name: ""
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
afe10983a0de23cf2e1400399bb8a757de90cff2c6120d90cd83b0d28bc2cad2 error-codes.json
|
||||
22133a5cbd5cd36588987d3540c9cadde063faa0ea529dc913f19ed2dc253dea manifest-signing.json
|
||||
04aedbaacdac72fa2a0df22edf6a8c402645a972713778c6031d6c643976a14c reject-vectors.json
|
||||
7d1a84f83f1075401e23b0c5fcf27b86ed669bcadd080079544419409cb1e905 typed-offline-import-vectors.json
|
||||
2656e32ae995fe1c50a47e46788c98389891e68045ab4ee020876b0b1da7534a typed-offline-import-vectors.json
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"evaluationTime": "2026-09-13T12:00:00Z",
|
||||
"authorizedBundle": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000003",
|
||||
"publicKey": "BN18AJq1Zx2KyCPAKIgx9QY4qcr-1RBOxBj8BACTGF4rRtUSgSWSw2nJP8Y11Bziw6REtEy90FT003jCkQChuBU",
|
||||
"keyId": "3192932cae8bbc8a5734a3605fa0e4430d8291de0fea389d28e831faf3ae8aa2"
|
||||
"publicKey": "BClMCdfwoCIi5L11hXBnQa1qUIfH9CWqAaC3IHH1weD6ZzWb1Vo7ibY44PDadmZ526WwDhsDDljQEupTF_KRZD0",
|
||||
"keyId": "9ed142fee51037d4780b32c943eda9502163e7bd2e900e41c4f79a57064c144b"
|
||||
},
|
||||
"vectors": [
|
||||
{
|
||||
@@ -19,10 +19,10 @@
|
||||
"classification": "L1",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmMiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjUsInNoYTI1NiI6IjQ5ODQ0ZGE3MzgxN2FjNDhhZmY2NGY0NzNhODczMWQxMWIxYjYyNmZiNDRjZDA3NDk4ZTc4ZDY2YzcwZGJkMDciLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJmZjRhNDA5OTY0OGE4M2E3NGEwMmU1OWUxNzkzMThmMTQ0YzM3NDNlNjhmNTM2YjI4ZGM0NjVjY2IxNjJiYmNjIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjUzLCJzaGEyNTYiOiI2ZGZkNjk3OWZhZTY5NjU3OTIzNTE1ZDI3NDlkOWI4MTNhNjFmMjY5YjMzZWVmZWNhZjFjMjM0MjYwMDQ0Nzk3IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiQW5nNmEwMm0xZXBCS2ppbWEweFo3RGpqMUtiV2ktakhxdUowY2lOZS0yZG5aaU00cWxMa3U5TG5ZSHB1Q29vZXB4ZXdIMjViTlJfZDhhMzJBLVJyZUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOCIsInRvb2xJZCI6ImhlYWx0aC5jaGVjayIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NTMsInNoYTI1NiI6IjZkZmQ2OTc5ZmFlNjk2NTc5MjM1MTVkMjc0OWQ5YjgxM2E2MWYyNjliMzNlZWZlY2FmMWMyMzQyNjAwNDQ3OTcifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiVjRzWURmeU5zc19ySDZLanUtSGFWZVF6RklObFFLclU0ZDRJRXhGZVRNbGcxcGY1dFNBNU5JLTI3TVRsZGR0akZPS2xSenlfbmRHWHV2dGwtNzNaRUEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmMiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjUsInNoYTI1NiI6IjRmZWE5MmFmZTk3MTJlZjNiNjk5ODQ4NTRjMmI1NjYxNDI0NjkzYjJmMThjNThhMWFjZjY1NmNlOWRmMjUxOTAiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJkYTdlZGQ3ODI2M2EyYTVmYTkxNDZjNzY4NTA5OTZlMWY1NzdlMThmMmMwNGEwMTk3ODk1Zjk0Njc4NzQzNWVkIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjUzLCJzaGEyNTYiOiI2ZGZkNjk3OWZhZTY5NjU3OTIzNTE1ZDI3NDlkOWI4MTNhNjFmMjY5YjMzZWVmZWNhZjFjMjM0MjYwMDQ0Nzk3IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoibEhKMU5WMW5NckQ4SGlxWDlaZnRzMXhLOEV6cTlqU3VvcWVsdlBvWUhJMDhKZGtkR21nVk11YV9yZjVNQmJKaTFIVThHZXZRSThoLUZLSFRRckNvLVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOCIsInRvb2xJZCI6ImhlYWx0aC5jaGVjayIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NTMsInNoYTI1NiI6IjZkZmQ2OTc5ZmFlNjk2NTc5MjM1MTVkMjc0OWQ5YjgxM2E2MWYyNjliMzNlZWZlY2FmMWMyMzQyNjAwNDQ3OTcifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiWW0zb3pIbV95dm9jVXVuQjhXTFJYbFVjWUhEU3hEZnVaNVZQTU9BU3ZyVlNibGJLQmM5SGhQYTd3TllKTzBNR3BDZE1FbHctOVVPdVhxbG9zSEdYNWcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjQiLCJ0b29sSWQiOiJoZWFsdGguY2hlY2siLCJjYXBhYmlsaXR5IjoiaGVhbHRoLmNoZWNrQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJDSEVDSyJ9LCJkYXRhIjp7ImNhdGFsb2dWZXJzaW9uIjoxLCJjaGVja3MiOlt7ImNoZWNrSWQiOiJjYXBhY2l0eS5leGhhdXN0ZWQiLCJvdXRjb21lIjoiRkFJTCIsInJlc291cmNlQWxpYXMiOiJyZXNvdXJjZS0xIn1dfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -36,8 +36,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOCIsInRvb2xJZCI6ImhlYWx0aC5jaGVjayIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NTMsInNoYTI1NiI6IjZkZmQ2OTc5ZmFlNjk2NTc5MjM1MTVkMjc0OWQ5YjgxM2E2MWYyNjliMzNlZWZlY2FmMWMyMzQyNjAwNDQ3OTcifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiQkN0Tks5YVpOUHRGZlp0LTE3TUZBVkVwRFQ5NFZFOF9uckF2NEdyaTd0c0N1cFM1OHo0NDVNM0tGTnFDN1NVQ3FhWS1ra0dkeFFZTnhKZTJaMUlzV0EifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOCIsInRvb2xJZCI6ImhlYWx0aC5jaGVjayIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJGeWk1V2V2NDZNWmdOQ2Y3ODdPSnBhWTlXRkM1aS11SzJQcnlFRE9wWGJjIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NTMsInNoYTI1NiI6IjZkZmQ2OTc5ZmFlNjk2NTc5MjM1MTVkMjc0OWQ5YjgxM2E2MWYyNjliMzNlZWZlY2FmMWMyMzQyNjAwNDQ3OTcifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiMlQ2WnlkejBQdjRtSXo1UkwtNWdVOGF4UkpZTnNFQWVYSTdGTThndFRxRmJKYjJYd251YzVOcFE4aEtTcG9lTUdNZ2RTSXJQX2phR0NjZFUzZWxBTXcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,10 @@
|
||||
"classification": "L1",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmQiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiI2RlA0WWtqRU1zS3dpbXNPUWMzXzVLcG9tdDFFSl84V3F2VF9UXzY2X0lRIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzIsInNoYTI1NiI6ImFhOGQyNDFlZjAyNjVlYWU1NWYwNzIwMzMxZWY2MzQ4OWVhNDE1MmVkZmUzNWRmYzVmOWYxNTgzZTdkZjAwM2EiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJlZjRlZDE1ZGIxOTIzMjdmYjNmYmM2NmQyZTY0OWVjZTVhNDBlMjVlM2ExZjEyMjQ0NjdmNGUxYjNlZjlhNWFjIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiQnNlNk1GM19hSHMyQlJKUU9LRENLLU1JQ0R2ZEFZbHQ0VVp1c2Vlbm1EOWh4NkthNXNPVVE5SGRqQXkteFFlSEFUNU9PTV9XM3VXS1FnNllCV1JEUWcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm5ldHdvcmsiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwxIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiNkZQNFlrakVNc0t3aW1zT1FjM181S3BvbXQxRUpfOFdxdlRfVF82Nl9JUSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiX1FuLVh0QnUzdXZ2MjBrMFlMVDhra01saEdFekxFRXZ3MEVFZUVGTVFvVlhLSHVnaTV1Ujl1dHUxMS0tNF9ObTZJT1FCcEJBbDVQd2txTW5nM0JOdVEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmQiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiI2RlA0WWtqRU1zS3dpbXNPUWMzXzVLcG9tdDFFSl84V3F2VF9UXzY2X0lRIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzIsInNoYTI1NiI6IjM1ZGEyM2E4NjUyYWYzMjVjNGUyZGI5MmRhOTE0MzdlZDc5NmY5M2YwZmE5MDUwZjdhMjAxMWExMjU2MjMzNWYiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIzNGIxZDJlNTEzOGVjY2Y1NDM1NzRjNzg2MTA0N2Q5NmFiODBmMTA4NzIwMjhlNTZhNTQ1ZDIyMDg4MmM1Mzk1IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiQVpaUXZoZE9sOHVfSjJvemwxUjRXU0UtajFPNzRWTVdlTEtlTGwtcUE3MGFVSkc5RXFOV01VOFh4VXhfSHRlaUVBYjBrTlIyOEdwNl80RWN5X1dMcXcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm5ldHdvcmsiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwxIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiNkZQNFlrakVNc0t3aW1zT1FjM181S3BvbXQxRUpfOFdxdlRfVF82Nl9JUSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiTE9UakEtczQtYlViWF9LV2xiN1VDQjM5VVZxbFdqS1NXWEZhODFkY1JRQjEtVkoxNHJ6OG1kUDJlTC1LanZXdDJlclhCa3R5U3hnRjJscTZwd2VVT1EifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjUiLCJ0b29sSWQiOiJwZXJmb3JtYW5jZS5uZXR3b3JrIiwiY2FwYWJpbGl0eSI6InBlcmZvcm1hbmNlLm5ldHdvcmtAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7InRyYW5zZmVycmVkQnl0ZXMiOjEwNDg1NzYsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJlcnJvckNvdW50IjowLCJwZWVyQ291bnQiOjJ9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -64,8 +64,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm5ldHdvcmsiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwxIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiNkZQNFlrakVNc0t3aW1zT1FjM181S3BvbXQxRUpfOFdxdlRfVF82Nl9JUSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiV05CZ2trLUtFWlF3ay01MzRBdzBOYTZyZWtIUmVCQjU0Vmt3Y1NfUkVnMThJemdFTVBZSndQYjFTS2Q2OXhvZ2FlQXF6MzVwNnFCNl9ONkE5QUZvNHcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjOSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm5ldHdvcmsiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwxIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiNkZQNFlrakVNc0t3aW1zT1FjM181S3BvbXQxRUpfOFdxdlRfVF82Nl9JUSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM3LCJzaGEyNTYiOiI1NDM0NTlkNGE2MTUwZDNlNmI4YTMzODQ1MDNmY2MyZTFhMmMxNmNhZmExODdlMDBhZWM1MGY1NmNjMTA3MWQyIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiNWFOTi05VnNZbnVXU1ZDM0pJQU9PYjRTVmRtQUttVnZBaldHZHBBQS1zWUlxU1h0N0JrN21DNU1jT29HVDQzZER6YkVqUVRTUUFTNnNhWW5ldnZ5cmcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -75,10 +75,10 @@
|
||||
"classification": "L1",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmUiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJkWEFjV19EcW81a2praW4xa0llOW9ybU1Xb3VYYTBScC1zV2Q4WFZCN0ZJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzAsInNoYTI1NiI6ImY2YWI5NTVhYmI0MmQ2MDZiMjcwNmFhZTc3ZmIzYjZkZTAwZTFhNDNmM2NkMTQzYzA1MmM1YjcwZDYxYmFkNDciLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI0OWM3YzhlYjM0YTkwNjZkNWJkMmJiNTk0N2QzNDNhYmNmODA3YTNjODgzNGI4NzliOTE5YmVmZmM3MGJlODZmIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ3LCJzaGEyNTYiOiJiOWExMWM0MzY5NDZhYjllYjcwZGQzNmYyMWMxZDk1ODMxZTQwYTE1NjEzNmQ2OTI5NTljODI5YmZhNzQxZTYzIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiWjVXVExJUGpRX0xUTGhYczhBZ2duTHJ1UHAwYUlGajI1LUlwdzIxVW16b1BDanA3QTdidTBlaVNHeV9wSkZDbTFvUWVuX18tZTdiMzhzZXdVcnFfVGcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmRyaXZlIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMSIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6ImRYQWNXX0RxbzVramtpbjFrSWU5b3JtTVdvdVhhMFJwLXNXZDhYVkI3RkkiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0Nywic2hhMjU2IjoiYjlhMTFjNDM2OTQ2YWI5ZWI3MGRkMzZmMjFjMWQ5NTgzMWU0MGExNTYxMzZkNjkyOTU5YzgyOWJmYTc0MWU2MyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiRW1fSXpZNEpaMnJtT3ZfeUhKZXdwMXV0S3lveVNoR2hDMU4xTUloUzNEOW9faTV0T0owMXZfZWJxR0twLU5Kc2pUc1NhMDBDV0F2SmVTMm9CN09ZLUEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmUiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJkWEFjV19EcW81a2praW4xa0llOW9ybU1Xb3VYYTBScC1zV2Q4WFZCN0ZJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzAsInNoYTI1NiI6IjYwOWZlMWE0NWU4ZTNiMDk2YWQyMDIzM2FmODUyYTFlYjM5NmY2NDExODI2ZGMxMDNmZjZjMTYwM2JjNzUxZTgiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJjMzNkMzNmZmRhNmYzYWY2MTJiZjhjNTc5ZmJiYmI3OGEyM2U3YTg4MGY3NDhhYzc3YmRmNjQyNjRkMDhmMTQ2IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ3LCJzaGEyNTYiOiJiOWExMWM0MzY5NDZhYjllYjcwZGQzNmYyMWMxZDk1ODMxZTQwYTE1NjEzNmQ2OTI5NTljODI5YmZhNzQxZTYzIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoieGx2OHR4RXhHQVduazdXYXo0SVJGLXdvek9rZHlWUm9mUFYyenhhNUVqZC1CLXlpVWhOZ2pQbEh0OHdwakFNSlBCLWZFLWNIc19ZSjBUOGtzUm9RSFEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmRyaXZlIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMSIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6ImRYQWNXX0RxbzVramtpbjFrSWU5b3JtTVdvdVhhMFJwLXNXZDhYVkI3RkkiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0Nywic2hhMjU2IjoiYjlhMTFjNDM2OTQ2YWI5ZWI3MGRkMzZmMjFjMWQ5NTgzMWU0MGExNTYxMzZkNjkyOTU5YzgyOWJmYTc0MWU2MyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiamc4SG5vOUdjN2hTc0NUandjbmJnLXpjS0hJUEQ0WFU1M0JvTzc0RHlwRkFSWGRCbzItOEpfbFE3NGszUXpkU0kxSVZYbGFWWmV5QTU4d0VTZFRhRncifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjYiLCJ0b29sSWQiOiJwZXJmb3JtYW5jZS5kcml2ZSIsImNhcGFiaWxpdHkiOiJwZXJmb3JtYW5jZS5kcml2ZUAxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJwcm92ZW5hbmNlIjp7InJlcG9zaXRvcnkiOiJydXN0ZnMvcnVzdGZzIiwic291cmNlQ29tbWl0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGFibGVTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVzdGZzVmVyc2lvbiI6IjEuMC4wLXJjLjYiLCJvc0ZhbWlseSI6IkxJTlVYIiwiYXJjaGl0ZWN0dXJlIjoiYWFyY2g2NCIsImJ1aWxkRmVhdHVyZXMiOltdfSwiY292ZXJhZ2UiOnsicmVxdWVzdGVkVW5pdHMiOjEsImNvbXBsZXRlZFVuaXRzIjoxLCJ1bml0IjoiV0lORE9XIn0sImRhdGEiOnsicmVhZEJ5dGVzIjoxMDQ4NTc2LCJ3cml0ZUJ5dGVzIjoxMDQ4NTc2LCJpb0NvdW50Ijo1MTIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJlcnJvckNvdW50IjowfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -92,8 +92,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmRyaXZlIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMSIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6ImRYQWNXX0RxbzVramtpbjFrSWU5b3JtTVdvdVhhMFJwLXNXZDhYVkI3RkkiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0Nywic2hhMjU2IjoiYjlhMTFjNDM2OTQ2YWI5ZWI3MGRkMzZmMjFjMWQ5NTgzMWU0MGExNTYxMzZkNjkyOTU5YzgyOWJmYTc0MWU2MyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoicUlRaUNXZHZSS2ZwNTI3bVpVQmFQZXppWTcwYlo2SG5PdG0zUEtYVjdRbDJ5VDNyRFdRTGJPQVdZUWxiVEQ3cTItVV92dE5KcWtwWTRUbzB0a0sxLUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYSIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmRyaXZlIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMSIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6ImRYQWNXX0RxbzVramtpbjFrSWU5b3JtTVdvdVhhMFJwLXNXZDhYVkI3RkkiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0Nywic2hhMjU2IjoiYjlhMTFjNDM2OTQ2YWI5ZWI3MGRkMzZmMjFjMWQ5NTgzMWU0MGExNTYxMzZkNjkyOTU5YzgyOWJmYTc0MWU2MyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiTzRZODlTVldXNkZsSFFVcndIZ1BjektGUXZqS0VGbm9fQUlvaVNQcnpKOHpBX0g1MUhrZkd6SXhJclBwQUxGU1Q0R1V1SVh6bjI1MlRKRWhMQklhcWcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -103,10 +103,10 @@
|
||||
"classification": "L1",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmYiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzEsInNoYTI1NiI6IjY5N2EyZDVhODEyOTM5MWRjMmU4NTdjZjUzYWIwNzRjMTljMWFjMTMyNjIzZjcwZjAxYWE0NzBkNTY4MDEwNDIiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI3ZTExY2MxNmUwMmMwNWQ0NDMxNTA2ZDU4NjBlZTQ0MjZmMWI4NWFkNDljMDM4MmM4YTEwNzFmNjRiNGE2NDRkIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjcwLCJzaGEyNTYiOiJmMGY1MDY0ZjNjNGRhYTYwZWJkNjVlMDQ3NDA5MTcxY2M1ODc0YWQ5NDlhMjBjMDMyNWZlNzJiYzc0MGRhNDI2IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiNlN2Y2wzYy1wUnlBRzNGOWF3WmxDa2UxVm4wSl9VUVliWFNPUTVuOC1Xb1R0RmtUQm5laFBCY2R1M3V4a2dUZEROTi1pMWNhcWQ0OFFhZ3VpTzNnMlEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjciLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYiIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm9iamVjdCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6ImYwZjUwNjRmM2M0ZGFhNjBlYmQ2NWUwNDc0MDkxNzFjYzU4NzRhZDk0OWEyMGMwMzI1ZmU3MmJjNzQwZGE0MjYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoidWhqYmxaeUNmOUlvZW1BUmUxZ0U0ZllLc1Y5ZjMtYnFuWERJMnJRWlZDeEE1aDU2OU54ZlNfNURBUjRHcm9nVUNjSk9vMU1JNG82ckdya043UDRTVFEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMmYiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzEsInNoYTI1NiI6IjdjMGI0OGRlNGIzYTlhZjIzNmFhNjk3MGMxMmUzYTM0ZWE2NWZhYTYwOGE5MGVhNTNlNWY0NDg2ZWQ5NTNmMTQiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIwNTMwZWU2YzZmMzgzMWJjMjlkMDNhY2E5OTQ3MDA3Yjk3YjJiNTZjOWVmOTlkY2M1MmQ2MTRlYzJlM2Y1NDVhIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjcwLCJzaGEyNTYiOiJmMGY1MDY0ZjNjNGRhYTYwZWJkNjVlMDQ3NDA5MTcxY2M1ODc0YWQ5NDlhMjBjMDMyNWZlNzJiYzc0MGRhNDI2IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoicVlnTFl1Skw3NDdFRTJZSzNUWDVHZzFzWXFRV0pNUWZ1elVkMlNOeVc5ZHU2LXp1bk1VNnJhYllGQ3N6RGRPQS1kSWNkcUxSU3h4X1FpaUN3ZU5HYkEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjciLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYiIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm9iamVjdCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6ImYwZjUwNjRmM2M0ZGFhNjBlYmQ2NWUwNDc0MDkxNzFjYzU4NzRhZDk0OWEyMGMwMzI1ZmU3MmJjNzQwZGE0MjYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiQ2FVODl4a19pcmZSem9JTmx1T2pDbloyR3M0dWtGN3VZXzR4Rzc2RjFsTlY5aWdCUmdhcVNPb013S2NBTDZEX3V5ckVBeTN6Qy01b2IyY2NCRlpiM1EifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjciLCJ0b29sSWQiOiJwZXJmb3JtYW5jZS5vYmplY3QiLCJjYXBhYmlsaXR5IjoicGVyZm9ybWFuY2Uub2JqZWN0QDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJvcGVyYXRpb24iOiJHRVRfT0JKRUNUIiwidHJhbnNmZXJyZWRCeXRlcyI6MTA0ODU3NiwiY29tcGxldGVkT3BlcmF0aW9ucyI6MSwiZHVyYXRpb25NaWxsaXMiOjEwMDAsImVycm9yQ291bnQiOjB9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -120,8 +120,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjciLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYiIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm9iamVjdCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6ImYwZjUwNjRmM2M0ZGFhNjBlYmQ2NWUwNDc0MDkxNzFjYzU4NzRhZDk0OWEyMGMwMzI1ZmU3MmJjNzQwZGE0MjYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiazRaS0FXWDZlVUdkaEt5SkRyY0dmSVZxeVcxUUNnVTVGTkpjWHRfX3NVc01BTHhTdlIxcnNzLW1xWnV3ZWxBMXFRYWRVTFlzWHNVR0pWTzZuT2oxeEEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjciLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYiIsInRvb2xJZCI6InBlcmZvcm1hbmNlLm9iamVjdCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJkU2ozbnVaWTFGYXUyTDRlQ04yaDVDT3gwai1peGFpR1h4Q19pcnNCT0hVIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6ImYwZjUwNjRmM2M0ZGFhNjBlYmQ2NWUwNDc0MDkxNzFjYzU4NzRhZDk0OWEyMGMwMzI1ZmU3MmJjNzQwZGE0MjYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoia2VPM05mUklrX0p6ckRhcVdfdVBNNUZKSW9tRF9ZSlhfLTBac094ejQzVkg2MGZRUHNNYl9DS0l4WEV6M3VRWk96TXA0dk9Td0l1ZU1tSU5PMk1Yd3cifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -131,10 +131,10 @@
|
||||
"classification": "L1",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzAiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzEsInNoYTI1NiI6IjQxZTc1NzhlODJlMjQwYWYyMWUwMWI2NWU2ZGE0YWFhMzBjNzI2MjI3MTE4OWQzNzFhOWE1NDRjYThjYjA5NTYiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIwYWE2OTRiNzk0OTg5MjdmNzBmYzAxODRmMDQ0ODc0ZDI3ODhiOGYwZmZlMzM0ZTM2ODUyYjRmY2I1ODAxYWU5IiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjcwLCJzaGEyNTYiOiIyZmYwZWZlMjJiYzIxMTJhZWMxM2NkNWM0OWRmZjUxNTFjYTU0NTk5ZmNhZTFkMzdiNmExOTA3ZTM4NDc0ZWEzIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiTDVlVFhUVmQ4SFdUZm42T2FvUWdqRDcyRkZlbUc0MW9YVGx4MVMxQWdqWW1kQnB0MFJNd2s1MFV2Zzg5SGNmZ2gtWlpkc25BOWlBVHBManNQb1V2S0EifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjgiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYyIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmNsaWVudCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6IjJmZjBlZmUyMmJjMjExMmFlYzEzY2Q1YzQ5ZGZmNTE1MWNhNTQ1OTlmY2FlMWQzN2I2YTE5MDdlMzg0NzRlYTMifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiV3RPaC1KcEQ5Q002VEtSLWZFWlVvdFdQQzVyQmt1Q3lsdHphRzZlOFJjOUhSNmRmSVNKSDVaUWxvaGRGVHp5dXJLLWJ3bEJVOU1meFFpVTRvcUZxcGcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzAiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMzEsInNoYTI1NiI6IjMwMDM4NTc5OTAwNWEyZDdjMDg0MDY0NGFmNDI2YTg3N2VkZjhiNWE0MDY4YjI2Nzg4YWVjZGI2MGQ4ODBkMjYiLCJjbGFzc2lmaWNhdGlvbiI6IkwxIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI3OGIxN2UwOGQ0ZGE4MjZhNGQ2NmFjMzA5YTY4Y2UwNTdiZWViZGI1MmQ1Nzk4M2VmOGU1MmNhZGM2NDI5MDhhIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjcwLCJzaGEyNTYiOiIyZmYwZWZlMjJiYzIxMTJhZWMxM2NkNWM0OWRmZjUxNTFjYTU0NTk5ZmNhZTFkMzdiNmExOTA3ZTM4NDc0ZWEzIiwiY2xhc3NpZmljYXRpb24iOiJMMSJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoicS0xTUVYQ25Bcl9xSFRTWmxSWmV3dktmczZTeHFRbElhRktvRWpzN09jWmFvV3k0RVpkWlRaWkE4NGpwNHVEczV2Z1o5M1lhc1M0YXNqb1VhN3NuVGcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjgiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYyIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmNsaWVudCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6IjJmZjBlZmUyMmJjMjExMmFlYzEzY2Q1YzQ5ZGZmNTE1MWNhNTQ1OTlmY2FlMWQzN2I2YTE5MDdlMzg0NzRlYTMifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoia3FLRWduaEFiRVVXVjRCYndhRndDSExsblBVaHZMOVhDaTAwS1RFdnE4STJUUzQtTDZ5M1pCWTRUSjhUYnlOd1hhNzdUUmZpT0hkb1BwZmlrQVBnSUEifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjgiLCJ0b29sSWQiOiJwZXJmb3JtYW5jZS5jbGllbnQiLCJjYXBhYmlsaXR5IjoicGVyZm9ybWFuY2UuY2xpZW50QDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJvcGVyYXRpb24iOiJQVVRfT0JKRUNUIiwidHJhbnNmZXJyZWRCeXRlcyI6MTA0ODU3NiwiY29tcGxldGVkT3BlcmF0aW9ucyI6MSwiZHVyYXRpb25NaWxsaXMiOjEwMDAsImVycm9yQ291bnQiOjB9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -148,8 +148,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjgiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYyIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmNsaWVudCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6IjJmZjBlZmUyMmJjMjExMmFlYzEzY2Q1YzQ5ZGZmNTE1MWNhNTQ1OTlmY2FlMWQzN2I2YTE5MDdlMzg0NzRlYTMifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoid0dKbTYxb3R3OURHTll6bkFJREZUSm5qakc4UHV6WnZkcF9GMXBQWmJrMFJVQkxKOE5oVG9KMkpzY1d3SGdvOV9CbjZJU1BVaWVSbFpkZFVDUTJ6ZXcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjgiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjYyIsInRvb2xJZCI6InBlcmZvcm1hbmNlLmNsaWVudCIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDEiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxRGhlc0NMTTIxV0RZVGRCd0pfTEFuQVpZVGFYTXRWY2lvdV9Zb0dKY0RRIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NzAsInNoYTI1NiI6IjJmZjBlZmUyMmJjMjExMmFlYzEzY2Q1YzQ5ZGZmNTE1MWNhNTQ1OTlmY2FlMWQzN2I2YTE5MDdlMzg0NzRlYTMifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiVlllUDViNVRzTlpoeXVVcjZhMGNFUlBWYXBpTGZKRXJ2RlF2QXpaRjBDazBldVkwUEc2RGV5aHZUSFRLbkNhYmJBSnJoZ1lGWmFSaGNoTUJUdzZzUEEifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -159,10 +159,10 @@
|
||||
"classification": "L2",
|
||||
"expectedDisposition": "REJECT_SITE_REPLICATION_TARGETS_UNAVAILABLE",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzEiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwNDAsInNoYTI1NiI6IjRjYzkwODMxMmRlNDZlMjJlNmQ0NTI2YjIxNmFhZTEzOGRiYzE3OWYyNmQxNWYyZmUxMGU0NTg4MWJkYWE1MDkiLCJjbGFzc2lmaWNhdGlvbiI6IkwyIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJmNjdkYmI4ZmQ4MjhmZjRjNmI2NmIyZWNhZDMyNTg0MDE0Y2ExYmEzZDdiOGVjOTI1YTQzMWIyNzg1ZWJlNzZiIiwiY2xhc3NpZmljYXRpb24iOiJMMiJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6Njg2LCJzaGEyNTYiOiI0MjQxZjRjY2UwZTI1ODU1ZjQ3ZTVhYzZiMWM4Y2UyNzI0ZGMyMTMzNmQzNTA2YTQ2MDkwMWQ1ZGVjY2ViNjdlIiwiY2xhc3NpZmljYXRpb24iOiJMMiJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiTU5WNXZGeFlSdXhMcWF4S3NneVpJbzRfVGFranh6T3NwUjlaMDJzc1J6WlNOXzZVbXA0emRCSzh4aEEzWW9ldU1DNlhoMm11cnZsMEVDX0Jzd1hDSWcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjkiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZCIsInRvb2xJZCI6InBlcmZvcm1hbmNlLnNpdGVSZXBsaWNhdGlvbiIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDIiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2ODYsInNoYTI1NiI6IjQyNDFmNGNjZTBlMjU4NTVmNDdlNWFjNmIxYzhjZTI3MjRkYzIxMzM2ZDM1MDZhNDYwOTAxZDVkZWNjZWI2N2UifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiU2EwMUFGMkx4Si1PSnZmUldMZGtDbEdpNEtnXzVDQmM0dUp6RHpKdEttQS0tN1JuTVJiOVFYVGtMek5yZ3RwX1hOWC1uY0JjWU1iZEhidkhnblFPUXcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzEiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwNDAsInNoYTI1NiI6ImRlYzg2YTljNDljMjNjZTZmMTQ1ZDllYTBlNDc3NWE2OThmMGUwODAyNzNmODk3N2UzZGFjMmQ2Y2EyMDQwOTciLCJjbGFzc2lmaWNhdGlvbiI6IkwyIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJmMDU0MGI2YjRiZmFlYzhjOTVhOWZkZGFiMmRiZjQ3YTZiMjUyZTBkMjI4N2RhMDkxZmYxZGViODM3OWQ3MjBmIiwiY2xhc3NpZmljYXRpb24iOiJMMiJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6Njg2LCJzaGEyNTYiOiI0MjQxZjRjY2UwZTI1ODU1ZjQ3ZTVhYzZiMWM4Y2UyNzI0ZGMyMTMzNmQzNTA2YTQ2MDkwMWQ1ZGVjY2ViNjdlIiwiY2xhc3NpZmljYXRpb24iOiJMMiJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiOUVwenhHeklXaDdQNjlyeHpaajFvYmZiQ3YzbkdWOE5vQ1JaYlpVNllnY08tNWRDVXBDdktxUEYybDB4WXUtU1FXeTMyQ0o3RTdPX1hidkRGaGlOSkEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjkiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZCIsInRvb2xJZCI6InBlcmZvcm1hbmNlLnNpdGVSZXBsaWNhdGlvbiIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDIiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2ODYsInNoYTI1NiI6IjQyNDFmNGNjZTBlMjU4NTVmNDdlNWFjNmIxYzhjZTI3MjRkYzIxMzM2ZDM1MDZhNDYwOTAxZDVkZWNjZWI2N2UifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiZ0Z5LXpaVkQyME1ycHNvR0MzLU1fS2dqQjVkSG5EeDhlVTdwOUk0R21BbGV6T25VUlIyMVY4M2ROVVh2b1h5YUpjdHY0Z3g3d3c5NHJoOEZrTTNnRmcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjkiLCJ0b29sSWQiOiJwZXJmb3JtYW5jZS5zaXRlUmVwbGljYXRpb24iLCJjYXBhYmlsaXR5IjoicGVyZm9ybWFuY2Uuc2l0ZVJlcGxpY2F0aW9uQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJyZXBsaWNhdGVkQnl0ZXMiOjEwNDg1NzYsImNvbmZpcm1lZE9iamVjdHMiOjEsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJtYXhPYnNlcnZlZExhZ01pbGxpcyI6MjUwLCJlcnJvckNvdW50IjowfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -176,8 +176,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjkiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZCIsInRvb2xJZCI6InBlcmZvcm1hbmNlLnNpdGVSZXBsaWNhdGlvbiIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDIiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2ODYsInNoYTI1NiI6IjQyNDFmNGNjZTBlMjU4NTVmNDdlNWFjNmIxYzhjZTI3MjRkYzIxMzM2ZDM1MDZhNDYwOTAxZDVkZWNjZWI2N2UifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiX2xxQ2pPUzdkaFgwU2k4Uno3ZDhKVzVvbVozTWhocE9uak0tNXd3MWd6VnhERnNmdkxFWUQ3YmQ0WDlQQ3B0czVqeVhfUzhPWFRZUW9SZjBrN19sX2cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNjkiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZCIsInRvb2xJZCI6InBlcmZvcm1hbmNlLnNpdGVSZXBsaWNhdGlvbiIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDIiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJ6eTVtZExyd1dwcUxNS2pCUWhta2FTQ3Y0cWE1czNwNVQzcGtZSjd6VFUwIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2ODYsInNoYTI1NiI6IjQyNDFmNGNjZTBlMjU4NTVmNDdlNWFjNmIxYzhjZTI3MjRkYzIxMzM2ZDM1MDZhNDYwOTAxZDVkZWNjZWI2N2UifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiaGEyUXQtVjYxZjhvcGVHaFU0T3pKbUlxUUk0WmttQWpDaVEzX29yck5YVjdSSGVfZWtKQUxzNjNLWXZWWFNzZEtEUjlPNFc1TWxnNi1MUGJ2a3RieXcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -187,10 +187,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzIiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjUsInNoYTI1NiI6ImFlZDM4YzdmOWRhNzQ1ZjM1NjJjN2VlMjQ1MGQ1ZDNmYjFhMmY2M2Q3Nzc5MzU3ZjVjODlkNjUxMWMxMWU2YTAiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI2ZmMyZGNlNTlhYzRlZjU0Y2ZhNjY1YTBiYTUzZjVhOTBiMDI4ZDI3OTI2MjcyYjM5NjdjODE4ZWMzNjNiN2ZkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ3LCJzaGEyNTYiOiJiODRkOWZlNjgwYmMyMDg0NTlmMTZmOTAxYzhiN2UyYzc2MGE3MDM1MDJmOWMzMjQ0ZWRiOTc0NjFlZWYzMDhmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiOFJHallYWjJoYzRlZUllcU9IOVJ1alB2R2NRT3FpcVFPeHdMSlNSZVNQSV9yV3VOV3kwWVNtRXhxZmFrSElpejF0a001UXFPdEdSMklUcS1zZlpDaUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZSIsInRvb2xJZCI6ImxvZ3MuY2FwdHVyZSIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NDcsInNoYTI1NiI6ImI4NGQ5ZmU2ODBiYzIwODQ1OWYxNmY5MDFjOGI3ZTJjNzYwYTcwMzUwMmY5YzMyNDRlZGI5NzQ2MWVlZjMwOGYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiMmJHcDZoV3RYbllzbDV0Qkp6Nkt0UEpINWJQc0sxZnJ4YkpMVFRvOHRaVmNaMlJMT0F3aUlOQkxBbGMyZW1fUTZETzhXVWRrNHY4bmVkQjdOMndzc0EifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzIiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjUsInNoYTI1NiI6IjNjNjhiYTc0ZjU1MDczNGE5MTEzNjNjOTMyNTk0NmQwMTNkNjI2ZTUzMmU1MDEzZGJmNWUzNTE0Nzc0ZDFkNzQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI4ZGNhMDQ0NjRhNDNjODQxYWM5NmI5Y2FhYzM4Mjg4NDI4NDU1ZTc5MTIyNTIxNWMyNTM5Mzg1YzJhZTE4NmU2IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ3LCJzaGEyNTYiOiJiODRkOWZlNjgwYmMyMDg0NTlmMTZmOTAxYzhiN2UyYzc2MGE3MDM1MDJmOWMzMjQ0ZWRiOTc0NjFlZWYzMDhmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoic1VCZHJGU3ZtU3FTLXNlM1V3eWJQdHlLQl9kZGVzc3lQRzN5bGd5dVhIb0c1QlhvR00ydXI0bEE5WHo4bTV2Ynl2dFV6MlVLVVBVRmM1MXA2WDJqMHcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZSIsInRvb2xJZCI6ImxvZ3MuY2FwdHVyZSIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NDcsInNoYTI1NiI6ImI4NGQ5ZmU2ODBiYzIwODQ1OWYxNmY5MDFjOGI3ZTJjNzYwYTcwMzUwMmY5YzMyNDRlZGI5NzQ2MWVlZjMwOGYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiM1ZXeFBOUXVHVEVQYllMR25HME9FREN2aFBad2tOaUZvcGlqMms0YWRrdF8zTnc1M1JOWVpsOEJ0WlNtS1B6Z3VnZlFfX1BIMWFBUFRmNmVXOW5FcXcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmEiLCJ0b29sSWQiOiJsb2dzLmNhcHR1cmUiLCJjYXBhYmlsaXR5IjoibG9ncy5jYXB0dXJlQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJldmVudHMiOlt7Im9mZnNldE1pbGxpcyI6MTAsInNldmVyaXR5IjoiRVJST1IiLCJldmVudElkIjoiRFJJVkVfVU5BVkFJTEFCTEUifV0sImRyb3BwZWRFdmVudENvdW50IjowfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -204,8 +204,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZSIsInRvb2xJZCI6ImxvZ3MuY2FwdHVyZSIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NDcsInNoYTI1NiI6ImI4NGQ5ZmU2ODBiYzIwODQ1OWYxNmY5MDFjOGI3ZTJjNzYwYTcwMzUwMmY5YzMyNDRlZGI5NzQ2MWVlZjMwOGYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoid0NEQTdxcVBWRkNfZ3JtSVQ4RTBHRGpLcFBpSmhvXzdKVUs0Z3Ixb0FGbzZZcG9TNzZWbEx0X1FHcDlqd0phYjkwR2pDZW9qRzJsWEpoNEpQWlFXT3cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZSIsInRvb2xJZCI6ImxvZ3MuY2FwdHVyZSIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiIxdmhnS3pqRUFIRmhVRnoyTktzSjNrQm9IaW5LVHZ4WGo4Q2RaanJQU3NnIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2NDcsInNoYTI1NiI6ImI4NGQ5ZmU2ODBiYzIwODQ1OWYxNmY5MDFjOGI3ZTJjNzYwYTcwMzUwMmY5YzMyNDRlZGI5NzQ2MWVlZjMwOGYifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoidFFlQUdxMkRqY2gwZkU2NUhMT1BZTWRDbGtLejI0UllaTElXcVI3dnAtcG9SOHBHTmwteG9oaWpxRmQ3SWFIeGl3ak5DWGIyUWdUaTFSbG9aVWtwY1EifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -215,10 +215,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzMiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJDWEVkdTlCRVh3YTgyZXFsbWZsbUpSQ1J0SFlRei1HR2dRV2dWa3hzQ2FjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjQsInNoYTI1NiI6Ijk4NmU1YTdkYzkxZTJlZTRkN2U1YTI0NGE3NTljNzk4Yjg1YjA2M2YyNzI3N2UxYmUzNzU0NDk4MjMwYjRlYWYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIxZWY5ODE2OTU0OWY4M2MwNzFiZjY0NmNlODU0MWQwY2Y5YjFiN2NjMmUzOTkxMzU0NjNlNzlhNjk1MGM2NGNkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ1LCJzaGEyNTYiOiI1NjkxMjYxODgwMjFhZGQ3MmNlM2JmY2M1Y2E4MDUyZTdlZGQ1YjZhYWRjMTUzZTU1OTJmNmRhOTU5NjViZDhlIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiZkktT1lGUUVoNnB3bFZMRVoxdHhEcWY4WG9qaC1RUGxINlk0Y1Nfd1JHUjdtOVJrTXViTjd0TEF1dEtST0wzLTNtdmozZG84NGZ1LU5UWnl1Smc3S3cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZiIsInRvb2xJZCI6InByb2ZpbGUuY3B1Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IkNYRWR1OUJFWHdhODJlcWxtZmxtSlJDUnRIWVF6LUdHZ1FXZ1ZreHNDYWMiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0NSwic2hhMjU2IjoiNTY5MTI2MTg4MDIxYWRkNzJjZTNiZmNjNWNhODA1MmU3ZWRkNWI2YWFkYzE1M2U1NTkyZjZkYTk1OTY1YmQ4ZSJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiR2EzcVR1T0tQV3I0UUxMWlFPeVAwQ3FCbFBaU1BSSEFraTFJaTBmajdjY2pwNjQyLWsxeDUxQzRjT00zeWdldmx6NGlXY0RJUEZGdWFiQWJ2OTdpNmcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzMiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJDWEVkdTlCRVh3YTgyZXFsbWZsbUpSQ1J0SFlRei1HR2dRV2dWa3hzQ2FjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjQsInNoYTI1NiI6IjRkYzZjYzFhMzI1YzJkM2UyMTE5YjgwYjc5ZWFiMGVmNWE5MDAxMmYyOTQ4MGQxMDUxN2I2MGMwY2UxYWRmMTQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJmYTNhNmVkOWUzNGQ4MjAwNjljYjNjY2RmNDhmMjkwNmJkMWE1MTk4OWJkZGI0M2JiZDIwZWZhNTllZTI5ZTkzIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ1LCJzaGEyNTYiOiI1NjkxMjYxODgwMjFhZGQ3MmNlM2JmY2M1Y2E4MDUyZTdlZGQ1YjZhYWRjMTUzZTU1OTJmNmRhOTU5NjViZDhlIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoid1pkdzZwOUQxZWVKNE9BUU91M19Md3labFZ4THBNbV91T0s5R1BsaGdQMXBMdFJOeFRILU5hNHMwOEtKY3c1d29rNEpRcmpzSmJPNUNPVzl1VUlUV2cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZiIsInRvb2xJZCI6InByb2ZpbGUuY3B1Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IkNYRWR1OUJFWHdhODJlcWxtZmxtSlJDUnRIWVF6LUdHZ1FXZ1ZreHNDYWMiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0NSwic2hhMjU2IjoiNTY5MTI2MTg4MDIxYWRkNzJjZTNiZmNjNWNhODA1MmU3ZWRkNWI2YWFkYzE1M2U1NTkyZjZkYTk1OTY1YmQ4ZSJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoidzJMdXJLYVJKOTlyVTZPNHh6MzRnS0U0OHZLU0tELXZZOFo0dVJOUzE3WjZfZGE1WlBPdVhhaXRzQUMzenE0RU9VelVVaUhHUXBSU0d2WjZrajg4QVEifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmIiLCJ0b29sSWQiOiJwcm9maWxlLmNwdSIsImNhcGFiaWxpdHkiOiJwcm9maWxlLmNwdUAxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJwcm92ZW5hbmNlIjp7InJlcG9zaXRvcnkiOiJydXN0ZnMvcnVzdGZzIiwic291cmNlQ29tbWl0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGFibGVTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVzdGZzVmVyc2lvbiI6IjEuMC4wLXJjLjYiLCJvc0ZhbWlseSI6IkxJTlVYIiwiYXJjaGl0ZWN0dXJlIjoiYWFyY2g2NCIsImJ1aWxkRmVhdHVyZXMiOltdfSwiY292ZXJhZ2UiOnsicmVxdWVzdGVkVW5pdHMiOjEsImNvbXBsZXRlZFVuaXRzIjoxLCJ1bml0IjoiV0lORE9XIn0sImRhdGEiOnsic2FtcGxlUGVyaW9kTWljcm9zIjoxMDAwMCwic2FtcGxlcyI6W3sic3ltYm9sSWQiOiJzeW1ib2wtMSIsInNhbXBsZUNvdW50IjozfV0sImRyb3BwZWRTYW1wbGVDb3VudCI6MH19"
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -232,8 +232,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZiIsInRvb2xJZCI6InByb2ZpbGUuY3B1Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IkNYRWR1OUJFWHdhODJlcWxtZmxtSlJDUnRIWVF6LUdHZ1FXZ1ZreHNDYWMiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0NSwic2hhMjU2IjoiNTY5MTI2MTg4MDIxYWRkNzJjZTNiZmNjNWNhODA1MmU3ZWRkNWI2YWFkYzE1M2U1NTkyZjZkYTk1OTY1YmQ4ZSJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoidVJ6N28xcGp1VDBrdTdpeExyYmQ4M1QzQ3RIc29MRTVSMWRNbE1fMUJ4d1hYRHlBQ3pOQzd0LWlUd3ZNTkRfUjJwUmdOY0ZDMXA3eTNCLTZLeDJWM2cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBjZiIsInRvb2xJZCI6InByb2ZpbGUuY3B1Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IkNYRWR1OUJFWHdhODJlcWxtZmxtSlJDUnRIWVF6LUdHZ1FXZ1ZreHNDYWMiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY0NSwic2hhMjU2IjoiNTY5MTI2MTg4MDIxYWRkNzJjZTNiZmNjNWNhODA1MmU3ZWRkNWI2YWFkYzE1M2U1NTkyZjZkYTk1OTY1YmQ4ZSJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiMXRxc3pWa2JRbkx0QjRsdFJpN00tQ1BndGNfaVFhTDZGZl9wV0IxXzAzRmgya0EyYWh3SWdkX1Riam5Uak84bDQ4bG04Skx5ZGUwdktNMGFHbG5xWGcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -243,10 +243,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzQiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiI2Zm1rS1lzTWlfRzU2UDZoRkRXdERFbTctc1BvcnJwVlhXWkljOWFpdlNrIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6IjY1MzNkMWVjNzZlN2ZiNjBlM2Q1NDkyY2NlNzg3NGIzNzE1Mjk5ZjUzODQwNmM4YjJiZmQxZjZmMTY2YThmZTMiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI4M2QwYzI5YTM3MmIxYTg0ZDQ2OWU0NTQxMTdkYTk0YjUyNGMzZTk0ZTc2ZjhhYTgxZDRlMGU5ZjJmZGU5YWE1IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjUwLCJzaGEyNTYiOiI2OTg3YjQwM2VjNzg5MDQyMjcyOGRmY2VmMDZjNzU3YWMyMDAyM2RjYjY5NWE1OGQxMDA5ZmM4YmRlM2FlZDNmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoieVR1RVJfMlMyMWN1R3hPSEFQZThTYVMwYWdkcHFkNm51R0lKOFp4a0RVd2lCbWlaektoZ1FZS2cxOVRwYkM5S0lxQ2FlUmswLVotSVEyZFhMdWt0cXcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMCIsInRvb2xJZCI6InByb2ZpbGUubWVtb3J5Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjZmbWtLWXNNaV9HNTZQNmhGRFd0REVtNy1zUG9ycnBWWFdaSWM5YWl2U2siLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY1MCwic2hhMjU2IjoiNjk4N2I0MDNlYzc4OTA0MjI3MjhkZmNlZjA2Yzc1N2FjMjAwMjNkY2I2OTVhNThkMTAwOWZjOGJkZTNhZWQzZiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiOTNLVkRYWFB1YnlxcVlXOVVVMGVyc0tEc29QX1dhOGx1d0U5OU1DT2stVWJpRno4cXJLb0dEMEp0MmhSd2JHcGJPaDBzcEplVGlubDNnR1JnLVZVencifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzQiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiI2Zm1rS1lzTWlfRzU2UDZoRkRXdERFbTctc1BvcnJwVlhXWkljOWFpdlNrIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6IjAwNTk2NzhlOGJhZGQ1NzYxMTEwOTVjYmU3MGU0ZTEyOTZlMTU4OTdiNmFmZjJkMDkxYzUxYzlhZjA3OGVkOTQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJkYzNhNThmMTA3YWZjY2Q0MmY0MGU2Y2FhYjM1YzRjMDQ4ZGViNTdmMDg4YjVjYTg1YTZlMTljYzNkNTkxMjBhIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjUwLCJzaGEyNTYiOiI2OTg3YjQwM2VjNzg5MDQyMjcyOGRmY2VmMDZjNzU3YWMyMDAyM2RjYjY5NWE1OGQxMDA5ZmM4YmRlM2FlZDNmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiZnV3Um1wNG9VRzNPSTFNTWZRRkxZZC11Q0pHRHNZcUlJaXVsbjBXWjlpMDZwRktsY0E0R2hZR0tfX2dKeTVoUzlqcmxrZ2xJOWNQVlgtazRyZGl2MFEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMCIsInRvb2xJZCI6InByb2ZpbGUubWVtb3J5Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjZmbWtLWXNNaV9HNTZQNmhGRFd0REVtNy1zUG9ycnBWWFdaSWM5YWl2U2siLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY1MCwic2hhMjU2IjoiNjk4N2I0MDNlYzc4OTA0MjI3MjhkZmNlZjA2Yzc1N2FjMjAwMjNkY2I2OTVhNThkMTAwOWZjOGJkZTNhZWQzZiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoidmlUU1ZfcHJteDB5aTRLZERlc0tHUzB5NmZmdDFJS1A3amFNOFFBMVZ2QTBFMUZrcUFzR2M2cGtSWU0wMlJXM0E0NEp6eGFnVS11dkJUMUlFaFp2NmcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmMiLCJ0b29sSWQiOiJwcm9maWxlLm1lbW9yeSIsImNhcGFiaWxpdHkiOiJwcm9maWxlLm1lbW9yeUAxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJwcm92ZW5hbmNlIjp7InJlcG9zaXRvcnkiOiJydXN0ZnMvcnVzdGZzIiwic291cmNlQ29tbWl0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGFibGVTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVzdGZzVmVyc2lvbiI6IjEuMC4wLXJjLjYiLCJvc0ZhbWlseSI6IkxJTlVYIiwiYXJjaGl0ZWN0dXJlIjoiYWFyY2g2NCIsImJ1aWxkRmVhdHVyZXMiOltdfSwiY292ZXJhZ2UiOnsicmVxdWVzdGVkVW5pdHMiOjEsImNvbXBsZXRlZFVuaXRzIjoxLCJ1bml0IjoiV0lORE9XIn0sImRhdGEiOnsic2NvcGUiOiJBTExPQ0FUSU9OX0FHR1JFR0FURVMiLCJhbGxvY2F0ZWRCeXRlcyI6NDA5NiwiYWxsb2NhdGlvbkNvdW50IjoyLCJzYW1wbGVQZXJpb2RNaWNyb3MiOjEwMDAwfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -260,8 +260,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMCIsInRvb2xJZCI6InByb2ZpbGUubWVtb3J5Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjZmbWtLWXNNaV9HNTZQNmhGRFd0REVtNy1zUG9ycnBWWFdaSWM5YWl2U2siLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY1MCwic2hhMjU2IjoiNjk4N2I0MDNlYzc4OTA0MjI3MjhkZmNlZjA2Yzc1N2FjMjAwMjNkY2I2OTVhNThkMTAwOWZjOGJkZTNhZWQzZiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiWElKU3h4UFRFbWhXLWdYR3NaWGlySXFUMi0tblFfU3gyNHVzQl83dmxrUTBZUWpqb3BjakIzeFgwRTZNWkkxenRsaXhNUWtkT2tBMnV1Q0FIZ01RTUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMCIsInRvb2xJZCI6InByb2ZpbGUubWVtb3J5Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjZmbWtLWXNNaV9HNTZQNmhGRFd0REVtNy1zUG9ycnBWWFdaSWM5YWl2U2siLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjY1MCwic2hhMjU2IjoiNjk4N2I0MDNlYzc4OTA0MjI3MjhkZmNlZjA2Yzc1N2FjMjAwMjNkY2I2OTVhNThkMTAwOWZjOGJkZTNhZWQzZiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiV0IySzFwMWZ5M2F4S2hQSjl5ZEdKb2MyNTNWSzZ2QUdFbnVGb0xlM1hfOWtjRmpIWFpzZUJvMm5lNk5mMnlUaldpckdTMXg3Y0lUTjRkNE81ZFd0eWcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -271,11 +271,11 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzUiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjgsInNoYTI1NiI6ImUzODIyZTAyY2Y0ZDBhZTA1ZjJhOTQ4MTMzYTRmZTg4ZDM5MjNjNzA0ODVhYjk1MTdmMDI3NmViMTRjM2M3NzciLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI4OTc5Zjk1ZTU3MzM1ZTA5NzgwNWI2NGVlOTM1MDgzMTYyOWVlMDg3MTY5MTYxY2I1Mzk1NGNkYzljZjJhOWM4IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjIyLCJzaGEyNTYiOiJhYTExZTI5MjgzOTRjZjA1MTBiNjAyZDY0MzA4NGMxYWYwMGM5NmNiM2Q3Y2E0YzMwY2MyZDhjNDM1ZTUyYzc5IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiZ0RXcVJoTE5SR1hpUjNlZXlIdTdHQXRfTjFKa0NFNXVSUmNnUVFldENxSUJKZU4xdTc2ZnNRUng2d0JOd3RoMnFSb0w3SVNjUWR5ZHVlT3NUM1Z2aVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMSIsInRvb2xJZCI6InByb2ZpbGUudGhyZWFkcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2MjIsInNoYTI1NiI6ImFhMTFlMjkyODM5NGNmMDUxMGI2MDJkNjQzMDg0YzFhZjAwYzk2Y2IzZDdjYTRjMzBjYzJkOGM0MzVlNTJjNzkifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiQ3RMalBZSk40VWRSSlhDT1pVSFNIZkd3TF9IeHBVYWlDLThyOGt3cHUtcDZ6U2VvR2wwRTlRMHFXVXBGYlAwdFM4Nnk0aUdPQkd3TUs5Y0RGQV9oNmcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJ0b29sSWQiOiJwcm9maWxlLnRocmVhZHMiLCJjYXBhYmlsaXR5IjoicHJvZmlsZS50aHJlYWRzQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJzY29wZSI6IlRPS0lPX1JVTlRJTUUiLCJzdGF0ZXMiOlt7InN0YXRlIjoiV0FJVElORyIsInRocmVhZENvdW50IjoyfV19fQ=="
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzUiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjgsInNoYTI1NiI6IjhkZjVhNGJjNjc5NmNiOTUzMmYxNWZkOThiYjM4NmY5ZjE0ZmViMzE5MDExOGQ2M2ZlM2Y1ZDcwMTA5MjlhMDYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIwZGU1MzQxYjY1Mzc5YzY4NzBlNjYzNDljNmNjZDRjZDExYTdjMjc1NGNlY2NkZmIwMWY4ZmMyOGNhMGI3NTUwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjIzLCJzaGEyNTYiOiI3OWQ3ZmQwNzYzYmFkNmI0ZjZlYTMzYTcwNTllNDM1MGZhYjQ2ZGQ4MzdkYTZlMzczZDE4MTFjMTMyMTQ5NjgwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiMGZPMkJ1UUcxbnhGZDJoXzA5ZWI0Y3k5MGlocU05ek05T3pqemhDc1hoa2pCLXJ0UkRLcEUtN212cHpMRE1XSGp0ZEcxVnFUS0RhM0N1cG1iN3NVRWcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMSIsInRvb2xJZCI6InByb2ZpbGUudGhyZWFkcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2MjMsInNoYTI1NiI6Ijc5ZDdmZDA3NjNiYWQ2YjRmNmVhMzNhNzA1OWU0MzUwZmFiNDZkZDgzN2RhNmUzNzNkMTgxMWMxMzIxNDk2ODAifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiY2Z2RVU0c1VhT0dMcXVrZEZBSmVOdzEwU2ZLY3hKZHNWbDE2aWJ6dXNnTlc0R1BvSE55UWtkekp1bHNBT1pINGVTdURPV3JhYi1mblBkLTJMZXFuTXcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJ0b29sSWQiOiJwcm9maWxlLnRocmVhZHMiLCJjYXBhYmlsaXR5IjoicHJvZmlsZS50aHJlYWRzQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJzY29wZSI6Ik5BVElWRV9USFJFQURTIiwic3RhdGVzIjpbeyJzdGF0ZSI6IldBSVRJTkciLCJ0aHJlYWRDb3VudCI6Mn1dfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
"tampered": {
|
||||
@@ -288,8 +288,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMSIsInRvb2xJZCI6InByb2ZpbGUudGhyZWFkcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2MjIsInNoYTI1NiI6ImFhMTFlMjkyODM5NGNmMDUxMGI2MDJkNjQzMDg0YzFhZjAwYzk2Y2IzZDdjYTRjMzBjYzJkOGM0MzVlNTJjNzkifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoieFF3MXRfYUljMnlWeFgyRVhMQmdkaGZjaWRsMkh4S2lGX2YyU1kzWVBTc3RwUDVVVUM3TktMck95SGxKWlFZQWdRRkh2YXZ6T1BVN1JnSkZXUlp2OWcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMSIsInRvb2xJZCI6InByb2ZpbGUudGhyZWFkcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJxMTIwQy1NN0xJZmNmUUpZWURKNUFQWktjZGhCNlVPSHUyMzR0U3NiXzA4IiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo2MjMsInNoYTI1NiI6Ijc5ZDdmZDA3NjNiYWQ2YjRmNmVhMzNhNzA1OWU0MzUwZmFiNDZkZDgzN2RhNmUzNzNkMTgxMWMxMzIxNDk2ODAifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiMU1LVkplMjZMb2wwVjhsbzVGSUFvRXRueXZPVW5YQmw0cW1NNW1EcFlnMHJjY0pUQzRxUFk2NmZtVEpJTWdqZ3VKOVFUanNQOG9aRHZ4M3g1UnJiQWcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -299,10 +299,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzYiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiI1UnV1eUxzbjBvdkJISHZ0cUU4S0dKWUxuX2o0Nlc4U1NIZEZoMWRpZ0s4IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6ImRiYjQ5YjhjZWNkMDM1MTFjOTY5MjE1OTdjYmRiODBlNWQ0ZWQ1ZTQzMmMzNDAyZDdjNDQ1ZjNjMzZlNjIwMDAiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJlZDIwZmNlMGNlZjhjZGIzNDZiNjA3YjZkNjFkNGUxMzQzNmUwMTQyNjAxMjEyZTcwODM0YTkwOGIyZWI1NTdiIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NzY0LCJzaGEyNTYiOiI0OTE2NTk4NjY3NjAzMTNhN2VlZWM0NzZiYjFiYjUzNTY3ZjA3NGU4NGUzNGQ3Y2NkOWU1MDQxOTk2OTJhMGFiIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiRklGd05yT2FSN0ZUbENRQjhyZlhmVzVSZmFEMjBTTVp5cTJMYkdtVHg1OUJFSzhMWi1SNnMzMk40ZGgwLVl5bXBPckNiZkxJY2Z6RFVISXRyVE85RkEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMiIsInRvb2xJZCI6Imluc3BlY3Qub2JqZWN0Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjVSdXV5THNuMG92QkhIdnRxRThLR0pZTG5fajQ2VzhTU0hkRmgxZGlnSzgiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjc2NCwic2hhMjU2IjoiNDkxNjU5ODY2NzYwMzEzYTdlZWVjNDc2YmIxYmI1MzU2N2YwNzRlODRlMzRkN2NjZDllNTA0MTk5NjkyYTBhYiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoia2Y0cV95X1pFbW5SQjNvNkNYeHdBMG1UVGhkRVRZZDJQNHkwV3pGMllRNTZvTl80WERvczRqMU1JQ1ozOE55MTBxb01lZUxCYl82Q0lOV3haYXRfZFEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzYiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiI1UnV1eUxzbjBvdkJISHZ0cUU4S0dKWUxuX2o0Nlc4U1NIZEZoMWRpZ0s4IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6Ijg3NWIwOWE0OTc3MTkzODU0NjlhMTNlNGU3ZmJlMDNjYjUxMzMxNTYzZjNjOWZkM2Y3Njg3MzYwZjc0YzM2NTciLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI4MTNjNjk1ODczZWViYjhlN2RiMTRlMTVjZGI2NWI4Mjk1ODY4NDIxZDA2OTM3ODU1NDAzYjNjZGNjYTFlYTk5IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NzY0LCJzaGEyNTYiOiI0OTE2NTk4NjY3NjAzMTNhN2VlZWM0NzZiYjFiYjUzNTY3ZjA3NGU4NGUzNGQ3Y2NkOWU1MDQxOTk2OTJhMGFiIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiSmsyTHlDTl9kSkx2Q3FMUzlLYUg1YWpKV3F1VGVtc2pnYTFHOFYxSmhJaGNLYWVTX2ZxMm4wVkxLTTExMHhwMF8wTW90ZGVfVi0xbzN4TndmWHg0SVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMiIsInRvb2xJZCI6Imluc3BlY3Qub2JqZWN0Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjVSdXV5THNuMG92QkhIdnRxRThLR0pZTG5fajQ2VzhTU0hkRmgxZGlnSzgiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjc2NCwic2hhMjU2IjoiNDkxNjU5ODY2NzYwMzEzYTdlZWVjNDc2YmIxYmI1MzU2N2YwNzRlODRlMzRkN2NjZDllNTA0MTk5NjkyYTBhYiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiQzlUcTNHaG5NemdweXBKdE5ydVNzSTJlTGJkR0M0Z3lmYlluaHdwdFppdE5zV0tDT1ROUXc1Y3dGYTVrYTFkaWxXRFBzY0tRdTNGb21CY3o2dlRaT2cifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmUiLCJ0b29sSWQiOiJpbnNwZWN0Lm9iamVjdCIsImNhcGFiaWxpdHkiOiJpbnNwZWN0Lm9iamVjdEAxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMCIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJidWlsZEZlYXR1cmVzIjpbImluc3BlY3QiXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IkNIRUNLIn0sImRhdGEiOnsic2NvcGUiOiJMT0NBTF9PQkpFQ1RfU1VNTUFSWSIsImZpbmRpbmdzIjpbeyJyZXNvdXJjZUFsaWFzIjoib2JqZWN0LTEiLCJydWxlSWQiOiJTSEFSRF9BVkFJTEFCSUxJVFkiLCJvdXRjb21lIjoiUEFTUyIsInJlYXNvbiI6IkFWQUlMQUJMRSIsIm1pc3NpbmdTaGFyZENvdW50IjowLCJjb3JydXB0U2hhcmRDb3VudCI6MCwicmVjb25zdHJ1Y3Rpb24iOiJOT1RfTkVFREVEIn1dfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -316,8 +316,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMiIsInRvb2xJZCI6Imluc3BlY3Qub2JqZWN0Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjVSdXV5THNuMG92QkhIdnRxRThLR0pZTG5fajQ2VzhTU0hkRmgxZGlnSzgiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjc2NCwic2hhMjU2IjoiNDkxNjU5ODY2NzYwMzEzYTdlZWVjNDc2YmIxYmI1MzU2N2YwNzRlODRlMzRkN2NjZDllNTA0MTk5NjkyYTBhYiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiRUxlUUlOcHR4d3M5X0hkN0ktMVZqVVlQRHA2ODZnSE5XNThndmpJUWNxUVowanQ5SVJ1aW5wdVA0cVNzZUtncmh2Q2FOcHlLTzdiZUdwSDFOR21aYVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMiIsInRvb2xJZCI6Imluc3BlY3Qub2JqZWN0Iiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IjVSdXV5THNuMG92QkhIdnRxRThLR0pZTG5fajQ2VzhTU0hkRmgxZGlnSzgiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjc2NCwic2hhMjU2IjoiNDkxNjU5ODY2NzYwMzEzYTdlZWVjNDc2YmIxYmI1MzU2N2YwNzRlODRlMzRkN2NjZDllNTA0MTk5NjkyYTBhYiJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoieXNDUVVpV1dJX1U2VnF5Z2JvdkUzZ1N6QnEyTXBkb0gxWHZyMzg1Vm5RRTJ2YXQ1UTNtbkRWX2JDQUNKNDlMdE4wZTdhMVVPdGlGYjd2V182QWJmd3cifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -327,10 +327,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzciLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJzT3ZtdkhKb2NwdElRaGY5cVlzTHA1bmI0dXB6UUVmQW1pLXBXNzdTS2lZIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjksInNoYTI1NiI6Ijc1MTNmODM5YTAyZGZhOGNiYWIwNGVkZTlhNTk1Yzk5YjU1ODBiMjQ5Njk1YWRhNWI4MGYzNDUxYzM0ODU1NTMiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI0MjI0ODZjZTQyMzdmNzYyMzUyMTc5NTg0ZmIzMDg5Y2I0ZTVjZGM0Njg5NDA3ZjA4MTZlNDUzNGVmZWRlNDQ2IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiY2RBbEZfN2VQVENndks5WVM5VW5fVzlHdHhOR1Bwb1VLSy04Q3V5X3ZpNERiNXNEWFMxVGFQWG5iZ2hwMkxWMHMxYzFxWG1BQWJnc04xeTlyakNQdVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMyIsInRvb2xJZCI6InRlbGVtZXRyeS5yZWNvcmQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoic092bXZISm9jcHRJUWhmOXFZc0xwNW5iNHVwelFFZkFtaS1wVzc3U0tpWSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiaUJwc2M1eGRVYjFqU3pEWVZESjNXR21kYWl2NExRRmJkVTJpYTZwRi1aQk5VdVJiQzdCVUROcnU0QzA3NE9BSHRTQU9jTHBKMi1mUXhBWHYwSVFJMXcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzciLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJzT3ZtdkhKb2NwdElRaGY5cVlzTHA1bmI0dXB6UUVmQW1pLXBXNzdTS2lZIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjksInNoYTI1NiI6ImExYmE1ZjkzNTEzYzdmYTJiOGY3YzE1YWRiNmVlN2Y2NWYxZTExZDM5NzUwMmNjZWNhZmI1NmE0MjE2ZTMyNzYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJjNjI2MmQyYjlmNWRiNTFjNWJlYTQ2Mjk1MmIyNGQ1MzI1OTc3ZmQ4ZjIzMGZhOWU1YmU3MjNiODQ4NzQwOGM1IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiWDZ1bHZ5bGNkbFlLN1BiMlM5YldLTnpVNTU5bEpwVm9yUEY4LVdsSm5yWllCOXJTbUNoNE5IQlFWQWtlUW1qR2FmczBhOHdHSUJTRV9oRmpjVTVFTlEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMyIsInRvb2xJZCI6InRlbGVtZXRyeS5yZWNvcmQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoic092bXZISm9jcHRJUWhmOXFZc0xwNW5iNHVwelFFZkFtaS1wVzc3U0tpWSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiOV9IU3hhQm9Dbnp0TnhsaU5wQkdWd0hQYnBfTWJxRE12M3AxMnZWaU5yY1JTTUJNSXI4SUloUWtCVXlvRHlQRXE2TjBKeHhKR0ZJWDVjd0VtV0M3LWcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmYiLCJ0b29sSWQiOiJ0ZWxlbWV0cnkucmVjb3JkIiwiY2FwYWJpbGl0eSI6InRlbGVtZXRyeS5yZWNvcmRAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7InNwYW5zIjpbeyJvcGVyYXRpb24iOiJHRVRfT0JKRUNUIiwiZHVyYXRpb25NaWNyb3MiOjUwMCwic3RhdHVzIjoiT0sifV0sImRyb3BwZWRTcGFuQ291bnQiOjB9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -344,8 +344,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMyIsInRvb2xJZCI6InRlbGVtZXRyeS5yZWNvcmQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoic092bXZISm9jcHRJUWhmOXFZc0xwNW5iNHVwelFFZkFtaS1wVzc3U0tpWSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiVXIzR3ZqOHh3T0Roc3I3S1BYUUQ1aHdqa2Y0Z0kxRXhUaWh0cy03RGhaNWswWFpxOFZRQ3BDSGxLUzVpck14UVJNUFdTTTdRdUxCd3pqaGZXcndaRlEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNmYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkMyIsInRvb2xJZCI6InRlbGVtZXRyeS5yZWNvcmQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoic092bXZISm9jcHRJUWhmOXFZc0xwNW5iNHVwelFFZkFtaS1wVzc3U0tpWSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjQ2LCJzaGEyNTYiOiJjNjdjMzdlNGU4M2ZmZTBhZjU4YTA5YjVkODJkNDFmZTFiMDA2MmM1YzI0ZjdiNjRmMGQ2NTk0NWZlYzA1OTBmIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoieElFcmVIdG1ScUREU1BoMW8zd0pjRjFUdWlLQ2VNalo0em9MZ3NYUlNlazVDWk42emxPWjZUby1MMEppbEpUZG1fSEJEOFUzSDZDU010Nm1DWm1fZncifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -355,10 +355,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzgiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJJRVEtZnFRUXY2cWo0TTBHVW5rbWxNREgycHFLMXV0ellBaEw5ZlhLTjlNIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6IjBkZGU3MWRhYjc0ZmE0NmMyZTg1NTM2NjE1ODRhNjU1ZmU5YjcwOWM3N2U5MTViNGViZThkYjc4OGUxMjdhMzciLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJiMDU1ZjZlODIzZTgwMTQzZmJlODBkZDFhYTcwYzYzNmExZTU3NjNmMjYyMDE0NjMwMWM4YTA0ZjQ1OTQ3ZDYyIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiJmMTQ2YmZhMzJhYTI2MjdlZWExZDhkNzI1NTYzNTVlOWQ2MTUwZjYxODNhMmM2MDFjNmJlMjY1Nzg2NjM3YzNjIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiRnIxYXlMd0w4S0JRMHFPNUJWeThHRktkWWRJcXpDUjJfcmFTT1J3ZjMzRS1pd2dHcFAtNGF4bTVlbDR3eVJfaFBnQ055TXdUVUtiRUZGWnhPMk9TNlEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzAiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNCIsInRvb2xJZCI6InRlbGVtZXRyeS5vdGxwIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IklFUS1mcVFRdjZxajRNMEdVbmttbE1ESDJwcUsxdXR6WUFoTDlmWEtOOU0iLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYxMywic2hhMjU2IjoiZjE0NmJmYTMyYWEyNjI3ZWVhMWQ4ZDcyNTU2MzU1ZTlkNjE1MGY2MTgzYTJjNjAxYzZiZTI2NTc4NjYzN2MzYyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiVF9DU1J4Q2pmelkxNWQzR3loVXZxSVY4UmNudE5WaXExNFhwQWtiYUJTeHpERmRFdTMwQndzb0V6WUlFdGYtaEJTUGRGQVJfSmlzemJyODFXUzEyQmcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzgiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJJRVEtZnFRUXY2cWo0TTBHVW5rbWxNREgycHFLMXV0ellBaEw5ZlhLTjlNIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjcsInNoYTI1NiI6IjRmNDM5OWJlZjMwNWYwN2RiY2UxM2Q3ZTE3OWE4ZTU2YWU5YzhiZjFiNGRiYTU4ZjEzYTU5MzY1MGZjMzhmNDQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIwMzU5NDQwOTA3ODA3NzllZTQzNDVlOTRkOWI3YmNiMTU2MDMyZWJhMjc5NTFhNzgwY2U3YWM4MjExMmJkOTIzIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiJmMTQ2YmZhMzJhYTI2MjdlZWExZDhkNzI1NTYzNTVlOWQ2MTUwZjYxODNhMmM2MDFjNmJlMjY1Nzg2NjM3YzNjIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoibl9DSVNqTmRVb1djdDJZdDlocHdxblRQVFFtRVd1QUktVnR0QWlwZTU4NUxxclhXaW1MZVZwNU03RDFYSDh6bHl3SHUyRDFMMWVCR3lfWTVEZ0VrZXcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzAiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNCIsInRvb2xJZCI6InRlbGVtZXRyeS5vdGxwIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IklFUS1mcVFRdjZxajRNMEdVbmttbE1ESDJwcUsxdXR6WUFoTDlmWEtOOU0iLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYxMywic2hhMjU2IjoiZjE0NmJmYTMyYWEyNjI3ZWVhMWQ4ZDcyNTU2MzU1ZTlkNjE1MGY2MTgzYTJjNjAxYzZiZTI2NTc4NjYzN2MzYyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiRkFkaTY2VmRUZlVvUVkzclVLc3lqekxnaVNYZGFacklPYnV6c1dEVENQbFBBNnhKSkVyYmpKaVYtdllkTmxuOEl3R2lDS3JKaHFLOHlTY1UyNjRXbWcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzAiLCJ0b29sSWQiOiJ0ZWxlbWV0cnkub3RscCIsImNhcGFiaWxpdHkiOiJ0ZWxlbWV0cnkub3RscEAxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJwcm92ZW5hbmNlIjp7InJlcG9zaXRvcnkiOiJydXN0ZnMvcnVzdGZzIiwic291cmNlQ29tbWl0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGFibGVTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVzdGZzVmVyc2lvbiI6IjEuMC4wLXJjLjYiLCJvc0ZhbWlseSI6IkxJTlVYIiwiYXJjaGl0ZWN0dXJlIjoiYWFyY2g2NCIsImJ1aWxkRmVhdHVyZXMiOltdfSwiY292ZXJhZ2UiOnsicmVxdWVzdGVkVW5pdHMiOjEsImNvbXBsZXRlZFVuaXRzIjoxLCJ1bml0IjoiV0lORE9XIn0sImRhdGEiOnsiYWNjZXB0ZWRTcGFuQ291bnQiOjEsInJlamVjdGVkU3BhbkNvdW50IjowLCJleHBvcnRlZEJ5dGVzIjoxMjh9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -372,8 +372,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzAiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNCIsInRvb2xJZCI6InRlbGVtZXRyeS5vdGxwIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IklFUS1mcVFRdjZxajRNMEdVbmttbE1ESDJwcUsxdXR6WUFoTDlmWEtOOU0iLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYxMywic2hhMjU2IjoiZjE0NmJmYTMyYWEyNjI3ZWVhMWQ4ZDcyNTU2MzU1ZTlkNjE1MGY2MTgzYTJjNjAxYzZiZTI2NTc4NjYzN2MzYyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoibms4eFlfVG0xdjlUbTJvdDB3NTJlYzRlZl9oZHh6TnQzVGZTQTNyUEJfa29OeVQySzdidnBWLUhkUWNIVUl0d3p4R21PczcyMVJTb1NkVDFiTFZPb0EifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzAiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNCIsInRvb2xJZCI6InRlbGVtZXRyeS5vdGxwIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6IklFUS1mcVFRdjZxajRNMEdVbmttbE1ESDJwcUsxdXR6WUFoTDlmWEtOOU0iLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYxMywic2hhMjU2IjoiZjE0NmJmYTMyYWEyNjI3ZWVhMWQ4ZDcyNTU2MzU1ZTlkNjE1MGY2MTgzYTJjNjAxYzZiZTI2NTc4NjYzN2MzYyJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiaHB4cjc3Qzd4MDAxcFR2VENJVTVleWw2LVBuOTBaZFRCWVV5RmVBVnNBb1FfYW4tcFQzTnFtM3hyOXJUWDlMLXdoNHFKb3hHbE15N0pRR2dzX2g1MHcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -383,10 +383,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzkiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJWN1pyNkp3SGdrY2s1RXllNXVWNV9mVFdJd1R4Rkp2SjVPWGQtdjBidXJNIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjksInNoYTI1NiI6IjU3ZTY3Y2MxZTA0NjZhYTQwOWNkZTVmMWQ1MWNkYmE1ZGI5MTJmZWIwMTRlMGQzNGY5MzAyODczMWU1NWNjNTEiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJmZTVmMzUxNGEyNzUxNmRlM2JlNGZlMzEwOWRiYWU4NTFjMWNiNzZmM2M5NjI2ZDM4YmUwMjYyZDgyZmE1ZGExIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiZHNDdUZSa2hfQ0UyTVF2alBTellFVXdyQVdQZ2RUQ0xzVjVzdlF1N2U4WkdjbmNudEtMc2hRZ2ZVU3VNTVpaem5ZdHZwM2hpQU93NHJjZ19QclN4c0EifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNSIsInRvb2xJZCI6InRlbGVtZXRyeS5yZXBsYXkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiVjdacjZKd0hna2NrNUV5ZTV1VjVfZlRXSXdUeEZKdko1T1hkLXYwYnVyTSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiWlREM1I0R3c1MFVDRWVON04xLVprRlh2Mm02YWdob2xCWUJuX2lJdkJHWnRSaXhlUTdxQVBfbHZUSFlrNWt3OUpudEcxZ2FJSWxVa1MyMkJ5T19DdkEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxMzkiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJWN1pyNkp3SGdrY2s1RXllNXVWNV9mVFdJd1R4Rkp2SjVPWGQtdjBidXJNIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjksInNoYTI1NiI6ImIyYjRjYmJhNTdjZThiMDAyODY3MDczMWE5OTVjZGVjOWE1MmYzZmQxYmJkMzU1Y2YxZWRmZDljM2U5OTA5NDMiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJjZTdlZGNhYmViMDBjYThkYzNlNWZlNDM1NGFmZTg4MGJmNTFkYTQ0YTczZDNhY2ZkZWEzMWE5NzkzYmViNzA1IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoibmRmVDZhTjh5c0V6YnJiY2JRN29tNlgyN2I2WHQ4eklNMzBjbXdESFpiVXVUcmxNMkJFOTNwbzI2ZEwwQnlndWplWFFnMWxnSG4tX2U2a2EzOFhZZlEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNSIsInRvb2xJZCI6InRlbGVtZXRyeS5yZXBsYXkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiVjdacjZKd0hna2NrNUV5ZTV1VjVfZlRXSXdUeEZKdko1T1hkLXYwYnVyTSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiTkhoU2doUmc4OGk5NlZHUkI0OWhpWUh5OTZYbE9ZcHhHOWFBNUNnbk1pVWM4Uld1Q0g1UjRLbFNOaXlvVVRxNW5YUFladV9DaVlOeXFQb1FpbGE1b3cifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzEiLCJ0b29sSWQiOiJ0ZWxlbWV0cnkucmVwbGF5IiwiY2FwYWJpbGl0eSI6InRlbGVtZXRyeS5yZXBsYXlAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7ImlucHV0QXJ0aWZhY3RTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic3BhbnMiOlt7Im9wZXJhdGlvbiI6IkdFVF9PQkpFQ1QiLCJkdXJhdGlvbk1pY3JvcyI6NTAwLCJzdGF0dXMiOiJPSyJ9XX19"
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -400,8 +400,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNSIsInRvb2xJZCI6InRlbGVtZXRyeS5yZXBsYXkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiVjdacjZKd0hna2NrNUV5ZTV1VjVfZlRXSXdUeEZKdko1T1hkLXYwYnVyTSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiV1VNRW9lWERwZkVBdk5vOTFZcFdkU3Jkb3RYeHRBQk4xSy1QODZZUkF1UmUwbEw4N1hqckNNVWJpNWtha3dENDFtR0xkSVZFank3MVdLR1ZmVl81bUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzEiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNSIsInRvb2xJZCI6InRlbGVtZXRyeS5yZXBsYXkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiVjdacjZKd0hna2NrNUV5ZTV1VjVfZlRXSXdUeEZKdko1T1hkLXYwYnVyTSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NzE0LCJzaGEyNTYiOiI1YjlmZjU4YTFkNjNkYzhhM2I5OTg0OTZhYjUxMmJiNGFlOGMxYzY4OWU3ZTVkYWUxZDhhODgwMThkOWU3MTRlIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiVTZaXzlvRlhOVUIzaU9Wam53SjVfZ2l2UzhDd28wQzRMYkZKMk5wRjBWNDNyWlBoUko3MFpzLWpwMlVMcHpsc2Y4bmM4dU1iX2NMSEhHNHJNTW1Rb2cifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -411,10 +411,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2EiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJybVdkVFZOQ1pyeGptRzFPZEZTcG85ZV9hMm5kSHFkeXV0b2RHQXJlU1hJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6IjRhMGZhYTlkZDFkNmI2M2Y2ZDBjMmQyYjY4YjEzMTcwNjFkZGM4MTk1NTc1YmI3NjAyNjc2ZjU4NDgyN2VjNjQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI5MzQyMmQyYjc4YWRlMmIyZDU3OTE2NGRmMzFiYTcyNTkzOGI1ZWJjODEwMjY1OWExZWM4Njc5ODI0YTU2OTdhIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiWFoxT2lrdzg4LU8yOEJwREVWQmtJdlNqNkQ4bDdhbFlPWEZOV2NuX0tNbEJMcVdvRzhGcVkyM0pYWWV4SUZ5VG9OLVpja0ZQeTRsR1pMbVMtSjFXUUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNiIsInRvb2xJZCI6InRvcC5hcGkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoicm1XZFRWTkNacnhqbUcxT2RGU3BvOWVfYTJuZEhxZHl1dG9kR0FyZVNYSSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiMUcxYlZkT01wd0trWFNUd29aVDlDX3FmYkdQVDgwVnIyYW9OTDBOVEhzTm11aFJxSDJWZ3ZKUmRMeG04MmlvQlZCZ1hTM2kwb0h6QUpQcENBT29qbGcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2EiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJybVdkVFZOQ1pyeGptRzFPZEZTcG85ZV9hMm5kSHFkeXV0b2RHQXJlU1hJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6IjBlNmY5ODg3MjdhZWNhYjU1OTJjMjQzMTA5NWMzNjcxMTllMDM0Y2Y4MGYyMDIzMmI2M2MxZjhlMDBkNWFjZmIiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIwNWU0NDNmYWE5ZmVmZDAxNDNlODg3MDQ1OGIyMzFjNmFjODBlZDk5YWNkMzdjNzNkMWI4MTk3NzUyNDcxYzAxIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiVV9PTGtha25zUXZoaVFnWUZYNUVXdTRIVlpFVVYzcmpvc0ZER3QwajhkZzNUclV2YjVEclAyVGhYWWlidUM3TTIyOUViTTlfcExuZzVpbnVObTNvUFEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNiIsInRvb2xJZCI6InRvcC5hcGkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoicm1XZFRWTkNacnhqbUcxT2RGU3BvOWVfYTJuZEhxZHl1dG9kR0FyZVNYSSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiSXN1WHBHdUR1R091d1dpM3RwSmo3UmV0MzRQSGU1MjZzbHpIbUNTNFFLZGJyQXc5aDYzWHEzcXlIem1sV1hOOGNUR2c1YTdEQ1lsN2FGaUgyWFFERHcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzIiLCJ0b29sSWQiOiJ0b3AuYXBpIiwiY2FwYWJpbGl0eSI6InRvcC5hcGlAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7Im9wZXJhdGlvbiI6IkdFVF9PQkpFQ1QiLCJyZXF1ZXN0Q291bnQiOjIsImVycm9yQ291bnQiOjAsIndpbmRvd01pbGxpcyI6MTAwMCwidG90YWxEdXJhdGlvbk1pY3JvcyI6NzAwfX0="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -428,8 +428,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNiIsInRvb2xJZCI6InRvcC5hcGkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoicm1XZFRWTkNacnhqbUcxT2RGU3BvOWVfYTJuZEhxZHl1dG9kR0FyZVNYSSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoibTZ0YUlxSVZaMDk0MW4yNFdzalZacUdRTEpFSlo1anlyLXJDMVhuVmJieGE4WUY3THpfOWZRRjFVNmNRU0lERHhxaEhpcVgzRlE4THg5ZE8xdFY2SXcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzIiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNiIsInRvb2xJZCI6InRvcC5hcGkiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoicm1XZFRWTkNacnhqbUcxT2RGU3BvOWVfYTJuZEhxZHl1dG9kR0FyZVNYSSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjM4LCJzaGEyNTYiOiJhNWFkOWQ4OTc4ZDllZjc4OTA3N2I2NDMzOWYzMmZhODA1ZjZiYTAzZWU0NWY4NGVlZjczN2Y5YmM5OTg5MDg3In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiYkF2WjNBcUFmeG5OV0dyYTY2VXhLcWgzNDlnaHlqakt3clJIQmxfbnh2azRDYmI0aGZxTXNyQW9JajRXTzJGOUVuQ1lSaHpmTGx6dDI3SkNuUlY0YmcifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -439,10 +439,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2IiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJNamQxdzhpVWp0SnlKdUtZTEdjRHF2eU1icmlFU1I5UDQwMXE1cEMxU2c0IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjEsInNoYTI1NiI6IjI4NzA1ODY0MTY0OGM2NzRkNzJmYjJhZmI3MGMxZDk3ZjQ1MTAwMDg4ZWQzNTU2ODAzYjc2ZmRhNGFjMTRhOTIiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJlMGEwYThiOGQ3MzExMmI5ZDkwZWY2OWE1YjZiZGE3YzQ3MWEwMDBmMzZhZTk0ZjI5OTUyMTdhYTk4NTFiZjYwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjMwLCJzaGEyNTYiOiI2NmVlZTY3MTQ5ODkwZTE1NGQ5NTliNzAxYWVhYjlhMjNhZDE3MzA2OTFlOTczZGU1ZGM1MmM4ZDU1NzQ4ZWMwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiTVd4cm9GWmJUVWJlU2xoemZxUXFMUkpFZkU4N3c1eHpyVFdYeDQzejJLdER1T21mdXR1dmdHeGFyaGh2YV9JVi02VHg5OGpBZV9KWXNjSWJpYi1QcEEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNyIsInRvb2xJZCI6InRvcC5kaXNrIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6Ik1qZDF3OGlVanRKeUp1S1lMR2NEcXZ5TWJyaUVTUjlQNDAxcTVwQzFTZzQiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYzMCwic2hhMjU2IjoiNjZlZWU2NzE0OTg5MGUxNTRkOTU5YjcwMWFlYWI5YTIzYWQxNzMwNjkxZTk3M2RlNWRjNTJjOGQ1NTc0OGVjMCJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiNGxkUDYtX0lkQTJiQVJxeThRRllZMWFSb1gwc3NYTEFNMzRXR1ZQQjl5OW15eHZGS1JLalRjbFdPMkhoOEI4NFozd2lFaHN2TmhHc1Bzcm9RZTBFaFEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2IiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJNamQxdzhpVWp0SnlKdUtZTEdjRHF2eU1icmlFU1I5UDQwMXE1cEMxU2c0IiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjEsInNoYTI1NiI6IjliMWJhNjExZDkzZWQ5NzRlYzdlNzY1ODYzYmVhNjQ0Y2MwODI1YmEwZjVlYjM5YTMyOGExZTJjYjBiYmM3ZWIiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiIyOTVmMjFkOTBiODU2ZWM3YTM1NDNhMTdlYTk0YmExZTViYzBjZGEwMWNiY2FlNjU5ZmYyOWQzYTIzNmQ0MjMwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjMwLCJzaGEyNTYiOiI2NmVlZTY3MTQ5ODkwZTE1NGQ5NTliNzAxYWVhYjlhMjNhZDE3MzA2OTFlOTczZGU1ZGM1MmM4ZDU1NzQ4ZWMwIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiUFY2NFBiSlc1ajc0dGNuSWtCRmJkWkU0NFdPX3dpWk1pd0NJRGRCaVFreFNydzNCUEx5alRLcXkxS3lxM3JxMEhmejQ5M2FBMmJkRF94ZjZSaG5abUEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNyIsInRvb2xJZCI6InRvcC5kaXNrIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6Ik1qZDF3OGlVanRKeUp1S1lMR2NEcXZ5TWJyaUVTUjlQNDAxcTVwQzFTZzQiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYzMCwic2hhMjU2IjoiNjZlZWU2NzE0OTg5MGUxNTRkOTU5YjcwMWFlYWI5YTIzYWQxNzMwNjkxZTk3M2RlNWRjNTJjOGQ1NTc0OGVjMCJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiTk5SQ0FzSWFZMDdnenR4MHROdEIybU5Pbk1MSzFLZXpTeUloLTNZWVAzdzA3YzVuM0FyWEotNTYyVzJlOW5CaG5xbFFkdUc3T2hfQzNQNUJmR3NFNXcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzMiLCJ0b29sSWQiOiJ0b3AuZGlzayIsImNhcGFiaWxpdHkiOiJ0b3AuZGlza0AxIiwib3V0Y29tZSI6IlNVQ0NFRURFRCIsInJlYXNvbkNvZGUiOiJDT01QTEVURSIsImR1cmF0aW9uTWlsbGlzIjoxMDAwLCJwcm92ZW5hbmNlIjp7InJlcG9zaXRvcnkiOiJydXN0ZnMvcnVzdGZzIiwic291cmNlQ29tbWl0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGFibGVTaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVzdGZzVmVyc2lvbiI6IjEuMC4wLXJjLjYiLCJvc0ZhbWlseSI6IkxJTlVYIiwiYXJjaGl0ZWN0dXJlIjoiYWFyY2g2NCIsImJ1aWxkRmVhdHVyZXMiOltdfSwiY292ZXJhZ2UiOnsicmVxdWVzdGVkVW5pdHMiOjEsImNvbXBsZXRlZFVuaXRzIjoxLCJ1bml0IjoiV0lORE9XIn0sImRhdGEiOnsicmVzb3VyY2VBbGlhcyI6InJlc291cmNlLTEiLCJyZWFkQnl0ZXMiOjQwOTYsIndyaXRlQnl0ZXMiOjAsImlvQ291bnQiOjEsIndpbmRvd01pbGxpcyI6MTAwMH19"
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -456,8 +456,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNyIsInRvb2xJZCI6InRvcC5kaXNrIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6Ik1qZDF3OGlVanRKeUp1S1lMR2NEcXZ5TWJyaUVTUjlQNDAxcTVwQzFTZzQiLCJkZXZpY2VLZXlJZCI6IjMxOTI5MzJjYWU4YmJjOGE1NzM0YTM2MDVmYTBlNDQzMGQ4MjkxZGUwZmVhMzg5ZDI4ZTgzMWZhZjNhZThhYTIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYzMCwic2hhMjU2IjoiNjZlZWU2NzE0OTg5MGUxNTRkOTU5YjcwMWFlYWI5YTIzYWQxNzMwNjkxZTk3M2RlNWRjNTJjOGQ1NTc0OGVjMCJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiQlNSbWxrMmp5bXp5ZHdlMUdzRFktdFVvejI1US01VnBNZkJlVjBQVl9yaFI3ZUtqNkZqcUMwUWVuckdjQ2U1U0paVkRVMDIzTUkxbFVLUndWZTJobHcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzMiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkNyIsInRvb2xJZCI6InRvcC5kaXNrIiwic2NoZW1hVmVyc2lvbiI6MSwiY2xhc3NpZmljYXRpb24iOiJMMyIsImNvbnNlbnRVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJwb2xpY3lSZXZpc2lvbiI6MiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwiZXhwaXJlc0F0IjoiMjAyNi0wOS0xM1QxMjowOTowMFoiLCJub25jZSI6Ik1qZDF3OGlVanRKeUp1S1lMR2NEcXZ5TWJyaUVTUjlQNDAxcTVwQzFTZzQiLCJkZXZpY2VLZXlJZCI6IjllZDE0MmZlZTUxMDM3ZDQ3ODBiMzJjOTQzZWRhOTUwMjE2M2U3YmQyZTkwMGU0MWM0Zjc5YTU3MDY0YzE0NGIiLCJwYXlsb2FkIjp7InBhdGgiOiJyZXN1bHQuanNvbiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJzaXplQnl0ZXMiOjYzMCwic2hhMjU2IjoiNjZlZWU2NzE0OTg5MGUxNTRkOTU5YjcwMWFlYWI5YTIzYWQxNzMwNjkxZTk3M2RlNWRjNTJjOGQ1NTc0OGVjMCJ9fQ==",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiWFc4VVJ2cklxZEJiNkNiNENqbG5kNUtYZ2JBSVlrUXpFUlVYUlNaNUl6ZGc4WnF5bnVzcjYwTXljZmhfRGxzcVJUUWR2LW9nTm1fUkctOTNLZUVuSUEifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -467,10 +467,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2MiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjIsInNoYTI1NiI6ImIyOTQwYjFlNGU1ZjNiOTFiYTk3Y2MxNTg4NTM0OTlkZWEzMDNkMDQ0NDkxZTMzMDJlYWU1YTA3ZDFkZTA2NWQiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJkNzU4NzAzODA5ZGNjYTQzNmJmY2VmNDY2MTlmZjcyOGNhMzNkNTY0NjNkYmVjODcxZThlMTZlNjZiNGZjNmEzIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NTg4LCJzaGEyNTYiOiIwMTI0MmM4OTE2N2Q0NmQ5ZjQyY2Q5OWU1M2JjNmU0MmEzYTRlNTdlMWE5ZDEwZGQ3YjM5MTEwYjk2NjM2ZGRkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiZGpLSlppSVFlV3J5UkRhblVGcktGMXJ2b1Z6STFuOEtrXzVTazAya09HUldNMjFmNGIzWjhLRVdTTWFLZWhraWV0WUVLZDBoTi14ck1tYTNHd18tN3cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOCIsInRvb2xJZCI6InRvcC5sb2NrcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo1ODgsInNoYTI1NiI6IjAxMjQyYzg5MTY3ZDQ2ZDlmNDJjZDk5ZTUzYmM2ZTQyYTNhNGU1N2UxYTlkMTBkZDdiMzkxMTBiOTY2MzZkZGQifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiLXByTHdyT1dKc1pnbTFidFE0MGVJT3FpbVBtNGk3eHZ2NUJONEt5Nmx5NGhNX3M0dGVPMXJETDdHMkVOZW04QzgwWVlrRnNvQ19Fa1BPME5zcTlvNVEifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2MiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjIsInNoYTI1NiI6ImI4ZmI4YjMxM2YxNjA1YjFhNjhiMzgyMDI2YjgwZTgwZTVkZmY0NzY3NzEwYjk5MjliYWYzMDRmYTU4YmJhOGYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiJlZWI5NTllOTQ5NTZhYWUyZDdiYTYxMDhiODcyMThmODJiOWVjYWNjZmI5YTZlYzY1OWY2OGFkYmU1MzkyOGVmIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NTg4LCJzaGEyNTYiOiIwMTI0MmM4OTE2N2Q0NmQ5ZjQyY2Q5OWU1M2JjNmU0MmEzYTRlNTdlMWE5ZDEwZGQ3YjM5MTEwYjk2NjM2ZGRkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiVnRVYjMzUmQtNHRoLWM5V2l4NDVnWE1LTE4xODlqR2hlOWFFdzlFSlV3VUpJaS1zVHpCMk5Pc2V3RVJzRjUyTU93Q01CMF9Ka0o1M3hHWGNmZlpBRVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOCIsInRvb2xJZCI6InRvcC5sb2NrcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo1ODgsInNoYTI1NiI6IjAxMjQyYzg5MTY3ZDQ2ZDlmNDJjZDk5ZTUzYmM2ZTQyYTNhNGU1N2UxYTlkMTBkZDdiMzkxMTBiOTY2MzZkZGQifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiY29DeTdyLURTN3NyMng3ZTh4T1N4el9DTVg5cWVhbTl1Yk5EZ0ZLVWdHRWhVSFRCYkpkbUdxMk8wV3hSTnAzaEdJdkIzWjk5OUZaM25CNWtBSk5NbFEifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzQiLCJ0b29sSWQiOiJ0b3AubG9ja3MiLCJjYXBhYmlsaXR5IjoidG9wLmxvY2tzQDEiLCJvdXRjb21lIjoiU1VDQ0VFREVEIiwicmVhc29uQ29kZSI6IkNPTVBMRVRFIiwiZHVyYXRpb25NaWxsaXMiOjEwMDAsInByb3ZlbmFuY2UiOnsicmVwb3NpdG9yeSI6InJ1c3Rmcy9ydXN0ZnMiLCJzb3VyY2VDb21taXQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0YWJsZVNoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydXN0ZnNWZXJzaW9uIjoiMS4wLjAtcmMuNiIsIm9zRmFtaWx5IjoiTElOVVgiLCJhcmNoaXRlY3R1cmUiOiJhYXJjaDY0IiwiYnVpbGRGZWF0dXJlcyI6W119LCJjb3ZlcmFnZSI6eyJyZXF1ZXN0ZWRVbml0cyI6MSwiY29tcGxldGVkVW5pdHMiOjEsInVuaXQiOiJXSU5ET1cifSwiZGF0YSI6eyJoZWxkQ291bnQiOjIsIndhaXRpbmdDb3VudCI6MSwidHJ1bmNhdGVkIjpmYWxzZX19"
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -484,8 +484,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOCIsInRvb2xJZCI6InRvcC5sb2NrcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo1ODgsInNoYTI1NiI6IjAxMjQyYzg5MTY3ZDQ2ZDlmNDJjZDk5ZTUzYmM2ZTQyYTNhNGU1N2UxYTlkMTBkZDdiMzkxMTBiOTY2MzZkZGQifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoidVpvTzJHTGlvN0V6TVp3blNDcU9TSmxMeFptYjB2LTY3Q1dPRkRHcVVCQmg1WllDN0E1NlZMQ080aEZ2ejRJYmdwRjlCZWk4QUJvWFRLaHVlX3pEQ1EifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzQiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOCIsInRvb2xJZCI6InRvcC5sb2NrcyIsInNjaGVtYVZlcnNpb24iOjEsImNsYXNzaWZpY2F0aW9uIjoiTDMiLCJjb25zZW50VWlkIjoiMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDA1IiwicG9saWN5UmV2aXNpb24iOjIsInByb2R1Y2VkQXQiOiIyMDI2LTA5LTEzVDExOjU5OjAwWiIsImV4cGlyZXNBdCI6IjIwMjYtMDktMTNUMTI6MDk6MDBaIiwibm9uY2UiOiJQSVN3VmpDU3ZVRzFuQWpUUDYzY052VGNTbmdtX3hraHo2VXVMSWVKeXFJIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwicGF5bG9hZCI6eyJwYXRoIjoicmVzdWx0Lmpzb24iLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2l6ZUJ5dGVzIjo1ODgsInNoYTI1NiI6IjAxMjQyYzg5MTY3ZDQ2ZDlmNDJjZDk5ZTUzYmM2ZTQyYTNhNGU1N2UxYTlkMTBkZDdiMzkxMTBiOTY2MzZkZGQifX0=",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiQTJlMWhVUVFNa1RwdGYyUUx1RTJrX3Vxd2k0NTBIdWxsak5WX1V1cDFhZ25tWm9WeXBFSWJGd29paGgyckFsLWI2VnlXcEpYdDA3ZmlQNlFkQ3lSSkEifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -495,10 +495,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2QiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJlckJPRVVMQm5mblZmRVJwTS0zUjdVUWxQWFI2RDBsVWVPRG1YSnI2eThjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6IjYyMDc5ZTE0NTc0ZWQwNzI3YzJlZjZkYWJkMWVhZGMzNTVjNjgyZjg2YWM0ZTZmZWFlODU1YzdkZTIxZmY4NTYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI5OGFkY2I5YWYwZTcyYTczNjRhZmNjZjBmNmVhYjNjNDcxNmVmZDA4NmJkMjY2NGEwNmU1NWE4YWMzYTE1NjAzIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiVDJ6a0FkeDMxeTMycnZRbEVDM1J4N0RoNE00WnN4NmhhQnF5QU9sQXdqTWJ0bVduNjRRQzlCcm5wRmttRk5WVjl3QmRueUNqSnZTbW9CX1ItVkE2encifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOSIsInRvb2xJZCI6InRvcC5uZXQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiZXJCT0VVTEJuZm5WZkVScE0tM1I3VVFsUFhSNkQwbFVlT0RtWEpyNnk4YyIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiZHVJZUVfejFpZkpOdTBJdGFwMU5pX1I5Ykt1b0hyR25XMGZKUW1Lazh0a0N0QmM3Q1J4MWFDSVlJYlpoNUhmRXE0U2hCSWVEN185bzFkU09DUDdqMWcifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2QiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJlckJPRVVMQm5mblZmRVJwTS0zUjdVUWxQWFI2RDBsVWVPRG1YSnI2eThjIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6IjhhODUzNjY4NjRiMGEyYTg3YTNkOGFiOGJiNzcyZTE0ZThmYTY4ZTZmMDNkYTk0ZGM0M2ZiY2Q0ODkwNzkyZDIiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI1YzllNzg4NGFhNWViZTI1NWJkY2QwZGI5OWI4YTc3MmViZmM0ZjRlMjc1YWRkZWU4NTU3N2Q0MzQzMDJhNmJiIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiaFhuUVZyM0NEUW8wZU5iMXdGaGlXdVBLV2lLRFNIX2xTd3Z4cG9PY1hpZHF6bkRCWkxiVTl0a25OSWF1emM2Y1hmNmo4YS1tNV9HUE0zRkNqTV9Gc3cifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOSIsInRvb2xJZCI6InRvcC5uZXQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiZXJCT0VVTEJuZm5WZkVScE0tM1I3VVFsUFhSNkQwbFVlT0RtWEpyNnk4YyIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoieTI4cG9sN19kak1XSHRfNTM1Qi1HWHRSLUZjc3pNMmV2aHFhSHl0cVNMY09pT0NXbnhyb29jeGZXcnlxUGk0TWlKZUhqakwxYmdPWmlPV1ZRWUJ1VGcifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzUiLCJ0b29sSWQiOiJ0b3AubmV0IiwiY2FwYWJpbGl0eSI6InRvcC5uZXRAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7InJlY2VpdmVkQnl0ZXMiOjQwOTYsInNlbnRCeXRlcyI6MTI4LCJ3aW5kb3dNaWxsaXMiOjEwMDB9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -512,8 +512,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOSIsInRvb2xJZCI6InRvcC5uZXQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiZXJCT0VVTEJuZm5WZkVScE0tM1I3VVFsUFhSNkQwbFVlT0RtWEpyNnk4YyIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiLWZsaUpIZXRiOVY3aFpFVkJxTVdMSXNzS2V0SDB5T1pBb1NyOEFiSVBwZFQ0WjZSTkpqUjVRMDBRVkd2cDFrU2NyMmxHUjRJNlAxcS1vc0xwT2JCNmcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzUiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkOSIsInRvb2xJZCI6InRvcC5uZXQiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiZXJCT0VVTEJuZm5WZkVScE0tM1I3VVFsUFhSNkQwbFVlT0RtWEpyNnk4YyIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NTkyLCJzaGEyNTYiOiIyN2NiNGY4NDM4ZTBlNjg0NGNhZDFmMGU4ZDU1YmU1NTk0ZjgxMTIyM2Y5ODM1OGZiNGZmY2MxYTk4ZGU2ZDVkIn19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoieVFMYVZtNXNkV2pJSDZhNnNBLTZoenU4RTJIRzFkV3Nha01jZHlVNzRRVTZtazJLWEJ3R2NmV1dMd3dkWXpPX2xraFN6RTZqbzBLVDcySTNfd3pXZ0EifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
@@ -523,10 +523,10 @@
|
||||
"classification": "L3",
|
||||
"expectedDisposition": "PUBLISH_TYPED_REPORT",
|
||||
"archive": {
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2UiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiIzMTkyOTMyY2FlOGJiYzhhNTczNGEzNjA1ZmEwZTQ0MzBkODI5MWRlMGZlYTM4OWQyOGU4MzFmYWYzYWU4YWEyIiwibm9uY2UiOiJLUnhnMF9zb2pDOTBwZ3BCUFh3STVBOXIyenI3R2JqQWZFNUZhRVoyQ19RIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6ImVkYzg0MjIwMmZlMDdjN2MzZTZjZDZmOTk5Y2U3MjVlZWVjMGIzMTYzZWJjODVkNWFkM2IwYjY3OWRhMzE1YTYiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI5OTgxMTYxNDU3MWY3MzUxNjViZDBhMjAzMjRmM2MwOTE4NjRkYmQ4NWRkNjUzZDg2OWNkYjE2MWIzMGViM2U0IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiRFpCeDJ2REs3U05zMlMyWWFkZmpzU19EbGZqSkxrSFpYSUdFR2p3Vk1BWVF5akNDU3REdkMza3hHMzhFRWZnWWd1Z0Q5cmEySGlKbXVCMmMyeXBGOEEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkYSIsInRvb2xJZCI6InRvcC5ycGMiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiS1J4ZzBfc29qQzkwcGdwQlBYd0k1QTlyMnpyN0diakFmRTVGYUVaMkNfUSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiYjhNek1YbE5jSU9wVUNBLXh6ODNhZ0xOS2JORXVHZkJoOURzVkVHNG1hTXd4eVVjVWtSR1NQcXNvRkZ5SHVaWkNXaXJrc1lOQjJTN3lOUjVqQVVsT0EifQ==",
|
||||
"manifestBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3Quc3VwcG9ydC5idW5kbGVNYW5pZmVzdC8xIiwicHJvdG9jb2xWZXJzaW9uIjoidjEiLCJidW5kbGVVaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAxM2UiLCJvcmdhbml6YXRpb25OYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJjbHVzdGVyTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiIsImRldmljZU5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMS9jbHVzdGVycy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDIvY2x1c3RlckRldmljZXMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwiZGV2aWNlS2V5SWQiOiI5ZWQxNDJmZWU1MTAzN2Q0NzgwYjMyYzk0M2VkYTk1MDIxNjNlN2JkMmU5MDBlNDFjNGY3OWE1NzA2NGMxNDRiIiwibm9uY2UiOiJLUnhnMF9zb2pDOTBwZ3BCUFh3STVBOXIyenI3R2JqQWZFNUZhRVoyQ19RIiwicHJvZHVjZWRBdCI6IjIwMjYtMDktMTNUMTE6NTk6MDBaIiwicmVkYWN0aW9uVmVyc2lvbiI6InJ1c3Rmcy5jb25uZWN0LnJlZGFjdGlvbi52MSIsInJ1bGVzZXRIYXNoIjoiYjM3NDM2ZDhlNzI1MTUzOTRhMTIyZDYzMzg2NWIxZGMwMjhkNGVjZTM0OTM1MmEwYTNhMjNmNTJjYTQyODVmMyIsImNsYXNzaWZpY2F0aW9uUmVnaXN0cnlWZXJzaW9uIjoxLCJlbnRyaWVzIjpbeyJwYXRoIjoiZW52ZWxvcGUuanNvbiIsInR5cGUiOiJvZmZsaW5lLWRpYWdub3N0aWMiLCJzaXplQnl0ZXMiOjEwMjAsInNoYTI1NiI6IjIzN2VmN2JhYzZiMTM4NDczZjcyZjlmOTY5MjNlYjVkMmVmZjJkNWRhZTNmZDQxYzM1ZTcxMjViMzI4NTE2YTAiLCJjbGFzc2lmaWNhdGlvbiI6IkwzIn0seyJwYXRoIjoiZW52ZWxvcGUuc2lnIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6MTkzLCJzaGEyNTYiOiI4MTQxY2ZiMWM1MTEyN2Y0NjMwMjRkYzA1YTMyNGNlODk1ODZjYjU4YWZlNTFhNDYxNDYyY2RlOTc5ZmQwMjQ3IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9LHsicGF0aCI6InJlc3VsdC5qc29uIiwidHlwZSI6Im9mZmxpbmUtZGlhZ25vc3RpYyIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2IiwiY2xhc3NpZmljYXRpb24iOiJMMyJ9XX0=",
|
||||
"manifestSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiUUwzQ3RzN3VWU04yZVIyWlZqQ3BodDJ0c1ZEMXphbWRTcVlRSnlYRXdHb1FVX2FXNUFRakpGN1ExZk9xZjFyeGVNQWdFLXhQWGtVRm5waC1nYjM5eGcifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkYSIsInRvb2xJZCI6InRvcC5ycGMiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiS1J4ZzBfc29qQzkwcGdwQlBYd0k1QTlyMnpyN0diakFmRTVGYUVaMkNfUSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoiY1lIRkRZNjhZSDBpWlh1Y2ZkYnhCTG9VZHVhQzN6eElfM2VJT2RhTTZLZGtiQ2hvRS0tWHVUVGUwVlp0enA2TC11SUdiYkY0b3ZRaDUzekV2dGtKRUEifQ==",
|
||||
"resultBytes": "eyJzY2hlbWFWZXJzaW9uIjoxLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzYiLCJ0b29sSWQiOiJ0b3AucnBjIiwiY2FwYWJpbGl0eSI6InRvcC5ycGNAMSIsIm91dGNvbWUiOiJTVUNDRUVERUQiLCJyZWFzb25Db2RlIjoiQ09NUExFVEUiLCJkdXJhdGlvbk1pbGxpcyI6MTAwMCwicHJvdmVuYW5jZSI6eyJyZXBvc2l0b3J5IjoicnVzdGZzL3J1c3RmcyIsInNvdXJjZUNvbW1pdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRhYmxlU2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1c3Rmc1ZlcnNpb24iOiIxLjAuMC1yYy42Iiwib3NGYW1pbHkiOiJMSU5VWCIsImFyY2hpdGVjdHVyZSI6ImFhcmNoNjQiLCJidWlsZEZlYXR1cmVzIjpbXX0sImNvdmVyYWdlIjp7InJlcXVlc3RlZFVuaXRzIjoxLCJjb21wbGV0ZWRVbml0cyI6MSwidW5pdCI6IldJTkRPVyJ9LCJkYXRhIjp7InJlcXVlc3RDb3VudCI6MiwiZXJyb3JDb3VudCI6MCwid2luZG93TWlsbGlzIjoxMDAwLCJ0b3RhbER1cmF0aW9uTWljcm9zIjo5MDB9fQ=="
|
||||
},
|
||||
"negativeVectors": {
|
||||
@@ -540,8 +540,8 @@
|
||||
},
|
||||
"foreignDevice": {
|
||||
"deviceName": "organizations/019f0000-0000-7000-8000-000000000001/clusters/019f0000-0000-7000-8000-000000000002/clusterDevices/019f0000-0000-7000-8000-000000000004",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkYSIsInRvb2xJZCI6InRvcC5ycGMiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiS1J4ZzBfc29qQzkwcGdwQlBYd0k1QTlyMnpyN0diakFmRTVGYUVaMkNfUSIsImRldmljZUtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiMzE5MjkzMmNhZThiYmM4YTU3MzRhMzYwNWZhMGU0NDMwZDgyOTFkZTBmZWEzODlkMjhlODMxZmFmM2FlOGFhMiIsInZhbHVlIjoiU04xUXZrcEhOUkhaWVF0NHZRNmtpVGJOa2JnVUlOdkhjRlVGMV8zS2Qzc0JJbDBYa3BzRDZuSG15a1JweW5zSm5Xb3RDdWplOGNsWmdXMk53dU91VVEifQ==",
|
||||
"envelopeBytes": "eyJmb3JtYXRWZXJzaW9uIjoicnVzdGZzLmNvbm5lY3QuZGlhZ25vc3RpY0VudmVsb3BlLzEiLCJwcm90b2NvbFZlcnNpb24iOiJ2MSIsIm9yZ2FuaXphdGlvbk5hbWUiOiJvcmdhbml6YXRpb25zLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImNsdXN0ZXJOYW1lIjoib3JnYW5pemF0aW9ucy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDEvY2x1c3RlcnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwiZGV2aWNlTmFtZSI6Im9yZ2FuaXphdGlvbnMvMDE5ZjAwMDAtMDAwMC03MDAwLTgwMDAtMDAwMDAwMDAwMDAxL2NsdXN0ZXJzLzAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMi9jbHVzdGVyRGV2aWNlcy8wMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJydW5VaWQiOiIwMTlmMDAwMC0wMDAwLTcwMDAtODAwMC0wMDAwMDAwMDAwNzYiLCJhcnRpZmFjdFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDBkYSIsInRvb2xJZCI6InRvcC5ycGMiLCJzY2hlbWFWZXJzaW9uIjoxLCJjbGFzc2lmaWNhdGlvbiI6IkwzIiwiY29uc2VudFVpZCI6IjAxOWYwMDAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInBvbGljeVJldmlzaW9uIjoyLCJwcm9kdWNlZEF0IjoiMjAyNi0wOS0xM1QxMTo1OTowMFoiLCJleHBpcmVzQXQiOiIyMDI2LTA5LTEzVDEyOjA5OjAwWiIsIm5vbmNlIjoiS1J4ZzBfc29qQzkwcGdwQlBYd0k1QTlyMnpyN0diakFmRTVGYUVaMkNfUSIsImRldmljZUtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInBheWxvYWQiOnsicGF0aCI6InJlc3VsdC5qc29uIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsInNpemVCeXRlcyI6NjEzLCJzaGEyNTYiOiIwYTFhMGM4ZmQzMWQ1YWE4MDBiYzY3MGY5NTdkNWQ2ZjBlYWIxNmJjOTA0NGJmYTI3Y2Q4ZTliNWQ1MWFkMjI2In19",
|
||||
"envelopeSignatureBytes": "eyJhbGdvcml0aG0iOiJFUzI1NiIsImtleUlkIjoiOWVkMTQyZmVlNTEwMzdkNDc4MGIzMmM5NDNlZGE5NTAyMTYzZTdiZDJlOTAwZTQxYzRmNzlhNTcwNjRjMTQ0YiIsInZhbHVlIjoicFJ4a3FDZk9zczFZa045cGI5cEhpSWowaVQ0eWpKQnJNVGxzVVVyejJVZFktem1uNld2dTRHd0JNMzNPckp4SU02R2UtenVIWlVrRDhYNlhzX0J1MVEifQ==",
|
||||
"expectedReason": "DEVICE_MISMATCH"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
29c93b4c8c27164896c5197e1df4ba03175425ccf5b12dc8e3f3d8d6bf19dc51 accept-vectors.json
|
||||
9423c9fd02b47940cfe54915e0f90706469de2111176053d8ac0c86f30e443ff health-service-accept-vectors.json
|
||||
166fbfb022ab7e6780547b21c53ca36f357e74b56355c84c8174393449942dfa health-service-reject-vectors.json
|
||||
fef527fdd89aa87fa9f81394eab6484fc2ac05a31559e1589149716193b336fb receipt-vectors.json
|
||||
219749ea7bdf73461108320e13ba14b604a1967e32139c5317a017ad95dab000 reject-vectors.json
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "diagnostic-scheduler",
|
||||
"fixture": "health-service-accept-vectors",
|
||||
"vectors": [
|
||||
{
|
||||
"id": "health-service.job.explicit-freshness",
|
||||
"schema": "job-envelope.schema.json",
|
||||
"value": {
|
||||
"jobId": "018cc251-f400-7abc-8def-0123456789ab",
|
||||
"protocolVersion": "v1",
|
||||
"jobType": "health.check",
|
||||
"schemaVersion": 1,
|
||||
"organizationName": "organizations/018cc251-f400-7abc-8def-0123456789ab",
|
||||
"clusterName": "organizations/018cc251-f400-7abc-8def-0123456789ab/clusters/018cc251-f400-7abc-8def-0123456789ac",
|
||||
"deviceName": "organizations/018cc251-f400-7abc-8def-0123456789ab/clusters/018cc251-f400-7abc-8def-0123456789ac/clusterDevices/018cc251-f400-7abc-8def-0123456789ad",
|
||||
"createTime": "2030-01-01T00:00:00Z",
|
||||
"expireTime": "2030-01-01T00:00:30Z",
|
||||
"nonce": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc",
|
||||
"requiredCapabilities": ["health.check.service@1"],
|
||||
"authorization": {
|
||||
"actorType": "BROWSER_USER",
|
||||
"actorName": "users/018cc251-f400-7abc-8def-0123456789ab",
|
||||
"requestId": "123e4567-e89b-42d3-a456-426614174001"
|
||||
},
|
||||
"limits": {
|
||||
"timeoutSeconds": 30,
|
||||
"maxOutputBytes": 262144,
|
||||
"maxMemoryBytes": 67108864,
|
||||
"maxCpuMillis": 5000
|
||||
},
|
||||
"parameters": {
|
||||
"artifactUid": "018cc251-f400-7abc-8def-0123456789ae",
|
||||
"consentUid": "018cc251-f400-7abc-8def-0123456789af",
|
||||
"consentPolicyRevision": 1,
|
||||
"consentExpiresAt": "2030-01-01T00:01:00Z",
|
||||
"maxEvidenceAgeSeconds": 300
|
||||
},
|
||||
"signature": {
|
||||
"algorithm": "Ed25519",
|
||||
"keyId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "health-service.result.current-partial-catalog",
|
||||
"schema": "health-service-result.schema.json",
|
||||
"value": {
|
||||
"schemaVersion": 1,
|
||||
"runUid": "018cc251-f400-7abc-8def-0123456789ab",
|
||||
"toolId": "health.check",
|
||||
"capability": "health.check.service@1",
|
||||
"outcome": "PARTIAL",
|
||||
"reasonCode": "CATALOG_PARTIAL",
|
||||
"durationMillis": 25,
|
||||
"provenance": {
|
||||
"repository": "rustfs/rustfs",
|
||||
"sourceCommit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"executableSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"rustfsVersion": "1.0.0",
|
||||
"osFamily": "LINUX",
|
||||
"architecture": "x86_64",
|
||||
"buildFeatures": []
|
||||
},
|
||||
"coverage": {
|
||||
"requestedUnits": 13,
|
||||
"completedUnits": 2,
|
||||
"unsupportedUnits": 11,
|
||||
"unit": "CHECK"
|
||||
},
|
||||
"data": {
|
||||
"catalogVersion": 1,
|
||||
"resourceAlias": "cluster-1",
|
||||
"observations": {
|
||||
"capacity": {
|
||||
"source": "LIVE_DISK_PROBE",
|
||||
"observedAt": "2030-01-01T00:00:00Z",
|
||||
"freshness": "CURRENT",
|
||||
"complete": true,
|
||||
"totalBytes": 100,
|
||||
"usedBytes": 50
|
||||
},
|
||||
"coarseFlags": {
|
||||
"source": "LIVE_DISK_PROBE",
|
||||
"observedAt": "2030-01-01T00:00:00Z",
|
||||
"freshness": "CURRENT",
|
||||
"complete": true,
|
||||
"values": []
|
||||
}
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"checkId": "capacity.exhausted",
|
||||
"outcome": "PASS",
|
||||
"reasonCode": "CAPACITY_REMAINING",
|
||||
"resourceAlias": "cluster-1"
|
||||
},
|
||||
{
|
||||
"checkId": "cluster.reportedFlags",
|
||||
"outcome": "PASS",
|
||||
"reasonCode": "NO_COARSE_CONDITION_REPORTED",
|
||||
"resourceAlias": "cluster-1"
|
||||
}
|
||||
],
|
||||
"unsupportedChecks": [
|
||||
{ "checkId": "memory.reportedPressure", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "tls.validity", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "cpu.sufficiency", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "swap.pressure", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "drive.health", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "inode.available", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "filesystem.compatibility", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "osKernel.compatibility", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "launch.permissions", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "version.consistency", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" },
|
||||
{ "checkId": "kubernetes.configuration", "reasonCode": "EVIDENCE_CONTRACT_UNAVAILABLE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "diagnostic-scheduler",
|
||||
"fixture": "health-service-reject-vectors",
|
||||
"baseFixture": "health-service-accept-vectors.json",
|
||||
"vectors": [
|
||||
{ "id": "health-service.job.old-node-capability", "base": "health-service.job.explicit-freshness", "path": "/requiredCapabilities/0", "replacement": "health.check@1", "reason": "UNSUPPORTED_TOOL" },
|
||||
{ "id": "health-service.job.missing-capability", "base": "health-service.job.explicit-freshness", "path": "/requiredCapabilities", "replacement": [], "reason": "UNSUPPORTED_TOOL" },
|
||||
{ "id": "health-service.job.missing-freshness-policy", "base": "health-service.job.explicit-freshness", "path": "/parameters/maxEvidenceAgeSeconds", "remove": true, "reason": "LIMIT_EXCEEDED" },
|
||||
{ "id": "health-service.job.profile-parameter", "base": "health-service.job.explicit-freshness", "path": "/parameters/durationMillis", "replacement": 1000, "reason": "INVALID" },
|
||||
{ "id": "health-service.job.oversized-result-budget", "base": "health-service.job.explicit-freshness", "path": "/limits/maxOutputBytes", "replacement": 262145, "reason": "LIMIT_EXCEEDED" },
|
||||
{ "id": "health-service.job.wrong-target", "base": "health-service.job.explicit-freshness", "path": "/deviceName", "replacement": "organizations/018cc251-f400-7abc-8def-0123456789ab/clusters/018cc251-f400-7abc-8def-0123456789ac/clusterDevices/018cc251-f400-7abc-8def-0123456789ae", "reason": "TARGET_MISMATCH" },
|
||||
{ "id": "health-service.job.wrong-nonce", "base": "health-service.job.explicit-freshness", "path": "/nonce", "replacement": "invalid", "reason": "INVALID" },
|
||||
{ "id": "health-service.job.wrong-signature", "base": "health-service.job.explicit-freshness", "path": "/signature/value", "replacement": "invalid", "reason": "SIGNATURE_INVALID" },
|
||||
{ "id": "health-service.job.revoked-consent", "base": "health-service.job.explicit-freshness", "path": "/parameters/consentExpiresAt", "replacement": "2029-12-31T23:59:59Z", "reason": "EXPIRED" },
|
||||
{ "id": "health-service.result.raw-admin-data", "base": "health-service.result.current-partial-catalog", "path": "/data/rawAdmin", "replacement": { "endpoint": "forbidden" }, "reason": "UNKNOWN_FIELD" },
|
||||
{ "id": "health-service.result.missing-unsupported", "base": "health-service.result.current-partial-catalog", "path": "/data/unsupportedChecks/10", "remove": true, "reason": "INCOMPLETE_COVERAGE" },
|
||||
{ "id": "health-service.result.path-leak", "base": "health-service.result.current-partial-catalog", "path": "/data/observations/capacity/path", "replacement": "/customer/data", "reason": "UNKNOWN_FIELD" }
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
975c1ca53eefeef6766a6fc0b3d3281f7408255342b0686e5e2aee5ad055414c duplicate.json
|
||||
963529a38a02849c6c2acc6d72668dca9f63218b49c89fae41a451b584850411 overflow.json
|
||||
60300d053dd3746a3aceb327315e8c5abe292fb6505dd02a64b8d6279fb56c09 producer-capabilities.json
|
||||
5d8ec114a5c3d9633d14ea3a227a0fdf6dfc04aca5de1fce39fce7d139b3dc08 producer-capabilities.json
|
||||
e3adeee1c8a19aa17e70894896fb79c072e3785bea3611b93c11e79f039ed5af stale.json
|
||||
22cc7337f545271ebf11ca6b76e7e479d62c51278ac001a8d870b07d0fa9884e unknown.json
|
||||
b06a72ff1d964efe65e996797705ec2ef72574a813aec5d8e9aff26c899f1352 valid.json
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"inventory.environment@1"
|
||||
],
|
||||
"producerCapabilities": [
|
||||
"health.check.service@1",
|
||||
"performance.client@1",
|
||||
"performance.drive@1",
|
||||
"performance.network@1",
|
||||
@@ -33,6 +34,7 @@
|
||||
"heartbeat",
|
||||
"diagnostics.policy.v1",
|
||||
"inventory.environment@1",
|
||||
"health.check.service@1",
|
||||
"performance.client@1",
|
||||
"performance.drive@1",
|
||||
"performance.network@1",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
a499742c06046e9cf2f781fc17d8b4cf95e018ab45fa41ee5944ba15b0e2902c auth
|
||||
9b54e39584a442270ae486e8378df1d6f168c45d0c59baba9459b46bb212d2df version
|
||||
159d116e965b51771f1687fc6ec9bdfbf2a4a0e6d6aa26d3f1046b3dac2697dd registration
|
||||
9b3862f5160968a2ff281afb3ea519a3d49ae1bc3d767153d70f309d2a7457a7 heartbeat
|
||||
8a92d233dd164f2648905595c9a31d533cc35b3bfcb4a79babd1f81a297332e6 heartbeat
|
||||
cc6c3f3bf5e6a938f2e00b8ce42d8fa7bfb974f4eb2927101d4ede4ebdc10dd5 inventory
|
||||
0207458ec9b97b368e2f347da112511a3aabd27d752890cacda4fa2408e7f19e object-performance
|
||||
a464d4c695d76dd985c0f2abf68c2135c080221098ad2427022f85243a7bf6ae network-performance
|
||||
813cbbf209e8fc9ddaafaeb4eb4745024b1c0edcb26634073a85023844141477 site-replication-performance
|
||||
fad21eec58d4d4d547893af9050cdc5eaf9d4f84b15fb423214b16be50e7ff7a telemetry
|
||||
484da2e7599148d36e1ac19d0ae5d305c4a96da7a00495a8ea202d3bae240a88 diagnostic-scheduler
|
||||
4f51f7916c8534ca6258785c0d90c83ea1465fba276ba180c73720ded2b71cd4 diagnostic-scheduler
|
||||
a2557ab1f7f70fb86a8affc8451596c272478fd2d5500c367b8f9e8385d72578 offline-enrollment
|
||||
8b6fd07759d1264489a8bb54d2dda4836ad0f59b5ee5d52966365918b3de4c9e bundle
|
||||
d9fcf28d326741e1d8f9be2cacf61bb93e7e1ac8a4260a6c1f68501ea050dd35 bundle
|
||||
42cc936dc8fe87335ebff5abe01e3ebf85bdada7f37b43bfd8add46435c9d6f9 redaction
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://connect.rustfs.com/schemas/agent/v1/health-service-result.schema.json",
|
||||
"title": "RustFS bounded health service result v1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schemaVersion", "runUid", "toolId", "capability", "outcome", "reasonCode", "durationMillis", "provenance", "coverage", "data"],
|
||||
"properties": {
|
||||
"schemaVersion": { "const": 1 },
|
||||
"runUid": { "$ref": "#/$defs/uuid7" },
|
||||
"toolId": { "const": "health.check" },
|
||||
"capability": { "const": "health.check.service@1" },
|
||||
"outcome": { "const": "PARTIAL" },
|
||||
"reasonCode": {
|
||||
"enum": ["CATALOG_PARTIAL", "EVIDENCE_INCOMPLETE", "EVIDENCE_STALE", "EVIDENCE_FRESHNESS_UNKNOWN", "CLOCK_SKEW"]
|
||||
},
|
||||
"durationMillis": { "type": "integer", "minimum": 0, "maximum": 30000 },
|
||||
"provenance": { "$ref": "#/$defs/provenance" },
|
||||
"coverage": { "$ref": "#/$defs/coverage" },
|
||||
"data": { "$ref": "#/$defs/data" }
|
||||
},
|
||||
"$defs": {
|
||||
"uuid7": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
},
|
||||
"provenance": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repository", "sourceCommit", "executableSha256", "rustfsVersion", "osFamily", "architecture", "buildFeatures"],
|
||||
"properties": {
|
||||
"repository": { "const": "rustfs/rustfs" },
|
||||
"sourceCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
||||
"executableSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
||||
"rustfsVersion": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"osFamily": { "enum": ["LINUX", "DARWIN", "WINDOWS", "FREEBSD", "OTHER"] },
|
||||
"architecture": { "enum": ["x86_64", "aarch64", "other"] },
|
||||
"buildFeatures": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,64}$" },
|
||||
"maxItems": 64,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"coverage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["requestedUnits", "completedUnits", "unsupportedUnits", "unit"],
|
||||
"properties": {
|
||||
"requestedUnits": { "const": 13 },
|
||||
"completedUnits": { "const": 2 },
|
||||
"unsupportedUnits": { "const": 11 },
|
||||
"unit": { "const": "CHECK" }
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["catalogVersion", "resourceAlias", "observations", "checks", "unsupportedChecks"],
|
||||
"properties": {
|
||||
"catalogVersion": { "const": 1 },
|
||||
"resourceAlias": { "const": "cluster-1" },
|
||||
"observations": { "$ref": "#/$defs/observations" },
|
||||
"checks": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/capacityCheck" },
|
||||
{ "$ref": "#/$defs/flagsCheck" }
|
||||
],
|
||||
"items": false,
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"unsupportedChecks": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/unsupportedMemory" },
|
||||
{ "$ref": "#/$defs/unsupportedTls" },
|
||||
{ "$ref": "#/$defs/unsupportedCpu" },
|
||||
{ "$ref": "#/$defs/unsupportedSwap" },
|
||||
{ "$ref": "#/$defs/unsupportedDrive" },
|
||||
{ "$ref": "#/$defs/unsupportedInode" },
|
||||
{ "$ref": "#/$defs/unsupportedFilesystem" },
|
||||
{ "$ref": "#/$defs/unsupportedOsKernel" },
|
||||
{ "$ref": "#/$defs/unsupportedLaunch" },
|
||||
{ "$ref": "#/$defs/unsupportedVersion" },
|
||||
{ "$ref": "#/$defs/unsupportedKubernetes" }
|
||||
],
|
||||
"items": false,
|
||||
"minItems": 11,
|
||||
"maxItems": 11
|
||||
}
|
||||
}
|
||||
},
|
||||
"observations": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["capacity", "coarseFlags"],
|
||||
"properties": {
|
||||
"capacity": { "$ref": "#/$defs/capacityObservation" },
|
||||
"coarseFlags": { "$ref": "#/$defs/flagsObservation" }
|
||||
}
|
||||
},
|
||||
"capacityObservation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["source", "observedAt", "freshness", "complete", "totalBytes", "usedBytes"],
|
||||
"properties": {
|
||||
"source": { "const": "LIVE_DISK_PROBE" },
|
||||
"observedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"freshness": { "$ref": "#/$defs/freshness" },
|
||||
"complete": { "type": "boolean" },
|
||||
"totalBytes": { "$ref": "#/$defs/nullableCounter" },
|
||||
"usedBytes": { "$ref": "#/$defs/nullableCounter" }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "complete": { "const": true } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"observedAt": { "type": "string", "format": "date-time" },
|
||||
"totalBytes": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
|
||||
"usedBytes": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"flagsObservation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["source", "observedAt", "freshness", "complete", "values"],
|
||||
"properties": {
|
||||
"source": { "const": "LIVE_DISK_PROBE" },
|
||||
"observedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||
"freshness": { "$ref": "#/$defs/freshness" },
|
||||
"complete": { "type": "boolean" },
|
||||
"values": {
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["capacity.critical", "capacity.warning", "clock.skew", "cluster.degraded", "cluster.healing", "cluster.readonly", "drive.offline", "node.offline"]
|
||||
},
|
||||
"maxItems": 8,
|
||||
"uniqueItems": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "complete": { "const": true } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"observedAt": { "type": "string", "format": "date-time" },
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["capacity.critical", "capacity.warning", "clock.skew", "cluster.degraded", "cluster.healing", "cluster.readonly", "drive.offline", "node.offline"]
|
||||
},
|
||||
"maxItems": 8,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"freshness": { "enum": ["CURRENT", "STALE", "UNKNOWN"] },
|
||||
"nullableCounter": {
|
||||
"type": ["integer", "null"],
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
},
|
||||
"capacityCheck": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/$defs/check" },
|
||||
{
|
||||
"properties": {
|
||||
"checkId": { "const": "capacity.exhausted" },
|
||||
"reasonCode": {
|
||||
"enum": ["CAPACITY_EXHAUSTED", "CAPACITY_REMAINING", "CAPACITY_UNAVAILABLE", "EVIDENCE_MISSING", "INVALID_EVIDENCE", "EVIDENCE_STALE", "EVIDENCE_FRESHNESS_UNKNOWN", "CLOCK_SKEW"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"flagsCheck": {
|
||||
"allOf": [
|
||||
{ "$ref": "#/$defs/check" },
|
||||
{
|
||||
"properties": {
|
||||
"checkId": { "const": "cluster.reportedFlags" },
|
||||
"reasonCode": {
|
||||
"enum": ["COARSE_CONDITION_REPORTED", "NO_COARSE_CONDITION_REPORTED", "EVIDENCE_MISSING", "INVALID_EVIDENCE", "EVIDENCE_STALE", "EVIDENCE_FRESHNESS_UNKNOWN", "CLOCK_SKEW"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"check": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["checkId", "outcome", "reasonCode", "resourceAlias"],
|
||||
"properties": {
|
||||
"checkId": { "type": "string" },
|
||||
"outcome": { "enum": ["PASS", "FAIL", "UNKNOWN"] },
|
||||
"reasonCode": { "type": "string" },
|
||||
"resourceAlias": { "const": "cluster-1" }
|
||||
}
|
||||
},
|
||||
"unsupported": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["checkId", "reasonCode"],
|
||||
"properties": {
|
||||
"checkId": { "type": "string" },
|
||||
"reasonCode": { "const": "EVIDENCE_CONTRACT_UNAVAILABLE" }
|
||||
}
|
||||
},
|
||||
"unsupportedMemory": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "memory.reportedPressure" } } }] },
|
||||
"unsupportedTls": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "tls.validity" } } }] },
|
||||
"unsupportedCpu": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "cpu.sufficiency" } } }] },
|
||||
"unsupportedSwap": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "swap.pressure" } } }] },
|
||||
"unsupportedDrive": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "drive.health" } } }] },
|
||||
"unsupportedInode": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "inode.available" } } }] },
|
||||
"unsupportedFilesystem": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "filesystem.compatibility" } } }] },
|
||||
"unsupportedOsKernel": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "osKernel.compatibility" } } }] },
|
||||
"unsupportedLaunch": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "launch.permissions" } } }] },
|
||||
"unsupportedVersion": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "version.consistency" } } }] },
|
||||
"unsupportedKubernetes": { "allOf": [{ "$ref": "#/$defs/unsupported" }, { "properties": { "checkId": { "const": "kubernetes.configuration" } } }] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://connect.rustfs.com/schemas/agent/v1/job-envelope.schema.json",
|
||||
"title": "RustFS Connect Agent Job Envelope v1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"jobId",
|
||||
"protocolVersion",
|
||||
"jobType",
|
||||
"schemaVersion",
|
||||
"organizationName",
|
||||
"clusterName",
|
||||
"deviceName",
|
||||
"createTime",
|
||||
"expireTime",
|
||||
"nonce",
|
||||
"requiredCapabilities",
|
||||
"authorization",
|
||||
"limits",
|
||||
"parameters",
|
||||
"signature"
|
||||
],
|
||||
"properties": {
|
||||
"jobId": { "type": "string", "format": "uuid", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"protocolVersion": { "const": "v1" },
|
||||
"jobType": { "enum": ["health.check", "profile.cpu", "profile.memory", "profile.threads", "performance.drive", "performance.network", "top.api", "top.disk", "top.locks", "top.rpc"] },
|
||||
"schemaVersion": { "const": 1 },
|
||||
"organizationName": { "type": "string", "pattern": "^organizations/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"clusterName": { "type": "string", "pattern": "^organizations/[0-9a-f-]{36}/clusters/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"deviceName": { "type": "string", "pattern": "^organizations/[0-9a-f-]{36}/clusters/[0-9a-f-]{36}/clusterDevices/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"createTime": { "type": "string", "format": "date-time" },
|
||||
"expireTime": { "type": "string", "format": "date-time" },
|
||||
"nonce": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43}$" },
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "enum": ["health.check.service@1", "profile.cpu@1", "profile.memory.service@1", "profile.threads@1", "performance.drive@1", "performance.network@1", "top.api@1", "top.disk@1", "top.locks@1", "top.rpc@1"] }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["timeoutSeconds", "maxOutputBytes", "maxMemoryBytes", "maxCpuMillis"],
|
||||
"properties": {
|
||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 30 },
|
||||
"maxOutputBytes": { "type": "integer", "minimum": 1, "maximum": 524288 },
|
||||
"maxMemoryBytes": { "type": "integer", "minimum": 1, "maximum": 67108864 },
|
||||
"maxCpuMillis": { "type": "integer", "minimum": 1, "maximum": 30000 }
|
||||
}
|
||||
},
|
||||
"authorization": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["actorType", "actorName", "requestId"],
|
||||
"properties": {
|
||||
"actorType": { "const": "BROWSER_USER" },
|
||||
"actorName": { "type": "string", "pattern": "^users/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"requestId": { "type": "string", "format": "uuid", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["artifactUid", "consentUid", "consentPolicyRevision", "consentExpiresAt"],
|
||||
"properties": {
|
||||
"artifactUid": { "type": "string", "format": "uuid", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"consentUid": { "type": "string", "format": "uuid", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
||||
"consentPolicyRevision": { "type": "integer", "minimum": 1 },
|
||||
"consentExpiresAt": { "type": "string", "format": "date-time" },
|
||||
"threadScope": { "enum": ["NATIVE_THREADS", "TOKIO_RUNTIME"] },
|
||||
"durationMillis": { "type": "integer", "minimum": 1, "maximum": 30000 },
|
||||
"samplePeriodMicros": { "type": "integer", "minimum": 1, "maximum": 30000000 },
|
||||
"maxEvidenceAgeSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||
"targetAlias": { "const": "drive-1" },
|
||||
"scratchBytes": { "const": 524288 },
|
||||
"blockBytes": { "const": 65536 },
|
||||
"trafficBytes": { "type": "integer", "minimum": 1, "maximum": 1048576 }
|
||||
}
|
||||
},
|
||||
"signature": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["algorithm", "keyId", "value"],
|
||||
"properties": {
|
||||
"algorithm": { "const": "Ed25519" },
|
||||
"keyId": { "type": "string", "minLength": 1 },
|
||||
"value": { "type": "string", "minLength": 43 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "health.check" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "health.check.service@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeoutSeconds": { "const": 30 },
|
||||
"maxOutputBytes": { "const": 262144 },
|
||||
"maxMemoryBytes": { "const": 67108864 },
|
||||
"maxCpuMillis": { "const": 5000 }
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"required": ["maxEvidenceAgeSeconds"],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{ "required": ["durationMillis"] },
|
||||
{ "required": ["samplePeriodMicros"] },
|
||||
{ "required": ["targetAlias"] },
|
||||
{ "required": ["scratchBytes"] },
|
||||
{ "required": ["blockBytes"] },
|
||||
{ "required": ["trafficBytes"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"required": ["durationMillis", "samplePeriodMicros"],
|
||||
"not": { "required": ["maxEvidenceAgeSeconds"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "performance.drive" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "performance.drive@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": { "maxCpuMillis": { "type": "integer", "maximum": 5000 } }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"required": ["targetAlias", "scratchBytes", "blockBytes"],
|
||||
"properties": {
|
||||
"durationMillis": { "const": 5000 },
|
||||
"targetAlias": { "const": "drive-1" },
|
||||
"scratchBytes": { "const": 524288 },
|
||||
"blockBytes": { "const": 65536 }
|
||||
},
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{ "required": ["targetAlias"] },
|
||||
{ "required": ["scratchBytes"] },
|
||||
{ "required": ["blockBytes"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "performance.network" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "performance.network@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": { "maxCpuMillis": { "type": "integer", "maximum": 5000 } }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"required": ["trafficBytes"],
|
||||
"properties": { "trafficBytes": { "type": "integer", "minimum": 1, "maximum": 1048576 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "profile.cpu" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "profile.cpu@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "profile.memory" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "profile.memory.service@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxCpuMillis": { "type": "integer", "maximum": 5000 },
|
||||
"maxMemoryBytes": { "const": 67108864 }
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "profile.threads" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"limits": { "properties": { "maxMemoryBytes": { "const": 67108864 }, "maxCpuMillis": { "const": 30000 } } },
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "profile.threads@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "top.api" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "top.api@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": { "maxCpuMillis": { "type": "integer", "maximum": 5000 } }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "top.disk" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "top.disk@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxCpuMillis": { "type": "integer", "maximum": 5000 },
|
||||
"maxMemoryBytes": { "type": "integer", "minimum": 1048576 }
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "top.rpc" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "top.rpc@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": { "maxCpuMillis": { "type": "integer", "maximum": 5000 } }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "top.locks" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"requiredCapabilities": {
|
||||
"type": "array",
|
||||
"prefixItems": [{ "const": "top.locks@1" }],
|
||||
"items": false,
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": { "maxCpuMillis": { "type": "integer", "maximum": 5000 } }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": { "trafficBytes": true },
|
||||
"required": ["trafficBytes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "jobType": { "const": "profile.threads" } } },
|
||||
"then": { "properties": { "parameters": { "required": ["threadScope"] } } },
|
||||
"else": { "properties": { "parameters": { "not": { "required": ["threadScope"] } } } }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -320,7 +320,7 @@ impl Operation for InfoCannedPolicy {
|
||||
error = ?e,
|
||||
"admin policy state"
|
||||
);
|
||||
S3Error::with_message(S3ErrorCode::InternalError, e.to_string())
|
||||
iam_error_to_s3_error(e)
|
||||
})?;
|
||||
|
||||
let body = serde_json::to_vec(&pd).map_err(|e| s3_error!(InternalError, "failed to serialize response: {:?}", e))?;
|
||||
@@ -1271,4 +1271,24 @@ mod tests {
|
||||
|
||||
assert!(!production.contains("check_key_valid(get_session_token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_canned_policy_maps_iam_errors_to_s3_errors() {
|
||||
let production = include_str!("policies.rs")
|
||||
.split("\n#[cfg(test)]\n")
|
||||
.next()
|
||||
.expect("production source must precede tests");
|
||||
let body = source_block(production, "impl Operation for InfoCannedPolicy");
|
||||
|
||||
let lookup = body
|
||||
.find("info_policy(")
|
||||
.expect("InfoCannedPolicy should look the policy up through the IAM store");
|
||||
let tail = &body[lookup..];
|
||||
let end = tail.find("?;").unwrap_or(tail.len());
|
||||
|
||||
assert!(
|
||||
tail[..end].contains("iam_error_to_s3_error(e)"),
|
||||
"a missing policy must map through iam_error_to_s3_error (404 NoSuchResource) instead of 500 InternalError"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4761,12 +4761,14 @@ where
|
||||
"manifest changed entries must belong to the committed snapshot"
|
||||
));
|
||||
}
|
||||
// Rewritten files may preserve an older data sequence number.
|
||||
if status == 1
|
||||
&& manifest.location.sequence_number.is_some_and(|sequence_number| {
|
||||
reference.sequence_number != Some(sequence_number) || reference.file_sequence_number != Some(sequence_number)
|
||||
})
|
||||
&& manifest
|
||||
.location
|
||||
.sequence_number
|
||||
.is_some_and(|sequence_number| reference.file_sequence_number != Some(sequence_number))
|
||||
{
|
||||
return Err(s3_error!(InvalidRequest, "added manifest entry sequence must match its manifest"));
|
||||
return Err(s3_error!(InvalidRequest, "added manifest entry file sequence must match its manifest"));
|
||||
}
|
||||
if status == 2
|
||||
&& context
|
||||
|
||||
@@ -8129,6 +8129,118 @@ async fn row_level_conflict_rejects_changed_inherited_manifest_identity() {
|
||||
assert_eq!(unchanged.generation, current.generation);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn row_level_conflict_rewrite_data_sequence_numbers() {
|
||||
use crate::table_catalog::test_support::{
|
||||
manifest_avro_bytes_with_entry_sequences, manifest_list_avro_entries_with_min_sequences,
|
||||
};
|
||||
|
||||
let invalid_data_sequence = "Iceberg v2 manifest entry sequence number exceeds its manifest sequence";
|
||||
let invalid_file_sequence = "added manifest entry file sequence must match its manifest";
|
||||
// A rewrite preserves the data age while adding a new physical file.
|
||||
for (data_sequence, file_sequence, expected_error) in [
|
||||
(Some(1), None, None),
|
||||
(Some(1), Some(2), None),
|
||||
(None, None, None),
|
||||
(Some(0), Some(2), None),
|
||||
(Some(2), Some(2), None),
|
||||
(Some(1), Some(1), Some(invalid_file_sequence)),
|
||||
(Some(-1), Some(2), Some(invalid_data_sequence)),
|
||||
(Some(3), Some(2), Some(invalid_data_sequence)),
|
||||
] {
|
||||
let store = TestTableCatalogStore::default();
|
||||
let backend = TestTableCatalogObjectBackend::content_addressed();
|
||||
let namespace = crate::table_catalog::Namespace::parse("analytics").expect("namespace should parse");
|
||||
let created = create_standard_events_table(&store, &backend, &namespace).await;
|
||||
let location = created.metadata["location"].as_str().expect("table location should exist");
|
||||
let old_file = format!("{location}/data/old.parquet");
|
||||
let old_list = format!("{location}/metadata/snap-10.avro");
|
||||
seed_test_snapshot_manifest(&backend, "warehouse", &old_list, 10, 1, &[(&old_file, 0, 1, 10, 1)]).await;
|
||||
let append = serde_json::from_value(serde_json::json!({
|
||||
"updates": [
|
||||
{"action": "add-snapshot", "snapshot": {
|
||||
"snapshot-id": 10, "sequence-number": 1, "timestamp-ms": 1234,
|
||||
"manifest-list": old_list, "summary": {"operation": "append"}
|
||||
}},
|
||||
{"action": "set-snapshot-ref", "ref-name": "main", "snapshot-id": 10, "type": "branch"}
|
||||
]
|
||||
}))
|
||||
.expect("append request should parse");
|
||||
commit_table_response(&store, &trusted_table_commit_backend(&backend), "warehouse", &namespace, "events", append)
|
||||
.await
|
||||
.expect("initial append should succeed");
|
||||
let before = store.load_table("warehouse", "analytics", "events").await.unwrap().unwrap();
|
||||
|
||||
let new_file = format!("{location}/data/rewritten.parquet");
|
||||
let manifest = format!("{location}/metadata/rewrite.avro");
|
||||
let manifest_list = format!("{location}/metadata/snap-11.avro");
|
||||
let manifest_bytes = manifest_avro_bytes_with_entry_sequences(&[
|
||||
(&old_file, 0, 2, 11, Some(1), Some(1)),
|
||||
(&new_file, 0, 1, 11, data_sequence, file_sequence),
|
||||
]);
|
||||
let list_bytes = manifest_list_avro_entries_with_min_sequences(&[(
|
||||
&manifest,
|
||||
manifest_bytes.len(),
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
data_sequence.unwrap_or(2).clamp(0, 2),
|
||||
11,
|
||||
)]);
|
||||
backend
|
||||
.put_bytes("warehouse", &test_snapshot_object_key("warehouse", &manifest), manifest_bytes)
|
||||
.await;
|
||||
backend
|
||||
.put_bytes("warehouse", &test_snapshot_object_key("warehouse", &manifest_list), list_bytes)
|
||||
.await;
|
||||
backend
|
||||
.put_bytes("warehouse", &test_snapshot_object_key("warehouse", &new_file), b"data".to_vec())
|
||||
.await;
|
||||
let rewrite = serde_json::from_value(serde_json::json!({
|
||||
"requirements": [{"type": "assert-ref-snapshot-id", "ref": "main", "snapshot-id": 10}],
|
||||
"updates": [
|
||||
{"action": "add-snapshot", "snapshot": {
|
||||
"snapshot-id": 11, "parent-snapshot-id": 10, "sequence-number": 2,
|
||||
"timestamp-ms": 2234, "manifest-list": manifest_list,
|
||||
"summary": {"operation": "replace"}
|
||||
}},
|
||||
{"action": "set-snapshot-ref", "ref-name": "main", "snapshot-id": 11, "type": "branch"}
|
||||
]
|
||||
}))
|
||||
.expect("rewrite request should parse");
|
||||
let result = commit_table_response(
|
||||
&store,
|
||||
&trusted_table_commit_backend(&backend),
|
||||
"warehouse",
|
||||
&namespace,
|
||||
"events",
|
||||
rewrite,
|
||||
)
|
||||
.await;
|
||||
let after = store.load_table("warehouse", "analytics", "events").await.unwrap().unwrap();
|
||||
if expected_error.is_none() {
|
||||
let commit = result.unwrap_or_else(|err| panic!("data={data_sequence:?}, file={file_sequence:?}: {err}"));
|
||||
assert_eq!(commit.metadata["current-snapshot-id"], 11);
|
||||
assert_eq!(commit.metadata["last-sequence-number"], 2);
|
||||
assert_eq!(after.generation, before.generation + 1);
|
||||
let live_files = load_snapshot_live_files(&backend, "warehouse", &after, &commit.metadata, Some(11))
|
||||
.await
|
||||
.expect("committed snapshot should load");
|
||||
assert!(!live_files.data_files.contains_key(&old_file));
|
||||
let added = live_files.data_files.get(&new_file).expect("rewritten file should be live");
|
||||
assert_eq!(added.sequence_number, Some(data_sequence.unwrap_or(2)));
|
||||
assert_eq!(added.file_sequence_number, Some(2));
|
||||
} else {
|
||||
let error = result.expect_err("invalid sequence numbers must be rejected");
|
||||
assert_eq!(error.code(), &S3ErrorCode::InvalidRequest);
|
||||
assert_eq!(error.message(), expected_error);
|
||||
assert_eq!(after.metadata_location, before.metadata_location);
|
||||
assert_eq!(after.generation, before.generation);
|
||||
assert_eq!(after.version_token, before.version_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table-driven coverage for stale or historical manifest sequence failures.
|
||||
#[tokio::test]
|
||||
async fn row_level_conflict_rejects_stale_or_historical_manifest_sequences() {
|
||||
@@ -8142,7 +8254,7 @@ async fn row_level_conflict_rejects_stale_or_historical_manifest_sequences() {
|
||||
"new manifest sequence must match the committed snapshot",
|
||||
),
|
||||
(
|
||||
"stale-added-entry-sequence",
|
||||
"stale-added-file-sequence",
|
||||
2,
|
||||
"11",
|
||||
11,
|
||||
|
||||
@@ -510,25 +510,18 @@ impl Operation for RemoveUser {
|
||||
return Err(s3_error!(InternalError, "iam is not initialized"));
|
||||
};
|
||||
|
||||
let (is_temp, _) = iam_store.is_temp_user(ak).await.map_err(|e| {
|
||||
S3Error::with_message(S3ErrorCode::InternalError, format!("failed to query temporary user state: {e}"))
|
||||
})?;
|
||||
let (is_temp, _) = iam_store.is_temp_user(ak).await.map_err(iam_error_to_s3_error)?;
|
||||
|
||||
if is_temp {
|
||||
return Err(s3_error!(InvalidArgument, "cannot remove a temporary user"));
|
||||
}
|
||||
|
||||
let (is_service_account, _) = iam_store.is_service_account(ak).await.map_err(|e| {
|
||||
S3Error::with_message(S3ErrorCode::InternalError, format!("failed to query service account state: {e}"))
|
||||
})?;
|
||||
let (is_service_account, _) = iam_store.is_service_account(ak).await.map_err(iam_error_to_s3_error)?;
|
||||
if is_service_account {
|
||||
return Err(s3_error!(InvalidArgument, "cannot remove a service account"));
|
||||
}
|
||||
|
||||
iam_store
|
||||
.delete_user(ak, true)
|
||||
.await
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("failed to delete user: {e}")))?;
|
||||
iam_store.delete_user(ak, true).await.map_err(iam_error_to_s3_error)?;
|
||||
|
||||
if let Err(err) = site_replication_iam_change_hook(SRIAMItem {
|
||||
r#type: "iam-user".to_string(),
|
||||
@@ -1442,6 +1435,27 @@ mod tests {
|
||||
assert!(include_str!("user.rs").contains(mapper_call));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_user_maps_iam_errors_to_s3_errors() {
|
||||
let production = include_str!("user.rs")
|
||||
.split("\n#[cfg(test)]\n")
|
||||
.next()
|
||||
.expect("production source must precede tests");
|
||||
let body = source_block(production, "impl Operation for RemoveUser");
|
||||
|
||||
for call in ["is_temp_user(ak)", "is_service_account(ak)", "delete_user(ak, true)"] {
|
||||
let start = body
|
||||
.find(call)
|
||||
.unwrap_or_else(|| panic!("RemoveUser should call {call} through the IAM store"));
|
||||
let tail = &body[start..];
|
||||
let end = tail.find("?;").unwrap_or(tail.len());
|
||||
assert!(
|
||||
tail[..end].contains("iam_error_to_s3_error"),
|
||||
"{call} must map through iam_error_to_s3_error so a missing user is 404 NoSuchResource, not 500 InternalError"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_iam_enqueues_a_site_replication_snapshot() {
|
||||
let body = source_block(include_str!("user.rs"), "impl Operation for ImportIam");
|
||||
|
||||
@@ -15,27 +15,50 @@
|
||||
//! MinIO-compatible metadata listing extension routes.
|
||||
|
||||
use super::bucket_usecase::DefaultBucketUsecase;
|
||||
use super::object_usecase::DefaultObjectUsecase;
|
||||
use crate::app::runtime_sources::ServerContextSlot;
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
use crate::storage::access::{ReqInfo, authorize_request, req_info_mut};
|
||||
use crate::storage::{get_bucket_website_config_for_store, validate_website_configuration};
|
||||
use async_trait::async_trait;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::header::HOST;
|
||||
use http::{Extensions, HeaderMap, HeaderValue, Method, Uri};
|
||||
use http::{Extensions, HeaderMap, HeaderValue, Method, StatusCode, Uri};
|
||||
use rustfs_policy::policy::action::{Action, S3Action};
|
||||
use s3s::dto::{EncodingType, ListObjectVersionsInput, ListObjectsV2Input};
|
||||
use s3s::access::S3Access;
|
||||
use s3s::dto::{
|
||||
ETagCondition, EncodingType, GetObjectInput, GetObjectOutput, HeadObjectInput, ListObjectVersionsInput, ListObjectsV2Input,
|
||||
Range, RoutingRule, Timestamp, TimestampFormat,
|
||||
};
|
||||
use s3s::host::{MultiDomain, S3Host};
|
||||
use s3s::route::S3Route;
|
||||
use s3s::xml;
|
||||
use s3s::{Body, S3Error, S3Request, S3Response, S3Result, s3_error};
|
||||
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
||||
use std::sync::Arc;
|
||||
use url::form_urlencoded;
|
||||
|
||||
pub(crate) struct MetadataRoute<A> {
|
||||
admin: A,
|
||||
host: Option<MultiDomain>,
|
||||
website_domains: Vec<String>,
|
||||
website_scheme: &'static str,
|
||||
server_ctx: Arc<ServerContextSlot>,
|
||||
}
|
||||
|
||||
pub(crate) fn with_metadata_route<A>(admin: A, host: Option<MultiDomain>) -> MetadataRoute<A> {
|
||||
MetadataRoute { admin, host }
|
||||
pub(crate) fn with_metadata_route<A>(
|
||||
admin: A,
|
||||
host: Option<MultiDomain>,
|
||||
website_domains: Vec<String>,
|
||||
website_scheme: &'static str,
|
||||
server_ctx: Arc<ServerContextSlot>,
|
||||
) -> MetadataRoute<A> {
|
||||
MetadataRoute {
|
||||
admin,
|
||||
host,
|
||||
website_domains,
|
||||
website_scheme,
|
||||
server_ctx,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -44,12 +67,16 @@ where
|
||||
A: S3Route,
|
||||
{
|
||||
fn is_match(&self, method: &Method, uri: &Uri, headers: &HeaderMap, extensions: &mut Extensions) -> bool {
|
||||
metadata_operation(method, uri, headers, self.host.as_ref()).is_some()
|
||||
website_target(uri, headers, &self.website_domains).is_some()
|
||||
|| metadata_operation(method, uri, headers, self.host.as_ref()).is_some()
|
||||
|| self.admin.is_match(method, uri, headers, extensions)
|
||||
}
|
||||
|
||||
async fn check_access(&self, req: &mut S3Request<Body>) -> S3Result<()> {
|
||||
if let Some(target) = metadata_operation(&req.method, &req.uri, &req.headers, self.host.as_ref()) {
|
||||
if let Some(target) = website_target(&req.uri, &req.headers, &self.website_domains) {
|
||||
req.extensions.insert(Arc::clone(&self.server_ctx));
|
||||
check_website_access(req, &target).await
|
||||
} else if let Some(target) = metadata_operation(&req.method, &req.uri, &req.headers, self.host.as_ref()) {
|
||||
check_metadata_access(req, target).await
|
||||
} else {
|
||||
self.admin.check_access(req).await
|
||||
@@ -57,6 +84,12 @@ where
|
||||
}
|
||||
|
||||
async fn call(&self, req: S3Request<Body>) -> S3Result<S3Response<Body>> {
|
||||
if let Some(target) = website_target(&req.uri, &req.headers, &self.website_domains) {
|
||||
let head = req.method == Method::HEAD;
|
||||
return call_website(req, target, Arc::clone(&self.server_ctx), self.website_scheme)
|
||||
.await
|
||||
.or_else(|err: S3Error| Ok(website_error(err.status_code().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), head)));
|
||||
}
|
||||
match metadata_operation(&req.method, &req.uri, &req.headers, self.host.as_ref()) {
|
||||
Some(BucketTarget {
|
||||
operation: MetadataOperation::ListObjectVersions,
|
||||
@@ -71,6 +104,511 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WebsiteTarget {
|
||||
bucket: String,
|
||||
key: String,
|
||||
trailing_slash: bool,
|
||||
invalid_path: bool,
|
||||
}
|
||||
|
||||
fn website_target(uri: &Uri, headers: &HeaderMap, domains: &[String]) -> Option<WebsiteTarget> {
|
||||
if domains.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let host_header = headers.get(HOST)?.to_str().ok()?;
|
||||
let authority = host_header.parse::<http::uri::Authority>().ok()?;
|
||||
let request_host = authority.host().trim_end_matches('.').to_ascii_lowercase();
|
||||
let bucket = domains.iter().find_map(|domain| {
|
||||
if request_host == *domain {
|
||||
return Some(String::new());
|
||||
}
|
||||
request_host.strip_suffix(&format!(".{domain}")).map(str::to_owned)
|
||||
})?;
|
||||
let raw_path = uri.path();
|
||||
let decoded = urlencoding::decode(raw_path);
|
||||
let invalid_path = decoded.is_err();
|
||||
let decoded = decoded.unwrap_or_default();
|
||||
let path = decoded.trim_start_matches('/');
|
||||
Some(WebsiteTarget {
|
||||
bucket,
|
||||
key: path.to_owned(),
|
||||
trailing_slash: path.is_empty() || path.ends_with('/'),
|
||||
invalid_path,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_website_access(req: &mut S3Request<Body>, target: &WebsiteTarget) -> S3Result<()> {
|
||||
if req_info_mut(req).is_err() {
|
||||
req.extensions.insert(ReqInfo::default());
|
||||
}
|
||||
let req_info = req_info_mut(req)?;
|
||||
req_info.cred = None;
|
||||
req_info.is_owner = false;
|
||||
req_info.bucket = Some(target.bucket.clone());
|
||||
req_info.object = Some(target.key.clone());
|
||||
req_info.version_id = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn call_website(
|
||||
req: S3Request<Body>,
|
||||
target: WebsiteTarget,
|
||||
server_ctx: Arc<ServerContextSlot>,
|
||||
website_scheme: &'static str,
|
||||
) -> S3Result<S3Response<Body>> {
|
||||
if !matches!(req.method, Method::GET | Method::HEAD) {
|
||||
return Ok(website_error(StatusCode::METHOD_NOT_ALLOWED, req.method == Method::HEAD));
|
||||
}
|
||||
if target.invalid_path {
|
||||
return Ok(website_error(StatusCode::BAD_REQUEST, req.method == Method::HEAD));
|
||||
}
|
||||
if target.bucket.is_empty() || target.bucket.starts_with('.') || target.bucket.ends_with('.') || target.bucket.contains("..")
|
||||
{
|
||||
return Ok(website_error(StatusCode::NOT_FOUND, req.method == Method::HEAD));
|
||||
}
|
||||
|
||||
let store = server_ctx
|
||||
.installed_object_store()
|
||||
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "website object store unavailable"))?;
|
||||
let config = match get_bucket_website_config_for_store(&store, &target.bucket).await {
|
||||
Ok(config) => config,
|
||||
Err(crate::storage::StorageError::ConfigNotFound) => {
|
||||
return Ok(website_error(StatusCode::NOT_FOUND, req.method == Method::HEAD));
|
||||
}
|
||||
Err(crate::storage::StorageError::BucketNotFound(_)) => {
|
||||
return Ok(website_error(StatusCode::NOT_FOUND, req.method == Method::HEAD));
|
||||
}
|
||||
Err(err) => return Err(crate::error::ApiError::from(err).into()),
|
||||
};
|
||||
if validate_website_configuration(&config).is_err() {
|
||||
return Ok(website_error(StatusCode::INTERNAL_SERVER_ERROR, req.method == Method::HEAD));
|
||||
}
|
||||
|
||||
if let Some(redirect) = config.redirect_all_requests_to.as_ref() {
|
||||
return website_redirect(
|
||||
&req,
|
||||
redirect.host_name.as_str(),
|
||||
redirect.protocol.as_ref().map(|protocol| protocol.as_str()),
|
||||
&target.key,
|
||||
StatusCode::MOVED_PERMANENTLY,
|
||||
website_scheme,
|
||||
);
|
||||
}
|
||||
|
||||
let key = if target.key.is_empty() || target.trailing_slash {
|
||||
let suffix = config
|
||||
.index_document
|
||||
.as_ref()
|
||||
.map(|doc| doc.suffix.as_str())
|
||||
.unwrap_or_default();
|
||||
format!("{}{}", target.key, suffix)
|
||||
} else {
|
||||
target.key.clone()
|
||||
};
|
||||
|
||||
let rules = config.routing_rules.as_deref().unwrap_or_default();
|
||||
// A rule requiring a status cannot be evaluated until the object lookup completes.
|
||||
for rule in rules {
|
||||
if rule
|
||||
.condition
|
||||
.as_ref()
|
||||
.and_then(|condition| condition.http_error_code_returned_equals.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
if website_rule_matches(rule, &target.key, StatusCode::OK) {
|
||||
return website_rule_redirect(&req, rule, &target.key, website_scheme);
|
||||
}
|
||||
}
|
||||
|
||||
let response = website_get_object(&req, &server_ctx, &target.bucket, &key, true).await;
|
||||
if let Err(err) = &response
|
||||
&& matches!(
|
||||
err.code(),
|
||||
S3ErrorCode::NotModified | S3ErrorCode::PreconditionFailed | S3ErrorCode::InvalidRange
|
||||
)
|
||||
&& let Ok(Some(location)) = website_head_redirect(&req, &server_ctx, &target.bucket, &key).await
|
||||
{
|
||||
return website_location(location, StatusCode::MOVED_PERMANENTLY);
|
||||
}
|
||||
let status = match &response {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(err) => err.status_code().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
if status == StatusCode::NOT_FOUND && !target.trailing_slash && !target.key.is_empty() {
|
||||
let index_key = format!(
|
||||
"{}/{}",
|
||||
target.key,
|
||||
config
|
||||
.index_document
|
||||
.as_ref()
|
||||
.map(|doc| doc.suffix.as_str())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
match website_get_object(&req, &server_ctx, &target.bucket, &index_key, false).await {
|
||||
Ok(_) => return website_relative_redirect(&req, &format!("/{}/", target.key), StatusCode::FOUND),
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.code(),
|
||||
S3ErrorCode::NoSuchKey | S3ErrorCode::NoSuchVersion | S3ErrorCode::AccessDenied
|
||||
) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
if let Some(rule) = rules.iter().find(|rule| website_rule_matches(rule, &target.key, status)) {
|
||||
return website_rule_redirect(&req, rule, &target.key, website_scheme);
|
||||
}
|
||||
match response {
|
||||
Ok(response) => {
|
||||
if let Some(location) = response.output.website_redirect_location.clone() {
|
||||
return website_location(location, StatusCode::MOVED_PERMANENTLY);
|
||||
}
|
||||
response_to_website(response, req.method == Method::HEAD)
|
||||
}
|
||||
Err(err) if matches!(err.code(), S3ErrorCode::NoSuchKey | S3ErrorCode::NoSuchVersion) => {
|
||||
website_error_with_document(&req, &server_ctx, &target.bucket, config.error_document.as_ref(), StatusCode::NOT_FOUND)
|
||||
.await
|
||||
}
|
||||
Err(err) if err.code() == &S3ErrorCode::NotModified => Ok(website_error(StatusCode::NOT_MODIFIED, true)),
|
||||
Err(_) if status.is_client_error() => {
|
||||
website_error_with_document(&req, &server_ctx, &target.bucket, config.error_document.as_ref(), status).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn website_get_object(
|
||||
original: &S3Request<Body>,
|
||||
server_ctx: &Arc<ServerContextSlot>,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
apply_conditions: bool,
|
||||
) -> S3Result<S3Response<GetObjectOutput>> {
|
||||
let header = |name: http::header::HeaderName| {
|
||||
original
|
||||
.headers
|
||||
.get(name)
|
||||
.map(|value| {
|
||||
value
|
||||
.to_str()
|
||||
.map(str::to_owned)
|
||||
.map_err(|_| S3Error::new(S3ErrorCode::InvalidArgument))
|
||||
})
|
||||
.transpose()
|
||||
};
|
||||
let input = if apply_conditions {
|
||||
GetObjectInput {
|
||||
bucket: bucket.to_owned(),
|
||||
key: key.to_owned(),
|
||||
range: header(http::header::RANGE)?
|
||||
.map(|value| Range::parse(&value).map_err(|_| S3Error::new(S3ErrorCode::InvalidRange)))
|
||||
.transpose()?,
|
||||
if_match: original
|
||||
.headers
|
||||
.get(http::header::IF_MATCH)
|
||||
.map(|value| {
|
||||
ETagCondition::parse_http_header(value.as_bytes()).map_err(|_| S3Error::new(S3ErrorCode::InvalidArgument))
|
||||
})
|
||||
.transpose()?,
|
||||
if_none_match: original
|
||||
.headers
|
||||
.get(http::header::IF_NONE_MATCH)
|
||||
.map(|value| {
|
||||
ETagCondition::parse_http_header(value.as_bytes()).map_err(|_| S3Error::new(S3ErrorCode::InvalidArgument))
|
||||
})
|
||||
.transpose()?,
|
||||
if_modified_since: header(http::header::IF_MODIFIED_SINCE)?
|
||||
.map(|value| {
|
||||
Timestamp::parse(TimestampFormat::HttpDate, &value).map_err(|_| S3Error::new(S3ErrorCode::InvalidArgument))
|
||||
})
|
||||
.transpose()?,
|
||||
if_unmodified_since: header(http::header::IF_UNMODIFIED_SINCE)?
|
||||
.map(|value| {
|
||||
Timestamp::parse(TimestampFormat::HttpDate, &value).map_err(|_| S3Error::new(S3ErrorCode::InvalidArgument))
|
||||
})
|
||||
.transpose()?,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
GetObjectInput {
|
||||
bucket: bucket.to_owned(),
|
||||
key: key.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let mut request = S3Request {
|
||||
input,
|
||||
method: Method::GET,
|
||||
uri: original.uri.clone(),
|
||||
headers: if apply_conditions {
|
||||
original.headers.clone()
|
||||
} else {
|
||||
let mut headers = original.headers.clone();
|
||||
for name in [
|
||||
http::header::RANGE,
|
||||
http::header::IF_MATCH,
|
||||
http::header::IF_NONE_MATCH,
|
||||
http::header::IF_MODIFIED_SINCE,
|
||||
http::header::IF_UNMODIFIED_SINCE,
|
||||
] {
|
||||
headers.remove(name);
|
||||
}
|
||||
headers
|
||||
},
|
||||
extensions: original.extensions.clone(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
request.extensions.insert(Arc::clone(server_ctx));
|
||||
let req_info = request
|
||||
.extensions
|
||||
.get_mut::<ReqInfo>()
|
||||
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "website request info missing"))?;
|
||||
req_info.bucket = Some(bucket.to_owned());
|
||||
req_info.object = Some(key.to_owned());
|
||||
req_info.cred = None;
|
||||
req_info.is_owner = false;
|
||||
request.extensions.insert(WebsiteRead);
|
||||
S3Access::get_object(&crate::storage::ecfs::FS::with_server_ctx(Arc::clone(server_ctx)), &mut request).await?;
|
||||
DefaultObjectUsecase::with_context(server_ctx.installed_app_context())
|
||||
.execute_get_object(request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn website_head_redirect(
|
||||
original: &S3Request<Body>,
|
||||
server_ctx: &Arc<ServerContextSlot>,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
) -> S3Result<Option<String>> {
|
||||
let mut headers = original.headers.clone();
|
||||
for name in [
|
||||
http::header::RANGE,
|
||||
http::header::IF_MATCH,
|
||||
http::header::IF_NONE_MATCH,
|
||||
http::header::IF_MODIFIED_SINCE,
|
||||
http::header::IF_UNMODIFIED_SINCE,
|
||||
] {
|
||||
headers.remove(name);
|
||||
}
|
||||
let mut request = S3Request {
|
||||
input: HeadObjectInput {
|
||||
bucket: bucket.to_owned(),
|
||||
key: key.to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
method: Method::HEAD,
|
||||
uri: original.uri.clone(),
|
||||
headers,
|
||||
extensions: original.extensions.clone(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
};
|
||||
request.extensions.insert(Arc::clone(server_ctx));
|
||||
let req_info = request
|
||||
.extensions
|
||||
.get_mut::<ReqInfo>()
|
||||
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "website request info missing"))?;
|
||||
req_info.bucket = Some(bucket.to_owned());
|
||||
req_info.object = Some(key.to_owned());
|
||||
req_info.cred = None;
|
||||
req_info.is_owner = false;
|
||||
S3Access::head_object(&crate::storage::ecfs::FS::with_server_ctx(Arc::clone(server_ctx)), &mut request).await?;
|
||||
let response = DefaultObjectUsecase::with_context(server_ctx.installed_app_context())
|
||||
.execute_head_object(request)
|
||||
.await?;
|
||||
Ok(response.output.website_redirect_location)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WebsiteRead;
|
||||
|
||||
fn response_to_website(response: S3Response<GetObjectOutput>, head: bool) -> S3Result<S3Response<Body>> {
|
||||
let output = response.output;
|
||||
let mut headers = response.headers;
|
||||
insert_website_header(&mut headers, http::header::CONTENT_TYPE, output.content_type.as_deref())?;
|
||||
insert_website_header(
|
||||
&mut headers,
|
||||
http::header::CONTENT_LENGTH,
|
||||
output.content_length.map(|length| length.to_string()).as_deref(),
|
||||
)?;
|
||||
insert_website_header(&mut headers, http::header::CONTENT_RANGE, output.content_range.as_deref())?;
|
||||
if let Some(tag) = output.e_tag.as_ref() {
|
||||
headers.insert(http::header::ETAG, tag.to_http_header().map_err(S3Error::internal_error)?);
|
||||
}
|
||||
insert_website_header(&mut headers, http::header::CACHE_CONTROL, output.cache_control.as_deref())?;
|
||||
insert_website_header(&mut headers, http::header::CONTENT_ENCODING, output.content_encoding.as_deref())?;
|
||||
insert_website_header(&mut headers, http::header::CONTENT_LANGUAGE, output.content_language.as_deref())?;
|
||||
insert_website_header(&mut headers, http::header::CONTENT_DISPOSITION, output.content_disposition.as_deref())?;
|
||||
if let Some(last_modified) = output.last_modified {
|
||||
let mut formatted = Vec::new();
|
||||
last_modified
|
||||
.format(TimestampFormat::HttpDate, &mut formatted)
|
||||
.map_err(S3Error::internal_error)?;
|
||||
headers.insert(
|
||||
http::header::LAST_MODIFIED,
|
||||
HeaderValue::from_bytes(&formatted).map_err(S3Error::internal_error)?,
|
||||
);
|
||||
}
|
||||
Ok(S3Response {
|
||||
output: if head {
|
||||
Body::empty()
|
||||
} else {
|
||||
output.body.map(Body::from).unwrap_or_default()
|
||||
},
|
||||
status: Some(if output.content_range.is_some() {
|
||||
StatusCode::PARTIAL_CONTENT
|
||||
} else {
|
||||
StatusCode::OK
|
||||
}),
|
||||
headers,
|
||||
extensions: response.extensions,
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_website_header(headers: &mut HeaderMap, name: http::header::HeaderName, value: Option<&str>) -> S3Result<()> {
|
||||
if let Some(value) = value {
|
||||
headers.insert(name, HeaderValue::from_str(value).map_err(S3Error::internal_error)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn website_error_with_document(
|
||||
req: &S3Request<Body>,
|
||||
server_ctx: &Arc<ServerContextSlot>,
|
||||
bucket: &str,
|
||||
error_document: Option<&s3s::dto::ErrorDocument>,
|
||||
status: http::StatusCode,
|
||||
) -> S3Result<S3Response<Body>> {
|
||||
let Some(error_document) = error_document else {
|
||||
return Ok(website_error(status, req.method == Method::HEAD));
|
||||
};
|
||||
match website_get_object(req, server_ctx, bucket, error_document.key.as_str(), false).await {
|
||||
Ok(response) => {
|
||||
let mut output = response_to_website(response, req.method == Method::HEAD)?;
|
||||
output.status = Some(status);
|
||||
Ok(output)
|
||||
}
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.code(),
|
||||
S3ErrorCode::NoSuchKey | S3ErrorCode::NoSuchVersion | S3ErrorCode::AccessDenied
|
||||
) =>
|
||||
{
|
||||
Ok(website_error(status, req.method == Method::HEAD))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn website_error(status: StatusCode, head: bool) -> S3Response<Body> {
|
||||
let message = status.canonical_reason().unwrap_or("Error");
|
||||
let body = format!("<html><body>{message}</body></html>");
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"));
|
||||
S3Response {
|
||||
output: if head { Body::empty() } else { Body::from(body.into_bytes()) },
|
||||
status: Some(status),
|
||||
headers,
|
||||
extensions: Extensions::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn website_location(location: String, status: StatusCode) -> S3Result<S3Response<Body>> {
|
||||
let mut response = S3Response::new(Body::empty());
|
||||
response.status = Some(status);
|
||||
response.headers.insert(
|
||||
http::header::LOCATION,
|
||||
HeaderValue::try_from(location)
|
||||
.map_err(|_| S3Error::with_message(S3ErrorCode::InternalError, "invalid website redirect location"))?,
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn website_relative_redirect(req: &S3Request<Body>, path: &str, status: StatusCode) -> S3Result<S3Response<Body>> {
|
||||
let mut url = url::Url::parse("http://localhost/").map_err(S3Error::internal_error)?;
|
||||
url.set_path(path);
|
||||
url.set_query(req.uri.query());
|
||||
let location = match url.query() {
|
||||
Some(query) => format!("{}?{query}", url.path()),
|
||||
None => url.path().to_owned(),
|
||||
};
|
||||
website_location(location, status)
|
||||
}
|
||||
|
||||
fn website_redirect(
|
||||
req: &S3Request<Body>,
|
||||
host: &str,
|
||||
protocol: Option<&str>,
|
||||
key: &str,
|
||||
status: StatusCode,
|
||||
website_scheme: &str,
|
||||
) -> S3Result<S3Response<Body>> {
|
||||
let scheme = protocol.unwrap_or(website_scheme);
|
||||
let host = if host.is_empty() {
|
||||
req.headers.get(HOST).and_then(|h| h.to_str().ok()).unwrap_or_default()
|
||||
} else {
|
||||
host
|
||||
};
|
||||
let mut url = url::Url::parse(&format!("{scheme}://{host}/")).map_err(S3Error::internal_error)?;
|
||||
url.set_path(&format!("/{key}"));
|
||||
url.set_query(req.uri.query());
|
||||
website_location(url.into(), status)
|
||||
}
|
||||
|
||||
fn website_rule_matches(rule: &RoutingRule, key: &str, status: StatusCode) -> bool {
|
||||
let Some(condition) = rule.condition.as_ref() else { return true };
|
||||
condition
|
||||
.key_prefix_equals
|
||||
.as_ref()
|
||||
.is_none_or(|prefix| key.starts_with(prefix))
|
||||
&& condition
|
||||
.http_error_code_returned_equals
|
||||
.as_ref()
|
||||
.is_none_or(|code| code == status.as_str())
|
||||
}
|
||||
|
||||
fn website_rule_redirect(
|
||||
req: &S3Request<Body>,
|
||||
rule: &RoutingRule,
|
||||
key: &str,
|
||||
website_scheme: &str,
|
||||
) -> S3Result<S3Response<Body>> {
|
||||
let redirect = &rule.redirect;
|
||||
let key = if let Some(replace) = &redirect.replace_key_with {
|
||||
replace.clone()
|
||||
} else if let Some(replace) = &redirect.replace_key_prefix_with {
|
||||
let prefix = rule
|
||||
.condition
|
||||
.as_ref()
|
||||
.and_then(|condition| condition.key_prefix_equals.as_deref())
|
||||
.unwrap_or_default();
|
||||
format!("{replace}{}", key.strip_prefix(prefix).unwrap_or(key))
|
||||
} else {
|
||||
key.to_owned()
|
||||
};
|
||||
let status = redirect
|
||||
.http_redirect_code
|
||||
.as_deref()
|
||||
.and_then(|code| code.parse::<u16>().ok())
|
||||
.and_then(|code| StatusCode::from_u16(code).ok())
|
||||
.unwrap_or(StatusCode::MOVED_PERMANENTLY);
|
||||
website_redirect(
|
||||
req,
|
||||
redirect.host_name.as_deref().unwrap_or_default(),
|
||||
redirect.protocol.as_ref().map(|protocol| protocol.as_str()),
|
||||
&key,
|
||||
status,
|
||||
website_scheme,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum MetadataOperation {
|
||||
ListObjectVersions,
|
||||
@@ -288,16 +826,123 @@ fn header_value(headers: &HeaderMap, name: &str) -> S3Result<Option<String>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MetadataOperation, list_object_versions_input, list_objects_v2_input, metadata_operation};
|
||||
use super::{
|
||||
MetadataOperation, list_object_versions_input, list_objects_v2_input, metadata_operation, website_redirect,
|
||||
website_relative_redirect, website_rule_matches, website_target,
|
||||
};
|
||||
use http::header::HOST;
|
||||
use http::{HeaderMap, Method, Uri};
|
||||
use s3s::dto::EncodingType;
|
||||
use http::{Extensions, HeaderMap, Method, StatusCode, Uri};
|
||||
use s3s::dto::{Condition, EncodingType, Redirect, RoutingRule};
|
||||
use s3s::host::MultiDomain;
|
||||
use s3s::{Body, S3Request};
|
||||
|
||||
fn uri(value: &str) -> Uri {
|
||||
value.parse().expect("test URI should parse")
|
||||
}
|
||||
|
||||
fn website_request(value: &str) -> S3Request<Body> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, "site.example.com".parse().expect("valid host"));
|
||||
S3Request {
|
||||
input: Body::empty(),
|
||||
method: Method::GET,
|
||||
uri: uri(value),
|
||||
headers,
|
||||
extensions: Extensions::new(),
|
||||
credentials: None,
|
||||
region: None,
|
||||
service: None,
|
||||
trailing_headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn website_host_is_isolated_from_s3_host_and_keeps_directory_suffix() {
|
||||
let domains = vec!["example.com".to_owned()];
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, "share.example.com:9000".parse().expect("valid host"));
|
||||
let target = website_target(&uri("/docs/"), &headers, &domains).expect("website host matches");
|
||||
assert_eq!(target.bucket, "share");
|
||||
assert_eq!(target.key, "docs/");
|
||||
assert!(target.trailing_slash);
|
||||
headers.insert(HOST, "s3.other.test".parse().expect("valid host"));
|
||||
assert!(website_target(&uri("/share/docs/"), &headers, &domains).is_none());
|
||||
headers.insert(HOST, "my.bucket.example.com".parse().expect("valid host"));
|
||||
assert_eq!(
|
||||
website_target(&uri("/"), &headers, &domains)
|
||||
.expect("dotted bucket host matches")
|
||||
.bucket,
|
||||
"my.bucket"
|
||||
);
|
||||
headers.insert(HOST, "example.com".parse().expect("valid host"));
|
||||
assert_eq!(
|
||||
website_target(&uri("/share/key"), &headers, &domains)
|
||||
.expect("base domain is reserved")
|
||||
.bucket,
|
||||
""
|
||||
);
|
||||
headers.insert(HOST, "share.example.com".parse().expect("valid host"));
|
||||
assert!(
|
||||
website_target(&uri("/%FF"), &headers, &domains)
|
||||
.expect("invalid path stays in website route")
|
||||
.invalid_path
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn website_redirect_encodes_path_and_preserves_query() {
|
||||
let req = website_request("/old?download=1");
|
||||
let redirect = website_redirect(
|
||||
&req,
|
||||
"new.example.com",
|
||||
Some("https"),
|
||||
"new path/#one",
|
||||
StatusCode::MOVED_PERMANENTLY,
|
||||
"http",
|
||||
)
|
||||
.expect("valid redirect");
|
||||
assert_eq!(
|
||||
redirect.headers[http::header::LOCATION],
|
||||
"https://new.example.com/new%20path/%23one?download=1"
|
||||
);
|
||||
let directory = website_relative_redirect(&req, "/new path/", StatusCode::FOUND).expect("valid directory redirect");
|
||||
assert_eq!(directory.headers[http::header::LOCATION], "/new%20path/?download=1");
|
||||
let secure = website_redirect(&req, "new.example.com", None, "key", StatusCode::MOVED_PERMANENTLY, "https")
|
||||
.expect("TLS listener default scheme");
|
||||
assert_eq!(secure.headers[http::header::LOCATION], "https://new.example.com/key?download=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn website_rules_choose_first_match_after_object_status_is_known() {
|
||||
let error_rule = RoutingRule {
|
||||
condition: Some(Condition {
|
||||
http_error_code_returned_equals: Some("404".to_owned()),
|
||||
key_prefix_equals: None,
|
||||
}),
|
||||
redirect: Redirect::default(),
|
||||
};
|
||||
let prefix_rule = RoutingRule {
|
||||
condition: Some(Condition {
|
||||
http_error_code_returned_equals: None,
|
||||
key_prefix_equals: Some("docs/".to_owned()),
|
||||
}),
|
||||
redirect: Redirect::default(),
|
||||
};
|
||||
let rules = [error_rule, prefix_rule];
|
||||
assert_eq!(
|
||||
rules
|
||||
.iter()
|
||||
.position(|rule| website_rule_matches(rule, "docs/missing", StatusCode::NOT_FOUND)),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
rules
|
||||
.iter()
|
||||
.position(|rule| website_rule_matches(rule, "docs/existing", StatusCode::OK)),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_operation_matches_path_style_extensions_only() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -2276,6 +2276,17 @@ impl DefaultObjectUsecase {
|
||||
Self::validate_get_object_part_number(part_number, info)
|
||||
}
|
||||
|
||||
/// The `x-amz-mp-parts-count` value for a response, if one is owed.
|
||||
///
|
||||
/// s3 reports the part count only on a response to a request that named a
|
||||
/// part, and only for an object created by a multipart upload.
|
||||
fn get_object_parts_count(part_number: Option<usize>, info: &ObjectInfo) -> Option<i32> {
|
||||
if part_number.is_none() || !info.is_multipart() {
|
||||
return None;
|
||||
}
|
||||
i32::try_from(info.parts.len()).ok()
|
||||
}
|
||||
|
||||
/// How long a GET waits for a disk read permit before degrading to a
|
||||
/// permit-less read. Cached: consulted per GET. Zero disables the bound.
|
||||
fn disk_permit_wait_timeout() -> Duration {
|
||||
@@ -3720,6 +3731,8 @@ impl DefaultObjectUsecase {
|
||||
let metadata = filter_object_metadata(&info.user_defined);
|
||||
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_METADATA_FILTER, metadata_filter_start);
|
||||
|
||||
let parts_count = Self::get_object_parts_count(part_number, &info);
|
||||
|
||||
let output = GetObjectOutput {
|
||||
body: Some(body),
|
||||
content_length: Some(response_content_length),
|
||||
@@ -3730,8 +3743,13 @@ impl DefaultObjectUsecase {
|
||||
cache_control,
|
||||
content_disposition,
|
||||
content_range,
|
||||
parts_count,
|
||||
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
|
||||
metadata,
|
||||
website_redirect_location: req
|
||||
.extensions
|
||||
.get::<crate::app::metadata_route::WebsiteRead>()
|
||||
.and_then(|_| info.user_defined.get(AMZ_WEBSITE_REDIRECT_LOCATION).cloned()),
|
||||
server_side_encryption,
|
||||
sse_customer_algorithm,
|
||||
sse_customer_key_md5,
|
||||
@@ -3843,6 +3861,7 @@ impl DefaultObjectUsecase {
|
||||
storage_class: remote.storage_class.map(|sc| StorageClass::from(sc.as_str().to_string())),
|
||||
expiration: remote.expiration,
|
||||
restore: remote.restore,
|
||||
website_redirect_location: remote.website_redirect_location,
|
||||
checksum_crc32: remote.checksum_crc32,
|
||||
checksum_crc32c: remote.checksum_crc32_c,
|
||||
checksum_crc64nvme: remote.checksum_crc64_nvme,
|
||||
@@ -4070,7 +4089,10 @@ impl DefaultObjectUsecase {
|
||||
{
|
||||
// Active-active replication lag window: proxy the GET to a
|
||||
// replication target (backlog#1675 P1-5).
|
||||
if let Some(output) = Self::proxy_get_object_to_replication_targets(&req, &bucket, &key, &opts).await {
|
||||
if let Some(mut output) = Self::proxy_get_object_to_replication_targets(&req, &bucket, &key, &opts).await {
|
||||
if req.extensions.get::<crate::app::metadata_route::WebsiteRead>().is_none() {
|
||||
output.website_redirect_location = None;
|
||||
}
|
||||
lifecycle.finish_ok();
|
||||
let mut response = wrap_response_with_cors(&bucket, &req.method, &req.headers, output).await;
|
||||
inject_accept_ranges_header(&mut response.headers);
|
||||
@@ -4086,7 +4108,10 @@ impl DefaultObjectUsecase {
|
||||
.await
|
||||
{
|
||||
None => {}
|
||||
Some(OdmGetOutcome::Respond(Ok(output))) => {
|
||||
Some(OdmGetOutcome::Respond(Ok(mut output))) => {
|
||||
if req.extensions.get::<crate::app::metadata_route::WebsiteRead>().is_none() {
|
||||
output.website_redirect_location = None;
|
||||
}
|
||||
lifecycle.finish_ok();
|
||||
let mut response = wrap_response_with_cors(&bucket, &req.method, &req.headers, *output).await;
|
||||
inject_accept_ranges_header(&mut response.headers);
|
||||
@@ -4628,6 +4653,7 @@ fn odm_get_output(head: &SourceHead, content_length: i64, content_range: Option<
|
||||
e_tag: head.etag.as_deref().map(to_s3s_etag),
|
||||
last_modified: head.last_modified.map(OffsetDateTime::from).map(Timestamp::from),
|
||||
metadata: (!head.user_metadata.is_empty()).then(|| head.user_metadata.clone()),
|
||||
website_redirect_location: head.website_redirect_location.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -10812,6 +10838,45 @@ mod tests {
|
||||
assert!(DefaultObjectUsecase::validate_get_object_part_number(Some(1), &info).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_object_reports_parts_count_for_multipart_objects() {
|
||||
fn info_with_parts(count: usize, etag: &str) -> ObjectInfo {
|
||||
ObjectInfo {
|
||||
parts: Arc::new(
|
||||
(1..=count)
|
||||
.map(|number| rustfs_filemeta::ObjectPartInfo {
|
||||
number,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
etag: Some(etag.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// A multipart object, asked for by part: the count is what lets the
|
||||
// client know three more parts follow.
|
||||
let multipart = info_with_parts(4, "d41d8cd98f00b204e9800998ecf8427e-4");
|
||||
assert!(multipart.is_multipart());
|
||||
assert_eq!(DefaultObjectUsecase::get_object_parts_count(Some(1), &multipart), Some(4));
|
||||
|
||||
// Not asked for by part: s3 omits the header entirely.
|
||||
assert_eq!(DefaultObjectUsecase::get_object_parts_count(None, &multipart), None);
|
||||
|
||||
// A plain PutObject is stored as one part and carries a bare 32-char
|
||||
// etag, so it is not multipart and is owed no count.
|
||||
let single = info_with_parts(1, "d41d8cd98f00b204e9800998ecf8427e");
|
||||
assert!(!single.is_multipart());
|
||||
assert_eq!(DefaultObjectUsecase::get_object_parts_count(Some(1), &single), None);
|
||||
|
||||
// A multipart upload that happened to have one part still carries the
|
||||
// `-1` etag suffix, so it is multipart and reports a count of 1.
|
||||
let single_part_multipart = info_with_parts(1, "d41d8cd98f00b204e9800998ecf8427e-1");
|
||||
assert!(single_part_multipart.is_multipart());
|
||||
assert_eq!(DefaultObjectUsecase::get_object_parts_count(Some(1), &single_part_multipart), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_fill_conditions_fail_before_phase_probe_advances() {
|
||||
fn run_phase_probe(headers: &HeaderMap, info: &ObjectInfo) -> (S3Result<()>, [usize; 3]) {
|
||||
|
||||
@@ -104,6 +104,7 @@ pub(super) fn odm_head_output(head: SourceHead) -> S3Result<HeadObjectOutput> {
|
||||
e_tag: head.etag.as_deref().map(to_s3s_etag),
|
||||
last_modified: head.last_modified.map(OffsetDateTime::from).map(Timestamp::from),
|
||||
metadata: (!head.user_metadata.is_empty()).then_some(head.user_metadata),
|
||||
website_redirect_location: head.website_redirect_location,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -286,6 +287,7 @@ impl DefaultObjectUsecase {
|
||||
storage_class: remote.storage_class.map(|sc| StorageClass::from(sc.as_str().to_string())),
|
||||
expiration: remote.expiration,
|
||||
restore: remote.restore,
|
||||
website_redirect_location: remote.website_redirect_location,
|
||||
checksum_crc32: remote.checksum_crc32,
|
||||
checksum_crc32c: remote.checksum_crc32_c,
|
||||
checksum_crc64nvme: remote.checksum_crc64_nvme,
|
||||
@@ -777,6 +779,7 @@ mod tests {
|
||||
content_language: Some("en".to_string()),
|
||||
cache_control: Some("max-age=60".to_string()),
|
||||
expires: Some("Thu, 01 Jan 2026 00:00:00 GMT".to_string()),
|
||||
website_redirect_location: None,
|
||||
user_metadata: HashMap::from([("owner".to_string(), "alice".to_string())]),
|
||||
version_id: Some("v1".to_string()),
|
||||
storage_class: Some("STANDARD_IA".to_string()),
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::app::object_data_cache::ObjectDataCacheAdapter;
|
||||
#[cfg(test)]
|
||||
use crate::app::storage_api::runtime_sources::TierConfigMgr;
|
||||
use crate::runtime_sources as root_runtime_sources;
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::runtime_sources::ServerContextSlot;
|
||||
pub(crate) use crate::runtime_sources::{
|
||||
AppContext, current_encryption_service, current_endpoints_handle, current_notification_system,
|
||||
|
||||
@@ -379,7 +379,7 @@ pub struct ConnectTopOpts {
|
||||
pub enum ConnectTopCommands {
|
||||
/// Capture API activity when an approved typed source is available
|
||||
Api(ConnectTopCaptureOpts),
|
||||
/// Capture process disk I/O on supported platforms
|
||||
/// Capture running service process disk I/O on supported platforms
|
||||
Disk(ConnectTopCaptureOpts),
|
||||
/// Capture lock activity when an approved typed source is available
|
||||
Locks(ConnectTopCaptureOpts),
|
||||
@@ -391,6 +391,9 @@ pub enum ConnectTopCommands {
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
pub struct ConnectTopCaptureOpts {
|
||||
/// Explicit existing offline identity pin, required for service disk capture
|
||||
#[arg(long = "offline-key-id", value_parser = NonEmptyStringValueParser::new())]
|
||||
pub offline_key_id: Option<String>,
|
||||
/// Directory containing an enrolled Connect device identity
|
||||
#[arg(long = "state-dir")]
|
||||
pub state_dir: PathBuf,
|
||||
@@ -1043,6 +1046,10 @@ pub struct ConnectProfileOpts {
|
||||
#[arg(long = "thread-scope", value_enum)]
|
||||
pub thread_scope: Option<ConnectThreadProfileScope>,
|
||||
|
||||
/// SHA-256 of the enrolled offline public key; required for service runtime capture
|
||||
#[arg(long = "offline-key-id")]
|
||||
pub offline_key_id: Option<String>,
|
||||
|
||||
/// Negotiated producer schema version
|
||||
#[arg(long = "schema-version", default_value_t = 1)]
|
||||
pub schema_version: u16,
|
||||
|
||||
@@ -0,0 +1,827 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Bounded online HealthCheck observation and signed export.
|
||||
//!
|
||||
//! Only the approved L0 cluster aggregate is exported: capacity counters and
|
||||
//! the existing coarse flag allow-list. Per-drive identity, endpoints, paths,
|
||||
//! raw admin responses and object data never enter the result.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::{Cursor, Write as _};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use p256::ecdsa::{Signature, SigningKey, signature::Signer as _};
|
||||
use p256::pkcs8::DecodePrivateKey as _;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use thiserror::Error;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::{Uuid, Variant, Version};
|
||||
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
|
||||
|
||||
use super::ProfileProvenance;
|
||||
use crate::connect::DeviceIdentity;
|
||||
use crate::storage_api::connect::contract::admin::{DiskSetSelector, StorageAdminApi};
|
||||
use crate::storage_api::connect::{DiskInfoOptions, StorageDiskRpcExt};
|
||||
|
||||
pub const HEALTH_SCHEMA_VERSION: u16 = 1;
|
||||
pub const HEALTH_TOOL_ID: &str = "health.check";
|
||||
pub const HEALTH_SERVICE_CAPABILITY: &str = "health.check.service@1";
|
||||
pub const HEALTH_TIMEOUT_SECONDS: u64 = 30;
|
||||
pub const MAX_HEALTH_OUTPUT_BYTES: u64 = 262_144;
|
||||
pub const MAX_HEALTH_MEMORY_BYTES: u64 = 67_108_864;
|
||||
pub const MAX_HEALTH_CPU_MILLIS: u64 = 5_000;
|
||||
pub const MAX_EVIDENCE_AGE_SECONDS: u64 = 86_400;
|
||||
pub const HEALTH_CATALOG_CHECKS: usize = 13;
|
||||
|
||||
const MAX_HEALTH_DRIVES: usize = 1_024;
|
||||
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
|
||||
const MAX_ENVELOPE_BYTES: usize = 16_384;
|
||||
const SIGNATURE_DOMAIN: &[u8] = b"rustfs-diagnostic-envelope-v1\0";
|
||||
const ENVELOPE_PATH: &str = "envelope.json";
|
||||
const SIGNATURE_PATH: &str = "envelope.sig";
|
||||
const RESULT_PATH: &str = "result.json";
|
||||
const RESOURCE_ALIAS: &str = "cluster-1";
|
||||
|
||||
const ALLOWED_FLAGS: [&str; 8] = [
|
||||
"capacity.critical",
|
||||
"capacity.warning",
|
||||
"clock.skew",
|
||||
"cluster.degraded",
|
||||
"cluster.healing",
|
||||
"cluster.readonly",
|
||||
"drive.offline",
|
||||
"node.offline",
|
||||
];
|
||||
|
||||
const UNSUPPORTED_CHECKS: [&str; 11] = [
|
||||
"memory.reportedPressure",
|
||||
"tls.validity",
|
||||
"cpu.sufficiency",
|
||||
"swap.pressure",
|
||||
"drive.health",
|
||||
"inode.available",
|
||||
"filesystem.compatibility",
|
||||
"osKernel.compatibility",
|
||||
"launch.permissions",
|
||||
"version.consistency",
|
||||
"kubernetes.configuration",
|
||||
];
|
||||
|
||||
static HEALTH_COLLECTOR_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LocalHealthConsent {
|
||||
pub consent_uid: String,
|
||||
pub policy_revision: u64,
|
||||
pub expires_at_unix: i64,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HealthServiceRequest {
|
||||
pub organization_name: String,
|
||||
pub cluster_name: String,
|
||||
pub device_name: String,
|
||||
pub run_uid: String,
|
||||
pub artifact_uid: String,
|
||||
pub schema_version: u16,
|
||||
pub capability: String,
|
||||
pub consent: LocalHealthConsent,
|
||||
pub produced_at_unix: i64,
|
||||
pub expires_at_unix: i64,
|
||||
pub nonce: [u8; 32],
|
||||
pub max_evidence_age_seconds: u64,
|
||||
pub provenance: ProfileProvenance,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HealthSourceObservation {
|
||||
pub observed_at_unix: Option<i64>,
|
||||
pub capacity_complete: bool,
|
||||
pub total_bytes: Option<u64>,
|
||||
pub used_bytes: Option<u64>,
|
||||
pub coarse_flags_complete: bool,
|
||||
pub coarse_flags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum HealthOutcome {
|
||||
Partial,
|
||||
}
|
||||
|
||||
impl HealthOutcome {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
"PARTIAL"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum HealthResultReason {
|
||||
CatalogPartial,
|
||||
EvidenceIncomplete,
|
||||
EvidenceStale,
|
||||
EvidenceFreshnessUnknown,
|
||||
ClockSkew,
|
||||
}
|
||||
|
||||
impl HealthResultReason {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::CatalogPartial => "CATALOG_PARTIAL",
|
||||
Self::EvidenceIncomplete => "EVIDENCE_INCOMPLETE",
|
||||
Self::EvidenceStale => "EVIDENCE_STALE",
|
||||
Self::EvidenceFreshnessUnknown => "EVIDENCE_FRESHNESS_UNKNOWN",
|
||||
Self::ClockSkew => "CLOCK_SKEW",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum HealthRuleOutcome {
|
||||
Pass,
|
||||
Fail,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum HealthFreshness {
|
||||
Current,
|
||||
Stale,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCheckResult {
|
||||
pub check_id: &'static str,
|
||||
pub outcome: HealthRuleOutcome,
|
||||
pub reason_code: &'static str,
|
||||
pub resource_alias: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UnsupportedHealthCheck {
|
||||
pub check_id: &'static str,
|
||||
pub reason_code: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCapacityObservation {
|
||||
source: &'static str,
|
||||
observed_at: Option<String>,
|
||||
freshness: HealthFreshness,
|
||||
complete: bool,
|
||||
total_bytes: Option<u64>,
|
||||
used_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthFlagsObservation {
|
||||
source: &'static str,
|
||||
observed_at: Option<String>,
|
||||
freshness: HealthFreshness,
|
||||
complete: bool,
|
||||
values: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthObservations {
|
||||
capacity: HealthCapacityObservation,
|
||||
coarse_flags: HealthFlagsObservation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthData {
|
||||
catalog_version: u16,
|
||||
resource_alias: &'static str,
|
||||
observations: HealthObservations,
|
||||
checks: Vec<HealthCheckResult>,
|
||||
unsupported_checks: Vec<UnsupportedHealthCheck>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthCoverage {
|
||||
requested_units: u32,
|
||||
completed_units: u32,
|
||||
unsupported_units: u32,
|
||||
unit: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HealthDiagnosticResult {
|
||||
schema_version: u16,
|
||||
run_uid: String,
|
||||
tool_id: &'static str,
|
||||
capability: &'static str,
|
||||
outcome: HealthOutcome,
|
||||
reason_code: HealthResultReason,
|
||||
duration_millis: u64,
|
||||
provenance: ProfileProvenance,
|
||||
coverage: HealthCoverage,
|
||||
data: HealthData,
|
||||
}
|
||||
|
||||
impl HealthDiagnosticResult {
|
||||
pub const fn outcome(&self) -> HealthOutcome {
|
||||
self.outcome
|
||||
}
|
||||
|
||||
pub const fn reason_code(&self) -> HealthResultReason {
|
||||
self.reason_code
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &HealthData {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SignedHealthExport {
|
||||
pub artifact_uid: String,
|
||||
pub outcome: HealthOutcome,
|
||||
pub reason_code: HealthResultReason,
|
||||
pub envelope_json: Vec<u8>,
|
||||
pub envelope_signature: Vec<u8>,
|
||||
pub result_json: Vec<u8>,
|
||||
pub archive_bytes: Vec<u8>,
|
||||
pub archive_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum HealthError {
|
||||
#[error("health_consent_required")]
|
||||
ConsentRequired,
|
||||
#[error("health_consent_expired")]
|
||||
ConsentExpired,
|
||||
#[error("health_request_expired")]
|
||||
Expired,
|
||||
#[error("health_invalid_request")]
|
||||
InvalidRequest,
|
||||
#[error("health_unsupported_contract")]
|
||||
Unsupported,
|
||||
#[error("health_limit_exceeded")]
|
||||
LimitExceeded,
|
||||
#[error("health_collection_cancelled")]
|
||||
Cancelled,
|
||||
#[error("health_collection_already_running")]
|
||||
Busy,
|
||||
#[error("health_source_unavailable")]
|
||||
SourceUnavailable,
|
||||
#[error("health_collection_failed")]
|
||||
CollectionFailed,
|
||||
#[error("health_export_signing_failed")]
|
||||
Signing,
|
||||
#[error("health_export_encoding_failed")]
|
||||
Encoding,
|
||||
}
|
||||
|
||||
struct CollectorLease;
|
||||
|
||||
impl CollectorLease {
|
||||
fn acquire() -> Result<Self, HealthError> {
|
||||
HEALTH_COLLECTOR_ACTIVE
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||
.map(|_| Self)
|
||||
.map_err(|_| HealthError::Busy)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CollectorLease {
|
||||
fn drop(&mut self) {
|
||||
HEALTH_COLLECTOR_ACTIVE.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect_runtime_health(
|
||||
request: &HealthServiceRequest,
|
||||
identity: &DeviceIdentity,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<SignedHealthExport, HealthError> {
|
||||
validate_request(request, Utc::now().timestamp())?;
|
||||
check_cancel(cancel)?;
|
||||
let _lease = CollectorLease::acquire()?;
|
||||
let started = Instant::now();
|
||||
let observation = collect_runtime_observation(cancel).await?;
|
||||
check_cancel(cancel)?;
|
||||
let result = evaluate_health_observation(request, observation, Utc::now(), started.elapsed());
|
||||
sign_health_export(request, &result, identity, cancel)
|
||||
}
|
||||
|
||||
pub fn evaluate_health_observation(
|
||||
request: &HealthServiceRequest,
|
||||
observation: HealthSourceObservation,
|
||||
evaluated_at: DateTime<Utc>,
|
||||
elapsed: std::time::Duration,
|
||||
) -> HealthDiagnosticResult {
|
||||
let observed_at = observation.observed_at_unix.and_then(DateTime::<Utc>::from_timestamp_secs);
|
||||
let freshness = freshness(observed_at, evaluated_at, request.max_evidence_age_seconds);
|
||||
let observed_at_text = observed_at.map(|value| value.to_rfc3339_opts(SecondsFormat::Secs, true));
|
||||
|
||||
let capacity = capacity_check(&observation, freshness);
|
||||
let flags = flags_check(&observation, freshness);
|
||||
let result_reason = result_reason(freshness, &observation);
|
||||
let unsupported_checks = UNSUPPORTED_CHECKS
|
||||
.into_iter()
|
||||
.map(|check_id| UnsupportedHealthCheck {
|
||||
check_id,
|
||||
reason_code: "EVIDENCE_CONTRACT_UNAVAILABLE",
|
||||
})
|
||||
.collect();
|
||||
|
||||
HealthDiagnosticResult {
|
||||
schema_version: HEALTH_SCHEMA_VERSION,
|
||||
run_uid: request.run_uid.clone(),
|
||||
tool_id: HEALTH_TOOL_ID,
|
||||
capability: HEALTH_SERVICE_CAPABILITY,
|
||||
outcome: HealthOutcome::Partial,
|
||||
reason_code: result_reason,
|
||||
duration_millis: u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
|
||||
provenance: request.provenance.clone(),
|
||||
coverage: HealthCoverage {
|
||||
requested_units: u32::try_from(HEALTH_CATALOG_CHECKS).unwrap_or(u32::MAX),
|
||||
completed_units: 2,
|
||||
unsupported_units: u32::try_from(UNSUPPORTED_CHECKS.len()).unwrap_or(u32::MAX),
|
||||
unit: "CHECK",
|
||||
},
|
||||
data: HealthData {
|
||||
catalog_version: 1,
|
||||
resource_alias: RESOURCE_ALIAS,
|
||||
observations: HealthObservations {
|
||||
capacity: HealthCapacityObservation {
|
||||
source: "LIVE_DISK_PROBE",
|
||||
observed_at: observed_at_text.clone(),
|
||||
freshness,
|
||||
complete: observation.capacity_complete,
|
||||
total_bytes: observation.total_bytes,
|
||||
used_bytes: observation.used_bytes,
|
||||
},
|
||||
coarse_flags: HealthFlagsObservation {
|
||||
source: "LIVE_DISK_PROBE",
|
||||
observed_at: observed_at_text,
|
||||
freshness,
|
||||
complete: observation.coarse_flags_complete,
|
||||
values: observation.coarse_flags,
|
||||
},
|
||||
},
|
||||
checks: vec![capacity, flags],
|
||||
unsupported_checks,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_runtime_observation(cancel: &CancellationToken) -> Result<HealthSourceObservation, HealthError> {
|
||||
let store = crate::runtime_sources::current_object_store_handle().ok_or(HealthError::SourceUnavailable)?;
|
||||
let backend = tokio::select! {
|
||||
_ = cancel.cancelled() => return Err(HealthError::Cancelled),
|
||||
backend = StorageAdminApi::backend_info(store.as_ref()) => backend,
|
||||
};
|
||||
let drive_counts = StorageAdminApi::set_drive_counts(store.as_ref());
|
||||
if backend.total_sets.is_empty()
|
||||
|| backend.total_sets.len() > MAX_HEALTH_DRIVES
|
||||
|| backend.total_sets.len() != drive_counts.len()
|
||||
|| drive_counts != backend.drives_per_set
|
||||
|| backend
|
||||
.total_sets
|
||||
.iter()
|
||||
.zip(&drive_counts)
|
||||
.any(|(&sets, &drives)| sets == 0 || drives == 0)
|
||||
{
|
||||
return Err(HealthError::SourceUnavailable);
|
||||
}
|
||||
let data_widths = if backend.standard_sc_data.is_empty() {
|
||||
None
|
||||
} else if backend.standard_sc_data.len() == drive_counts.len()
|
||||
&& backend
|
||||
.standard_sc_data
|
||||
.iter()
|
||||
.zip(&drive_counts)
|
||||
.all(|(&data, &drives)| (1..=drives).contains(&data))
|
||||
{
|
||||
Some(backend.standard_sc_data.as_slice())
|
||||
} else {
|
||||
return Err(HealthError::SourceUnavailable);
|
||||
};
|
||||
let expected = backend
|
||||
.total_sets
|
||||
.iter()
|
||||
.zip(&drive_counts)
|
||||
.try_fold(0usize, |total, (&sets, &drives)| {
|
||||
sets.checked_mul(drives).and_then(|count| total.checked_add(count))
|
||||
})
|
||||
.filter(|count| (1..=MAX_HEALTH_DRIVES).contains(count))
|
||||
.ok_or(HealthError::LimitExceeded)?;
|
||||
|
||||
let mut disks = Vec::with_capacity(expected);
|
||||
for (pool_idx, (&sets, &drives)) in backend.total_sets.iter().zip(&drive_counts).enumerate() {
|
||||
for set_idx in 0..sets {
|
||||
let set = tokio::select! {
|
||||
_ = cancel.cancelled() => return Err(HealthError::Cancelled),
|
||||
result = StorageAdminApi::disk_set_inventory(store.as_ref(), DiskSetSelector::new(pool_idx, set_idx)) => {
|
||||
result.map_err(|_| HealthError::SourceUnavailable)?
|
||||
}
|
||||
};
|
||||
if set.len() != drives || disks.len().saturating_add(set.len()) > expected {
|
||||
return Err(HealthError::SourceUnavailable);
|
||||
}
|
||||
disks.extend(
|
||||
set.into_iter()
|
||||
.enumerate()
|
||||
.map(|(disk_idx, disk)| (data_widths.is_none_or(|widths| disk_idx < widths[pool_idx]), disk)),
|
||||
);
|
||||
}
|
||||
}
|
||||
if disks.len() != expected {
|
||||
return Err(HealthError::SourceUnavailable);
|
||||
}
|
||||
|
||||
let observed_at_unix = Utc::now().timestamp();
|
||||
let mut total_bytes = 0u64;
|
||||
let mut used_bytes = 0u64;
|
||||
let mut capacity_complete = true;
|
||||
let mut flags_complete = true;
|
||||
let mut flags = BTreeSet::new();
|
||||
|
||||
for (include_capacity, disk) in disks {
|
||||
check_cancel(cancel)?;
|
||||
let Some(disk) = disk else {
|
||||
capacity_complete = false;
|
||||
flags.insert("cluster.degraded".to_owned());
|
||||
flags.insert("drive.offline".to_owned());
|
||||
continue;
|
||||
};
|
||||
let options = DiskInfoOptions {
|
||||
fresh_capacity: true,
|
||||
..Default::default()
|
||||
};
|
||||
let info = tokio::select! {
|
||||
_ = cancel.cancelled() => return Err(HealthError::Cancelled),
|
||||
result = disk.disk_info(&options) => result,
|
||||
};
|
||||
let Ok(info) = info else {
|
||||
capacity_complete = false;
|
||||
flags_complete = false;
|
||||
continue;
|
||||
};
|
||||
if !info.fresh_capacity || info.used > info.total {
|
||||
capacity_complete = false;
|
||||
flags_complete = false;
|
||||
continue;
|
||||
}
|
||||
match disk.runtime_state().as_str() {
|
||||
"online" | "returning" => {}
|
||||
"offline" => {
|
||||
flags.insert("cluster.degraded".to_owned());
|
||||
flags.insert("drive.offline".to_owned());
|
||||
}
|
||||
_ => {
|
||||
flags.insert("cluster.degraded".to_owned());
|
||||
}
|
||||
}
|
||||
if include_capacity {
|
||||
match (
|
||||
total_bytes.checked_add(info.total).filter(|value| *value <= MAX_SAFE_INTEGER),
|
||||
used_bytes.checked_add(info.used).filter(|value| *value <= MAX_SAFE_INTEGER),
|
||||
) {
|
||||
(Some(total), Some(used)) => {
|
||||
total_bytes = total;
|
||||
used_bytes = used;
|
||||
}
|
||||
_ => capacity_complete = false,
|
||||
}
|
||||
}
|
||||
if info.healing {
|
||||
flags.insert("cluster.healing".to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HealthSourceObservation {
|
||||
observed_at_unix: Some(observed_at_unix),
|
||||
capacity_complete,
|
||||
total_bytes: capacity_complete.then_some(total_bytes),
|
||||
used_bytes: capacity_complete.then_some(used_bytes),
|
||||
coarse_flags_complete: flags_complete,
|
||||
coarse_flags: flags_complete.then(|| flags.into_iter().collect()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sign_health_export(
|
||||
request: &HealthServiceRequest,
|
||||
result: &HealthDiagnosticResult,
|
||||
identity: &DeviceIdentity,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<SignedHealthExport, HealthError> {
|
||||
validate_request(request, Utc::now().timestamp())?;
|
||||
check_cancel(cancel)?;
|
||||
if result.schema_version != HEALTH_SCHEMA_VERSION
|
||||
|| result.run_uid != request.run_uid
|
||||
|| result.tool_id != HEALTH_TOOL_ID
|
||||
|| result.capability != HEALTH_SERVICE_CAPABILITY
|
||||
{
|
||||
return Err(HealthError::InvalidRequest);
|
||||
}
|
||||
let result_json = serde_json::to_vec(result).map_err(|_| HealthError::Encoding)?;
|
||||
if result_json.is_empty() || u64::try_from(result_json.len()).unwrap_or(u64::MAX) > MAX_HEALTH_OUTPUT_BYTES {
|
||||
return Err(HealthError::LimitExceeded);
|
||||
}
|
||||
let device_key_id = hex_lower(&Sha256::digest(identity.public_key_der()));
|
||||
let result_sha256 = hex_lower(&Sha256::digest(&result_json));
|
||||
let envelope = HealthEnvelope {
|
||||
format_version: "rustfs.connect.diagnosticEnvelope/1",
|
||||
protocol_version: "v1",
|
||||
organization_name: &request.organization_name,
|
||||
cluster_name: &request.cluster_name,
|
||||
device_name: &request.device_name,
|
||||
run_uid: &request.run_uid,
|
||||
artifact_uid: &request.artifact_uid,
|
||||
tool_id: HEALTH_TOOL_ID,
|
||||
schema_version: HEALTH_SCHEMA_VERSION,
|
||||
classification: "L0",
|
||||
consent_uid: &request.consent.consent_uid,
|
||||
policy_revision: request.consent.policy_revision,
|
||||
produced_at: timestamp(request.produced_at_unix)?,
|
||||
expires_at: timestamp(request.expires_at_unix)?,
|
||||
nonce: URL_SAFE_NO_PAD.encode_to_string(request.nonce),
|
||||
device_key_id: &device_key_id,
|
||||
payload: HealthPayload {
|
||||
path: RESULT_PATH,
|
||||
media_type: "application/json",
|
||||
size_bytes: u64::try_from(result_json.len()).unwrap_or(u64::MAX),
|
||||
sha256: &result_sha256,
|
||||
},
|
||||
};
|
||||
let envelope_json = serde_json::to_vec(&envelope).map_err(|_| HealthError::Encoding)?;
|
||||
if envelope_json.is_empty() || envelope_json.len() > MAX_ENVELOPE_BYTES {
|
||||
return Err(HealthError::LimitExceeded);
|
||||
}
|
||||
let envelope_signature = signature_document(identity, &device_key_id, &envelope_json)?;
|
||||
check_cancel(cancel)?;
|
||||
let archive_bytes = archive(&envelope_json, &envelope_signature, &result_json)?;
|
||||
if u64::try_from(archive_bytes.len()).unwrap_or(u64::MAX) > MAX_HEALTH_OUTPUT_BYTES {
|
||||
return Err(HealthError::LimitExceeded);
|
||||
}
|
||||
Ok(SignedHealthExport {
|
||||
artifact_uid: request.artifact_uid.clone(),
|
||||
outcome: result.outcome,
|
||||
reason_code: result.reason_code,
|
||||
envelope_json,
|
||||
envelope_signature,
|
||||
result_json,
|
||||
archive_sha256: hex_lower(&Sha256::digest(&archive_bytes)),
|
||||
archive_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn result_reason(freshness: HealthFreshness, observation: &HealthSourceObservation) -> HealthResultReason {
|
||||
match freshness {
|
||||
HealthFreshness::Stale => HealthResultReason::EvidenceStale,
|
||||
HealthFreshness::Unknown if observation.observed_at_unix.is_some() => HealthResultReason::ClockSkew,
|
||||
HealthFreshness::Unknown => HealthResultReason::EvidenceFreshnessUnknown,
|
||||
HealthFreshness::Current if !observation.capacity_complete || !observation.coarse_flags_complete => {
|
||||
HealthResultReason::EvidenceIncomplete
|
||||
}
|
||||
HealthFreshness::Current => HealthResultReason::CatalogPartial,
|
||||
}
|
||||
}
|
||||
|
||||
fn capacity_check(observation: &HealthSourceObservation, freshness: HealthFreshness) -> HealthCheckResult {
|
||||
let (outcome, reason_code) = match freshness {
|
||||
HealthFreshness::Stale => (HealthRuleOutcome::Unknown, "EVIDENCE_STALE"),
|
||||
HealthFreshness::Unknown if observation.observed_at_unix.is_some() => (HealthRuleOutcome::Unknown, "CLOCK_SKEW"),
|
||||
HealthFreshness::Unknown => (HealthRuleOutcome::Unknown, "EVIDENCE_FRESHNESS_UNKNOWN"),
|
||||
HealthFreshness::Current if !observation.capacity_complete => (HealthRuleOutcome::Unknown, "EVIDENCE_MISSING"),
|
||||
HealthFreshness::Current => match (observation.total_bytes, observation.used_bytes) {
|
||||
(Some(0), Some(_)) => (HealthRuleOutcome::Unknown, "CAPACITY_UNAVAILABLE"),
|
||||
(Some(total), Some(used)) if used > total => (HealthRuleOutcome::Unknown, "INVALID_EVIDENCE"),
|
||||
(Some(total), Some(used)) if used == total => (HealthRuleOutcome::Fail, "CAPACITY_EXHAUSTED"),
|
||||
(Some(total), Some(used)) if used < total => (HealthRuleOutcome::Pass, "CAPACITY_REMAINING"),
|
||||
_ => (HealthRuleOutcome::Unknown, "EVIDENCE_MISSING"),
|
||||
},
|
||||
};
|
||||
HealthCheckResult {
|
||||
check_id: "capacity.exhausted",
|
||||
outcome,
|
||||
reason_code,
|
||||
resource_alias: RESOURCE_ALIAS,
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_check(observation: &HealthSourceObservation, freshness: HealthFreshness) -> HealthCheckResult {
|
||||
let (outcome, reason_code) = match freshness {
|
||||
HealthFreshness::Stale => (HealthRuleOutcome::Unknown, "EVIDENCE_STALE"),
|
||||
HealthFreshness::Unknown if observation.observed_at_unix.is_some() => (HealthRuleOutcome::Unknown, "CLOCK_SKEW"),
|
||||
HealthFreshness::Unknown => (HealthRuleOutcome::Unknown, "EVIDENCE_FRESHNESS_UNKNOWN"),
|
||||
HealthFreshness::Current if !observation.coarse_flags_complete => (HealthRuleOutcome::Unknown, "EVIDENCE_MISSING"),
|
||||
HealthFreshness::Current => match observation.coarse_flags.as_deref() {
|
||||
Some(flags) if flags.iter().any(|flag| !ALLOWED_FLAGS.contains(&flag.as_str())) => {
|
||||
(HealthRuleOutcome::Unknown, "INVALID_EVIDENCE")
|
||||
}
|
||||
Some([]) => (HealthRuleOutcome::Pass, "NO_COARSE_CONDITION_REPORTED"),
|
||||
Some(_) => (HealthRuleOutcome::Fail, "COARSE_CONDITION_REPORTED"),
|
||||
None => (HealthRuleOutcome::Unknown, "EVIDENCE_MISSING"),
|
||||
},
|
||||
};
|
||||
HealthCheckResult {
|
||||
check_id: "cluster.reportedFlags",
|
||||
outcome,
|
||||
reason_code,
|
||||
resource_alias: RESOURCE_ALIAS,
|
||||
}
|
||||
}
|
||||
|
||||
fn freshness(observed_at: Option<DateTime<Utc>>, evaluated_at: DateTime<Utc>, max_age: u64) -> HealthFreshness {
|
||||
let Some(observed_at) = observed_at else {
|
||||
return HealthFreshness::Unknown;
|
||||
};
|
||||
let Ok(age) = evaluated_at.signed_duration_since(observed_at).to_std() else {
|
||||
return HealthFreshness::Unknown;
|
||||
};
|
||||
if age.as_secs() > max_age {
|
||||
HealthFreshness::Stale
|
||||
} else {
|
||||
HealthFreshness::Current
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_request(request: &HealthServiceRequest, now: i64) -> Result<(), HealthError> {
|
||||
if request.schema_version != HEALTH_SCHEMA_VERSION || request.capability != HEALTH_SERVICE_CAPABILITY {
|
||||
return Err(HealthError::Unsupported);
|
||||
}
|
||||
if !request.consent.active || request.consent.policy_revision == 0 {
|
||||
return Err(HealthError::ConsentRequired);
|
||||
}
|
||||
if request.consent.expires_at_unix <= now || request.expires_at_unix > request.consent.expires_at_unix {
|
||||
return Err(HealthError::ConsentExpired);
|
||||
}
|
||||
if request.produced_at_unix > now.saturating_add(300)
|
||||
|| request.expires_at_unix <= now
|
||||
|| request.expires_at_unix <= request.produced_at_unix
|
||||
{
|
||||
return Err(HealthError::Expired);
|
||||
}
|
||||
if !(1..=MAX_EVIDENCE_AGE_SECONDS).contains(&request.max_evidence_age_seconds)
|
||||
|| !uuid7(&request.run_uid)
|
||||
|| !uuid7(&request.artifact_uid)
|
||||
|| !uuid7(&request.consent.consent_uid)
|
||||
|| !resource_names_match(request)
|
||||
|| !request.provenance.is_valid()
|
||||
{
|
||||
return Err(HealthError::InvalidRequest);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_cancel(cancel: &CancellationToken) -> Result<(), HealthError> {
|
||||
if cancel.is_cancelled() {
|
||||
Err(HealthError::Cancelled)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealthEnvelope<'a> {
|
||||
format_version: &'static str,
|
||||
protocol_version: &'static str,
|
||||
organization_name: &'a str,
|
||||
cluster_name: &'a str,
|
||||
device_name: &'a str,
|
||||
run_uid: &'a str,
|
||||
artifact_uid: &'a str,
|
||||
tool_id: &'static str,
|
||||
schema_version: u16,
|
||||
classification: &'static str,
|
||||
consent_uid: &'a str,
|
||||
policy_revision: u64,
|
||||
produced_at: String,
|
||||
expires_at: String,
|
||||
nonce: String,
|
||||
device_key_id: &'a str,
|
||||
payload: HealthPayload<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealthPayload<'a> {
|
||||
path: &'static str,
|
||||
media_type: &'static str,
|
||||
size_bytes: u64,
|
||||
sha256: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealthSignature<'a> {
|
||||
algorithm: &'static str,
|
||||
key_id: &'a str,
|
||||
value: String,
|
||||
}
|
||||
|
||||
fn signature_document(identity: &DeviceIdentity, key_id: &str, envelope: &[u8]) -> Result<Vec<u8>, HealthError> {
|
||||
let pkcs8 = identity.to_pkcs8_der().map_err(|_| HealthError::Signing)?;
|
||||
let signing_key = SigningKey::from_pkcs8_der(pkcs8.as_slice()).map_err(|_| HealthError::Signing)?;
|
||||
let mut input = Vec::with_capacity(SIGNATURE_DOMAIN.len() + envelope.len());
|
||||
input.extend_from_slice(SIGNATURE_DOMAIN);
|
||||
input.extend_from_slice(envelope);
|
||||
let signature: Signature = signing_key.sign(&input);
|
||||
serde_json::to_vec(&HealthSignature {
|
||||
algorithm: "ES256",
|
||||
key_id,
|
||||
value: URL_SAFE_NO_PAD.encode_to_string(signature.normalize_s().to_bytes()),
|
||||
})
|
||||
.map_err(|_| HealthError::Encoding)
|
||||
}
|
||||
|
||||
fn archive(envelope: &[u8], signature: &[u8], result: &[u8]) -> Result<Vec<u8>, HealthError> {
|
||||
let cursor = Cursor::new(Vec::with_capacity(envelope.len() + signature.len() + result.len() + 512));
|
||||
let mut writer = ZipWriter::new(cursor);
|
||||
let options = SimpleFileOptions::DEFAULT
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.unix_permissions(0o600);
|
||||
for (name, bytes) in [(ENVELOPE_PATH, envelope), (SIGNATURE_PATH, signature), (RESULT_PATH, result)] {
|
||||
writer.start_file(name, options).map_err(|_| HealthError::Encoding)?;
|
||||
writer.write_all(bytes).map_err(|_| HealthError::Encoding)?;
|
||||
}
|
||||
writer
|
||||
.finish()
|
||||
.map(|cursor| cursor.into_inner())
|
||||
.map_err(|_| HealthError::Encoding)
|
||||
}
|
||||
|
||||
fn timestamp(value: i64) -> Result<String, HealthError> {
|
||||
DateTime::<Utc>::from_timestamp(value, 0)
|
||||
.map(|value| value.to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||
.ok_or(HealthError::InvalidRequest)
|
||||
}
|
||||
|
||||
fn resource_names_match(request: &HealthServiceRequest) -> bool {
|
||||
let Some(organization_uid) = request.organization_name.strip_prefix("organizations/") else {
|
||||
return false;
|
||||
};
|
||||
if !uuid7(organization_uid) {
|
||||
return false;
|
||||
}
|
||||
let cluster_prefix = format!("{}/clusters/", request.organization_name);
|
||||
let Some(cluster_uid) = request.cluster_name.strip_prefix(&cluster_prefix) else {
|
||||
return false;
|
||||
};
|
||||
if !uuid7(cluster_uid) {
|
||||
return false;
|
||||
}
|
||||
let device_prefix = format!("{}/clusterDevices/", request.cluster_name);
|
||||
request.device_name.strip_prefix(&device_prefix).is_some_and(uuid7)
|
||||
}
|
||||
|
||||
fn uuid7(value: &str) -> bool {
|
||||
Uuid::parse_str(value).is_ok_and(|uuid| {
|
||||
uuid.get_version() == Some(Version::SortRand) && uuid.get_variant() == Variant::RFC4122 && uuid.to_string() == value
|
||||
})
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
bytes.iter().fold(String::with_capacity(bytes.len() * 2), |mut value, byte| {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(value, "{byte:02x}");
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn collector_lease_limits_health_jobs_to_one() {
|
||||
let active = CollectorLease::acquire().expect("first health collector");
|
||||
assert!(matches!(CollectorLease::acquire(), Err(HealthError::Busy)));
|
||||
drop(active);
|
||||
CollectorLease::acquire().expect("collector lease should be released");
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,16 @@ use super::{
|
||||
DriveProvenance, LocalDriveConsent, LocalNetworkConsent, LocalProfileConsent, LocalTopConsent, MAX_NETWORK_TRAFFIC_BYTES,
|
||||
MAX_TOP_EXPORT_VALIDITY, NETWORK_CAPABILITY, NETWORK_SCHEMA_VERSION, NetworkOutcome, NetworkPerformanceError,
|
||||
NetworkPerformanceRequest, NetworkProvenance, NetworkReasonCode, PROFILE_SCHEMA_VERSION, ProfileCaptureRequest,
|
||||
ProfileOutcome, ProfileProvenance, THREAD_PROFILE_CAPABILITY, TOP_API_CAPABILITY, TOP_CLASSIFICATION, TOP_LOCKS_CAPABILITY,
|
||||
TOP_RPC_CAPABILITY, TOP_SCHEMA_VERSION, ThreadProfileScope, TopApiOperation, TopCaptureLimits, TopCaptureRequest,
|
||||
TopCaptureScope, TopOutcome, capture_cpu_profile, capture_thread_profile, capture_top_api, capture_top_locks,
|
||||
capture_top_rpc, encode_signed_profile_export, measure_drive, measure_network, runtime_network_peer_aliases,
|
||||
sign_drive_export, sign_network_export, sign_top_export_with_nonce,
|
||||
ProfileOutcome, ProfileProvenance, THREAD_PROFILE_CAPABILITY, TOP_API_CAPABILITY, TOP_CLASSIFICATION, TOP_DISK_CAPABILITY,
|
||||
TOP_LOCKS_CAPABILITY, TOP_RPC_CAPABILITY, TOP_SCHEMA_VERSION, ThreadProfileScope, TopApiOperation, TopCaptureLimits,
|
||||
TopCaptureRequest, TopCaptureScope, TopOutcome, capture_cpu_profile, capture_thread_profile, capture_top_api,
|
||||
capture_top_disk, capture_top_locks, capture_top_rpc, encode_signed_profile_export, measure_drive, measure_network,
|
||||
runtime_network_peer_aliases, sign_drive_export, sign_network_export, sign_top_export_with_nonce,
|
||||
};
|
||||
use super::{
|
||||
HEALTH_SCHEMA_VERSION, HEALTH_SERVICE_CAPABILITY, HEALTH_TIMEOUT_SECONDS, HealthError, HealthServiceRequest,
|
||||
LocalHealthConsent, MAX_EVIDENCE_AGE_SECONDS, MAX_HEALTH_CPU_MILLIS, MAX_HEALTH_MEMORY_BYTES, MAX_HEALTH_OUTPUT_BYTES,
|
||||
collect_runtime_health,
|
||||
};
|
||||
use crate::connect::DeviceIdentity;
|
||||
|
||||
@@ -50,8 +55,10 @@ const PROFILE_MEMORY_JOB_TYPE: &str = "profile.memory";
|
||||
const PERFORMANCE_DRIVE_JOB_TYPE: &str = "performance.drive";
|
||||
const PERFORMANCE_NETWORK_JOB_TYPE: &str = "performance.network";
|
||||
const TOP_API_JOB_TYPE: &str = "top.api";
|
||||
const TOP_DISK_JOB_TYPE: &str = "top.disk";
|
||||
const TOP_LOCKS_JOB_TYPE: &str = "top.locks";
|
||||
const TOP_RPC_JOB_TYPE: &str = "top.rpc";
|
||||
const HEALTH_JOB_TYPE: &str = "health.check";
|
||||
pub const DIAGNOSTIC_JOB_SIGNATURE_DOMAIN: &[u8] = b"rustfs-connect-agent-job-v1\0";
|
||||
const MAX_JOB_LIFETIME_SECONDS: i64 = 1_800;
|
||||
const MAX_FUTURE_SKEW_SECONDS: i64 = 300;
|
||||
@@ -67,6 +74,8 @@ const DRIVE_SCRATCH_BYTES: u64 = 524_288;
|
||||
const DRIVE_BLOCK_BYTES: u64 = 65_536;
|
||||
const DRIVE_SAMPLE_PERIOD_MICROS: u64 = 10_000;
|
||||
const DRIVE_SCRATCH_DIRECTORY: &str = ".rustfs-connect-drive-scratch";
|
||||
const MAX_TOP_DISK_CPU_MILLIS: u64 = 5_000;
|
||||
const MIN_TOP_DISK_MEMORY_BYTES: u64 = 1_048_576;
|
||||
const MAX_TOP_API_CPU_MILLIS: u64 = 5_000;
|
||||
const MIN_TOP_API_MEMORY_BYTES: u64 = 1_048_576;
|
||||
const MAX_TOP_LOCKS_CPU_MILLIS: u64 = 5_000;
|
||||
@@ -76,12 +85,14 @@ const MIN_TOP_RPC_MEMORY_BYTES: u64 = 1_048_576;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum DiagnosticJobKind {
|
||||
Health,
|
||||
ProfileCpu,
|
||||
ProfileThreads,
|
||||
ProfileMemory,
|
||||
PerformanceDrive,
|
||||
PerformanceNetwork,
|
||||
TopApi,
|
||||
TopDisk,
|
||||
TopLocks,
|
||||
TopRpc,
|
||||
}
|
||||
@@ -109,8 +120,18 @@ pub struct DiagnosticJobParameters {
|
||||
pub consent_uid: String,
|
||||
pub consent_policy_revision: u64,
|
||||
pub consent_expires_at: String,
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
pub duration_millis: u64,
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
pub sample_period_micros: u64,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_thread_scope"
|
||||
)]
|
||||
pub thread_scope: Option<ThreadProfileScope>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_evidence_age_seconds: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub traffic_bytes: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -121,6 +142,10 @@ pub struct DiagnosticJobParameters {
|
||||
pub block_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
fn deserialize_thread_scope<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<ThreadProfileScope>, D::Error> {
|
||||
ThreadProfileScope::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct DiagnosticJobSignature {
|
||||
@@ -371,6 +396,9 @@ impl DiagnosticJobEnvelope {
|
||||
|
||||
fn validate(&self, target: &DiagnosticJobTarget, now: DateTime<Utc>) -> Result<(), DiagnosticJobError> {
|
||||
let kind = self.kind()?;
|
||||
if (kind == DiagnosticJobKind::ProfileThreads) != self.parameters.thread_scope.is_some() {
|
||||
return Err(DiagnosticJobError::Invalid);
|
||||
}
|
||||
if self.protocol_version != PROTOCOL_VERSION || self.schema_version != PROFILE_SCHEMA_VERSION {
|
||||
return Err(DiagnosticJobError::Unsupported);
|
||||
}
|
||||
@@ -415,19 +443,45 @@ impl DiagnosticJobEnvelope {
|
||||
|| self.limits.max_memory_bytes > MAX_MEMORY_BYTES
|
||||
|| self.limits.max_cpu_millis == 0
|
||||
|| self.limits.max_cpu_millis > MAX_CPU_MILLIS
|
||||
|| self.parameters.duration_millis == 0
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
if kind == DiagnosticJobKind::Health {
|
||||
if self.limits.timeout_seconds != HEALTH_TIMEOUT_SECONDS
|
||||
|| self.limits.max_output_bytes != MAX_HEALTH_OUTPUT_BYTES
|
||||
|| self.limits.max_memory_bytes != MAX_HEALTH_MEMORY_BYTES
|
||||
|| self.limits.max_cpu_millis != MAX_HEALTH_CPU_MILLIS
|
||||
|| self.parameters.duration_millis != 0
|
||||
|| self.parameters.sample_period_micros != 0
|
||||
|| !matches!(self.parameters.max_evidence_age_seconds, Some(1..=MAX_EVIDENCE_AGE_SECONDS))
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
} else if self.parameters.duration_millis == 0
|
||||
|| self.parameters.duration_millis > self.limits.timeout_seconds.saturating_mul(1_000)
|
||||
|| self.parameters.duration_millis > self.limits.max_cpu_millis
|
||||
|| self.parameters.sample_period_micros == 0
|
||||
|| self.parameters.sample_period_micros > self.parameters.duration_millis.saturating_mul(1_000)
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
} else if self.parameters.max_evidence_age_seconds.is_some() {
|
||||
return Err(DiagnosticJobError::Invalid);
|
||||
}
|
||||
if kind == DiagnosticJobKind::TopApi
|
||||
&& (self.limits.max_cpu_millis > MAX_TOP_API_CPU_MILLIS || self.limits.max_memory_bytes < MIN_TOP_API_MEMORY_BYTES)
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
if kind == DiagnosticJobKind::TopDisk
|
||||
&& (self.limits.max_cpu_millis > MAX_TOP_DISK_CPU_MILLIS || self.limits.max_memory_bytes < MIN_TOP_DISK_MEMORY_BYTES)
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
if kind == DiagnosticJobKind::ProfileThreads
|
||||
&& (self.limits.max_memory_bytes != MAX_MEMORY_BYTES || self.limits.max_cpu_millis != MAX_CPU_MILLIS)
|
||||
{
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
if kind == DiagnosticJobKind::ProfileMemory
|
||||
&& (self.limits.max_cpu_millis > MAX_MEMORY_PROFILE_CPU_MILLIS || self.limits.max_memory_bytes != MAX_MEMORY_BYTES)
|
||||
{
|
||||
@@ -481,6 +535,9 @@ impl DiagnosticJobEnvelope {
|
||||
|
||||
fn kind(&self) -> Result<DiagnosticJobKind, DiagnosticJobError> {
|
||||
match (self.job_type.as_str(), self.required_capabilities.as_slice(), self.schema_version) {
|
||||
(HEALTH_JOB_TYPE, [capability], HEALTH_SCHEMA_VERSION) if capability == HEALTH_SERVICE_CAPABILITY => {
|
||||
Ok(DiagnosticJobKind::Health)
|
||||
}
|
||||
(PROFILE_CPU_JOB_TYPE, [capability], PROFILE_SCHEMA_VERSION) if capability == CPU_PROFILE_CAPABILITY => {
|
||||
Ok(DiagnosticJobKind::ProfileCpu)
|
||||
}
|
||||
@@ -503,6 +560,11 @@ impl DiagnosticJobEnvelope {
|
||||
{
|
||||
Ok(DiagnosticJobKind::TopApi)
|
||||
}
|
||||
(TOP_DISK_JOB_TYPE, [capability], version)
|
||||
if capability == TOP_DISK_CAPABILITY && version == u16::from(TOP_SCHEMA_VERSION) =>
|
||||
{
|
||||
Ok(DiagnosticJobKind::TopDisk)
|
||||
}
|
||||
(TOP_LOCKS_JOB_TYPE, [capability], version)
|
||||
if capability == TOP_LOCKS_CAPABILITY && version == u16::from(TOP_SCHEMA_VERSION) =>
|
||||
{
|
||||
@@ -530,6 +592,7 @@ pub async fn execute_diagnostic_job(
|
||||
let nonce = job.nonce;
|
||||
let envelope = job.envelope;
|
||||
match envelope.kind()? {
|
||||
DiagnosticJobKind::Health => execute_health_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::ProfileCpu => execute_profile_cpu_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::ProfileThreads => execute_profile_threads_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::ProfileMemory => execute_profile_memory_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
@@ -545,12 +608,95 @@ pub async fn execute_diagnostic_job(
|
||||
DiagnosticJobKind::PerformanceNetwork => {
|
||||
execute_performance_network_job(envelope, nonce, identity, provenance, cancel).await
|
||||
}
|
||||
DiagnosticJobKind::TopDisk => execute_top_disk_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::TopApi => execute_top_api_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::TopLocks => execute_top_locks_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
DiagnosticJobKind::TopRpc => execute_top_rpc_job(envelope, nonce, identity, provenance, cancel).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_health_job(
|
||||
envelope: DiagnosticJobEnvelope,
|
||||
nonce: [u8; 32],
|
||||
identity: &DeviceIdentity,
|
||||
provenance: ProfileProvenance,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<DiagnosticJobExecution, DiagnosticJobError> {
|
||||
let expire = parse_time(&envelope.expire_time)?;
|
||||
let consent_expire = parse_time(&envelope.parameters.consent_expires_at)?;
|
||||
let request = HealthServiceRequest {
|
||||
organization_name: envelope.organization_name,
|
||||
cluster_name: envelope.cluster_name,
|
||||
device_name: envelope.device_name,
|
||||
run_uid: envelope.job_id.clone(),
|
||||
artifact_uid: envelope.parameters.artifact_uid,
|
||||
schema_version: envelope.schema_version,
|
||||
capability: HEALTH_SERVICE_CAPABILITY.to_owned(),
|
||||
consent: LocalHealthConsent {
|
||||
consent_uid: envelope.parameters.consent_uid,
|
||||
policy_revision: envelope.parameters.consent_policy_revision,
|
||||
expires_at_unix: consent_expire.timestamp(),
|
||||
active: true,
|
||||
},
|
||||
produced_at_unix: Utc::now().timestamp(),
|
||||
expires_at_unix: expire.timestamp(),
|
||||
nonce,
|
||||
max_evidence_age_seconds: envelope
|
||||
.parameters
|
||||
.max_evidence_age_seconds
|
||||
.ok_or(DiagnosticJobError::LimitExceeded)?,
|
||||
provenance,
|
||||
};
|
||||
let timeout = Duration::from_secs(envelope.limits.timeout_seconds);
|
||||
let health_cancel = cancel.child_token();
|
||||
let collected = tokio::time::timeout(timeout, collect_runtime_health(&request, identity, &health_cancel)).await;
|
||||
let export = match collected {
|
||||
Ok(Ok(export)) => export,
|
||||
Ok(Err(HealthError::SourceUnavailable)) => {
|
||||
return Ok(DiagnosticJobExecution {
|
||||
job_id: envelope.job_id,
|
||||
outcome: "FAILED".to_owned(),
|
||||
reason: "SOURCE_UNAVAILABLE".to_owned(),
|
||||
artifact_uid: None,
|
||||
artifact_sha256: None,
|
||||
artifact_bytes: None,
|
||||
});
|
||||
}
|
||||
Ok(Err(error)) => return Err(health_failure(error)),
|
||||
Err(_) => {
|
||||
health_cancel.cancel();
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
};
|
||||
if export.archive_bytes.len() > usize::try_from(envelope.limits.max_output_bytes).unwrap_or(usize::MAX) {
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
Ok(DiagnosticJobExecution {
|
||||
job_id: envelope.job_id,
|
||||
outcome: export.outcome.as_str().to_owned(),
|
||||
reason: export.reason_code.as_str().to_owned(),
|
||||
artifact_uid: Some(export.artifact_uid),
|
||||
artifact_sha256: Some(export.archive_sha256),
|
||||
artifact_bytes: Some(export.archive_bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn health_failure(error: HealthError) -> DiagnosticJobError {
|
||||
match error {
|
||||
HealthError::Cancelled => DiagnosticJobError::Cancelled,
|
||||
HealthError::LimitExceeded | HealthError::Busy => DiagnosticJobError::LimitExceeded,
|
||||
HealthError::SourceUnavailable | HealthError::CollectionFailed => DiagnosticJobError::CollectionFailed,
|
||||
HealthError::Signing | HealthError::Encoding => DiagnosticJobError::ExportFailed,
|
||||
HealthError::ConsentExpired | HealthError::Expired => DiagnosticJobError::Expired,
|
||||
HealthError::Unsupported => DiagnosticJobError::Unsupported,
|
||||
HealthError::ConsentRequired | HealthError::InvalidRequest => DiagnosticJobError::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_zero(value: &u64) -> bool {
|
||||
*value == 0
|
||||
}
|
||||
|
||||
fn runtime_drive_scratch_root() -> Option<std::path::PathBuf> {
|
||||
let endpoint_pools = crate::runtime_sources::current_endpoints_handle()?;
|
||||
drive_scratch_root_from_endpoints(&endpoint_pools)
|
||||
@@ -835,10 +981,16 @@ async fn execute_profile_threads_job(
|
||||
sample_period: Duration::from_micros(envelope.parameters.sample_period_micros),
|
||||
provenance,
|
||||
};
|
||||
let scope = envelope.parameters.thread_scope.ok_or(DiagnosticJobError::Invalid)?;
|
||||
let metrics = tokio::runtime::Handle::current().metrics();
|
||||
let timeout = Duration::from_secs(envelope.limits.timeout_seconds);
|
||||
let owned_request = request.clone();
|
||||
let owned_cancel = cancel.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
capture_thread_profile(&owned_request, ThreadProfileScope::NativeThreads, &owned_cancel)
|
||||
let result = tokio::task::spawn_blocking(move || match scope {
|
||||
ThreadProfileScope::NativeThreads => capture_thread_profile(&owned_request, scope, &owned_cancel),
|
||||
ThreadProfileScope::TokioRuntime => {
|
||||
super::profile_threads::capture_runtime_profile(&owned_request, &metrics, timeout, &owned_cancel)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| DiagnosticJobError::CollectionFailed)?
|
||||
@@ -980,6 +1132,70 @@ async fn execute_top_api_job(
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_top_disk_job(
|
||||
envelope: DiagnosticJobEnvelope,
|
||||
nonce: [u8; 32],
|
||||
identity: &DeviceIdentity,
|
||||
provenance: ProfileProvenance,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<DiagnosticJobExecution, DiagnosticJobError> {
|
||||
let expire = parse_time(&envelope.expire_time)?;
|
||||
let consent_expire = parse_time(&envelope.parameters.consent_expires_at)?;
|
||||
let request = TopCaptureRequest {
|
||||
scope: TopCaptureScope {
|
||||
organization_name: envelope.organization_name,
|
||||
cluster_name: envelope.cluster_name,
|
||||
device_name: envelope.device_name,
|
||||
run_uid: envelope.job_id.clone(),
|
||||
artifact_uid: envelope.parameters.artifact_uid,
|
||||
policy_revision: envelope.parameters.consent_policy_revision,
|
||||
run_expires_at_unix: expire.timestamp(),
|
||||
executable_sha256: provenance.executable_sha256().to_owned(),
|
||||
build_features: provenance.build_features().to_vec(),
|
||||
consent: LocalTopConsent {
|
||||
uid: envelope.parameters.consent_uid,
|
||||
tool_id: TOP_DISK_JOB_TYPE.to_owned(),
|
||||
classification: TOP_CLASSIFICATION.to_owned(),
|
||||
active: true,
|
||||
expires_at_unix: consent_expire.timestamp(),
|
||||
},
|
||||
},
|
||||
limits: TopCaptureLimits {
|
||||
max_duration_millis: envelope.parameters.duration_millis,
|
||||
max_working_memory_bytes: envelope.limits.max_memory_bytes,
|
||||
max_cpu_millis: envelope.limits.max_cpu_millis,
|
||||
..TopCaptureLimits::default()
|
||||
},
|
||||
window: Duration::from_millis(envelope.parameters.duration_millis),
|
||||
export_validity: MAX_TOP_EXPORT_VALIDITY,
|
||||
};
|
||||
let result = capture_top_disk(&request, cancel).await.map_err(top_capture_failure)?;
|
||||
let outcome = result.outcome.as_str().to_owned();
|
||||
let reason = result.reason_code.as_str().to_owned();
|
||||
if !matches!(result.outcome, TopOutcome::Succeeded | TopOutcome::Partial) {
|
||||
return Ok(DiagnosticJobExecution {
|
||||
job_id: envelope.job_id,
|
||||
outcome,
|
||||
reason,
|
||||
artifact_uid: None,
|
||||
artifact_sha256: None,
|
||||
artifact_bytes: None,
|
||||
});
|
||||
}
|
||||
let export = sign_top_export_with_nonce(&request, &result, identity, cancel, nonce).map_err(top_export_failure)?;
|
||||
if export.archive_bytes.len() > usize::try_from(envelope.limits.max_output_bytes).unwrap_or(usize::MAX) {
|
||||
return Err(DiagnosticJobError::LimitExceeded);
|
||||
}
|
||||
Ok(DiagnosticJobExecution {
|
||||
job_id: envelope.job_id,
|
||||
outcome,
|
||||
reason,
|
||||
artifact_uid: Some(export.artifact_uid),
|
||||
artifact_sha256: Some(export.archive_sha256),
|
||||
artifact_bytes: Some(export.archive_bytes),
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_top_locks_job(
|
||||
envelope: DiagnosticJobEnvelope,
|
||||
nonce: [u8; 32],
|
||||
@@ -1227,12 +1443,14 @@ mod tests {
|
||||
max_cpu_millis: 30_000,
|
||||
},
|
||||
parameters: DiagnosticJobParameters {
|
||||
thread_scope: None,
|
||||
artifact_uid: "018cc251-f400-7abc-8def-0123456789ae".to_owned(),
|
||||
consent_uid: "018cc251-f400-7abc-8def-0123456789af".to_owned(),
|
||||
consent_policy_revision: 1,
|
||||
consent_expires_at: "2030-01-01T00:01:00Z".to_owned(),
|
||||
duration_millis: 1_000,
|
||||
sample_period_micros: 10_000,
|
||||
max_evidence_age_seconds: None,
|
||||
traffic_bytes: None,
|
||||
target_alias: None,
|
||||
scratch_bytes: None,
|
||||
@@ -1275,6 +1493,7 @@ mod tests {
|
||||
use clap::CommandFactory;
|
||||
|
||||
let expected = [
|
||||
("health.check.service@1", &[][..]),
|
||||
("performance.client@1", &["performance", "client"][..]),
|
||||
("performance.drive@1", &["performance", "drive"][..]),
|
||||
("performance.network@1", &[][..]),
|
||||
@@ -1306,6 +1525,7 @@ mod tests {
|
||||
if path.is_empty() {
|
||||
let mut job = envelope();
|
||||
let (job_type, kind) = match capability {
|
||||
HEALTH_SERVICE_CAPABILITY => (HEALTH_JOB_TYPE, DiagnosticJobKind::Health),
|
||||
NETWORK_CAPABILITY => (PERFORMANCE_NETWORK_JOB_TYPE, DiagnosticJobKind::PerformanceNetwork),
|
||||
MEMORY_PROFILE_SERVICE_CAPABILITY => (PROFILE_MEMORY_JOB_TYPE, DiagnosticJobKind::ProfileMemory),
|
||||
_ => panic!("{capability} is missing service dispatch"),
|
||||
@@ -1313,6 +1533,17 @@ mod tests {
|
||||
job.job_type = job_type.to_owned();
|
||||
job.required_capabilities = vec![capability.to_owned()];
|
||||
job.schema_version = NETWORK_SCHEMA_VERSION;
|
||||
if capability == HEALTH_SERVICE_CAPABILITY {
|
||||
job.limits = DiagnosticJobLimits {
|
||||
timeout_seconds: HEALTH_TIMEOUT_SECONDS,
|
||||
max_output_bytes: MAX_HEALTH_OUTPUT_BYTES,
|
||||
max_memory_bytes: MAX_HEALTH_MEMORY_BYTES,
|
||||
max_cpu_millis: MAX_HEALTH_CPU_MILLIS,
|
||||
};
|
||||
job.parameters.duration_millis = 0;
|
||||
job.parameters.sample_period_micros = 0;
|
||||
job.parameters.max_evidence_age_seconds = Some(300);
|
||||
}
|
||||
assert_eq!(job.kind(), Ok(kind));
|
||||
continue;
|
||||
}
|
||||
@@ -1333,10 +1564,75 @@ mod tests {
|
||||
.expect("valid job");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_the_bounded_health_service_contract() {
|
||||
let mut health = envelope();
|
||||
health.job_type = HEALTH_JOB_TYPE.to_owned();
|
||||
health.required_capabilities = vec![HEALTH_SERVICE_CAPABILITY.to_owned()];
|
||||
health.limits = DiagnosticJobLimits {
|
||||
timeout_seconds: HEALTH_TIMEOUT_SECONDS,
|
||||
max_output_bytes: MAX_HEALTH_OUTPUT_BYTES,
|
||||
max_memory_bytes: MAX_HEALTH_MEMORY_BYTES,
|
||||
max_cpu_millis: MAX_HEALTH_CPU_MILLIS,
|
||||
};
|
||||
health.parameters.duration_millis = 0;
|
||||
health.parameters.sample_period_micros = 0;
|
||||
health.parameters.max_evidence_age_seconds = Some(300);
|
||||
let (health, signer) = signed_envelope(health);
|
||||
signer
|
||||
.verify(&health, &target(&health), "2030-01-01T00:00:10Z".parse().expect("time"))
|
||||
.expect("valid health service job");
|
||||
|
||||
let unsigned = serde_json::to_value(health.unsigned()).expect("unsigned health job");
|
||||
assert!(unsigned["parameters"].get("durationMillis").is_none());
|
||||
assert!(unsigned["parameters"].get("samplePeriodMicros").is_none());
|
||||
assert_eq!(unsigned["parameters"]["maxEvidenceAgeSeconds"], 300);
|
||||
|
||||
let mut old_capability = health.clone();
|
||||
old_capability.required_capabilities = vec!["health.check@1".to_owned()];
|
||||
assert_eq!(
|
||||
signer.verify(&old_capability, &target(&old_capability), "2030-01-01T00:00:10Z".parse().expect("time")),
|
||||
Err(DiagnosticJobError::Unsupported)
|
||||
);
|
||||
|
||||
let mut oversized = health.clone();
|
||||
oversized.limits.max_output_bytes += 1;
|
||||
let (oversized, oversized_signer) = signed_envelope(oversized);
|
||||
assert_eq!(
|
||||
oversized_signer.verify(&oversized, &target(&oversized), "2030-01-01T00:00:10Z".parse().expect("time")),
|
||||
Err(DiagnosticJobError::LimitExceeded)
|
||||
);
|
||||
|
||||
let mut missing_freshness = health.clone();
|
||||
missing_freshness.parameters.max_evidence_age_seconds = None;
|
||||
let (missing_freshness, missing_signer) = signed_envelope(missing_freshness);
|
||||
assert_eq!(
|
||||
missing_signer.verify(
|
||||
&missing_freshness,
|
||||
&target(&missing_freshness),
|
||||
"2030-01-01T00:00:10Z".parse().expect("time")
|
||||
),
|
||||
Err(DiagnosticJobError::LimitExceeded)
|
||||
);
|
||||
|
||||
let mut profile_parameter = health;
|
||||
profile_parameter.parameters.duration_millis = 1;
|
||||
let (profile_parameter, profile_parameter_signer) = signed_envelope(profile_parameter);
|
||||
assert_eq!(
|
||||
profile_parameter_signer.verify(
|
||||
&profile_parameter,
|
||||
&target(&profile_parameter),
|
||||
"2030-01-01T00:00:10Z".parse().expect("time")
|
||||
),
|
||||
Err(DiagnosticJobError::LimitExceeded)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_only_the_thread_profile_capability_pair() {
|
||||
let mut threads = envelope();
|
||||
threads.job_type = PROFILE_THREADS_JOB_TYPE.to_owned();
|
||||
threads.parameters.thread_scope = Some(ThreadProfileScope::NativeThreads);
|
||||
threads.required_capabilities = vec![THREAD_PROFILE_CAPABILITY.to_owned()];
|
||||
let (threads, signer) = signed_envelope(threads);
|
||||
signer
|
||||
@@ -1351,33 +1647,201 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_thread_profile_jobs_against_the_service_process() {
|
||||
let now = Utc::now();
|
||||
let mut envelope = envelope();
|
||||
envelope.job_type = PROFILE_THREADS_JOB_TYPE.to_owned();
|
||||
envelope.required_capabilities = vec![THREAD_PROFILE_CAPABILITY.to_owned()];
|
||||
envelope.create_time = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
envelope.expire_time = (now + chrono::Duration::seconds(30)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
envelope.parameters.consent_expires_at =
|
||||
(now + chrono::Duration::seconds(60)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let execution = execute_diagnostic_job(
|
||||
VerifiedDiagnosticJob {
|
||||
envelope,
|
||||
nonce: [7_u8; 32],
|
||||
},
|
||||
&DeviceIdentity::generate(),
|
||||
ProfileProvenance::new("a".repeat(40), "b".repeat(64), "1.0.0", vec![]),
|
||||
&CancellationToken::new(),
|
||||
)
|
||||
.await
|
||||
.expect("native thread profile job should execute");
|
||||
#[test]
|
||||
fn thread_scope_is_required_and_forbidden_on_other_jobs() {
|
||||
let now = "2030-01-01T00:00:10Z".parse().unwrap();
|
||||
let mut job = envelope();
|
||||
job.parameters.thread_scope = Some(ThreadProfileScope::TokioRuntime);
|
||||
assert_eq!(job.validate(&target(&job), now), Err(DiagnosticJobError::Invalid));
|
||||
job.job_type = PROFILE_THREADS_JOB_TYPE.to_owned();
|
||||
job.required_capabilities = vec![THREAD_PROFILE_CAPABILITY.to_owned()];
|
||||
assert!(job.validate(&target(&job), now).is_ok());
|
||||
job.limits.max_memory_bytes = 1;
|
||||
assert_eq!(job.validate(&target(&job), now), Err(DiagnosticJobError::LimitExceeded));
|
||||
job.limits.max_memory_bytes = MAX_MEMORY_BYTES;
|
||||
job.parameters.duration_millis = 1;
|
||||
job.parameters.sample_period_micros = 1;
|
||||
job.limits.max_cpu_millis = 1;
|
||||
assert_eq!(job.validate(&target(&job), now), Err(DiagnosticJobError::LimitExceeded));
|
||||
job.limits.max_cpu_millis = MAX_CPU_MILLIS;
|
||||
job.parameters.thread_scope = None;
|
||||
assert_eq!(job.validate(&target(&job), now), Err(DiagnosticJobError::Invalid));
|
||||
let mut json = serde_json::to_value(&job).unwrap();
|
||||
json["parameters"]["threadScope"] = serde_json::json!("ALL_THREADS");
|
||||
assert!(serde_json::from_value::<DiagnosticJobEnvelope>(json.clone()).is_err());
|
||||
json["parameters"]["threadScope"] = serde_json::Value::Null;
|
||||
assert!(serde_json::from_value::<DiagnosticJobEnvelope>(json).is_err());
|
||||
job.parameters.thread_scope = Some(ThreadProfileScope::TokioRuntime);
|
||||
let (mut signed, signer) = signed_envelope(job);
|
||||
signed.parameters.thread_scope = Some(ThreadProfileScope::NativeThreads);
|
||||
assert!(signer.verify(&signed, &target(&signed), now).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn executes_thread_profile_jobs_against_the_service_process() {
|
||||
for scope in [ThreadProfileScope::NativeThreads, ThreadProfileScope::TokioRuntime] {
|
||||
let now = Utc::now();
|
||||
let mut envelope = envelope();
|
||||
envelope.job_type = PROFILE_THREADS_JOB_TYPE.to_owned();
|
||||
envelope.parameters.thread_scope = Some(scope);
|
||||
envelope.required_capabilities = vec![THREAD_PROFILE_CAPABILITY.to_owned()];
|
||||
envelope.create_time = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
envelope.expire_time = (now + chrono::Duration::seconds(30)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
envelope.parameters.consent_expires_at =
|
||||
(now + chrono::Duration::seconds(60)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let execution = execute_diagnostic_job(
|
||||
VerifiedDiagnosticJob {
|
||||
envelope,
|
||||
nonce: [7_u8; 32],
|
||||
},
|
||||
&DeviceIdentity::generate(),
|
||||
ProfileProvenance::new("a".repeat(40), "b".repeat(64), "1.0.0", vec![]),
|
||||
&CancellationToken::new(),
|
||||
)
|
||||
.await
|
||||
.expect("thread profile job should execute");
|
||||
|
||||
let supported = match scope {
|
||||
ThreadProfileScope::NativeThreads => cfg!(target_os = "linux"),
|
||||
ThreadProfileScope::TokioRuntime => cfg!(target_has_atomic = "64"),
|
||||
};
|
||||
if supported {
|
||||
assert_eq!(execution.outcome, "SUCCEEDED");
|
||||
assert_eq!(execution.reason, "COMPLETE");
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(execution.artifact_bytes.unwrap())).unwrap();
|
||||
let result: serde_json::Value = serde_json::from_reader(archive.by_name("result.json").unwrap()).unwrap();
|
||||
assert_eq!(result["data"]["scope"], serde_json::to_value(scope).unwrap());
|
||||
if scope == ThreadProfileScope::TokioRuntime {
|
||||
assert_eq!(result["data"]["workerCount"], 2);
|
||||
assert_eq!(result["data"]["samples"].as_array().unwrap().len(), 2);
|
||||
}
|
||||
let envelope: serde_json::Value = serde_json::from_reader(archive.by_name("envelope.json").unwrap()).unwrap();
|
||||
assert_eq!(envelope["classification"], "L3");
|
||||
} else {
|
||||
assert_eq!(execution.outcome, "UNSUPPORTED");
|
||||
assert_eq!(execution.reason, "UNSUPPORTED_PLATFORM");
|
||||
assert!(execution.artifact_bytes.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disk_envelope() -> DiagnosticJobEnvelope {
|
||||
let mut top = envelope();
|
||||
top.job_type = TOP_DISK_JOB_TYPE.to_owned();
|
||||
top.required_capabilities = vec![TOP_DISK_CAPABILITY.to_owned()];
|
||||
top.limits.max_cpu_millis = MAX_TOP_DISK_CPU_MILLIS;
|
||||
top
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_disk_job_rejects_mismatched_capabilities_parameters_consent_and_budgets() {
|
||||
let now = "2030-01-01T00:00:10Z".parse().unwrap();
|
||||
let (top, signer) = signed_envelope(disk_envelope());
|
||||
assert!(signer.verify(&top, &target(&top), now).is_ok());
|
||||
for case in 0..11 {
|
||||
let mut invalid = disk_envelope();
|
||||
let expected = match case {
|
||||
0 => {
|
||||
invalid.required_capabilities = vec![TOP_API_CAPABILITY.to_owned()];
|
||||
DiagnosticJobError::Unsupported
|
||||
}
|
||||
1 => {
|
||||
invalid.schema_version = 2;
|
||||
DiagnosticJobError::Unsupported
|
||||
}
|
||||
2 => {
|
||||
invalid.parameters.thread_scope = Some(ThreadProfileScope::TokioRuntime);
|
||||
DiagnosticJobError::Invalid
|
||||
}
|
||||
3 => {
|
||||
invalid.parameters.traffic_bytes = Some(1);
|
||||
DiagnosticJobError::Invalid
|
||||
}
|
||||
4 => {
|
||||
invalid.parameters.target_alias = Some("drive-1".to_owned());
|
||||
DiagnosticJobError::Invalid
|
||||
}
|
||||
5 => {
|
||||
invalid.parameters.consent_policy_revision = 0;
|
||||
DiagnosticJobError::Invalid
|
||||
}
|
||||
6 => {
|
||||
invalid.parameters.consent_expires_at = invalid.create_time.clone();
|
||||
DiagnosticJobError::Expired
|
||||
}
|
||||
7 => {
|
||||
invalid.limits.max_cpu_millis = MAX_TOP_DISK_CPU_MILLIS + 1;
|
||||
DiagnosticJobError::LimitExceeded
|
||||
}
|
||||
8 => {
|
||||
invalid.limits.max_memory_bytes = MIN_TOP_DISK_MEMORY_BYTES - 1;
|
||||
DiagnosticJobError::LimitExceeded
|
||||
}
|
||||
9 => {
|
||||
invalid.parameters.duration_millis = MAX_TOP_DISK_CPU_MILLIS + 1;
|
||||
DiagnosticJobError::LimitExceeded
|
||||
}
|
||||
_ => {
|
||||
invalid.parameters.sample_period_micros = 0;
|
||||
DiagnosticJobError::LimitExceeded
|
||||
}
|
||||
};
|
||||
let (invalid, signer) = signed_envelope(invalid);
|
||||
assert_eq!(signer.verify(&invalid, &target(&invalid), now), Err(expected), "case {case}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn top_disk_job_collects_service_io_and_preserves_signed_nonce() {
|
||||
let now = Utc::now();
|
||||
let mut top = disk_envelope();
|
||||
top.parameters.duration_millis = 1_000;
|
||||
top.create_time = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
top.expire_time = (now + chrono::Duration::seconds(30)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
top.parameters.consent_expires_at =
|
||||
(now + chrono::Duration::seconds(60)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let (top, signer) = signed_envelope(top);
|
||||
let verified = signer.verify(&top, &target(&top), now).unwrap();
|
||||
let identity = DeviceIdentity::generate();
|
||||
let cancel = CancellationToken::new();
|
||||
let collect = execute_diagnostic_job(
|
||||
verified,
|
||||
&identity,
|
||||
ProfileProvenance::new("a".repeat(40), "b".repeat(64), "1.0.0", vec![]),
|
||||
&cancel,
|
||||
);
|
||||
let workload = async {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::io::Write as _;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&[1u8; 65_536]).unwrap();
|
||||
file.sync_all().unwrap();
|
||||
}
|
||||
};
|
||||
let (result, ()) = tokio::join!(collect, workload);
|
||||
let execution = result.unwrap();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(execution.outcome, "SUCCEEDED");
|
||||
assert_eq!(execution.reason, "COMPLETE");
|
||||
assert!(execution.artifact_bytes.is_some_and(|bytes| !bytes.is_empty()));
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(execution.artifact_bytes.unwrap())).unwrap();
|
||||
let result: serde_json::Value = serde_json::from_reader(archive.by_name("result.json").unwrap()).unwrap();
|
||||
assert_eq!(result["toolId"], "top.disk");
|
||||
assert_eq!(result["data"]["resourceAlias"], "resource-1");
|
||||
assert!(result["data"]["writeBytes"].as_u64().unwrap() >= 65_536);
|
||||
assert!(result["data"]["ioCount"].as_u64().unwrap() > 0);
|
||||
use std::io::Read as _;
|
||||
let mut bytes = Vec::new();
|
||||
archive.by_name("envelope.json").unwrap().read_to_end(&mut bytes).unwrap();
|
||||
let envelope: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(envelope["nonce"], top.nonce);
|
||||
assert_eq!(envelope["classification"], "L3");
|
||||
let signature: serde_json::Value = serde_json::from_reader(archive.by_name("envelope.sig").unwrap()).unwrap();
|
||||
let mut signed = b"rustfs-diagnostic-envelope-v1\0".to_vec();
|
||||
signed.extend_from_slice(&bytes);
|
||||
assert!(identity.verifies_pending_registration_state(&signed, signature["value"].as_str().unwrap()));
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
|
||||
@@ -385,7 +385,7 @@ fn target_and_identity(config: &HeartbeatConfig) -> Result<(DiagnosticJobTarget,
|
||||
))
|
||||
}
|
||||
|
||||
async fn executable_provenance() -> Result<ProfileProvenance, &'static str> {
|
||||
pub(super) async fn executable_provenance() -> Result<ProfileProvenance, &'static str> {
|
||||
let digest = tokio::task::spawn_blocking(hash_current_executable)
|
||||
.await
|
||||
.map_err(|_| "PROVENANCE_FAILED")?
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod health;
|
||||
mod inspect;
|
||||
mod job;
|
||||
pub(crate) mod job_delivery;
|
||||
@@ -40,6 +41,7 @@ mod trace_runtime;
|
||||
|
||||
/// Signed diagnostic producers available through the CLI or authenticated service jobs.
|
||||
pub const CONNECT_DIAGNOSTIC_CAPABILITIES: &[&str] = &[
|
||||
health::HEALTH_SERVICE_CAPABILITY,
|
||||
perf_client::CLIENT_CAPABILITY,
|
||||
perf_drive::DRIVE_CAPABILITY,
|
||||
perf_network::NETWORK_CAPABILITY,
|
||||
@@ -64,6 +66,13 @@ pub const CONNECT_DIAGNOSTIC_CAPABILITIES: &[&str] = &[
|
||||
#[path = "trace_runtime_unsupported.rs"]
|
||||
mod trace_runtime;
|
||||
|
||||
pub use health::{
|
||||
HEALTH_CATALOG_CHECKS, HEALTH_SCHEMA_VERSION, HEALTH_SERVICE_CAPABILITY, HEALTH_TIMEOUT_SECONDS, HealthCheckResult,
|
||||
HealthDiagnosticResult, HealthError, HealthFreshness, HealthOutcome, HealthResultReason, HealthRuleOutcome,
|
||||
HealthServiceRequest, HealthSourceObservation, LocalHealthConsent, MAX_EVIDENCE_AGE_SECONDS, MAX_HEALTH_CPU_MILLIS,
|
||||
MAX_HEALTH_MEMORY_BYTES, MAX_HEALTH_OUTPUT_BYTES, SignedHealthExport, collect_runtime_health, evaluate_health_observation,
|
||||
sign_health_export,
|
||||
};
|
||||
pub use inspect::{
|
||||
INSPECT_CAPABILITY, INSPECT_SCHEMA_VERSION, InspectArtifactConsent, InspectDiagnosticResult, InspectError, InspectFinding,
|
||||
InspectOutcome, InspectProvenance, InspectReason, InspectReasonCode, InspectRequest, InspectRule, InspectRuleOutcome,
|
||||
@@ -122,6 +131,7 @@ pub use perf_site_replication::{
|
||||
SiteReplicationTargetResult, measure_site_replication, read_protected_site_replication_credential,
|
||||
save_signed_site_replication_export, sign_site_replication_export, validate_site_replication_limits,
|
||||
};
|
||||
pub(crate) use profile_cpu::LocalRuntimeProfileRequest;
|
||||
pub use profile_cpu::{
|
||||
CPU_PROFILE_CAPABILITY, LocalProfileConsent, MAX_PROFILE_DURATION, MEMORY_PROFILE_CAPABILITY, PROFILE_SCHEMA_VERSION,
|
||||
ProfileCaptureRequest, ProfileData, ProfileError, ProfileOutcome, ProfileProvenance, ProfileReasonCode, ProfileResult,
|
||||
@@ -162,5 +172,9 @@ pub use trace_record::{
|
||||
};
|
||||
pub use trace_replay::{LocallyReviewedTraceArtifact, ReplayedTrace, TraceReplayError, replay_trace, replay_trace_result};
|
||||
pub(crate) use trace_runtime::{
|
||||
LocalTraceCaptureError, LocalTraceCaptureRuntime, request_local_trace_capture, spawn_local_trace_capture_runtime,
|
||||
LocalTraceCaptureError, LocalTraceCaptureRuntime, request_local_runtime_profile, request_local_top_disk,
|
||||
request_local_trace_capture, spawn_local_trace_capture_runtime,
|
||||
};
|
||||
|
||||
pub(crate) use top_api::save_top_archive;
|
||||
pub(crate) use top_disk::LocalTopDiskRequest;
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::io::{Cursor, Write as _};
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use p256::ecdsa::{Signature, SigningKey, signature::Signer as _};
|
||||
@@ -29,6 +29,7 @@ use serde::Serialize;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use thiserror::Error;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::{Uuid, Variant, Version};
|
||||
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
|
||||
@@ -416,7 +417,14 @@ pub async fn measure_network(
|
||||
if aliases != request.peer_aliases {
|
||||
return Err(NetworkPerformanceError::InvalidRequest);
|
||||
}
|
||||
measure_network_with_harness(request, &harness, cancel).await
|
||||
let started = Instant::now();
|
||||
let harness = RuntimeNetworkPeerHarness {
|
||||
client: harness
|
||||
.client
|
||||
.with_diagnostic_pacing(started, request.duration)
|
||||
.map_err(|_| NetworkPerformanceError::LimitExceeded)?,
|
||||
};
|
||||
measure_network_with_harness_at(request, &harness, cancel, started).await
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_network_peer_aliases() -> Option<Vec<String>> {
|
||||
@@ -433,14 +441,24 @@ pub async fn measure_network_with_harness(
|
||||
request: &NetworkPerformanceRequest,
|
||||
harness: &dyn NetworkPeerHarness,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<NetworkMeasurement, NetworkPerformanceError> {
|
||||
measure_network_with_harness_at(request, harness, cancel, Instant::now()).await
|
||||
}
|
||||
|
||||
async fn measure_network_with_harness_at(
|
||||
request: &NetworkPerformanceRequest,
|
||||
harness: &dyn NetworkPeerHarness,
|
||||
cancel: &CancellationToken,
|
||||
started: Instant,
|
||||
) -> Result<NetworkMeasurement, NetworkPerformanceError> {
|
||||
request.validate(unix_now()?)?;
|
||||
if cancel.is_cancelled() {
|
||||
return Ok(terminal_measurement(request, NetworkOutcome::Cancelled, NetworkReasonCode::Cancelled));
|
||||
}
|
||||
let _lease = CollectorLease::acquire()?;
|
||||
let started = Instant::now();
|
||||
let deadline = tokio::time::Instant::now() + request.duration;
|
||||
let deadline = started
|
||||
.checked_add(request.duration)
|
||||
.ok_or(NetworkPerformanceError::LimitExceeded)?;
|
||||
let mut peers = Vec::with_capacity(request.peer_aliases.len());
|
||||
let mut transferred_bytes = 0_u64;
|
||||
let mut completed_units = 0_u32;
|
||||
@@ -448,6 +466,7 @@ pub async fn measure_network_with_harness(
|
||||
for alias in &request.peer_aliases {
|
||||
let peer_started = Instant::now();
|
||||
let outcome = tokio::select! {
|
||||
biased;
|
||||
() = cancel.cancelled() => {
|
||||
return Ok(cancelled_measurement(request, started.elapsed(), completed_units, peers));
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use p256::ecdsa::{Signature, SigningKey, signature::Signer as _};
|
||||
use p256::pkcs8::DecodePrivateKey as _;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use thiserror::Error;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
@@ -185,6 +185,14 @@ impl ProfileProvenance {
|
||||
pub(crate) fn build_features(&self) -> &[String] {
|
||||
&self.build_features
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid(&self) -> bool {
|
||||
lower_hex(&self.source_commit, 40)
|
||||
&& lower_hex(&self.executable_sha256, 64)
|
||||
&& version(&self.rustfs_version)
|
||||
&& self.build_features.len() <= MAX_BUILD_FEATURES
|
||||
&& self.build_features.iter().all(|feature| build_feature(feature))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
@@ -236,6 +244,27 @@ pub struct LocalProfileConsent {
|
||||
pub confirmed: bool,
|
||||
}
|
||||
|
||||
/// Owner-authorized request for the running service, never a CLI process profile.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct LocalRuntimeProfileRequest {
|
||||
pub offline_key_id: String,
|
||||
pub organization_name: String,
|
||||
pub cluster_name: String,
|
||||
pub device_name: String,
|
||||
pub run_uid: String,
|
||||
pub artifact_uid: String,
|
||||
pub schema_version: u16,
|
||||
pub capability: String,
|
||||
pub consent_uid: String,
|
||||
pub policy_revision: u64,
|
||||
pub consent_expires_at_unix: i64,
|
||||
pub acknowledge_l3: bool,
|
||||
pub expires_at_unix: i64,
|
||||
pub duration_millis: u64,
|
||||
pub sample_period_micros: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ProfileCaptureRequest {
|
||||
pub organization_name: String,
|
||||
@@ -355,7 +384,32 @@ impl ThreadStateCount {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeProfileData {
|
||||
pub(super) scope: ThreadProfileScope,
|
||||
pub(super) worker_count: usize,
|
||||
pub(super) samples: [RuntimeProfileSample; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RuntimeProfileSample {
|
||||
pub elapsed_micros: u64,
|
||||
pub alive_task_count: u64,
|
||||
pub global_queue_depth: u64,
|
||||
pub workers: Vec<RuntimeWorkerSample>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RuntimeWorkerSample {
|
||||
pub worker_index: usize,
|
||||
pub busy_duration_micros: u64,
|
||||
pub park_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ThreadProfileScope {
|
||||
TokioRuntime,
|
||||
@@ -368,6 +422,7 @@ pub enum ProfileData {
|
||||
Cpu(CpuProfileData),
|
||||
Memory(MemoryProfileData),
|
||||
Threads(ThreadProfileData),
|
||||
Runtime(RuntimeProfileData),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
@@ -413,6 +468,19 @@ impl ProfileResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn runtime_succeeded(
|
||||
request: &ProfileCaptureRequest,
|
||||
duration: Duration,
|
||||
data: RuntimeProfileData,
|
||||
) -> Result<Self, ProfileError> {
|
||||
if duration > MAX_PROFILE_DURATION {
|
||||
return Err(ProfileError::TimedOut);
|
||||
}
|
||||
let mut result = Self::succeeded(request, ProfileTool::Threads, duration, ProfileData::Runtime(data));
|
||||
result.duration_millis = u64::try_from(duration.as_millis()).map_err(|_| ProfileError::LimitExceeded)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "pyroscope",
|
||||
any(
|
||||
@@ -969,11 +1037,7 @@ impl ProfileCaptureRequest {
|
||||
|| !uuid7(&self.artifact_uid)
|
||||
|| !uuid7(&self.consent.consent_uid)
|
||||
|| !resource_names_match(self)
|
||||
|| !lower_hex(&self.provenance.source_commit, 40)
|
||||
|| !lower_hex(&self.provenance.executable_sha256, 64)
|
||||
|| !version(&self.provenance.rustfs_version)
|
||||
|| self.provenance.build_features.len() > MAX_BUILD_FEATURES
|
||||
|| !self.provenance.build_features.iter().all(|feature| build_feature(feature))
|
||||
|| !self.provenance.is_valid()
|
||||
{
|
||||
return Err(ProfileError::InvalidRequest);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
//! Linux native collection reads only the state byte from this process's
|
||||
//! `/proc/self/task/*/stat` records. Thread names, identifiers, stacks, paths,
|
||||
//! addresses, and raw procfs bytes cannot enter the exported result. Tokio
|
||||
//! runtime state remains explicitly unsupported because Dial9 does not expose
|
||||
//! the contract's RUNNABLE, WAITING, BLOCKED, and UNKNOWN counts.
|
||||
//! service metrics are collected only from an explicitly supplied runtime handle.
|
||||
//! Standalone exports cannot observe the server runtime and remain unsupported.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::fs::{self, File};
|
||||
@@ -82,6 +82,134 @@ pub fn capture_thread_profile(
|
||||
}
|
||||
}
|
||||
|
||||
/// The caller must supply the serving runtime's metrics before entering the blocking collector.
|
||||
pub(super) fn capture_runtime_profile(
|
||||
request: &ProfileCaptureRequest,
|
||||
metrics: &tokio::runtime::RuntimeMetrics,
|
||||
timeout: std::time::Duration,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<ProfileResult, ProfileError> {
|
||||
request.validate(ProfileTool::Threads, unix_now()?)?;
|
||||
check_cancel(cancel)?;
|
||||
#[cfg(not(target_has_atomic = "64"))]
|
||||
{
|
||||
let _ = (metrics, timeout);
|
||||
return Ok(ProfileResult::unsupported(
|
||||
request,
|
||||
ProfileTool::Threads,
|
||||
ProfileReasonCode::UnsupportedPlatform,
|
||||
));
|
||||
}
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
{
|
||||
use super::profile_cpu::{CollectorLease, RuntimeProfileData};
|
||||
use std::time::{Duration, Instant};
|
||||
if timeout.is_zero() || timeout > super::profile_cpu::MAX_PROFILE_DURATION || request.duration > timeout {
|
||||
return Err(ProfileError::LimitExceeded);
|
||||
}
|
||||
let _lease = CollectorLease::acquire()?;
|
||||
let worker_count = metrics.num_workers();
|
||||
validate_runtime_workers(worker_count)?;
|
||||
let started = Instant::now();
|
||||
let first = runtime_sample(metrics, worker_count, 0)?;
|
||||
while started.elapsed() < request.duration {
|
||||
check_cancel(cancel)?;
|
||||
request.validate(ProfileTool::Threads, unix_now()?)?;
|
||||
std::thread::sleep(
|
||||
request
|
||||
.duration
|
||||
.saturating_sub(started.elapsed())
|
||||
.min(Duration::from_millis(10)),
|
||||
);
|
||||
}
|
||||
let offset = u64::try_from(started.elapsed().as_micros()).map_err(|_| ProfileError::LimitExceeded)?;
|
||||
let second = runtime_sample(metrics, worker_count, offset)?;
|
||||
validate_runtime_counters(&first, &second)?;
|
||||
check_cancel(cancel)?;
|
||||
request.validate(ProfileTool::Threads, unix_now()?)?;
|
||||
let elapsed = started.elapsed();
|
||||
if elapsed > timeout {
|
||||
return Err(ProfileError::TimedOut);
|
||||
}
|
||||
let result = ProfileResult::runtime_succeeded(
|
||||
request,
|
||||
elapsed,
|
||||
RuntimeProfileData {
|
||||
scope: ThreadProfileScope::TokioRuntime,
|
||||
worker_count,
|
||||
samples: [first, second],
|
||||
},
|
||||
)?;
|
||||
if serde_json::to_vec(&result).map_err(|_| ProfileError::Encoding)?.len() > super::profile_cpu::MAX_RESULT_BYTES {
|
||||
return Err(ProfileError::LimitExceeded);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
fn validate_runtime_counters(
|
||||
first: &super::profile_cpu::RuntimeProfileSample,
|
||||
second: &super::profile_cpu::RuntimeProfileSample,
|
||||
) -> Result<(), ProfileError> {
|
||||
if first.workers.len() != second.workers.len()
|
||||
|| first.workers.iter().zip(&second.workers).any(|(a, b)| {
|
||||
a.worker_index != b.worker_index || b.busy_duration_micros < a.busy_duration_micros || b.park_count < a.park_count
|
||||
})
|
||||
{
|
||||
return Err(ProfileError::CollectionFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
fn validate_runtime_workers(worker_count: usize) -> Result<(), ProfileError> {
|
||||
// Each of the two snapshots includes one summary and one record per worker.
|
||||
if worker_count == 0
|
||||
|| worker_count
|
||||
.checked_add(1)
|
||||
.and_then(|n| n.checked_mul(2))
|
||||
.is_none_or(|n| n > 1024)
|
||||
{
|
||||
return Err(ProfileError::LimitExceeded);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
fn runtime_count(value: u128) -> Result<u64, ProfileError> {
|
||||
if value > 9_007_199_254_740_991 {
|
||||
return Err(ProfileError::LimitExceeded);
|
||||
}
|
||||
Ok(value as u64)
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
fn runtime_sample(
|
||||
metrics: &tokio::runtime::RuntimeMetrics,
|
||||
worker_count: usize,
|
||||
elapsed_micros: u64,
|
||||
) -> Result<super::profile_cpu::RuntimeProfileSample, ProfileError> {
|
||||
use super::profile_cpu::{RuntimeProfileSample, RuntimeWorkerSample};
|
||||
if metrics.num_workers() != worker_count {
|
||||
return Err(ProfileError::CollectionFailed);
|
||||
}
|
||||
let mut workers = Vec::with_capacity(worker_count);
|
||||
for worker_index in 0..worker_count {
|
||||
workers.push(RuntimeWorkerSample {
|
||||
worker_index,
|
||||
busy_duration_micros: runtime_count(metrics.worker_total_busy_duration(worker_index).as_micros())?,
|
||||
park_count: runtime_count(u128::from(metrics.worker_park_count(worker_index)))?,
|
||||
});
|
||||
}
|
||||
Ok(RuntimeProfileSample {
|
||||
elapsed_micros,
|
||||
alive_task_count: runtime_count(metrics.num_alive_tasks() as u128)?,
|
||||
global_queue_depth: runtime_count(metrics.global_queue_depth() as u128)?,
|
||||
workers,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn export_thread_profile(
|
||||
request: &ProfileCaptureRequest,
|
||||
scope: ThreadProfileScope,
|
||||
@@ -198,3 +326,45 @@ mod tests {
|
||||
assert!(matches!(parse_proc_stat_state(b"123 malformed"), Err(ProfileError::SourceUnavailable)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_has_atomic = "64"))]
|
||||
mod runtime_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn runtime_counter_resets_fail_without_substituting_zero_deltas() {
|
||||
use super::super::profile_cpu::{RuntimeProfileSample, RuntimeWorkerSample};
|
||||
let first = RuntimeProfileSample {
|
||||
elapsed_micros: 0,
|
||||
alive_task_count: 1,
|
||||
global_queue_depth: 2,
|
||||
workers: vec![RuntimeWorkerSample {
|
||||
worker_index: 0,
|
||||
busy_duration_micros: 10,
|
||||
park_count: 4,
|
||||
}],
|
||||
};
|
||||
let mut second = first.clone();
|
||||
second.alive_task_count = 0;
|
||||
second.global_queue_depth = 3;
|
||||
assert!(validate_runtime_counters(&first, &second).is_ok());
|
||||
second.workers[0].busy_duration_micros = 9;
|
||||
assert!(matches!(validate_runtime_counters(&first, &second), Err(ProfileError::CollectionFailed)));
|
||||
second.workers[0].busy_duration_micros = 11;
|
||||
second.workers[0].park_count = 3;
|
||||
assert!(matches!(validate_runtime_counters(&first, &second), Err(ProfileError::CollectionFailed)));
|
||||
second.workers.clear();
|
||||
assert!(matches!(validate_runtime_counters(&first, &second), Err(ProfileError::CollectionFailed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_allocation_and_json_integer_bounds_are_exact() {
|
||||
assert!(validate_runtime_workers(1).is_ok());
|
||||
assert!(validate_runtime_workers(511).is_ok());
|
||||
for workers in [0, 512, usize::MAX] {
|
||||
assert!(matches!(validate_runtime_workers(workers), Err(ProfileError::LimitExceeded)));
|
||||
}
|
||||
assert_eq!(runtime_count(0).unwrap(), 0);
|
||||
assert_eq!(runtime_count(9_007_199_254_740_991).unwrap(), 9_007_199_254_740_991);
|
||||
assert!(matches!(runtime_count(9_007_199_254_740_992), Err(ProfileError::LimitExceeded)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const PROTOCOL_VERSION: &str = "v1";
|
||||
const SIGNATURE_ALGORITHM: &str = "ES256";
|
||||
const SIGNATURE_DOMAIN: &[u8] = b"rustfs-diagnostic-envelope-v1";
|
||||
const MAX_ENVELOPE_BYTES: usize = 16_384;
|
||||
const MAX_ARCHIVE_BYTES: usize = 524_288;
|
||||
pub(crate) const MAX_ARCHIVE_BYTES: usize = 524_288;
|
||||
const MAX_DECOMPRESSED_BYTES: usize = 278_528;
|
||||
pub const MAX_TOP_EXPORT_VALIDITY: Duration = Duration::from_secs(2_592_000);
|
||||
const TOP_CAPTURE_WORKING_SET_BYTES: u64 = 1_048_576;
|
||||
@@ -635,16 +635,26 @@ pub fn save_signed_top_export(
|
||||
output: &Path,
|
||||
export: &SignedTopExport,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<SavedTopExport, TopCaptureError> {
|
||||
save_top_archive(output, &export.artifact_uid, &export.archive_bytes, &export.archive_sha256, cancel)
|
||||
}
|
||||
|
||||
pub(crate) fn save_top_archive(
|
||||
output: &Path,
|
||||
artifact_uid: &str,
|
||||
archive_bytes: &[u8],
|
||||
archive_sha256: &str,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<SavedTopExport, TopCaptureError> {
|
||||
if cancel.is_cancelled() {
|
||||
return Err(TopCaptureError::Cancelled);
|
||||
}
|
||||
if !is_uuid_v7(&export.artifact_uid) {
|
||||
if !is_uuid_v7(artifact_uid) {
|
||||
return Err(TopCaptureError::Scope);
|
||||
}
|
||||
if export.archive_bytes.is_empty()
|
||||
|| export.archive_bytes.len() > MAX_ARCHIVE_BYTES
|
||||
|| hex_lower(&Sha256::digest(&export.archive_bytes)) != export.archive_sha256
|
||||
if archive_bytes.is_empty()
|
||||
|| archive_bytes.len() > MAX_ARCHIVE_BYTES
|
||||
|| hex_lower(&Sha256::digest(archive_bytes)) != archive_sha256
|
||||
{
|
||||
return Err(TopCaptureError::Result);
|
||||
}
|
||||
@@ -653,7 +663,7 @@ pub fn save_signed_top_export(
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
let filename = output.file_name().ok_or(TopCaptureError::Scope)?.to_string_lossy();
|
||||
let temporary = parent.join(format!(".{filename}.{}.partial", export.artifact_uid));
|
||||
let temporary = parent.join(format!(".{filename}.{}.partial", artifact_uid));
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
@@ -663,7 +673,7 @@ pub fn save_signed_top_export(
|
||||
}
|
||||
let mut file = options.open(&temporary).map_err(map_create_error)?;
|
||||
let result = (|| {
|
||||
file.write_all(&export.archive_bytes).map_err(io_error)?;
|
||||
file.write_all(archive_bytes).map_err(io_error)?;
|
||||
if cancel.is_cancelled() {
|
||||
return Err(TopCaptureError::Cancelled);
|
||||
}
|
||||
@@ -680,9 +690,9 @@ pub fn save_signed_top_export(
|
||||
return Err(TopCaptureError::DurabilityAfterCommit(error.kind()));
|
||||
}
|
||||
Ok(SavedTopExport {
|
||||
artifact_uid: export.artifact_uid.clone(),
|
||||
archive_size_bytes: export.archive_bytes.len() as u64,
|
||||
archive_sha256: export.archive_sha256.clone(),
|
||||
artifact_uid: artifact_uid.to_owned(),
|
||||
archive_size_bytes: archive_bytes.len() as u64,
|
||||
archive_sha256: archive_sha256.to_owned(),
|
||||
})
|
||||
})();
|
||||
if result.is_err() {
|
||||
@@ -1003,6 +1013,29 @@ mod tests {
|
||||
use rustfs_s3_ops::S3Operation;
|
||||
use serial_test::serial;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn queued_disk_capture_rechecks_expiry_before_sampling() {
|
||||
let mut request = capture_request(Duration::from_secs(2));
|
||||
request.scope.consent.tool_id = "top.disk".to_owned();
|
||||
request.scope.run_expires_at_unix = OffsetDateTime::now_utc().unix_timestamp() + 3;
|
||||
let cancel = CancellationToken::new();
|
||||
let permit = request.acquire(&cancel).await.unwrap().unwrap();
|
||||
let capture = super::super::top_disk::capture_top_disk(&request, &cancel);
|
||||
tokio::pin!(capture);
|
||||
assert!(tokio::time::timeout(Duration::from_millis(20), &mut capture).await.is_err());
|
||||
while OffsetDateTime::now_utc().unix_timestamp() < request.scope.run_expires_at_unix {
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
drop(permit);
|
||||
// The old path sampled first and waited the entire two-second window.
|
||||
let result = tokio::time::timeout(Duration::from_millis(500), capture)
|
||||
.await
|
||||
.expect("expired queued capture must not start its sampling window");
|
||||
assert!(matches!(result, Err(TopCaptureError::Expired)));
|
||||
}
|
||||
|
||||
fn capture_request(window: Duration) -> TopCaptureRequest {
|
||||
let organization_uid = Uuid::now_v7();
|
||||
let cluster_uid = Uuid::now_v7();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! Bounded process disk-I/O window backed by RustFS's existing process sampler.
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(target_os = "linux")]
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -24,6 +24,32 @@ use super::top_api::{MAX_SAFE_INTEGER, TopCaptureError, TopCaptureRequest, TopRe
|
||||
const TOOL_ID: &str = "top.disk";
|
||||
pub const TOP_DISK_CAPABILITY: &str = "top.disk@1";
|
||||
|
||||
/// Closed local-service request: no process selector, paths, or supplied provenance.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct LocalTopDiskRequest {
|
||||
pub offline_key_id: String,
|
||||
pub organization_name: String,
|
||||
pub cluster_name: String,
|
||||
pub device_name: String,
|
||||
pub run_uid: String,
|
||||
pub artifact_uid: String,
|
||||
pub consent_uid: String,
|
||||
pub policy_revision: u64,
|
||||
pub consent_expires_at_unix: i64,
|
||||
pub acknowledge_l3: bool,
|
||||
pub run_expires_at_unix: i64,
|
||||
pub window_millis: u64,
|
||||
pub export_validity_seconds: u64,
|
||||
}
|
||||
|
||||
/// Archive received from the owner-only service socket.
|
||||
pub(crate) struct LocalTopDiskArchive {
|
||||
pub artifact_uid: String,
|
||||
pub archive_bytes: Vec<u8>,
|
||||
pub archive_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DiskCounterSnapshot {
|
||||
pub read_bytes: u64,
|
||||
@@ -58,6 +84,11 @@ pub async fn capture_top_disk(
|
||||
let Some(_permit) = request.acquire(cancel).await? else {
|
||||
return request.cancelled(TOOL_ID);
|
||||
};
|
||||
// A queued request may outlive its authorization while another capture owns the lease.
|
||||
request.validate_capture(TOOL_ID)?;
|
||||
if cancel.is_cancelled() {
|
||||
return request.cancelled(TOOL_ID);
|
||||
}
|
||||
let mut sampler = rustfs_io_metrics::ProcessSampler::new();
|
||||
let before = process_snapshot(&mut sampler)?;
|
||||
let started = Instant::now();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,3 +55,19 @@ pub(crate) async fn request_local_trace_capture(
|
||||
) -> Result<TraceRecordCapture, LocalTraceCaptureError> {
|
||||
Err(LocalTraceCaptureError::RuntimeUnavailable)
|
||||
}
|
||||
|
||||
pub(crate) async fn request_local_runtime_profile(
|
||||
_state_root: &Path,
|
||||
_request: super::profile_cpu::LocalRuntimeProfileRequest,
|
||||
_cancel: &CancellationToken,
|
||||
) -> Result<super::profile_cpu::SignedProfileExport, LocalTraceCaptureError> {
|
||||
Err(LocalTraceCaptureError::RuntimeUnavailable)
|
||||
}
|
||||
|
||||
pub(crate) async fn request_local_top_disk(
|
||||
_state_root: &Path,
|
||||
_request: super::top_disk::LocalTopDiskRequest,
|
||||
_cancel: &CancellationToken,
|
||||
) -> Result<super::top_disk::LocalTopDiskArchive, LocalTraceCaptureError> {
|
||||
Err(LocalTraceCaptureError::RuntimeUnavailable)
|
||||
}
|
||||
|
||||
@@ -112,13 +112,15 @@ impl PendingHeartbeat {
|
||||
]
|
||||
|| self.capabilities == heartbeat_capabilities(false)
|
||||
|| self.capabilities == heartbeat_capabilities(true)
|
||||
// RUSTFS_COMPAT_TODO(connect-894) Remove after upgrades from the pre-service-memory capability set are unsupported.
|
||||
// Preserve exact pending requests; never add the capability to a retry.
|
||||
// RUSTFS_COMPAT_TODO(connect-894) Remove after upgrades from the pre-health and pre-service-memory sets are unsupported.
|
||||
// Compare frozen historical advertisements, not subsets of today's capabilities.
|
||||
|| [false, true].into_iter().any(|job_capable| {
|
||||
heartbeat_capabilities(job_capable)
|
||||
.iter()
|
||||
.filter(|capability| capability.as_str() != "profile.memory.service@1")
|
||||
.eq(self.capabilities.iter())
|
||||
let legacy = pre_health_heartbeat_capabilities(job_capable);
|
||||
self.capabilities == legacy
|
||||
|| legacy
|
||||
.iter()
|
||||
.filter(|capability| capability.as_str() != "profile.memory.service@1")
|
||||
.eq(self.capabilities.iter())
|
||||
}))
|
||||
&& self.sequence <= MAX_SEQUENCE
|
||||
&& self.coarse_node_summary.is_valid()
|
||||
@@ -331,6 +333,40 @@ impl HeartbeatStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_health_heartbeat_capabilities(job_capable: bool) -> Vec<String> {
|
||||
let mut capabilities = [
|
||||
"heartbeat",
|
||||
"diagnostics.policy.v1",
|
||||
"inventory.environment@1",
|
||||
"performance.client@1",
|
||||
"performance.drive@1",
|
||||
"performance.network@1",
|
||||
"performance.object@1",
|
||||
"performance.siteReplication@1",
|
||||
"logs.capture@1",
|
||||
"profile.cpu@1",
|
||||
"profile.memory@1",
|
||||
"profile.memory.service@1",
|
||||
"profile.threads@1",
|
||||
"telemetry.record@1",
|
||||
"telemetry.otlp@1",
|
||||
"telemetry.replay@1",
|
||||
"top.api@1",
|
||||
"top.disk@1",
|
||||
"top.locks@1",
|
||||
"top.net@1",
|
||||
"top.rpc@1",
|
||||
"inspect.object@1",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
if job_capable {
|
||||
capabilities.insert(3, "jobs".to_owned());
|
||||
}
|
||||
capabilities
|
||||
}
|
||||
|
||||
fn heartbeat_capabilities(job_capable: bool) -> Vec<String> {
|
||||
let mut capabilities = vec![
|
||||
"heartbeat".to_owned(),
|
||||
|
||||
@@ -86,6 +86,13 @@ pub use diagnostics::{
|
||||
DIAGNOSTIC_JOB_SIGNATURE_DOMAIN, DiagnosticJobEnvelope, DiagnosticJobError, DiagnosticJobExecution, DiagnosticJobLimits,
|
||||
DiagnosticJobParameters, DiagnosticJobTarget, TrustedDiagnosticJobSigner, VerifiedDiagnosticJob, execute_diagnostic_job,
|
||||
};
|
||||
pub use diagnostics::{
|
||||
HEALTH_CATALOG_CHECKS, HEALTH_SCHEMA_VERSION, HEALTH_SERVICE_CAPABILITY, HEALTH_TIMEOUT_SECONDS, HealthCheckResult,
|
||||
HealthDiagnosticResult, HealthError, HealthFreshness, HealthOutcome, HealthResultReason, HealthRuleOutcome,
|
||||
HealthServiceRequest, HealthSourceObservation, LocalHealthConsent, MAX_EVIDENCE_AGE_SECONDS, MAX_HEALTH_CPU_MILLIS,
|
||||
MAX_HEALTH_MEMORY_BYTES, MAX_HEALTH_OUTPUT_BYTES, SignedHealthExport, collect_runtime_health, evaluate_health_observation,
|
||||
sign_health_export,
|
||||
};
|
||||
pub use diagnostics::{
|
||||
LocalNetworkConsent, MAX_NETWORK_ARCHIVE_BYTES, MAX_NETWORK_BANDWIDTH_BYTES_PER_SECOND, MAX_NETWORK_DECOMPRESSED_BYTES,
|
||||
MAX_NETWORK_DURATION, MAX_NETWORK_ENVELOPE_BYTES, MAX_NETWORK_OPERATIONS, MAX_NETWORK_PEERS, MAX_NETWORK_RESULT_BYTES,
|
||||
@@ -122,7 +129,8 @@ pub use diagnostics::{
|
||||
evaluate_network_window, save_signed_top_export, sign_top_export,
|
||||
};
|
||||
pub(crate) use diagnostics::{
|
||||
LocalTraceCaptureError, LocalTraceCaptureRuntime, request_local_trace_capture, spawn_local_trace_capture_runtime,
|
||||
LocalTraceCaptureError, LocalTraceCaptureRuntime, request_local_runtime_profile, request_local_top_disk,
|
||||
request_local_trace_capture, spawn_local_trace_capture_runtime,
|
||||
};
|
||||
pub use environment::{
|
||||
ENVIRONMENT_CAPABILITY, ENVIRONMENT_SCHEMA_VERSION, EnvironmentCollectionRequest, EnvironmentError, EnvironmentExportRequest,
|
||||
|
||||
@@ -309,6 +309,7 @@ pub(super) fn native_source_head(
|
||||
content_language: header_string(headers, "content-language"),
|
||||
cache_control: header_string(headers, "cache-control"),
|
||||
expires: header_string(headers, "expires"),
|
||||
website_redirect_location: header_string(headers, "x-amz-website-redirect-location"),
|
||||
user_metadata,
|
||||
version_id: fields.version_id,
|
||||
storage_class: fields.storage_class,
|
||||
|
||||
@@ -413,6 +413,7 @@ pub struct SourceHead {
|
||||
pub content_language: Option<String>,
|
||||
pub cache_control: Option<String>,
|
||||
pub expires: Option<String>,
|
||||
pub website_redirect_location: Option<String>,
|
||||
/// `x-amz-meta-*` values keyed without the prefix, matching the stored
|
||||
/// user-metadata shape.
|
||||
pub user_metadata: HashMap<String, String>,
|
||||
@@ -438,6 +439,7 @@ struct HeadParts {
|
||||
content_language: Option<String>,
|
||||
cache_control: Option<String>,
|
||||
expires: Option<String>,
|
||||
website_redirect_location: Option<String>,
|
||||
metadata: Option<HashMap<String, String>>,
|
||||
version_id: Option<String>,
|
||||
storage_class: Option<String>,
|
||||
@@ -490,6 +492,7 @@ fn source_head(parts: HeadParts) -> Result<SourceHead, SourceError> {
|
||||
content_language: parts.content_language,
|
||||
cache_control: parts.cache_control,
|
||||
expires: parts.expires,
|
||||
website_redirect_location: parts.website_redirect_location,
|
||||
user_metadata: parts.metadata.unwrap_or_default(),
|
||||
version_id: parts.version_id,
|
||||
storage_class: parts.storage_class,
|
||||
@@ -510,6 +513,7 @@ fn source_head_from_head_output(output: HeadObjectOutput) -> Result<SourceHead,
|
||||
content_language: output.content_language,
|
||||
cache_control: output.cache_control,
|
||||
expires: output.expires_string,
|
||||
website_redirect_location: output.website_redirect_location,
|
||||
metadata: output.metadata,
|
||||
version_id: output.version_id,
|
||||
storage_class: output.storage_class.map(|class| class.as_str().to_string()),
|
||||
@@ -549,6 +553,7 @@ fn source_get_from_output(output: GetObjectOutput) -> Result<SourceGet, SourceEr
|
||||
content_language: output.content_language,
|
||||
cache_control: output.cache_control,
|
||||
expires: output.expires_string,
|
||||
website_redirect_location: output.website_redirect_location,
|
||||
metadata: output.metadata,
|
||||
version_id: output.version_id,
|
||||
storage_class: output.storage_class.map(|class| class.as_str().to_string()),
|
||||
|
||||
+46
-12
@@ -355,11 +355,12 @@ pub(crate) fn build_health_response_parts(
|
||||
kms_ready,
|
||||
include_dependency_details,
|
||||
});
|
||||
if let Some(details) = readiness_report.and_then(|report| report.storage_details)
|
||||
if let Some(details) = readiness_report.and_then(|report| report.storage_details.as_ref())
|
||||
&& payload.get("details").is_some()
|
||||
{
|
||||
payload["details"]["storage"]["readQuorum"] = json!(details.read_quorum_ready);
|
||||
payload["details"]["storage"]["writeQuorum"] = json!(details.write_quorum_ready);
|
||||
payload["details"]["storage"]["unavailableDrives"] = json!(details.unavailable_drives);
|
||||
payload["details"]["poolMetadata"] = json!({
|
||||
"ready": details.pool_metadata_write_ready,
|
||||
"status": if details.pool_metadata_write_ready { "writable" } else { "unavailable" },
|
||||
@@ -402,7 +403,9 @@ pub(crate) fn build_health_payload(ctx: HealthPayloadContext<'_>) -> Value {
|
||||
if ctx.include_dependency_details {
|
||||
payload["details"] = build_component_details(ctx.storage_ready, ctx.iam_ready, ctx.lock_quorum_ready, ctx.kms_ready);
|
||||
payload["details"]["storage"]["readinessScope"] = json!(match ctx.probe {
|
||||
HealthProbe::ClusterRead => "read_quorum",
|
||||
// Node readiness is the Service membership probe: it follows read
|
||||
// quorum. Cluster write keeps the write-quorum scope.
|
||||
HealthProbe::ClusterRead | HealthProbe::Readiness => "read_quorum",
|
||||
_ => "write_quorum_and_pool_metadata",
|
||||
});
|
||||
payload["details"]["storage"]["source"] = json!(match ctx.probe {
|
||||
@@ -461,21 +464,25 @@ mod tests {
|
||||
with_var(rustfs_config::ENV_HEALTH_MINIMAL_RESPONSE_ENABLE, Some("false"), || {
|
||||
for (read_quorum, write_quorum, metadata_ready, lock_ready) in [
|
||||
(true, true, true, true),
|
||||
(true, false, true, true),
|
||||
(true, false, false, true),
|
||||
(true, false, true, false),
|
||||
(false, false, true, false),
|
||||
(true, true, false, true),
|
||||
(true, true, true, true),
|
||||
] {
|
||||
let mut report = ready_report();
|
||||
report.readiness.storage_ready = write_quorum && metadata_ready;
|
||||
// The node collector stores read quorum here. Write quorum and the
|
||||
// pool-metadata writer remain detail fields and do not set HTTP status.
|
||||
report.readiness.storage_ready = read_quorum;
|
||||
report.readiness.lock_quorum_ready = lock_ready;
|
||||
report.storage_details = Some(crate::shared_types::StorageReadinessDetails {
|
||||
read_quorum_ready: read_quorum,
|
||||
write_quorum_ready: write_quorum,
|
||||
pool_metadata_write_ready: metadata_ready,
|
||||
..Default::default()
|
||||
});
|
||||
let parts = build_health_response_parts(Method::GET, HealthProbe::Readiness, Some(&report), "rustfs", None, None);
|
||||
let expected_ready = write_quorum && metadata_ready && lock_ready;
|
||||
let expected_ready = read_quorum && lock_ready;
|
||||
assert_eq!(
|
||||
parts.status_code,
|
||||
if expected_ready {
|
||||
@@ -486,20 +493,16 @@ mod tests {
|
||||
);
|
||||
let payload = parts.payload.expect("GET readiness body");
|
||||
assert_eq!(payload["ready"], expected_ready);
|
||||
assert_eq!(payload["details"]["storage"]["ready"], write_quorum && metadata_ready);
|
||||
assert_eq!(payload["details"]["storage"]["ready"], read_quorum);
|
||||
assert_eq!(
|
||||
payload["details"]["storage"]["status"],
|
||||
if write_quorum && metadata_ready {
|
||||
"connected"
|
||||
} else {
|
||||
"disconnected"
|
||||
}
|
||||
if read_quorum { "connected" } else { "disconnected" }
|
||||
);
|
||||
assert_eq!(payload["details"]["storage"]["readQuorum"], read_quorum);
|
||||
assert_eq!(payload["details"]["storage"]["writeQuorum"], write_quorum);
|
||||
assert_eq!(payload["details"]["poolMetadata"]["ready"], metadata_ready);
|
||||
assert_eq!(payload["details"]["storage"]["source"], "local_runtime");
|
||||
assert_eq!(payload["details"]["storage"]["readinessScope"], "write_quorum_and_pool_metadata");
|
||||
assert_eq!(payload["details"]["storage"]["readinessScope"], "read_quorum");
|
||||
assert!(
|
||||
build_health_response_parts(Method::HEAD, HealthProbe::Readiness, Some(&report), "rustfs", None, None)
|
||||
.payload
|
||||
@@ -509,6 +512,36 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn node_storage_details_explain_excluded_drives_without_exposing_endpoints() {
|
||||
with_var(rustfs_config::ENV_HEALTH_MINIMAL_RESPONSE_ENABLE, Some("false"), || {
|
||||
let mut report = ready_report();
|
||||
report.readiness.storage_ready = false;
|
||||
report.storage_details = Some(crate::shared_types::StorageReadinessDetails {
|
||||
pool_metadata_write_ready: true,
|
||||
unavailable_drives: vec![crate::shared_types::UnavailableReadinessDrive {
|
||||
pool_index: 0,
|
||||
set_index: 1,
|
||||
disk_index: 2,
|
||||
runtime_state: "suspect".to_string(),
|
||||
host_online: true,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
let parts = build_health_response_parts(Method::GET, HealthProbe::Readiness, Some(&report), "rustfs", None, None);
|
||||
assert_eq!(parts.status_code, StatusCode::SERVICE_UNAVAILABLE);
|
||||
let payload = parts.payload.expect("readiness GET body");
|
||||
assert_eq!(
|
||||
payload["details"]["storage"]["unavailableDrives"],
|
||||
json!([{
|
||||
"poolIndex": 0, "setIndex": 1, "diskIndex": 2,
|
||||
"runtimeState": "suspect", "hostOnline": true,
|
||||
}])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn node_storage_details_do_not_expand_minimal_or_liveness_payloads() {
|
||||
@@ -517,6 +550,7 @@ mod tests {
|
||||
read_quorum_ready: true,
|
||||
write_quorum_ready: true,
|
||||
pool_metadata_write_ready: true,
|
||||
..Default::default()
|
||||
});
|
||||
with_var(rustfs_config::ENV_HEALTH_MINIMAL_RESPONSE_ENABLE, Some("true"), || {
|
||||
let parts = build_health_response_parts(Method::GET, HealthProbe::Readiness, Some(&report), "rustfs", None, None);
|
||||
|
||||
@@ -191,6 +191,39 @@ fn s3_host_domains(config: &config::Config) -> Result<Option<Vec<String>>> {
|
||||
Ok(Some(domains))
|
||||
}
|
||||
|
||||
fn website_host_domains(config: &config::Config) -> Result<Option<Vec<String>>> {
|
||||
if config.console_enable {
|
||||
return Ok(None);
|
||||
}
|
||||
let raw = std::env::var("RUSTFS_WEBSITE_DOMAINS").unwrap_or_default();
|
||||
let mut domains = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for domain in raw.split(',').map(str::trim).filter(|domain| !domain.is_empty()) {
|
||||
if domain.contains(':') || domain.ends_with('.') {
|
||||
return Err(Error::other(format!("invalid RUSTFS_WEBSITE_DOMAINS entry: {domain}")));
|
||||
}
|
||||
let normalized = domain.to_ascii_lowercase();
|
||||
if !seen.insert(normalized.clone()) {
|
||||
return Err(Error::other(format!("duplicate RUSTFS_WEBSITE_DOMAINS entry: {domain}")));
|
||||
}
|
||||
if config
|
||||
.server_domains
|
||||
.iter()
|
||||
.map(|s| strip_valid_port_suffix(s).to_ascii_lowercase())
|
||||
.any(|s| normalized == s || normalized.ends_with(&format!(".{s}")) || s.ends_with(&format!(".{normalized}")))
|
||||
{
|
||||
return Err(Error::other(format!("website domain overlaps RUSTFS_SERVER_DOMAINS: {domain}")));
|
||||
}
|
||||
domains.push(normalized);
|
||||
}
|
||||
if domains.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
MultiDomain::new(&domains).map_err(|err| Error::other(format!("invalid RUSTFS_WEBSITE_DOMAINS {:?}: {err}", domains)))?;
|
||||
Ok(Some(domains))
|
||||
}
|
||||
}
|
||||
|
||||
const LOG_COMPONENT_SERVER: &str = "server";
|
||||
const LOG_SUBSYSTEM_HTTP: &str = "http";
|
||||
const LOG_SUBSYSTEM_TRANSPORT: &str = "transport";
|
||||
@@ -1358,12 +1391,17 @@ pub async fn start_http_server(
|
||||
.map(MultiDomain::new)
|
||||
.transpose()
|
||||
.map_err(Error::other)?;
|
||||
let website_host_sets = website_host_domains(config)?;
|
||||
let website_route_domains = website_host_sets.unwrap_or_default();
|
||||
|
||||
b.set_auth(IAMAuth::with_server_context(access_key, secret_key, server_ctx.clone()));
|
||||
b.set_access(store);
|
||||
b.set_route(storage::metadata_route::with_metadata_route(
|
||||
admin::make_admin_route(config.console_enable, admin_server_ctx)?,
|
||||
metadata_route_host,
|
||||
website_route_domains,
|
||||
protocol,
|
||||
Arc::clone(&server_ctx),
|
||||
));
|
||||
|
||||
// Normalize leading/duplicate forward slashes in object keys (MinIO parity).
|
||||
|
||||
+211
-42
@@ -77,7 +77,9 @@ const METRIC_RUNTIME_READINESS_READY: &str = "rustfs_runtime_readiness_ready";
|
||||
const METRIC_RUNTIME_READINESS_DEGRADED_TOTAL: &str = "rustfs_runtime_readiness_degraded_total";
|
||||
const METRIC_POOL_METADATA_CHECK_TIMEOUT_TOTAL: &str = "rustfs_pool_metadata_check_timeouts_total";
|
||||
|
||||
pub use crate::shared_types::{DependencyReadiness, DependencyReadinessReport, ReadinessDegradedReason, StorageReadinessDetails};
|
||||
pub use crate::shared_types::{
|
||||
DependencyReadiness, DependencyReadinessReport, ReadinessDegradedReason, StorageReadinessDetails, UnavailableReadinessDrive,
|
||||
};
|
||||
|
||||
/// ReadinessGateLayer ensures that the system components (IAM, Storage)
|
||||
/// are fully initialized before allowing any request to proceed.
|
||||
@@ -375,10 +377,15 @@ enum ClusterHealthProbeKind {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct LockQuorumStatus {
|
||||
/// Exclusive-lock quorum. Cluster write probes and startup publication use this.
|
||||
pub ready: bool,
|
||||
/// Shared-lock quorum. Node `/health/ready` uses this so a readable survivor
|
||||
/// stays in the Service after exclusive locks are no longer obtainable.
|
||||
pub read_ready: bool,
|
||||
pub connected_clients: usize,
|
||||
pub total_clients: usize,
|
||||
pub required_quorum: usize,
|
||||
pub required_read_quorum: usize,
|
||||
}
|
||||
|
||||
const DISK_STATE_OK: &str = "ok";
|
||||
@@ -635,6 +642,20 @@ fn pool_read_quorum(info: &StorageInfo, pool_idx: usize, set_drive_count: usize)
|
||||
pool_erasure_layout(info, pool_idx, set_drive_count).map(|(data_drives, _)| data_drives)
|
||||
}
|
||||
|
||||
/// Exclusive namespace locks use a strict majority of lock clients.
|
||||
fn exclusive_lock_quorum(total_clients: usize) -> usize {
|
||||
if total_clients > 1 { (total_clients / 2) + 1 } else { 1 }
|
||||
}
|
||||
|
||||
/// Shared namespace locks use the same client threshold as `DistributedLock::read_quorum`.
|
||||
fn shared_lock_quorum(total_clients: usize) -> usize {
|
||||
if total_clients > 1 {
|
||||
total_clients - (total_clients / 2)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_readiness_topology(info: &StorageInfo) -> Option<(&[usize], &[usize])> {
|
||||
if info.backend.total_sets.is_empty()
|
||||
|| info.backend.total_sets.len() != info.backend.drives_per_set.len()
|
||||
@@ -841,6 +862,27 @@ fn dependency_readiness_report_from_write_status(
|
||||
}
|
||||
}
|
||||
|
||||
/// `/health/ready` reports whether this process can serve reads. Write quorum,
|
||||
/// the pool-metadata writer, and exclusive locks stay on `/minio/health/cluster`.
|
||||
fn apply_node_service_readiness(
|
||||
mut storage: StorageWriteReadinessStatus,
|
||||
mut details: StorageReadinessDetails,
|
||||
lock_read_ready: bool,
|
||||
iam_ready: bool,
|
||||
peer_health_ready: bool,
|
||||
) -> (DependencyReadiness, StorageWriteReadinessStatus, StorageReadinessDetails) {
|
||||
details.pool_metadata_write_ready = storage.ready;
|
||||
storage.ready = details.read_quorum_ready;
|
||||
storage.pool_metadata_reason = None;
|
||||
let readiness = DependencyReadiness {
|
||||
storage_ready: storage.ready,
|
||||
iam_ready,
|
||||
lock_quorum_ready: lock_read_ready,
|
||||
peer_health_ready,
|
||||
};
|
||||
(readiness, storage, details)
|
||||
}
|
||||
|
||||
pub async fn collect_dependency_readiness_report() -> DependencyReadinessReport {
|
||||
let iam_ready_raw = runtime_sources::current_iam_ready();
|
||||
let storage = if let Some(cached) = load_cached_storage_readiness().await {
|
||||
@@ -879,9 +921,9 @@ pub async fn collect_node_readiness_report() -> DependencyReadinessReport {
|
||||
if let Some(store) = runtime_sources::current_object_store_handle() {
|
||||
let pool_metadata_status = store.pool_meta_write_status().await;
|
||||
storage = apply_node_pool_metadata_timeout_policy(pool_metadata_write_readiness(pool_metadata_status), Instant::now());
|
||||
details.pool_metadata_write_ready = storage.ready;
|
||||
match node_storage_snapshot(store.as_ref(), &lock_observation.online_hosts).await {
|
||||
Ok(info) => {
|
||||
Ok((info, unavailable_drives)) => {
|
||||
details.unavailable_drives = unavailable_drives;
|
||||
details.read_quorum_ready = storage_read_ready_from_runtime_state(&info);
|
||||
details.write_quorum_ready = storage_ready_from_runtime_state(&info);
|
||||
}
|
||||
@@ -889,13 +931,14 @@ pub async fn collect_node_readiness_report() -> DependencyReadinessReport {
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
storage.ready &= details.write_quorum_ready;
|
||||
let readiness = DependencyReadiness {
|
||||
storage_ready: storage.ready,
|
||||
iam_ready: runtime_sources::current_iam_ready(),
|
||||
lock_quorum_ready: lock_observation.status.ready,
|
||||
peer_health_ready: collect_peer_health_readiness(),
|
||||
};
|
||||
// The HTTP layer still returns 503 until startup publishes `FullReady`.
|
||||
let (readiness, storage, details) = apply_node_service_readiness(
|
||||
storage,
|
||||
details,
|
||||
lock_observation.status.read_ready,
|
||||
runtime_sources::current_iam_ready(),
|
||||
collect_peer_health_readiness(),
|
||||
);
|
||||
let mut report = dependency_readiness_report_from_write_status(readiness, storage);
|
||||
report.storage_details = Some(details);
|
||||
if storage_check_timed_out {
|
||||
@@ -907,7 +950,10 @@ pub async fn collect_node_readiness_report() -> DependencyReadinessReport {
|
||||
report
|
||||
}
|
||||
|
||||
async fn node_storage_snapshot<S>(store: &S, online_hosts: &HashSet<String>) -> Result<StorageInfo, StorageError>
|
||||
async fn node_storage_snapshot<S>(
|
||||
store: &S,
|
||||
online_hosts: &HashSet<String>,
|
||||
) -> Result<(StorageInfo, Vec<UnavailableReadinessDrive>), StorageError>
|
||||
where
|
||||
S: StorageAdminApi<BackendInfo = BackendInfo, Disk = DiskStore, Error = StorageError>,
|
||||
{
|
||||
@@ -916,8 +962,9 @@ where
|
||||
backend: store.backend_info().await,
|
||||
..Default::default()
|
||||
};
|
||||
let mut unavailable_drives = Vec::new();
|
||||
if configured_readiness_topology(&info).is_none() {
|
||||
return Ok(info);
|
||||
return Ok((info, unavailable_drives));
|
||||
}
|
||||
|
||||
// Inventory and runtime health are local snapshots. Reuse the lock probe's
|
||||
@@ -931,26 +978,38 @@ where
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
info.disks.push(node_disk_snapshot(
|
||||
disk_endpoint_snapshot(&disk),
|
||||
disk.runtime_state().as_str(),
|
||||
online_hosts,
|
||||
));
|
||||
let (snapshot, unavailable) =
|
||||
node_disk_snapshot(disk_endpoint_snapshot(&disk), disk.runtime_state().as_str(), online_hosts);
|
||||
if let Some(unavailable) = unavailable {
|
||||
unavailable_drives.push(unavailable);
|
||||
}
|
||||
info.disks.push(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
Ok((info, unavailable_drives))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| StorageError::Timeout)?
|
||||
}
|
||||
|
||||
fn node_disk_snapshot(endpoint: Endpoint, runtime_state: &str, online_hosts: &HashSet<String>) -> Disk {
|
||||
fn node_disk_snapshot(
|
||||
endpoint: Endpoint,
|
||||
runtime_state: &str,
|
||||
online_hosts: &HashSet<String>,
|
||||
) -> (Disk, Option<UnavailableReadinessDrive>) {
|
||||
let reachable = endpoint.is_local || online_hosts.contains(&endpoint.host_port());
|
||||
// Returning drives can still reject data I/O as faulty. Without a fresh
|
||||
// disk-info probe, only an Online runtime observation can supply quorum.
|
||||
let online = reachable && runtime_state == rustfs_madmin::ITEM_ONLINE;
|
||||
Disk {
|
||||
let unavailable = (!online).then(|| UnavailableReadinessDrive {
|
||||
pool_index: endpoint.pool_idx,
|
||||
set_index: endpoint.set_idx,
|
||||
disk_index: endpoint.disk_idx,
|
||||
runtime_state: runtime_state.to_string(),
|
||||
host_online: reachable,
|
||||
});
|
||||
let disk = Disk {
|
||||
endpoint: endpoint.to_string(),
|
||||
drive_path: endpoint.get_file_path(),
|
||||
pool_index: endpoint.pool_idx,
|
||||
@@ -959,7 +1018,8 @@ fn node_disk_snapshot(endpoint: Endpoint, runtime_state: &str, online_hosts: &Ha
|
||||
state: if online { DISK_STATE_OK } else { "offline" }.to_string(),
|
||||
runtime_state: Some(runtime_state.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
(disk, unavailable)
|
||||
}
|
||||
|
||||
async fn collect_cluster_health_report_with<LoadFn, Fut>(
|
||||
@@ -1101,17 +1161,16 @@ fn set_lock_quorum_status(online_hosts: &HashSet<String>, set_endpoints: &[Endpo
|
||||
}
|
||||
|
||||
let connected_clients = total_clients.iter().filter(|host| online_hosts.contains(*host)).count();
|
||||
let required_quorum = if total_clients_len > 1 {
|
||||
(total_clients_len / 2) + 1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let required_quorum = exclusive_lock_quorum(total_clients_len);
|
||||
let required_read_quorum = shared_lock_quorum(total_clients_len);
|
||||
|
||||
LockQuorumStatus {
|
||||
ready: connected_clients >= required_quorum,
|
||||
read_ready: connected_clients >= required_read_quorum,
|
||||
connected_clients,
|
||||
total_clients: total_clients_len,
|
||||
required_quorum,
|
||||
required_read_quorum,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1119,6 +1178,10 @@ fn aggregate_lock_quorum_status(pool_endpoints: &EndpointServerPools, online_hos
|
||||
let mut connected_clients = 0usize;
|
||||
let mut total_clients = 0usize;
|
||||
let mut required_quorum = 0usize;
|
||||
let mut required_read_quorum = 0usize;
|
||||
let mut write_ready = true;
|
||||
let mut read_ready = true;
|
||||
let mut saw_set = false;
|
||||
|
||||
for pool in pool_endpoints.as_ref() {
|
||||
for set_idx in 0..pool.set_count {
|
||||
@@ -1135,29 +1198,26 @@ fn aggregate_lock_quorum_status(pool_endpoints: &EndpointServerPools, online_hos
|
||||
return LockQuorumStatus::default();
|
||||
}
|
||||
|
||||
saw_set = true;
|
||||
connected_clients += status.connected_clients;
|
||||
total_clients += status.total_clients;
|
||||
required_quorum += status.required_quorum;
|
||||
|
||||
if !status.ready {
|
||||
return LockQuorumStatus {
|
||||
ready: false,
|
||||
connected_clients,
|
||||
total_clients,
|
||||
required_quorum,
|
||||
};
|
||||
}
|
||||
required_read_quorum += status.required_read_quorum;
|
||||
write_ready &= status.ready;
|
||||
read_ready &= status.read_ready;
|
||||
}
|
||||
}
|
||||
|
||||
if total_clients == 0 {
|
||||
if !saw_set {
|
||||
LockQuorumStatus::default()
|
||||
} else {
|
||||
LockQuorumStatus {
|
||||
ready: true,
|
||||
ready: write_ready,
|
||||
read_ready,
|
||||
connected_clients,
|
||||
total_clients,
|
||||
required_quorum,
|
||||
required_read_quorum,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1171,9 +1231,11 @@ async fn collect_lock_quorum_observation_uncached() -> LockQuorumObservation {
|
||||
return LockQuorumObservation {
|
||||
status: LockQuorumStatus {
|
||||
ready: true,
|
||||
read_ready: true,
|
||||
connected_clients: 1,
|
||||
total_clients: 1,
|
||||
required_quorum: 1,
|
||||
required_read_quorum: 1,
|
||||
},
|
||||
online_hosts: HashSet::new(),
|
||||
};
|
||||
@@ -1350,10 +1412,16 @@ mod tests {
|
||||
let write_quorum = data + usize::from(data == parity);
|
||||
for survivors in (0..=drive_count).rev().chain(std::iter::once(drive_count)) {
|
||||
let online_hosts = (0..survivors).map(|idx| format!("node-{idx}:9000")).collect();
|
||||
let info = node_storage_snapshot(&store, &online_hosts)
|
||||
let (info, unavailable_drives) = node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("read local runtime inventory");
|
||||
assert_eq!(info.disks.len(), drive_count, "offline members retain their topology slots");
|
||||
assert_eq!(unavailable_drives.len(), drive_count - survivors);
|
||||
assert!(
|
||||
unavailable_drives
|
||||
.iter()
|
||||
.all(|disk| !disk.host_online && disk.runtime_state == "online")
|
||||
);
|
||||
assert_eq!(
|
||||
storage_read_ready_from_runtime_state(&info),
|
||||
survivors >= data,
|
||||
@@ -1386,7 +1454,22 @@ mod tests {
|
||||
disk_idx: 2,
|
||||
};
|
||||
let mut disks = online_readiness_disks(0, 2);
|
||||
disks.push(node_disk_snapshot(endpoint, runtime_state, &online_hosts));
|
||||
let (disk, unavailable) = node_disk_snapshot(endpoint, runtime_state, &online_hosts);
|
||||
if (is_local || reachable) && runtime_state == "online" {
|
||||
assert!(unavailable.is_none());
|
||||
} else {
|
||||
assert_eq!(
|
||||
unavailable,
|
||||
Some(UnavailableReadinessDrive {
|
||||
pool_index: 0,
|
||||
set_index: 0,
|
||||
disk_index: 2,
|
||||
runtime_state: runtime_state.to_string(),
|
||||
host_online: is_local || reachable,
|
||||
})
|
||||
);
|
||||
}
|
||||
disks.push(disk);
|
||||
let info = StorageInfo {
|
||||
backend: BackendInfo {
|
||||
total_sets: vec![1],
|
||||
@@ -1413,21 +1496,21 @@ mod tests {
|
||||
async fn node_storage_snapshot_requires_each_configured_set_and_distinct_drives() {
|
||||
let mut store = runtime_inventory(&[(2, 4, 2), (1, 8, 2)]).await;
|
||||
let online_hosts = (0..8).map(|idx| format!("node-{idx}:9000")).collect();
|
||||
let info = node_storage_snapshot(&store, &online_hosts)
|
||||
let (info, _) = node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("healthy mixed layout");
|
||||
assert!(storage_ready_from_runtime_state(&info));
|
||||
|
||||
let selector = DiskSetSelector::new(0, 1);
|
||||
let original = store.disks.remove(&selector).expect("second configured set");
|
||||
let info = node_storage_snapshot(&store, &online_hosts)
|
||||
let (info, _) = node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("missing set snapshot");
|
||||
assert!(!storage_read_ready_from_runtime_state(&info));
|
||||
assert!(!storage_ready_from_runtime_state(&info));
|
||||
|
||||
store.disks.insert(selector, vec![original[0].clone(); 4]);
|
||||
let info = node_storage_snapshot(&store, &online_hosts)
|
||||
let (info, _) = node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("duplicate drive snapshot");
|
||||
assert!(!storage_read_ready_from_runtime_state(&info));
|
||||
@@ -1438,6 +1521,7 @@ mod tests {
|
||||
&node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("restored inventory")
|
||||
.0
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1460,6 +1544,7 @@ mod tests {
|
||||
&node_storage_snapshot(&store, &online_hosts)
|
||||
.await
|
||||
.expect("inspection recovered")
|
||||
.0
|
||||
));
|
||||
}
|
||||
|
||||
@@ -2254,9 +2339,11 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(status.ready);
|
||||
assert!(status.read_ready);
|
||||
assert_eq!(status.connected_clients, 4);
|
||||
assert_eq!(status.total_clients, 4);
|
||||
assert_eq!(status.required_quorum, 4);
|
||||
assert_eq!(status.required_read_quorum, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2304,6 +2391,86 @@ mod tests {
|
||||
aggregate_lock_quorum_status(&pools, &["node1:9000"].into_iter().map(str::to_string).collect::<HashSet<_>>());
|
||||
|
||||
assert!(!status.ready);
|
||||
assert!(!status.read_ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_service_readiness_follows_read_quorum_not_write_or_pool_metadata() {
|
||||
let blocked = StorageWriteReadinessStatus {
|
||||
ready: false,
|
||||
pool_metadata_reason: Some(ReadinessDegradedReason::PoolMetaWriteBlocked),
|
||||
};
|
||||
let (readiness, storage, details) = apply_node_service_readiness(
|
||||
blocked,
|
||||
StorageReadinessDetails {
|
||||
read_quorum_ready: true,
|
||||
write_quorum_ready: false,
|
||||
pool_metadata_write_ready: true,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert!(readiness.storage_ready);
|
||||
assert!(readiness.lock_quorum_ready);
|
||||
assert!(!details.write_quorum_ready);
|
||||
assert!(!details.pool_metadata_write_ready);
|
||||
assert!(storage.pool_metadata_reason.is_none());
|
||||
let report = dependency_readiness_report_from_write_status(readiness, storage);
|
||||
assert!(
|
||||
report.degraded_reasons.is_empty(),
|
||||
"a readable node must stay ready when write quorum and the metadata writer are lost, got {:?}",
|
||||
report.degraded_reasons
|
||||
);
|
||||
|
||||
let (readiness, storage, _) = apply_node_service_readiness(
|
||||
StorageWriteReadinessStatus {
|
||||
ready: true,
|
||||
pool_metadata_reason: None,
|
||||
},
|
||||
StorageReadinessDetails {
|
||||
read_quorum_ready: false,
|
||||
write_quorum_ready: false,
|
||||
pool_metadata_write_ready: false,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
dependency_readiness_report_from_write_status(readiness, storage).degraded_reasons,
|
||||
vec![ReadinessDegradedReason::StorageAndLockUnavailable]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_lock_quorum_stays_ready_when_two_of_four_clients_remain() {
|
||||
let endpoints = (0..4)
|
||||
.map(|disk_idx| Endpoint {
|
||||
url: url::Url::parse(&format!("http://node{disk_idx}:9000/data")).expect("valid test endpoint"),
|
||||
is_local: disk_idx == 0,
|
||||
pool_idx: 0,
|
||||
set_idx: 0,
|
||||
disk_idx,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let online = ["node0:9000", "node1:9000"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
let status = set_lock_quorum_status(&online, &endpoints);
|
||||
|
||||
assert_eq!(status.total_clients, 4);
|
||||
assert_eq!(status.connected_clients, 2);
|
||||
assert_eq!(status.required_quorum, 3);
|
||||
assert_eq!(status.required_read_quorum, 2);
|
||||
assert!(!status.ready, "exclusive locks need three of four clients");
|
||||
assert!(status.read_ready, "shared locks remain available at two of four clients");
|
||||
|
||||
let one = ["node0:9000"].into_iter().map(str::to_string).collect::<HashSet<_>>();
|
||||
let below_read = set_lock_quorum_status(&one, &endpoints);
|
||||
assert!(!below_read.ready);
|
||||
assert!(!below_read.read_ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2628,9 +2795,11 @@ mod tests {
|
||||
let observation = LockQuorumObservation {
|
||||
status: LockQuorumStatus {
|
||||
ready: true,
|
||||
read_ready: true,
|
||||
connected_clients: 2,
|
||||
total_clients: 3,
|
||||
required_quorum: 2,
|
||||
required_read_quorum: 2,
|
||||
},
|
||||
online_hosts: HashSet::from(["node-a:9000".to_owned(), "node-b:9000".to_owned()]),
|
||||
};
|
||||
|
||||
@@ -85,11 +85,24 @@ pub struct DependencyReadinessReport {
|
||||
pub storage_details: Option<StorageReadinessDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct StorageReadinessDetails {
|
||||
pub read_quorum_ready: bool,
|
||||
pub write_quorum_ready: bool,
|
||||
pub pool_metadata_write_ready: bool,
|
||||
pub unavailable_drives: Vec<UnavailableReadinessDrive>,
|
||||
}
|
||||
|
||||
/// Node-local reasons for excluding an inventoried drive from readiness quorum.
|
||||
/// Use topology indices instead of exposing internal addresses or filesystem paths.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UnavailableReadinessDrive {
|
||||
pub pool_index: i32,
|
||||
pub set_index: i32,
|
||||
pub disk_index: i32,
|
||||
pub runtime_state: String,
|
||||
pub host_online: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn convert_ecstore_object_info(object: StorageObjectInfo) -> NotifyObjectInfo {
|
||||
|
||||
@@ -675,8 +675,8 @@ fn unix_now() -> Result<i64> {
|
||||
async fn execute_connect_top(command: ConnectTopCommands) -> Result<()> {
|
||||
use crate::connect::{
|
||||
IdentityStore, LocalTopConsent, MAX_TOP_DURATION, MAX_TOP_EXPORT_VALIDITY, TOP_CLASSIFICATION, TopApiOperation,
|
||||
TopCaptureLimits, TopCaptureRequest, TopCaptureScope, capture_top_api, capture_top_disk, capture_top_locks,
|
||||
capture_top_net, capture_top_rpc,
|
||||
TopCaptureLimits, TopCaptureRequest, TopCaptureScope, capture_top_api, capture_top_locks, capture_top_net,
|
||||
capture_top_rpc,
|
||||
};
|
||||
|
||||
let (tool_id, options) = match command {
|
||||
@@ -692,6 +692,68 @@ async fn execute_connect_top(command: ConnectTopCommands) -> Result<()> {
|
||||
return Err(Error::other("connect_top_limits_invalid"));
|
||||
}
|
||||
|
||||
if tool_id == "top.disk" {
|
||||
let offline_key_id = options
|
||||
.offline_key_id
|
||||
.ok_or_else(|| Error::other("--offline-key-id must select an existing offline identity"))?;
|
||||
let input = crate::connect::diagnostics::LocalTopDiskRequest {
|
||||
offline_key_id,
|
||||
organization_name: options.organization,
|
||||
cluster_name: options.cluster,
|
||||
device_name: options.device,
|
||||
run_uid: options.run_uid,
|
||||
artifact_uid: options.artifact_uid,
|
||||
consent_uid: options.consent_uid,
|
||||
policy_revision: options.policy_revision,
|
||||
consent_expires_at_unix: options.consent_expires_at_unix,
|
||||
acknowledge_l3: options.acknowledge_l3,
|
||||
run_expires_at_unix: options.run_expires_at_unix,
|
||||
window_millis: options.window_millis,
|
||||
export_validity_seconds: options.export_validity_seconds,
|
||||
};
|
||||
let cancel = CancellationToken::new();
|
||||
let capture = crate::connect::request_local_top_disk(&options.state_dir, input, &cancel);
|
||||
tokio::pin!(capture);
|
||||
let export = tokio::select! {
|
||||
biased;
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(Error::other)?;
|
||||
cancel.cancel();
|
||||
return match capture.await {
|
||||
Err(error) => Err(Error::other(error)),
|
||||
Ok(_) => Err(Error::other("top disk collection cancelled")),
|
||||
};
|
||||
}
|
||||
result = &mut capture => result.map_err(Error::other)?,
|
||||
};
|
||||
let writer_cancel = cancel.clone();
|
||||
let mut writer = tokio::task::spawn_blocking(move || {
|
||||
crate::connect::diagnostics::save_top_archive(
|
||||
&options.output,
|
||||
&export.artifact_uid,
|
||||
&export.archive_bytes,
|
||||
&export.archive_sha256,
|
||||
&writer_cancel,
|
||||
)
|
||||
});
|
||||
let receipt = tokio::select! {
|
||||
biased;
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(Error::other)?; cancel.cancel();
|
||||
writer.await.map_err(Error::other)?.map_err(Error::other)?
|
||||
}
|
||||
result = &mut writer => result.map_err(Error::other)?.map_err(Error::other)?,
|
||||
};
|
||||
println!(
|
||||
"artifact={} bytes={} sha256={}",
|
||||
receipt.artifact_uid, receipt.archive_size_bytes, receipt.archive_sha256
|
||||
);
|
||||
println!("upload=not-performed");
|
||||
return Ok(());
|
||||
}
|
||||
if options.offline_key_id.is_some() {
|
||||
return Err(Error::other("--offline-key-id is only supported for top disk"));
|
||||
}
|
||||
let identity = IdentityStore::new(options.state_dir.join("identity"))
|
||||
.load()
|
||||
.map_err(Error::other)?
|
||||
@@ -725,10 +787,6 @@ async fn execute_connect_top(command: ConnectTopCommands) -> Result<()> {
|
||||
let result = await_top_capture(capture_top_api(&request, TopApiOperation::GetObject, &cancel), &cancel).await?;
|
||||
finish_top_capture(&request, result, &identity, options.output, &cancel).await
|
||||
}
|
||||
"top.disk" => {
|
||||
let result = await_top_capture(capture_top_disk(&request, &cancel), &cancel).await?;
|
||||
finish_top_capture(&request, result, &identity, options.output, &cancel).await
|
||||
}
|
||||
"top.locks" => {
|
||||
let result = await_top_capture(capture_top_locks(&request, &cancel), &cancel).await?;
|
||||
finish_top_capture(&request, result, &identity, options.output, &cancel).await
|
||||
@@ -1343,80 +1401,123 @@ async fn execute_connect_profile(options: ConnectProfileOpts) -> Result<()> {
|
||||
};
|
||||
use rand::{TryRng as _, rngs::SysRng};
|
||||
|
||||
let key = IdentityStore::new(options.state_dir.join("identity"))
|
||||
.load()
|
||||
.map_err(Error::other)?
|
||||
.ok_or_else(|| Error::other("connect profile requires an enrolled device identity"))?;
|
||||
let executable_sha256 = hash_current_executable()?;
|
||||
let produced_at_unix = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(Error::other)
|
||||
.and_then(|duration| i64::try_from(duration.as_secs()).map_err(Error::other))?;
|
||||
let mut nonce = [0_u8; 32];
|
||||
SysRng.try_fill_bytes(&mut nonce).map_err(Error::other)?;
|
||||
let request = ProfileCaptureRequest {
|
||||
organization_name: options.organization,
|
||||
cluster_name: options.cluster,
|
||||
device_name: options.device,
|
||||
run_uid: options.run_uid,
|
||||
artifact_uid: options.artifact_uid,
|
||||
schema_version: options.schema_version,
|
||||
capability: options.capability,
|
||||
consent: LocalProfileConsent {
|
||||
let runtime_scope =
|
||||
options.tool == ConnectProfileTool::Threads && options.thread_scope == Some(ConnectThreadProfileScope::TokioRuntime);
|
||||
if !runtime_scope && options.offline_key_id.is_some() {
|
||||
return Err(Error::other("--offline-key-id is valid only for a service runtime profile"));
|
||||
}
|
||||
let cancel = tokio_util::sync::CancellationToken::new();
|
||||
let export = if runtime_scope {
|
||||
let offline_key_id = options
|
||||
.offline_key_id
|
||||
.ok_or_else(|| Error::other("--offline-key-id must explicitly select an existing offline identity"))?;
|
||||
let input = crate::connect::diagnostics::LocalRuntimeProfileRequest {
|
||||
offline_key_id,
|
||||
organization_name: options.organization,
|
||||
cluster_name: options.cluster,
|
||||
device_name: options.device,
|
||||
run_uid: options.run_uid,
|
||||
artifact_uid: options.artifact_uid,
|
||||
schema_version: options.schema_version,
|
||||
capability: options.capability,
|
||||
consent_uid: options.consent_uid,
|
||||
policy_revision: options.policy_revision,
|
||||
expires_at_unix: options.consent_expires_at_unix,
|
||||
confirmed: options.acknowledge_l3,
|
||||
},
|
||||
produced_at_unix,
|
||||
expires_at_unix: options.expires_at_unix,
|
||||
nonce,
|
||||
duration: Duration::from_millis(options.duration_millis),
|
||||
sample_period: Duration::from_micros(options.sample_period_micros),
|
||||
provenance: ProfileProvenance::new(
|
||||
crate::version::build::COMMIT_HASH,
|
||||
executable_sha256,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
enabled_build_features(),
|
||||
),
|
||||
};
|
||||
let cancel = tokio_util::sync::CancellationToken::new();
|
||||
let export = {
|
||||
let capture = async {
|
||||
match options.tool {
|
||||
ConnectProfileTool::Cpu => {
|
||||
if options.thread_scope.is_some() {
|
||||
return Err(Error::other("--thread-scope is valid only for the threads profile"));
|
||||
}
|
||||
export_cpu_profile(&request, &key, &cancel).await.map_err(Error::other)
|
||||
}
|
||||
ConnectProfileTool::Memory => {
|
||||
if options.thread_scope.is_some() {
|
||||
return Err(Error::other("--thread-scope is valid only for the threads profile"));
|
||||
}
|
||||
export_memory_profile(&request, &key, &cancel).await.map_err(Error::other)
|
||||
}
|
||||
ConnectProfileTool::Threads => {
|
||||
let scope = match options.thread_scope {
|
||||
Some(ConnectThreadProfileScope::TokioRuntime) => ThreadProfileScope::TokioRuntime,
|
||||
Some(ConnectThreadProfileScope::NativeThreads) => ThreadProfileScope::NativeThreads,
|
||||
None => return Err(Error::other("--thread-scope is required for the threads profile")),
|
||||
};
|
||||
export_thread_profile(&request, scope, &key, &cancel)
|
||||
.await
|
||||
.map_err(Error::other)
|
||||
}
|
||||
}
|
||||
consent_expires_at_unix: options.consent_expires_at_unix,
|
||||
acknowledge_l3: options.acknowledge_l3,
|
||||
expires_at_unix: options.expires_at_unix,
|
||||
duration_millis: options.duration_millis,
|
||||
sample_period_micros: options.sample_period_micros,
|
||||
};
|
||||
let capture = crate::connect::request_local_runtime_profile(&options.state_dir, input, &cancel);
|
||||
tokio::pin!(capture);
|
||||
tokio::select! {
|
||||
biased;
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(Error::other)?;
|
||||
cancel.cancel();
|
||||
return Err(Error::other("profile collection cancelled"));
|
||||
return match capture.await {
|
||||
Err(error) => Err(Error::other(error)),
|
||||
Ok(_) => Err(Error::other("profile collection cancelled")),
|
||||
};
|
||||
}
|
||||
result = &mut capture => result.map_err(Error::other)?,
|
||||
}
|
||||
} else {
|
||||
let key = IdentityStore::new(options.state_dir.join("identity"))
|
||||
.load()
|
||||
.map_err(Error::other)?
|
||||
.ok_or_else(|| Error::other("connect profile requires an enrolled device identity"))?;
|
||||
let executable_sha256 = hash_current_executable()?;
|
||||
let produced_at_unix = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(Error::other)
|
||||
.and_then(|duration| i64::try_from(duration.as_secs()).map_err(Error::other))?;
|
||||
let mut nonce = [0_u8; 32];
|
||||
SysRng.try_fill_bytes(&mut nonce).map_err(Error::other)?;
|
||||
let request = ProfileCaptureRequest {
|
||||
organization_name: options.organization,
|
||||
cluster_name: options.cluster,
|
||||
device_name: options.device,
|
||||
run_uid: options.run_uid,
|
||||
artifact_uid: options.artifact_uid,
|
||||
schema_version: options.schema_version,
|
||||
capability: options.capability,
|
||||
consent: LocalProfileConsent {
|
||||
consent_uid: options.consent_uid,
|
||||
policy_revision: options.policy_revision,
|
||||
expires_at_unix: options.consent_expires_at_unix,
|
||||
confirmed: options.acknowledge_l3,
|
||||
},
|
||||
produced_at_unix,
|
||||
expires_at_unix: options.expires_at_unix,
|
||||
nonce,
|
||||
duration: Duration::from_millis(options.duration_millis),
|
||||
sample_period: Duration::from_micros(options.sample_period_micros),
|
||||
provenance: ProfileProvenance::new(
|
||||
crate::version::build::COMMIT_HASH,
|
||||
executable_sha256,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
enabled_build_features(),
|
||||
),
|
||||
};
|
||||
|
||||
{
|
||||
let capture = async {
|
||||
match options.tool {
|
||||
ConnectProfileTool::Cpu => {
|
||||
if options.thread_scope.is_some() {
|
||||
return Err(Error::other("--thread-scope is valid only for the threads profile"));
|
||||
}
|
||||
export_cpu_profile(&request, &key, &cancel).await.map_err(Error::other)
|
||||
}
|
||||
ConnectProfileTool::Memory => {
|
||||
if options.thread_scope.is_some() {
|
||||
return Err(Error::other("--thread-scope is valid only for the threads profile"));
|
||||
}
|
||||
export_memory_profile(&request, &key, &cancel).await.map_err(Error::other)
|
||||
}
|
||||
ConnectProfileTool::Threads => {
|
||||
let scope = match options.thread_scope {
|
||||
Some(ConnectThreadProfileScope::TokioRuntime) => ThreadProfileScope::TokioRuntime,
|
||||
Some(ConnectThreadProfileScope::NativeThreads) => ThreadProfileScope::NativeThreads,
|
||||
None => return Err(Error::other("--thread-scope is required for the threads profile")),
|
||||
};
|
||||
export_thread_profile(&request, scope, &key, &cancel)
|
||||
.await
|
||||
.map_err(Error::other)
|
||||
}
|
||||
}
|
||||
};
|
||||
tokio::pin!(capture);
|
||||
tokio::select! {
|
||||
biased;
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.map_err(Error::other)?;
|
||||
cancel.cancel();
|
||||
return Err(Error::other("profile collection cancelled"));
|
||||
}
|
||||
result = capture.as_mut() => result?,
|
||||
}
|
||||
result = capture.as_mut() => result?,
|
||||
}
|
||||
};
|
||||
let tool = export.tool;
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::storage::storage_api::ecfs_consumer::object_lock::{
|
||||
parse_object_lock_legal_hold, parse_object_lock_retention, validate_bucket_object_lock_enabled,
|
||||
};
|
||||
use crate::storage::storage_api::runtime_sources_consumer::{ECStore, runtime_sources};
|
||||
use crate::storage::validate_website_configuration;
|
||||
use crate::table_catalog;
|
||||
use http::StatusCode;
|
||||
use metrics::{counter, histogram};
|
||||
@@ -1512,6 +1513,8 @@ impl S3 for FS {
|
||||
.await
|
||||
.map_err(crate::error::ApiError::from)?;
|
||||
|
||||
validate_website_configuration(&req.input.website_configuration)?;
|
||||
|
||||
let website_config = serialize(&req.input.website_configuration)
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::MalformedXML, format!("{err}")))?;
|
||||
update_bucket_metadata_config_if_incarnation(
|
||||
@@ -2038,12 +2041,48 @@ mod tests {
|
||||
FS, SITE_REPLICATION_GATE_FORCE_DISABLED, SITE_REPLICATION_GATE_FORCE_ENABLED, SITE_REPLICATION_GATE_TEST_OVERRIDE,
|
||||
};
|
||||
use crate::storage::access::ReqInfo;
|
||||
use crate::storage::validate_website_configuration;
|
||||
use http::Method;
|
||||
use http::StatusCode;
|
||||
use s3s::dto::{DeleteBucketReplicationInput, PutBucketReplicationInput, ReplicationConfiguration};
|
||||
use s3s::dto::{
|
||||
Condition, DeleteBucketReplicationInput, IndexDocument, PutBucketReplicationInput, Redirect, ReplicationConfiguration,
|
||||
RoutingRule, WebsiteConfiguration,
|
||||
};
|
||||
use s3s::{S3, S3Error, S3ErrorCode, S3Request};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[test]
|
||||
fn website_config_rejects_missing_index_and_invalid_redirect_rules() {
|
||||
assert!(validate_website_configuration(&WebsiteConfiguration::default()).is_err());
|
||||
let config = WebsiteConfiguration {
|
||||
index_document: Some(IndexDocument {
|
||||
suffix: "index.html".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate_website_configuration(&config).is_ok());
|
||||
let mut bad = config;
|
||||
bad.routing_rules = Some(vec![RoutingRule {
|
||||
condition: Some(Condition::default()),
|
||||
redirect: Redirect {
|
||||
host_name: Some("other.test".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
}]);
|
||||
assert!(validate_website_configuration(&bad).is_err());
|
||||
bad.routing_rules = Some(vec![RoutingRule {
|
||||
condition: Some(Condition {
|
||||
key_prefix_equals: Some("old/".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
redirect: Redirect {
|
||||
protocol: Some("ftp".to_owned().into()),
|
||||
..Default::default()
|
||||
},
|
||||
}]);
|
||||
assert!(validate_website_configuration(&bad).is_err());
|
||||
}
|
||||
|
||||
fn replication_config_edit_request<T>(input: T, is_owner: bool) -> S3Request<T> {
|
||||
let mut req = S3Request {
|
||||
input,
|
||||
|
||||
@@ -66,14 +66,14 @@ pub(crate) use storage_api::{
|
||||
ecstore_rebalance, ecstore_rio, ecstore_rpc, ecstore_set_disk, ecstore_storage, ecstore_tier, encode_tags,
|
||||
find_local_disk_by_ref, get_bucket_accelerate_config, get_bucket_cors_config, get_bucket_logging_config, get_bucket_metadata,
|
||||
get_bucket_notification_config, get_bucket_object_lock_config, get_bucket_request_payment_config, get_bucket_sse_config,
|
||||
get_bucket_website_config, get_local_server_property, get_lock_acquire_timeout, head_prefix_consumer, helper_consumer,
|
||||
init_background_replication, init_bucket_metadata_sys, init_ecstore_config, init_local_disks_with_instance_ctx,
|
||||
init_lock_clients, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found, is_valid_storage_class,
|
||||
options_consumer, prewarm_local_disk_id_map_with_instance_ctx, read_config, record_replication_proxy, rpc_consumer,
|
||||
runtime_sources_consumer, s3_api_consumer, serialize, table_catalog_path_hash, to_s3s_etag,
|
||||
topology_snapshot_from_endpoint_pools_with_capabilities, try_migrate_bucket_metadata, try_migrate_iam_config,
|
||||
try_migrate_server_config, update_bucket_metadata_config, update_bucket_metadata_config_if_incarnation, verify_rpc_signature,
|
||||
wrap_reader,
|
||||
get_bucket_website_config, get_bucket_website_config_for_store, get_local_server_property, get_lock_acquire_timeout,
|
||||
head_prefix_consumer, helper_consumer, init_background_replication, init_bucket_metadata_sys, init_ecstore_config,
|
||||
init_local_disks_with_instance_ctx, init_lock_clients, is_err_bucket_not_found, is_err_object_not_found,
|
||||
is_err_version_not_found, is_valid_storage_class, options_consumer, prewarm_local_disk_id_map_with_instance_ctx, read_config,
|
||||
record_replication_proxy, rpc_consumer, runtime_sources_consumer, s3_api_consumer, serialize, table_catalog_path_hash,
|
||||
to_s3s_etag, topology_snapshot_from_endpoint_pools_with_capabilities, try_migrate_bucket_metadata, try_migrate_iam_config,
|
||||
try_migrate_server_config, update_bucket_metadata_config, update_bucket_metadata_config_if_incarnation,
|
||||
validate_website_configuration, verify_rpc_signature, wrap_reader,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user