diff --git a/.github/labeler.yml b/.github/labeler.yml index a08c77bdbd5..de544b4917f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -3,11 +3,6 @@ # - https://github.com/actions/labeler/issues/112 # - https://github.com/actions/labeler/issues/104 -# Add 'Run nested -auto-' label to either any change on nested lib or nested test -Run nested -auto-: - - tests/lib/nested.sh - - tests/nested/**/* - # Add 'Needs Documentation -auto-' label to indicate a change needs changes in the docs Needs Documentation -auto-: - cmd/snap/**/*" diff --git a/.github/workflows/actions/download-install-debian-deps/action.yaml b/.github/workflows/actions/download-install-debian-deps/action.yaml new file mode 100644 index 00000000000..b0d3883c648 --- /dev/null +++ b/.github/workflows/actions/download-install-debian-deps/action.yaml @@ -0,0 +1,28 @@ +name: 'Download cached dependencies and install Debian deps' +description: 'Download and install Debian dependencies' +inputs: + snapd-src-dir: + description: 'The snapd source code directory' + required: true + type: string + +runs: + using: "composite" + steps: + - name: Download Debian dependencies + uses: actions/download-artifact@v4 + with: + name: debian-dependencies + path: ./debian-deps/ + + - name: Copy dependencies + shell: bash + run: | + test -f ./debian-deps/cached-apt.tar + sudo tar xvf ./debian-deps/cached-apt.tar -C / + + - name: Install Debian dependencies + shell: bash + run: | + sudo apt update + sudo apt build-dep -y "${{ inputs.snapd-src-dir }}" diff --git a/.github/workflows/snap-builds.yaml b/.github/workflows/snap-builds.yaml new file mode 100644 index 00000000000..b66a9f180d9 --- /dev/null +++ b/.github/workflows/snap-builds.yaml @@ -0,0 +1,83 @@ +on: + workflow_call: + inputs: + runs-on: + description: 'A json list of tags to indicate which runner to use' + required: true + type: string + toolchain: + description: 'The go toolchain to use {default, FIPS}' + required: true + type: string + variant: + description: 'The type of snapd build {pristine, test}' + required: true + type: string + +jobs: + snap-builds: + runs-on: ${{ fromJSON(inputs.runs-on) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set artifact name + id: set_artifact_name + run: | + postfix="${{ inputs.toolchain }}-${{ inputs.variant }}" + if grep -iq "arm64" <<<"${{ inputs.runs-on }}"; then + echo "artifact_name=snap-files-arm64-${postfix}" >> $GITHUB_OUTPUT + else + echo "artifact_name=snap-files-amd64-${postfix}" >> $GITHUB_OUTPUT + fi + + - name: Select Go toolchain + run: | + case "${{ inputs.toolchain }}" in + default) + rm -f fips-build + ;; + FIPS) + touch fips-build + ;; + *) + echo "unknown toolchain ${{ inputs.toolchain }}" + exit 1 + ;; + esac + case "${{ inputs.variant }}" in + pristine) + rm -f test-build + ;; + test) + touch test-build + ;; + esac + + - name: Build snapd snap + uses: snapcore/action-build@v1 + with: + snapcraft-channel: 8.x/stable + snapcraft-args: --verbose + + - name: Check built artifact + run: | + unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/ + if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then + echo "PR produces dirty snapd snap version" + cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt + exit 1 + elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then + echo "PR produces dirty internal snapd info version" + cat squashfs-root/usr/lib/snapd/info + cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt + exit 1 + fi + + - name: Uploading snapd snap artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.set_artifact_name.outputs.artifact_name }} + path: "*.snap" diff --git a/.github/workflows/spread-tests.yaml b/.github/workflows/spread-tests.yaml new file mode 100644 index 00000000000..2fabd866bd7 --- /dev/null +++ b/.github/workflows/spread-tests.yaml @@ -0,0 +1,293 @@ +on: + workflow_call: + inputs: + runs-on: + description: 'A json list of tags to indicate which runner to use' + required: true + type: string + group: + description: 'The name of the group of backends, systems, tests, and rules' + required: true + type: string + backend: + description: 'The spread backend to use (for possible values, check spread.yaml)' + required: true + type: string + systems: + description: 'The spread system(s) to use (for possible values, check spread.yaml). If more than one, separate them with a space' + required: true + type: string + tasks: + description: 'The spread tasks to run. It may be a space-separated list and may contain directories of many tasks or individual ones' + required: true + type: string + rules: + description: 'The rule .yaml file to use (found under tests/lib/spread/rules) for test discovery' + required: true + type: string + + +jobs: + run-spread: + runs-on: ${{ fromJSON(inputs.runs-on) }} + steps: + - name: Cleanup job workspace + id: cleanup-job-workspace + run: | + rm -rf "${{ github.workspace }}" + mkdir "${{ github.workspace }}" + + - name: Checkout code + uses: actions/checkout@v4 + with: + # spread uses tags as delta reference + fetch-depth: 0 + + - name: Get previous attempt + id: get-previous-attempt + run: | + echo "previous_attempt=$(( ${{ github.run_attempt }} - 1 ))" >> $GITHUB_OUTPUT + shell: bash + + - name: Get previous cache + uses: actions/cache@v4 + with: + path: "${{ github.workspace }}/.test-results" + key: "${{ github.job }}-results-${{ github.run_id }}-${{ inputs.group }}-${{ steps.get-previous-attempt.outputs.previous_attempt }}" + + - name: Prepare test results env and vars + id: prepare-test-results-env + run: | + # Create test results directories and save vars + TEST_RESULTS_DIR="${{ github.workspace }}/.test-results" + echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV + + # Save the var with the failed tests file + echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/failed-tests" >> $GITHUB_ENV + + # Make sure the test results dirs are created + # This step has to be after the cache is restored + mkdir -p "$TEST_RESULTS_DIR" + + - name: Prepare nested env vars + if: contains(github.event.pull_request.labels.*.name, 'Run nested') && startsWith(matrix.group, 'nested-') + run: | + echo "RUN_NESTED=true" >> "$GITHUB_ENV" + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v41.0.0 + + - name: Save changes files + run: | + CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}" + echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV + echo "The changed files found are: $CHANGED_FILES" + + - name: Check failed tests to run + if: "!contains(github.event.pull_request.labels.*.name, 'Run all')" + run: | + # Save previous failed test results in FAILED_TESTS env var + FAILED_TESTS="" + if [ -f "$FAILED_TESTS_FILE" ]; then + echo "Failed tests file found" + FAILED_TESTS="$(cat $FAILED_TESTS_FILE)" + if [ -n "$FAILED_TESTS" ]; then + echo "Failed tests to run: $FAILED_TESTS" + echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV + fi + fi + + - name: Setup run tests variable + if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" + run: | + RUN_TESTS="" + SUGGESTED_TESTS="" + # Save previous failed test results in FAILED_TESTS env var + if [ -n "$FAILED_TESTS" ]; then + RUN_TESTS="$FAILED_TESTS" + else + CHANGES_PARAM="" + for CHANGE in $CHANGED_FILES; do + CHANGES_PARAM="$CHANGES_PARAM -c $CHANGE" + done + for SYSTEM in ${{ inputs.systems }}; do + # Configure parameters to run tests based on current changes + # The tests are just filtered when the change is a PR + # When 'Run Nested' label is added in a PR, all the nested tests have to be executed + if [ -z "${{ github.event.number }}" ] || [ "$RUN_NESTED" = 'true' ]; then + for TESTS in ${{ inputs.tasks }}; do + RUN_TESTS="$RUN_TESTS ${{ inputs.backend }}:$SYSTEM:$TESTS" + done + else + NEW_TESTS="$(./tests/lib/external/snapd-testing-tools/utils/spread-filter -r ./tests/lib/spread/rules/${{ inputs.rules }}.yaml -p "${{ inputs.backend }}:$SYSTEM" $CHANGES_PARAM)" + if [ -z "$RUN_TESTS" ]; then + RUN_TESTS="$NEW_TESTS" + else + RUN_TESTS="$RUN_TESTS $NEW_TESTS" + fi + fi + done + fi + echo RUN_TESTS="$RUN_TESTS" >> $GITHUB_ENV + + - name: Setup grafana parameters + if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" + run: | + # Configure parameters to filter logs (these logs are sent read by grafana agent) + CHANGE_ID="${{ github.event.number }}" + if [ -z "$CHANGE_ID" ]; then + CHANGE_ID="main" + fi + FILTERED_LOG_FILE="spread_${CHANGE_ID}_n${{ github.run_attempt }}.filtered.log" + # The log-filter tool is used to filter the spread logs to be stored + echo FILTER_PARAMS="-o $FILTERED_LOG_FILE -e Debug -e WARNING: -f Failed=NO_LINES -f Error=NO_LINES" >> $GITHUB_ENV + echo FILTERED_LOG_FILE="$FILTERED_LOG_FILE" >> $GITHUB_ENV + + # Add start line to filtered log + echo "GRAFANA START: pr ${CHANGE_ID} attempt ${{ github.run_attempt }} run ${{ github.run_id }} group ${{ inputs.group }}" > "$FILTERED_LOG_FILE" + + - name: Download built snap (amd64) + uses: actions/download-artifact@v4 + if: "!contains(inputs.group, '-arm64') && !endsWith(inputs.group, '-fips')" + with: + name: snap-files-amd64-default-test + # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap + pattern: snapd_1337.*.snap + path: "${{ github.workspace }}/built-snap" + + - name: Download built snap (arm64) + if: "contains(inputs.group, '-arm64') && !endsWith(inputs.group, '-fips')" + uses: actions/download-artifact@v4 + with: + name: snap-files-arm64-default-test + pattern: snapd_1337.*.snap + # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap + path: "${{ github.workspace }}/built-snap" + + - name: Download built FIPS snap (amd64) + uses: actions/download-artifact@v4 + # eg. ubuntu-fips + if: "!contains(inputs.group, '-arm64') && endsWith(inputs.group, '-fips')" + with: + name: snap-files-amd64-FIPS-test + # eg. snapd_1337.2.65.1+git97.gd35b459-fips_amd64.snap + pattern: snapd_1337.*-fips_*.snap + path: "${{ github.workspace }}/built-snap" + + - name: Rename imported snap + run: | + for snap in built-snap/snapd_1337.*.snap; do + mv -v "${snap}" "${snap}.keep" + done + + - name: Run spread tests + if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" + env: + SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }} + run: | + # Register a problem matcher to highlight spread failures + echo "::add-matcher::.github/spread-problem-matcher.json" + set -x + SPREAD=spread + if [[ "${{ inputs.group }}" =~ nested- ]]; then + export NESTED_BUILD_SNAPD_FROM_CURRENT=true + export NESTED_ENABLE_KVM=true + fi + + export SPREAD_USE_PREBUILT_SNAPD_SNAP=true + + if [[ "${{ inputs.systems }}" =~ amazon-linux-2023 ]]; then + # Amazon Linux 2023 has no xdelta, however we cannot disable + # xdelta on a per-target basis as it's used in the repack section + # of spread.yaml, which is shared by all targets, so all systems + # in this batch will not use delta for transferring project data + echo "Disabling xdelta support" + export NO_DELTA=1 + fi + + # Add openstack backend definition to spread.yaml + if [ "${{ inputs.backend }}" = openstack ]; then + ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml + fi + + # This could be the case when either there are not systems for a group or + # the list of tests to run is empty + if [ -z "$RUN_TESTS" ]; then + echo "No tests to run, exiting..." + exit 0 + fi + + if "$SPREAD" -list $RUN_TESTS 2>&1 | grep -q "nothing matches provider filter"; then + echo "No tests to run, exiting..." + exit 0 + fi + + # Run spread tests + # "pipefail" ensures that a non-zero status from the spread is + # propagated; and we use a subshell as this option could trigger + # undesired changes elsewhere + echo "Running command: $SPREAD $RUN_TESTS" + (set -o pipefail; $SPREAD -no-debug-output -logs spread-logs $RUN_TESTS | PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-filter $FILTER_PARAMS | tee spread.log) + + - name: Upload spread logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: spread-logs-${{ inputs.systems }} + path: "spread-logs/*.log" + if-no-files-found: ignore + + - name: Discard spread workers + if: always() + run: | + shopt -s nullglob; + for r in .spread-reuse.*.yaml; do + spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; + done + + - name: Report spread errors + if: always() + run: | + if [ -e spread.log ]; then + echo "Running spread log analyzer" + ACTIONS_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}" + PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json --cut 1 >/dev/null + while IFS= read -r line; do + if [ ! -z "$line" ]; then + echo "Adding failed test line to filtered log" + echo "GRAFANA FAILED: $line $ACTIONS_URL" | tee -a "$FILTERED_LOG_FILE" + fi + done <<< $(jq -r '.[] | select( .type == "info" ) | select( .info_type == "Error" ) | "\(.verb) \(.task)"' spread-results.json) + else + echo "No spread log found, skipping errors reporting" + fi + + - name: Analyze spread test results + if: always() + run: | + if [ -f spread.log ]; then + echo "Running spread log parser" + PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json + + # Add openstack backend definition to spread.yaml + if [ "${{ inputs.backend }}" = openstack ]; then + ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml + fi + + echo "Running spread log analyzer" + PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE" + + echo "List of failed tests saved" + cat "$FAILED_TESTS_FILE" + else + echo "No spread log found, saving empty list of failed tests" + touch "$FAILED_TESTS_FILE" + fi + + - name: Save spread test results to cache + if: always() + uses: actions/cache/save@v4 + with: + path: "${{ github.workspace }}/.test-results" + key: "${{ github.job }}-results-${{ github.run_id }}-${{ inputs.group }}-${{ github.run_attempt }}" diff --git a/.github/workflows/static-checks.yaml b/.github/workflows/static-checks.yaml new file mode 100644 index 00000000000..f1a2bd19837 --- /dev/null +++ b/.github/workflows/static-checks.yaml @@ -0,0 +1,126 @@ +on: + workflow_call: + inputs: + runs-on: + description: 'A tag to indicate which runner to use' + required: true + type: string + gochannel: + description: 'The snap store channel to use to install the go snap' + required: true + type: string + +jobs: + static-checks: + runs-on: ${{ inputs.runs-on }} + env: + GOPATH: ${{ github.workspace }} + # Set PATH to ignore the load of magic binaries from /usr/local/bin And + # to use the go snap automatically. Note that we install go from the + # snap in a step below. Without this we get the GitHub-controlled latest + # version of go. + PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin + GOROOT: "" + GITHUB_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.base_ref }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # needed for git commit history + fetch-depth: 0 + # NOTE: checkout the code in a fixed location, even for forks, as this + # is relevant for go's import system. + path: ./src/github.com/snapcore/snapd + + # Fetch base ref, needed for golangci-lint + - name: Fetching base ref ${{ github.base_ref }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd + git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} + + - name: Download and install Debian dependencies + # Github does not allow variables in "uses"; this has to be a hard-coded path + uses: ./src/github.com/snapcore/snapd/.github/workflows/actions/download-install-debian-deps + with: + snapd-src-dir: "${{ github.workspace }}/src/github.com/snapcore/snapd" + + # golang latest ensures things work on the edge + - name: Install the go snap + run: | + sudo snap install --classic --channel=${{ inputs.gochannel }} go + + - name: Install ShellCheck as a snap + run: | + sudo apt-get remove --purge shellcheck + sudo snap install shellcheck + + - name: Get C vendoring + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh + + - name: Install golangci-lint snap + run: | + sudo snap install --classic golangci-lint + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v41.0.0 + with: + path: ./src/github.com/snapcore/snapd + + - name: Save changes files + run: | + CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}" + echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV + echo "The changed files found are: $CHANGED_FILES" + + - name: Run static checks + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + # run gofmt checks only with the latest stable Go + if [ "${{ matrix.gochannel }}" != "latest/stable" ]; then + export SKIP_GOFMT=1 + echo "Formatting checks will be skipped due to the use of Go version ${{ inputs.gochannel }}" + fi + sudo apt-get install -y python3-yamlordereddictloader + ./run-checks --static + + - name: Cache prebuilt indent + id: cache-indent-bin + uses: actions/cache@v4 + with: + path: indent-bin + key: ${{ runner.os }}-indent-2.2.13 + + # build indent 2.2.13 which has this patch + # https://git.savannah.gnu.org/cgit/indent.git/commit/?id=22b83d68e9a8b429590f42920e9f473a236123cf + - name: Build indent 2.2.13 + if: steps.cache-indent-bin.outputs.cache-hit != 'true' + run: | + sudo apt install texinfo autopoint + curl -O https://ftp.gnu.org/gnu/indent/indent-2.2.13.tar.xz + tar xvf indent-2.2.13.tar.xz + cd indent-2.2.13 + autoreconf -if + # set prefix in case we want to pack to tar/extract into system + ./configure --prefix=/opt/indent + make -j + make install DESTDIR=${{ github.workspace }}/indent-bin + find ${{ github.workspace }}/indent-bin -ls + + - name: Check C source code formatting + run: | + set -x + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ + ./autogen.sh + # apply formatting + PATH=${{ github.workspace }}/indent-bin/opt/indent/bin:$PATH make fmt + set +x + if [ -n "$(git diff --stat)" ]; then + git diff + echo "C files are not fomratted correctly, run 'make fmt'" + echo "make sure to have clang-format and indent 2.2.13+ installed" + exit 1 + fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1d3c24573a4..7f5d43669c6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,72 +11,36 @@ concurrency: jobs: snap-builds: - runs-on: ubuntu-22.04 + uses: ./.github/workflows/snap-builds.yaml + with: + runs-on: ${{ matrix.runs-on }} + toolchain: ${{ matrix.toolchain }} + variant: ${{ matrix.variant }} strategy: matrix: + runs-on: + - '["ubuntu-22.04"]' + # Tags to identify the self-hosted runners to use from + # internal runner collection. See internal self-hosted + # runners doc for the complete list of options. + - '["self-hosted", "Linux", "jammy", "ARM64", "large"]' toolchain: - default - FIPS - version: + variant: # test version is a build of snapd with test keys and should # only be installed by test runners. The pristine versions # are the build that should be installed by human users. - pristine - test - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Select Go toolchain - run: | - case "${{ matrix.toolchain }}" in - default) - rm -f fips-build - ;; - FIPS) - touch fips-build - ;; - *) - echo "unknown toolchain ${{ matrix.toolchain }}" - exit 1 - ;; - esac - case "${{ matrix.version }}" in - pristine) - rm -f test-build - ;; - test) - touch test-build - ;; - esac - - - name: Build snapd snap - uses: snapcore/action-build@v1 - with: - snapcraft-channel: 8.x/stable - snapcraft-args: --verbose - - - name: Check built artifact - run: | - unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/ - if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then - echo "PR produces dirty snapd snap version" - cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt - exit 1 - elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then - echo "PR produces dirty internal snapd info version" - cat squashfs-root/usr/lib/snapd/info - cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt - exit 1 - fi - - - name: Uploading snapd snap artifact - uses: actions/upload-artifact@v4 - with: - name: snap-files-${{ matrix.toolchain }}-${{ matrix.version }} - path: "*.snap" + # Exclude building everything for ARM but the version for testing + # to keep the number of builds down as we currently don't have a + # clear need for these excluded builds. + exclude: + - runs-on: '["self-hosted", "Linux", "jammy", "ARM64", "large"]' + toolchain: FIPS + - runs-on: '["self-hosted", "Linux", "jammy", "ARM64", "large"]' + variant: pristine cache-build-deps: runs-on: ubuntu-20.04 @@ -116,18 +80,11 @@ jobs: path: ./cached-apt.tar static-checks: - runs-on: ubuntu-latest + uses: ./.github/workflows/static-checks.yaml needs: [cache-build-deps] - env: - GOPATH: ${{ github.workspace }} - # Set PATH to ignore the load of magic binaries from /usr/local/bin And - # to use the go snap automatically. Note that we install go from the - # snap in a step below. Without this we get the GitHub-controlled latest - # version of go. - PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin - GOROOT: "" - GITHUB_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} - BASE_REF: ${{ github.base_ref }} + with: + runs-on: ubuntu-latest + gochannel: ${{ matrix.gochannel }} strategy: # we cache successful runs so it's fine to keep going @@ -137,117 +94,6 @@ jobs: - 1.18 - latest/stable - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - # needed for git commit history - fetch-depth: 0 - # NOTE: checkout the code in a fixed location, even for forks, as this - # is relevant for go's import system. - path: ./src/github.com/snapcore/snapd - - # Fetch base ref, needed for golangci-lint - - name: Fetching base ref ${{ github.base_ref }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd - git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} - - - name: Download Debian dependencies - uses: actions/download-artifact@v4 - with: - name: debian-dependencies - path: ./debian-deps/ - - - name: Copy dependencies - run: | - test -f ./debian-deps/cached-apt.tar - sudo tar xvf ./debian-deps/cached-apt.tar -C / - - - name: Install Debian dependencies - run: | - sudo apt update - sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd - - # golang latest ensures things work on the edge - - name: Install the go snap - run: | - sudo snap install --classic --channel=${{ matrix.gochannel }} go - - - name: Install ShellCheck as a snap - run: | - sudo apt-get remove --purge shellcheck - sudo snap install shellcheck - - - name: Get C vendoring - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh - - - name: Install golangci-lint snap - run: | - sudo snap install --classic golangci-lint - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v41.0.0 - with: - path: ./src/github.com/snapcore/snapd - - - name: Save changes files - run: | - CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}" - echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV - echo "The changed files found are: $CHANGED_FILES" - - - name: Run static checks - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - # run gofmt checks only with the latest stable Go - if [ "${{ matrix.gochannel }}" != "latest/stable" ]; then - export SKIP_GOFMT=1 - echo "Formatting checks will be skipped due to the use of Go version ${{ matrix.gochannel }}" - fi - sudo apt-get install -y python3-yamlordereddictloader - ./run-checks --static - - - name: Cache prebuilt indent - id: cache-indent-bin - uses: actions/cache@v4 - with: - path: indent-bin - key: ${{ runner.os }}-indent-2.2.13 - - # build indent 2.2.13 which has this patch - # https://git.savannah.gnu.org/cgit/indent.git/commit/?id=22b83d68e9a8b429590f42920e9f473a236123cf - - name: Build indent 2.2.13 - if: steps.cache-indent-bin.outputs.cache-hit != 'true' - run: | - sudo apt install texinfo autopoint - curl -O https://ftp.gnu.org/gnu/indent/indent-2.2.13.tar.xz - tar xvf indent-2.2.13.tar.xz - cd indent-2.2.13 - autoreconf -if - # set prefix in case we want to pack to tar/extract into system - ./configure --prefix=/opt/indent - make -j - make install DESTDIR=${{ github.workspace }}/indent-bin - find ${{ github.workspace }}/indent-bin -ls - - - name: Check C source code formatting - run: | - set -x - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ - ./autogen.sh - # apply formatting - PATH=${{ github.workspace }}/indent-bin/opt/indent/bin:$PATH make fmt - set +x - if [ -n "$(git diff --stat)" ]; then - git diff - echo "C files are not fomratted correctly, run 'make fmt'" - echo "make sure to have clang-format and indent 2.2.13+ installed" - exit 1 - fi - branch-static-checks: runs-on: ubuntu-latest needs: [cache-build-deps] @@ -270,16 +116,12 @@ jobs: shell: bash unit-tests: + uses: ./.github/workflows/unit-tests.yaml needs: [static-checks] - runs-on: ubuntu-22.04 - env: - GOPATH: ${{ github.workspace }} - # Set PATH to ignore the load of magic binaries from /usr/local/bin And - # to use the go snap automatically. Note that we install go from the - # snap in a step below. Without this we get the GitHub-controlled latest - # version of go. - PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin - GOROOT: "" + with: + runs-on: ubuntu-22.04 + gochannel: ${{ matrix.gochannel }} + unit-scenario: ${{ matrix.unit-scenario }} strategy: # we cache successful runs so it's fine to keep going fail-fast: false @@ -290,93 +132,14 @@ jobs: unit-scenario: - normal - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - # needed for git commit history - fetch-depth: 0 - # NOTE: checkout the code in a fixed location, even for forks, as this - # is relevant for go's import system. - path: ./src/github.com/snapcore/snapd - - # Fetch base ref, needed for golangci-lint - - name: Fetching base ref ${{ github.base_ref }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd - git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} - - - name: Download Debian dependencies - uses: actions/download-artifact@v4 - with: - name: debian-dependencies - path: ./debian-deps/ - - - name: Copy dependencies - run: | - test -f ./debian-deps/cached-apt.tar - sudo tar xvf ./debian-deps/cached-apt.tar -C / - - - name: Install Debian dependencies - run: | - sudo apt update - sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd - - # golang latest ensures things work on the edge - - name: Install the go snap - run: | - sudo snap install --classic --channel=${{ matrix.gochannel }} go - - - name: Get deps - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh - - - name: Build C - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ - ./autogen.sh - make -j$(nproc) - - - name: Build Go - run: | - go build github.com/snapcore/snapd/... - - - name: Test C - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck - - - name: Reset code coverage data - run: | - rm -rf ${{ github.workspace }}/.coverage/ - COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ matrix.unit-scenario}}.cov" - echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV - - - name: Test Go - if: ${{ matrix.unit-scenario == 'normal' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - ./run-checks --unit - - - name: Upload the coverage results - if: ${{ matrix.gochannel != 'latest/stable' }} - uses: actions/upload-artifact@v4 - with: - include-hidden-files: true - name: "coverage-files-${{ matrix.unit-scenario }}" - path: "${{ github.workspace }}/coverage/coverage*.cov" - # TODO run unit tests of C code unit-tests-special: + uses: ./.github/workflows/unit-tests.yaml needs: [static-checks] - runs-on: ubuntu-22.04 - env: - GOPATH: ${{ github.workspace }} - # Set PATH to ignore the load of magic binaries from /usr/local/bin And - # to use the go snap automatically. Note that we install go from the - # snap in a step below. Without this we get the GitHub-controlled latest - # version of go. - PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin - GOROOT: "" + with: + runs-on: ubuntu-22.04 + gochannel: ${{ matrix.gochannel }} + unit-scenario: ${{ matrix.unit-scenario }} strategy: # we cache successful runs so it's fine to keep going fail-fast: false @@ -392,125 +155,12 @@ jobs: - race - snapdusergo - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - # needed for git commit history - fetch-depth: 0 - # NOTE: checkout the code in a fixed location, even for forks, as this - # is relevant for go's import system. - path: ./src/github.com/snapcore/snapd - - # Fetch base ref, needed for golangci-lint - - name: Fetching base ref ${{ github.base_ref }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd - git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} - - - name: Download Debian dependencies - uses: actions/download-artifact@v4 - with: - name: debian-dependencies - path: ./debian-deps/ - - - name: Copy dependencies - run: | - test -f ./debian-deps/cached-apt.tar - sudo tar xvf ./debian-deps/cached-apt.tar -C / - - - name: Install Debian dependencies - run: | - sudo apt update - sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd - - # golang latest ensures things work on the edge - - name: Install the go snap - run: | - sudo snap install --classic --channel=${{ matrix.gochannel }} go - - - name: Get deps - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh - - - name: Build C - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ - ./autogen.sh - make -j$(nproc) - - - name: Build Go - run: | - go build github.com/snapcore/snapd/... - - - name: Test C - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck - - - name: Reset code coverage data - run: | - rm -rf ${{ github.workspace }}/.coverage/ - COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ matrix.unit-scenario}}.cov" - echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV - - - name: Test Go (SNAPD_DEBUG=1) - if: ${{ matrix.unit-scenario == 'snapd_debug' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - SKIP_DIRTY_CHECK=1 SNAPD_DEBUG=1 ./run-checks --unit - - - name: Test Go (withbootassetstesting) - if: ${{ matrix.unit-scenario == 'withbootassetstesting' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=withbootassetstesting ./run-checks --unit - - - name: Test Go (nosecboot) - if: ${{ matrix.unit-scenario == 'nosecboot' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - echo "Dropping github.com/snapcore/secboot" - # use govendor remove so that a subsequent govendor sync does not - # install secboot again - # ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot - # ${{ github.workspace }}/bin/govendor remove +unused - SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit - - - name: Test Go (faultinject) - if: ${{ matrix.unit-scenario == 'faultinject' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit - - - name: Test Go (-race) - if: ${{ matrix.unit-scenario == 'race' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - SKIP_DIRTY_CHECK=1 GO_TEST_RACE=1 SKIP_COVERAGE=1 ./run-checks --unit - - - name: Test Go (snapdusergo) - if: ${{ matrix.unit-scenario == 'snapdusergo' }} - run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=snapdusergo ./run-checks --unit - - - name: Upload the coverage results - if: ${{ matrix.gochannel != 'latest/stable' && matrix.unit-scenario != 'race' }} - uses: actions/upload-artifact@v4 - with: - include-hidden-files: true - name: "coverage-files-${{ matrix.unit-scenario }}" - path: "${{ github.workspace }}/coverage/coverage*.cov" - - unit-tests-cross-distro: + uses: ./.github/workflows/unit-tests-cross-distro.yaml needs: [static-checks] - env: - # Set PATH to ignore the load of magic binaries from /usr/local/bin And - # to use the go snap automatically. Note that we install go from the - # snap in a step below. Without this we get the GitHub-controlled latest - # version of go. - PATH: /usr/sbin:/usr/bin:/sbin:/bin + with: + runs-on: ubuntu-latest + distro: ${{ matrix.distro }} strategy: fail-fast: false @@ -520,52 +170,6 @@ jobs: - fedora:latest - opensuse/tumbleweed - runs-on: ubuntu-latest - container: ${{ matrix.distro }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - # needed for git commit history - fetch-depth: 0 - - - name: Install dependencies - run: | - # approximation to handle both typical foo:bar (tagged) and foo/bar - # (with implicit :latest) - distroname="$(echo "${{ matrix.distro }}" | tr : - | tr / -)" - case "${{ matrix.distro }}" in - fedora:*) - dnf install -y rpmdevtools - dnf install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec") - # TODO these are needed only by cmd/snap-seccomp unit tests, and - # should be added to BuildRequires - dnf install -y glibc-devel.i686 glibc-static.i686 - ;; - opensuse/*) - zypper --non-interactive install -y rpmdevtools rpm-build git - zypper --non-interactive install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec") - ;; - *) - echo "Unsupported distribution variant ${{ matrix.distro }}" - exit 1 - ;; - esac - - name: Set up test user - run: | - useradd -U -m test-user - chown -R test-user:test-user $PWD - - - name: Unit tests (Go) - run: | - su test-user sh -c "SKIP_DIRTY_CHECK=1 ./run-checks --unit" - - - name: Unit tests (C) - run: | - su test-user sh -c "./mkversion.sh 1337-git && cd ./cmd && ./autogen.sh && make -j && make distcheck" - - code-coverage: needs: [unit-tests, unit-tests-special] runs-on: ubuntu-20.04 @@ -598,9 +202,20 @@ jobs: verbose: true spread: + uses: ./.github/workflows/spread-tests.yaml needs: [unit-tests, snap-builds] - name: ${{ matrix.group }} - runs-on: [self-hosted, spread-enabled] + name: "spread ${{ matrix.group }}" + with: + # Github doesn't support passing sequences as parameters. + # Instead here we create a json array and pass it as a string. + # Then in the spread workflow it turns it into a sequence + # using the fromJSON expression. + runs-on: '["self-hosted", "spread-enabled"]' + group: ${{ matrix.group }} + backend: ${{ matrix.backend }} + systems: ${{ matrix.systems }} + tasks: ${{ matrix.tasks }} + rules: ${{ matrix.rules }} strategy: # FIXME: enable fail-fast mode once spread can cancel an executing job. # Disable fail-fast mode as it doesn't function with spread. It seems @@ -613,377 +228,120 @@ jobs: - group: amazon-linux backend: google-distro-1 systems: 'amazon-linux-2-64 amazon-linux-2023-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: arch-linux backend: google-distro-2 systems: 'arch-linux-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: centos backend: google-distro-2 systems: 'centos-9-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: debian-req backend: google-distro-1 systems: 'debian-11-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: debian-not-req backend: google-distro-1 systems: 'debian-12-64 debian-sid-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: fedora - backend: google-distro-1 - systems: 'fedora-38-64 fedora-39-64' - tests: 'tests/...' - rules: 'main' - - group: fedora-os backend: openstack - systems: 'fedora-40-64' - tests: 'tests/...' + systems: 'fedora-40-64 fedora-41-64' + tasks: 'tests/...' rules: 'main' - group: opensuse backend: google-distro-2 systems: 'opensuse-15.5-64 opensuse-15.6-64 opensuse-tumbleweed-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-trusty backend: google systems: 'ubuntu-14.04-64' - tests: 'tests/smoke/ tests/main/canonical-livepatch tests/main/canonical-livepatch-14.04' + tasks: 'tests/smoke/ tests/main/canonical-livepatch tests/main/canonical-livepatch-14.04' rules: 'trusty' - group: ubuntu-xenial-bionic backend: google systems: 'ubuntu-16.04-64 ubuntu-18.04-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-focal-jammy backend: google systems: 'ubuntu-20.04-64 ubuntu-22.04-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-noble backend: google systems: 'ubuntu-24.04-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-no-lts backend: google systems: '' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-daily backend: google systems: 'ubuntu-24.10-64' - tests: 'tests/...' - rules: 'main' - - group: ubuntu-core-16 - backend: google-core - systems: 'ubuntu-core-16-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-core-18 backend: google-core systems: 'ubuntu-core-18-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-core-20 backend: google-core systems: 'ubuntu-core-20-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-core-22 backend: google-core systems: 'ubuntu-core-22-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-core-24 backend: google-core systems: 'ubuntu-core-24-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - - group: ubuntu-arm + - group: ubuntu-arm64 backend: google-arm systems: 'ubuntu-20.04-arm-64 ubuntu-core-22-arm-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-secboot backend: google systems: 'ubuntu-secboot-20.04-64' - tests: 'tests/...' + tasks: 'tests/...' rules: 'main' - group: ubuntu-fips backend: google-pro systems: 'ubuntu-fips-22.04-64' - tests: 'tests/fips/...' + tasks: 'tests/fips/...' rules: 'main' - - group: nested-ubuntu-16.04 - backend: google-nested - systems: 'ubuntu-16.04-64' - tests: 'tests/nested/...' - rules: 'nested' - group: nested-ubuntu-18.04 backend: google-nested systems: 'ubuntu-18.04-64' - tests: 'tests/nested/...' + tasks: 'tests/nested/...' rules: 'nested' - group: nested-ubuntu-20.04 backend: google-nested systems: 'ubuntu-20.04-64' - tests: 'tests/nested/...' + tasks: 'tests/nested/...' rules: 'nested' - group: nested-ubuntu-22.04 backend: google-nested systems: 'ubuntu-22.04-64' - tests: 'tests/nested/...' + tasks: 'tests/nested/...' rules: 'nested' - group: nested-ubuntu-24.04 backend: google-nested systems: 'ubuntu-24.04-64' - tests: 'tests/nested/...' + tasks: 'tests/nested/...' rules: 'nested' - steps: - - name: Cleanup job workspace - id: cleanup-job-workspace - run: | - rm -rf "${{ github.workspace }}" - mkdir "${{ github.workspace }}" - - - name: Checkout code - uses: actions/checkout@v4 - with: - # spread uses tags as delta reference - fetch-depth: 0 - - - name: Get previous attempt - id: get-previous-attempt - run: | - echo "previous_attempt=$(( ${{ github.run_attempt }} - 1 ))" >> $GITHUB_OUTPUT - shell: bash - - - name: Get previous cache - uses: actions/cache@v4 - with: - path: "${{ github.workspace }}/.test-results" - key: "${{ github.job }}-results-${{ github.run_id }}-${{ matrix.group }}-${{ steps.get-previous-attempt.outputs.previous_attempt }}" - - - - name: Prepare test results env and vars - id: prepare-test-results-env - run: | - # Create test results directories and save vars - TEST_RESULTS_DIR="${{ github.workspace }}/.test-results" - echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV - - # Save the var with the failed tests file - echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/failed-tests" >> $GITHUB_ENV - - # Make sure the test results dirs are created - # This step has to be after the cache is restored - mkdir -p "$TEST_RESULTS_DIR" - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v41.0.0 - - - name: Save changes files - run: | - CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}" - echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV - echo "The changed files found are: $CHANGED_FILES" - - - name: Check failed tests to run - if: "!contains(github.event.pull_request.labels.*.name, 'Run all')" - run: | - # Save previous failed test results in FAILED_TESTS env var - FAILED_TESTS="" - if [ -f "$FAILED_TESTS_FILE" ]; then - echo "Failed tests file found" - FAILED_TESTS="$(cat $FAILED_TESTS_FILE)" - if [ -n "$FAILED_TESTS" ]; then - echo "Failed tests to run: $FAILED_TESTS" - echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV - fi - fi - - - name: Setup run tests variable - if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" - run: | - RUN_TESTS="" - SUGGESTED_TESTS="" - # Save previous failed test results in FAILED_TESTS env var - if [ -n "$FAILED_TESTS" ]; then - RUN_TESTS="$FAILED_TESTS" - else - for SYSTEM in ${{ matrix.systems }}; do - for TESTS in ${{ matrix.tests }}; do - RUN_TESTS="$RUN_TESTS ${{ matrix.backend }}:$SYSTEM:$TESTS" - done - CHANGES_PARAM="" - for CHANGE in $CHANGED_FILES; do - CHANGES_PARAM="$CHANGES_PARAM -c $CHANGE" - done - SUGGESTED_TESTS="$SUGGESTED_TESTS $(./tests/lib/external/snapd-testing-tools/utils/spread-filter -r ./tests/lib/spread/rules/${{ matrix.rules }}.yaml -p "${{ matrix.backend }}:$SYSTEM" $CHANGES_PARAM)" - done - fi - echo RUN_TESTS="$RUN_TESTS" >> $GITHUB_ENV - echo "Suggested tests by spread-filter tool" - echo "$SUGGESTED_TESTS" - - - name: Setup grafana parameters - if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" - run: | - # Configure parameters to filter logs (these logs are sent read by grafana agent) - CHANGE_ID="${{ github.event.number }}" - if [ -z "$CHANGE_ID" ]; then - CHANGE_ID="main" - fi - FILTERED_LOG_FILE="spread_${CHANGE_ID}_n${{ github.run_attempt }}.filtered.log" - # The log-filter tool is used to filter the spread logs to be stored - echo FILTER_PARAMS="-o $FILTERED_LOG_FILE -e Debug -e WARNING: -f Failed=NO_LINES -f Error=NO_LINES" >> $GITHUB_ENV - echo FILTERED_LOG_FILE="$FILTERED_LOG_FILE" >> $GITHUB_ENV - - # Add start line to filtered log - echo "GRAFANA START: pr ${CHANGE_ID} attempt ${{ github.run_attempt }} run ${{ github.run_id }} group ${{ matrix.group }}" > "$FILTERED_LOG_FILE" - - - name: Download built snap - uses: actions/download-artifact@v4 - if: "!endsWith(matrix.group, '-fips')" - with: - name: snap-files-default-test - # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap - pattern: snapd_1337.*.snap - path: "${{ github.workspace }}/built-snap" - - - name: Download built FIPS snap - uses: actions/download-artifact@v4 - # eg. ubuntu-fips - if: "endsWith(matrix.group, '-fips')" - with: - name: snap-files-FIPS-test - # eg. snapd_1337.2.65.1+git97.gd35b459-fips_amd64.snap - pattern: snapd_1337.*-fips_*.snap - path: "${{ github.workspace }}/built-snap" - - - name: Rename imported snap - run: | - for snap in built-snap/snapd_1337.*.snap; do - mv -v "${snap}" "${snap}.keep" - done - - - name: Run spread tests - if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread') && ( !startsWith(matrix.group, 'nested-') || contains(github.event.pull_request.labels.*.name, 'Run nested') )" - env: - SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }} - run: | - # Register a problem matcher to highlight spread failures - echo "::add-matcher::.github/spread-problem-matcher.json" - set -x - SPREAD=spread - if [[ "${{ matrix.group }}" =~ nested- ]]; then - export NESTED_BUILD_SNAPD_FROM_CURRENT=true - export NESTED_ENABLE_KVM=true - fi - - case "${{ matrix.systems }}" in - *-arm-*) - SPREAD_USE_PREBUILT_SNAPD_SNAP=false - ;; - *) - SPREAD_USE_PREBUILT_SNAPD_SNAP=true - ;; - esac - export SPREAD_USE_PREBUILT_SNAPD_SNAP - - if [[ "${{ matrix.systems }}" =~ amazon-linux-2023 ]]; then - # Amazon Linux 2023 has no xdelta, however we cannot disable - # xdelta on a per-target basis as it's used in the repack section - # of spread.yaml, which is shared by all targets, so all systems - # in this batch will not use delta for transferring project data - echo "Disabling xdelta support" - export NO_DELTA=1 - fi - - # Add openstack backend definition to spread.yaml - if [ "${{ matrix.backend }}" = openstack ]; then - ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml - fi - - # This coud be the case when either there are not systems for a group or - # the list of tests to run is empty - if [ -z "$RUN_TESTS" ]; then - echo "No tests to run, skiping..." - exit 0 - fi - - # Run spread tests - # "pipefail" ensures that a non-zero status from the spread is - # propagated; and we use a subshell as this option could trigger - # undesired changes elsewhere - echo "Running command: $SPREAD $RUN_TESTS" - (set -o pipefail; $SPREAD -no-debug-output -logs spread-logs $RUN_TESTS | PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-filter $FILTER_PARAMS | tee spread.log) - - - name: Uploading spread logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: spread-logs-${{ matrix.systems }} - path: "spread-logs/*.log" - if-no-files-found: ignore - - - name: Discard spread workers - if: always() - run: | - shopt -s nullglob; - for r in .spread-reuse.*.yaml; do - spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; - done - - - name: Report spread errors - if: always() - run: | - if [ -e spread.log ]; then - echo "Running spread log analyzer" - ACTIONS_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}" - PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json --cut 1 >/dev/null - while IFS= read -r line; do - if [ ! -z "$line" ]; then - echo "Adding failed test line to filtered log" - echo "GRAFANA FAILED: $line $ACTIONS_URL" | tee -a "$FILTERED_LOG_FILE" - fi - done <<< $(jq -r '.[] | select( .type == "info" ) | select( .info_type == "Error" ) | "\(.verb) \(.task)"' spread-results.json) - else - echo "No spread log found, skipping errors reporting" - fi - - - name: Analyze spread test results - if: always() - run: | - if [ -f spread.log ]; then - echo "Running spread log parser" - PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json - - # Add openstack backend definition to spread.yaml - if [ "${{ matrix.backend }}" = openstack ]; then - ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml - fi - - echo "Running spread log analyzer" - PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE" - - echo "List of failed tests saved" - cat "$FAILED_TESTS_FILE" - else - echo "No spread log found, saving empty list of failed tests" - touch "$FAILED_TESTS_FILE" - fi - - - name: Save spread test results to cache - if: always() - uses: actions/cache/save@v4 - with: - path: "${{ github.workspace }}/.test-results" - key: "${{ github.job }}-results-${{ github.run_id }}-${{ matrix.group }}-${{ github.run_attempt }}" diff --git a/.github/workflows/unit-tests-cross-distro.yaml b/.github/workflows/unit-tests-cross-distro.yaml new file mode 100644 index 00000000000..fcbd918a477 --- /dev/null +++ b/.github/workflows/unit-tests-cross-distro.yaml @@ -0,0 +1,65 @@ +on: + workflow_call: + inputs: + runs-on: + description: 'A tag to indicate which runner to use' + required: true + type: string + distro: + description: 'The name of the github container image to use to run the unit tests' + required: true + type: string + + +jobs: + unit-tests-cross-distro: + runs-on: ${{ inputs.runs-on }} + container: ${{ inputs.distro }} + env: + # Set PATH to ignore the load of magic binaries from /usr/local/bin And + # to use the go snap automatically. Note that we install go from the + # snap in a step below. Without this we get the GitHub-controlled latest + # version of go. + PATH: /usr/sbin:/usr/bin:/sbin:/bin + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # needed for git commit history + fetch-depth: 0 + + - name: Install dependencies + run: | + # approximation to handle both typical foo:bar (tagged) and foo/bar + # (with implicit :latest) + distroname="$(echo "${{ inputs.distro }}" | tr : - | tr / -)" + case "${{ inputs.distro }}" in + fedora:*) + dnf install -y rpmdevtools + dnf install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec") + # TODO these are needed only by cmd/snap-seccomp unit tests, and + # should be added to BuildRequires + dnf install -y glibc-devel.i686 glibc-static.i686 + ;; + opensuse/*) + zypper --non-interactive install -y rpmdevtools rpm-build git + zypper --non-interactive install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec") + ;; + *) + echo "Unsupported distribution variant ${{ inputs.distro }}" + exit 1 + ;; + esac + - name: Set up test user + run: | + useradd -U -m test-user + chown -R test-user:test-user $PWD + + - name: Unit tests (Go) + run: | + su test-user sh -c "SKIP_DIRTY_CHECK=1 ./run-checks --unit" + + - name: Unit tests (C) + run: | + su test-user sh -c "./mkversion.sh 1337-git && cd ./cmd && ./autogen.sh && make -j && make distcheck" diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml new file mode 100644 index 00000000000..21c54b2fa10 --- /dev/null +++ b/.github/workflows/unit-tests.yaml @@ -0,0 +1,133 @@ +on: + workflow_call: + inputs: + runs-on: + description: 'A tag to indicate which runner to use' + required: true + type: string + gochannel: + description: 'The snap store channel to use to install the go snap' + required: true + type: string + unit-scenario: + description: 'The name of the scenario being tested: {normal, snapd-debug, withbootassetstesting, nosecboot, faultinject, race, snapdusergo}' + required: true + type: string + +jobs: + unit-tests: + runs-on: ${{ inputs.runs-on }} + env: + GOPATH: ${{ github.workspace }} + # Set PATH to ignore the load of magic binaries from /usr/local/bin And + # to use the go snap automatically. Note that we install go from the + # snap in a step below. Without this we get the GitHub-controlled latest + # version of go. + PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin + GOROOT: "" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # needed for git commit history + fetch-depth: 0 + # NOTE: checkout the code in a fixed location, even for forks, as this + # is relevant for go's import system. + path: ./src/github.com/snapcore/snapd + + # Fetch base ref, needed for golangci-lint + - name: Fetching base ref ${{ github.base_ref }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd + git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} + + - name: Download and install Debian dependencies + # Github does not allow variables in "uses"; this has to be a hard-coded path + uses: ./src/github.com/snapcore/snapd/.github/workflows/actions/download-install-debian-deps + with: + snapd-src-dir: "${{ github.workspace }}/src/github.com/snapcore/snapd" + + # golang latest ensures things work on the edge + - name: Install the go snap + run: | + sudo snap install --classic --channel=${{ inputs.gochannel }} go + + - name: Get deps + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh + + - name: Build C + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ + ./autogen.sh + make -j$(nproc) + + - name: Build Go + run: | + go build github.com/snapcore/snapd/... + + - name: Test C + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck + + - name: Reset code coverage data + run: | + rm -rf ${{ github.workspace }}/.coverage/ + COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ inputs.unit-scenario}}.cov" + echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV + + - name: Test Go + if: ${{ inputs.unit-scenario == 'normal' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + ./run-checks --unit + + - name: Test Go (SNAPD_DEBUG=1) + if: ${{ inputs.unit-scenario == 'snapd_debug' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 SNAPD_DEBUG=1 ./run-checks --unit + + - name: Test Go (withbootassetstesting) + if: ${{ inputs.unit-scenario == 'withbootassetstesting' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=withbootassetstesting ./run-checks --unit + + - name: Test Go (nosecboot) + if: ${{ inputs.unit-scenario == 'nosecboot' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + echo "Dropping github.com/snapcore/secboot" + # use govendor remove so that a subsequent govendor sync does not + # install secboot again + # ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot + # ${{ github.workspace }}/bin/govendor remove +unused + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit + + - name: Test Go (faultinject) + if: ${{ inputs.unit-scenario == 'faultinject' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit + + - name: Test Go (-race) + if: ${{ inputs.unit-scenario == 'race' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 GO_TEST_RACE=1 SKIP_COVERAGE=1 ./run-checks --unit + + - name: Test Go (snapdusergo) + if: ${{ inputs.unit-scenario == 'snapdusergo' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=snapdusergo ./run-checks --unit + + - name: Upload the coverage results + if: ${{ inputs.gochannel != 'latest/stable' && inputs.unit-scenario != 'race' }} + uses: actions/upload-artifact@v4 + with: + include-hidden-files: true + name: "coverage-files-${{ inputs.unit-scenario }}" + path: "${{ github.workspace }}/coverage/coverage*.cov" diff --git a/.golangci.yml b/.golangci.yml index 71a0cecb78e..041a726f685 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -129,6 +129,14 @@ linters-settings: deny: - pkg: "os/user" desc: "Please use osutil/user instead. See https://github.com/canonical/snapd/pull/13776" + testtime: + files: + - "!$test" + deny: + - pkg: "github.com/snapcore/snapd/testtime" + desc: "Cannot use testtime outside of test code" + - pkg: "github.com/canonical/snapd/testtime" + desc: "Cannot use testtime outside of test code" misspell: # Correct spellings using locale preferences for US or UK. @@ -189,6 +197,7 @@ linters: # disabling until https://github.com/daixiang0/gci/issues/54 is fixed # - gci - testpackage + - depguard # disable everything else disable-all: true diff --git a/client/snap_op.go b/client/snap_op.go index e5d274e5923..d3369f7d32d 100644 --- a/client/snap_op.go +++ b/client/snap_op.go @@ -162,12 +162,12 @@ func (client *Client) RemoveMany(names []string, components map[string][]string, // Refresh refreshes the snap with the given name (switching it to track // the given channel if given). -func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { - return client.doSnapAction("refresh", name, nil, options) +func (client *Client) Refresh(name string, components []string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("refresh", name, components, options) } -func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { - return client.doMultiSnapAction("refresh", names, nil, options) +func (client *Client) RefreshMany(names []string, components map[string][]string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("refresh", names, components, options) } func (client *Client) HoldRefreshes(name string, options *SnapOptions) (changeID string, err error) { diff --git a/client/snap_op_test.go b/client/snap_op_test.go index e638cade2eb..5a8f604c27f 100644 --- a/client/snap_op_test.go +++ b/client/snap_op_test.go @@ -48,7 +48,12 @@ var ops = []struct { }, action: "install", }, - {(*client.Client).Refresh, "refresh"}, + { + op: func(c *client.Client, name string, options *client.SnapOptions) (string, error) { + return c.Refresh(name, nil, options) + }, + action: "refresh", + }, { op: func(c *client.Client, name string, options *client.SnapOptions) (string, error) { return c.Remove(name, nil, options) @@ -67,7 +72,12 @@ var multiOps = []struct { op func(*client.Client, []string, *client.SnapOptions) (string, error) action string }{ - {(*client.Client).RefreshMany, "refresh"}, + { + op: func(c *client.Client, names []string, options *client.SnapOptions) (string, error) { + return c.RefreshMany(names, nil, options) + }, + action: "refresh", + }, { op: func(c *client.Client, names []string, options *client.SnapOptions) (string, error) { return c.InstallMany(names, nil, options) @@ -874,7 +884,7 @@ func (cs *clientSuite) TestClientRefreshWithValidationSets(c *check.C) { }` sets := []string{"foo/bar=2", "foo/baz"} - chgID, err := cs.cli.RefreshMany(nil, &client.SnapOptions{ + chgID, err := cs.cli.RefreshMany(nil, nil, &client.SnapOptions{ ValidationSets: sets, }) c.Assert(err, check.IsNil) @@ -957,6 +967,10 @@ func (cs *clientSuite) TestClientOpInstallWithComponents(c *check.C) { cs.testClientOpWithComponents(c, cs.cli.Install) } +func (cs *clientSuite) TestClientOpRefreshWithComponents(c *check.C) { + cs.testClientOpWithComponents(c, cs.cli.Refresh) +} + func (cs *clientSuite) TestClientOpRemoveWithComponents(c *check.C) { cs.testClientOpWithComponents(c, cs.cli.Remove) } @@ -991,6 +1005,10 @@ func (cs *clientSuite) TestClientOpInstallManyWithComponents(c *check.C) { cs.testClientOpManyWithComponents(c, cs.cli.InstallMany) } +func (cs *clientSuite) TestClientOpRefreshManyWithComponents(c *check.C) { + cs.testClientOpManyWithComponents(c, cs.cli.RefreshMany) +} + func (cs *clientSuite) TestClientOpRemoveManyWithComponents(c *check.C) { cs.testClientOpManyWithComponents(c, cs.cli.RemoveMany) } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index 7f12943b699..edadde3fb86 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -1361,6 +1361,9 @@ func (m *recoverModeStateMachine) mountSave() (stateFunc, error) { // TODO: should we fsck ubuntu-save ? mountOpts := &systemdMountOptions{ Private: true, + NoDev: true, + NoSuid: true, + NoExec: true, } mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, mountOpts) if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { @@ -2012,7 +2015,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error { isRunMode := true // 2. mount ubuntu-seed (optional for classic) - systemdOpts := &systemdMountOptions{ + seedMountOpts := &systemdMountOptions{ NeedsFsck: true, Private: true, } @@ -2038,7 +2041,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error { // need it to be writable for i.e. transitioning to recover mode if partUUID != "" { if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), - boot.InitramfsUbuntuSeedDir, systemdOpts); err != nil { + boot.InitramfsUbuntuSeedDir, seedMountOpts); err != nil { return err } } @@ -2089,7 +2092,14 @@ func generateMountsModeRun(mst *initramfsMountsState) error { rootfsDir := boot.InitramfsWritableDir(model, isRunMode) // 3.2. mount ubuntu-save (if present) - haveSave, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, systemdOpts) + saveMountOpts := &systemdMountOptions{ + NeedsFsck: true, + Private: true, + NoDev: true, + NoSuid: true, + NoExec: true, + } + haveSave, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, saveMountOpts) if err != nil { return err } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index 0d6b6ac48d5..91fa6daef6b 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -97,6 +97,19 @@ var ( NoSuid: true, Private: true, } + needsNoSuidNoDevNoExecMountOpts = &main.SystemdMountOptions{ + Private: true, + NoSuid: true, + NoExec: true, + NoDev: true, + } + needsFsckAndNoSuidNoDevNoExecMountOpts = &main.SystemdMountOptions{ + Private: true, + NeedsFsck: true, + NoSuid: true, + NoExec: true, + NoDev: true, + } needsFsckNoPrivateDiskMountOpts = &main.SystemdMountOptions{ NeedsFsck: true, } @@ -117,9 +130,6 @@ var ( ReadOnly: true, Private: true, } - mountOpts = &main.SystemdMountOptions{ - Private: true, - } bindOpts = &main.SystemdMountOptions{ Bind: true, } @@ -615,6 +625,11 @@ func (s *baseInitramfsMountsSuite) ubuntuPartUUIDMount(partuuid string, mode str } case strings.Contains(partuuid, "ubuntu-save"): mnt.where = boot.InitramfsUbuntuSaveDir + if mode == "run" { + mnt.opts = needsFsckAndNoSuidNoDevNoExecMountOpts + } else { + mnt.opts = needsNoSuidNoDevNoExecMountOpts + } } return mnt @@ -1879,7 +1894,7 @@ Wants=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", - "--options=private", + "--options=nodev,nosuid,noexec,private", "--property=Before=initrd-fs.target", }, }) @@ -2159,7 +2174,7 @@ Wants=%[1]s "--no-pager", "--no-ask-password", "--fsck=yes", - "--options=private", + "--options=nodev,nosuid,noexec,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -2331,7 +2346,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - needsFsckDiskMountOpts, + needsFsckAndNoSuidNoDevNoExecMountOpts, nil, }, s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), @@ -3373,7 +3388,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappy(c *C) { { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -3462,7 +3477,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeTimeMovesForwardHap { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -3546,7 +3561,7 @@ defaults: { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -3631,7 +3646,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyBootedKernelPa { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -3751,7 +3766,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -3910,7 +3925,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDa { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -4088,7 +4103,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedSa { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -4253,7 +4268,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -4416,7 +4431,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -4584,7 +4599,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDa { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -4774,7 +4789,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeDegradedAbsentDataU { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -5234,7 +5249,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeUnencryptedDataUnen { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -5374,7 +5389,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -5755,7 +5770,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedMismatched { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -5967,7 +5982,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFS { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -6033,7 +6048,7 @@ func (s *initramfsMountsSuite) testInitramfsMountsInstallRecoverModeMeasure(c *C systemdMount{ "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }) @@ -6157,7 +6172,7 @@ func (s *baseInitramfsMountsSuite) runInitramfsMountsUnencryptedTryRecovery(c *C { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -6802,7 +6817,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeHappyEncrypted { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -6911,7 +6926,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeHappyUnencrypt { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) @@ -7203,7 +7218,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeUnhappyMountEn { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, fmt.Errorf("mount failed"), }, }, nil) @@ -7598,7 +7613,7 @@ func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeEncryptedDataHap { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - needsFsckDiskMountOpts, + needsFsckAndNoSuidNoDevNoExecMountOpts, nil, }, s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), @@ -8536,7 +8551,7 @@ func (s *initramfsClassicMountsSuite) TestInitramfsMountsRecoveryModeHybridSyste { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - mountOpts, + needsNoSuidNoDevNoExecMountOpts, nil, }, }, nil) diff --git a/cmd/snap-seccomp/syscalls/syscalls.go b/cmd/snap-seccomp/syscalls/syscalls.go index 7e60e3b6b2f..a5e16167534 100644 --- a/cmd/snap-seccomp/syscalls/syscalls.go +++ b/cmd/snap-seccomp/syscalls/syscalls.go @@ -46,6 +46,7 @@ var SeccompSyscalls = []string{ "brk", "cachectl", "cacheflush", + "cachestat", "capget", "capset", "chdir", @@ -99,6 +100,7 @@ var SeccompSyscalls = []string{ "fchdir", "fchmod", "fchmodat", + "fchmodat2", "fchown", "fchown32", "fchownat", @@ -126,8 +128,11 @@ var SeccompSyscalls = []string{ "ftruncate", "ftruncate64", "futex", + "futex_requeue", "futex_time64", + "futex_wait", "futex_waitv", + "futex_wake", "futimesat", "get_kernel_syms", "get_mempolicy", @@ -207,6 +212,7 @@ var SeccompSyscalls = []string{ "link", "linkat", "listen", + "listmount", "listxattr", "llistxattr", "lock", @@ -214,9 +220,13 @@ var SeccompSyscalls = []string{ "lremovexattr", "lseek", "lsetxattr", + "lsm_get_self_attr", + "lsm_list_modules", + "lsm_set_self_attr", "lstat", "lstat64", "madvise", + "map_shadow_stack", "mbind", "membarrier", "memfd_create", @@ -248,6 +258,7 @@ var SeccompSyscalls = []string{ "mq_timedsend_time64", "mq_unlink", "mremap", + "mseal", "msgctl", "msgget", "msgrcv", @@ -331,6 +342,7 @@ var SeccompSyscalls = []string{ "request_key", "restart_syscall", "riscv_flush_icache", + "riscv_hwprobe", "rmdir", "rseq", "rt_sigaction", @@ -436,6 +448,7 @@ var SeccompSyscalls = []string{ "stat64", "statfs", "statfs64", + "statmount", "statx", "stime", "stty", @@ -486,6 +499,7 @@ var SeccompSyscalls = []string{ "unlink", "unlinkat", "unshare", + "uretprobe", "uselib", "userfaultfd", "usr26", diff --git a/cmd/snap/cmd_debug_api.go b/cmd/snap/cmd_debug_api.go index b2fb7016634..01dcd093e4b 100644 --- a/cmd/snap/cmd_debug_api.go +++ b/cmd/snap/cmd_debug_api.go @@ -32,7 +32,7 @@ import ( "github.com/snapcore/snapd/logger" ) -var longDebugAPIHelp = ` +const longDebugAPIHelp = ` Execute a raw query to snapd API. Complex input can be read from stdin, while output is printed to stdout. See examples below: diff --git a/cmd/snap/cmd_debug_seeding.go b/cmd/snap/cmd_debug_seeding.go index 81b72191625..37a59f01e7b 100644 --- a/cmd/snap/cmd_debug_seeding.go +++ b/cmd/snap/cmd_debug_seeding.go @@ -36,13 +36,12 @@ type cmdSeeding struct { } func init() { - cmd := addDebugCommand("seeding", - "(internal) obtain seeding and preseeding details", - "(internal) obtain seeding and preseeding details", + addDebugCommand("seeding", + "Obtain seeding and preseeding details", + "Obtain seeding and preseeding details", func() flags.Commander { return &cmdSeeding{} }, nil, nil) - cmd.hidden = true } func (x *cmdSeeding) Execute(args []string) error { diff --git a/cmd/snap/cmd_debug_validate_seed.go b/cmd/snap/cmd_debug_validate_seed.go index 68e8fa555cb..b60b69d7236 100644 --- a/cmd/snap/cmd_debug_validate_seed.go +++ b/cmd/snap/cmd_debug_validate_seed.go @@ -33,14 +33,18 @@ type cmdValidateSeed struct { } `positional-args:"true" required:"true"` } +const longDebugValidateSeedHelp = ` +Validate correctness of snap seed located in the directory +containing seed.yaml file. +` + func init() { - cmd := addDebugCommand("validate-seed", - "(internal) validate seed.yaml", - "(internal) validate seed.yaml", + addDebugCommand("validate-seed", + "Validate snap seed", + longDebugValidateSeedHelp, func() flags.Commander { return &cmdValidateSeed{} }, nil, nil) - cmd.hidden = true } func (x *cmdValidateSeed) Execute(args []string) error { diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 4418e53fa24..cc096a67e8c 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -501,7 +501,7 @@ func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) { func checkSnapRunInhibitionConflict(app *snap.AppInfo) error { // Remove hint check takes precedence because we want to exit early snapName := app.Snap.InstanceName() - hint, _, err := runinhibit.IsLocked(snapName) + hint, _, err := runinhibit.IsLocked(snapName, nil) if err != nil { return err } diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 5df4a2cc5c0..ee0b5eeb22d 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -281,7 +281,7 @@ func (s *RunSuite) TestSnapRunAppRunsChecksRefreshInhibitionLock(c *check.C) { defer restorer() inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil) c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil) c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) @@ -321,7 +321,7 @@ func (s *RunSuite) TestSnapRunAppRunsChecksRefreshInhibitionLock(c *check.C) { func (s *RunSuite) testSnapRunAppRunsChecksRemoveInhibitionLock(c *check.C, svc bool) { inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo, nil), check.IsNil) cmd := "snapname.app" if svc { @@ -355,7 +355,7 @@ func (s *RunSuite) TestSnapRunAppRefreshAppAwarenessUnsetSkipsInhibitionLockWait // mark snap as inhibited inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil) c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil) // unset refresh-app-awareness flag c.Assert(os.RemoveAll(features.RefreshAppAwareness.ControlFile()), check.IsNil) @@ -379,7 +379,7 @@ func (s *RunSuite) TestSnapRunAppNewRevisionAfterInhibition(c *check.C) { // mark snap as inhibited inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil) // unset refresh-app-awareness flag c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil) c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) @@ -498,7 +498,7 @@ func (s *RunSuite) TestSnapRunHookNoRuninhibit(c *check.C) { defer restore() inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(42)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil) c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil) c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) @@ -532,7 +532,7 @@ func (s *RunSuite) TestSnapRunAppRuninhibitSkipsServices(c *check.C) { defer restorer() inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil) c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil) c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) @@ -718,7 +718,7 @@ func (s *RunSuite) testSnapRunAppRetryNoInhibitHintFileThenOngoingRefresh(c *che // mock snap inhibited to trigger race condition detection // i.e. we started without a hint lock file (snap on first install) // then a refresh started which created the hint lock file. - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil) // nil FileLock means no inhibit file exists return nil, nil @@ -847,7 +847,7 @@ func (s *RunSuite) testSnapRunAppRetryNoInhibitHintFileThenOngoingRemove(c *chec // mock snap inhibited to trigger race condition detection // i.e. we started without a hint lock file (snap on first install) // then a remove started which created the hint lock file. - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil) // nil FileLock means no inhibit file exists return nil, nil @@ -955,7 +955,7 @@ func (s *RunSuite) TestSnapRunAppRetryNoInhibitHintFileThenOngoingRefreshMissing // and we have an ongoing refresh which removed current symlink. c.Assert(err, testutil.ErrorIs, snaprun.ErrSnapRefreshConflict) // and created the inhibition hint lock file. - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil) return nil, err } else { var err error @@ -1054,7 +1054,7 @@ func (s *RunSuite) TestSnapRunAppMaxRetry(c *check.C) { // mock snap inhibited to trigger race condition detection // i.e. we started without a hint lock file (snap on first install) // then a refresh started which created the hint lock file. - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil) // nil FileLock means no inhibit file exists return nil, nil diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index 4a59c5ccb14..9cf67b46427 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -929,7 +929,13 @@ type cmdRefresh struct { } func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { - changeID, err := x.client.RefreshMany(snaps, opts) + const forInstall = true + names, compsBySnap, err := snapInstancesAndComponentsFromNames(snaps, forInstall) + if err != nil { + return err + } + + changeID, err := x.client.RefreshMany(names, compsBySnap, opts) if err != nil { return err } @@ -958,9 +964,14 @@ func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error } func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { - changeID, err := x.client.Refresh(name, opts) + snapName, comps := snap.SplitSnapInstanceAndComponents(name) + if name == "" { + return errors.New(i18n.G("no snap for the component(s) was specified")) + } + + changeID, err := x.client.Refresh(snapName, comps, opts) if err != nil { - msg, err := errorToCmdMessage(name, "refresh", err, opts) + msg, err := errorToCmdMessage(snapName, "refresh", err, opts) if err != nil { return err } @@ -978,7 +989,7 @@ func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { // TODO: this doesn't really tell about all the things you // could set while refreshing (something switch does) - return showDone(x.client, chg, &changedSnapsData{names: []string{name}, comps: nil}, "refresh", opts, x.getEscapes()) + return showDone(x.client, chg, &changedSnapsData{names: []string{snapName}, comps: nil}, "refresh", opts, x.getEscapes()) } func parseSysinfoTime(s string) time.Time { diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go index 9cba3ff9625..54ac7399531 100644 --- a/cmd/snap/cmd_snap_op_test.go +++ b/cmd/snap/cmd_snap_op_test.go @@ -144,6 +144,83 @@ func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) { t.n++ } +func multiHandler(c *check.C, snaps []string, snapToComps map[string][]string, checker func(*http.Request), calls int) http.HandlerFunc { + var n int + return func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + checker(r) + method := "POST" + c.Check(r.Method, check.Equals, method) + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + var data struct { + SnapNames []string `json:"snap-names,omitempty"` + Components map[string][]string `json:"components,omitempty"` + } + + data.Components = snapToComps + data.SnapNames = snaps + + encoded, err := json.Marshal(data) + c.Assert(err, check.IsNil) + + fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": %s}}\n`, string(encoded)) + case 3: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + + var results []interface{} + for _, snap := range snaps { + result := map[string]interface{}{ + "name": snap, + "status": "active", + "version": "1.0", + "developer": "bar", + "publisher": map[string]interface{}{ + "id": "bar-id", + "username": "bar", + "display-name": "Bar", + "validation": "unproven", + }, + "revision": 42, + "channel": "latest/stable", + "tracking-channel": "latest/stable", + "confinement": "strict", + } + + comps := make([]map[string]string, 0, len(snapToComps[snap])) + for _, comp := range snapToComps[snap] { + comps = append(comps, map[string]string{"name": comp, "version": "3.2"}) + } + + if len(comps) > 0 { + result["components"] = comps + } + + results = append(results, result) + } + + err := json.NewEncoder(w).Encode(map[string]interface{}{ + "type": "sync", + "result": results, + }) + c.Assert(err, check.IsNil) + default: + c.Fatalf("expected to get %d requests, now on %d", calls, n+1) + } + + n++ + } +} + type SnapOpSuite struct { BaseSnapSuite @@ -2260,7 +2337,51 @@ func (s *SnapOpSuite) TestRefreshOne(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo"}) c.Assert(err, check.IsNil) c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneAdditionalComponents(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "transaction": string(client.TransactionPerSnap), + "components": []interface{}{"comp1", "comp2"}, + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo+comp1+comp2"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + +func (s *SnapOpSuite) TestRefreshManyAdditionalComponents(c *check.C) { + checker := func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "transaction": string(client.TransactionPerSnap), + "snaps": []interface{}{"foo", "bar"}, + "components": map[string]interface{}{ + "foo": []interface{}{"comp1", "comp2"}, + "bar": []interface{}{"comp3", "comp4"}, + }, + }) + } + s.RedirectClientToTestServer(multiHandler(c, []string{"foo", "bar"}, map[string][]string{ + "foo": {"comp1", "comp2"}, + "bar": {"comp3", "comp4"}, + }, checker, 3)) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo+comp1+comp2", "bar+comp3+comp4"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*bar 1.0 from Bar refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*component comp1 3.2 for foo 1.0 refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*component comp2 3.2 for foo 1.0 refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*component comp3 3.2 for bar 1.0 refreshed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*component comp4 3.2 for bar 1.0 refreshed`) } func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) { diff --git a/cmd/snap/cmd_validate.go b/cmd/snap/cmd_validate.go index d07f29345d2..f18b4467a78 100644 --- a/cmd/snap/cmd_validate.go +++ b/cmd/snap/cmd_validate.go @@ -126,7 +126,7 @@ func (cmd *cmdValidate) Execute(args []string) error { } if cmd.Refresh { - changeID, err := cmd.client.RefreshMany(nil, &client.SnapOptions{ + changeID, err := cmd.client.RefreshMany(nil, nil, &client.SnapOptions{ ValidationSets: []string{cmd.Positional.ValidationSet}, }) if err != nil { diff --git a/cmd/snap/inhibit_test.go b/cmd/snap/inhibit_test.go index ce1b6f76759..dbf7ca9d361 100644 --- a/cmd/snap/inhibit_test.go +++ b/cmd/snap/inhibit_test.go @@ -71,7 +71,7 @@ func (s *RunSuite) TestWaitWhileInhibitedRunThrough(c *C) { c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil) var waitWhileInhibitedCalled int restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) { @@ -130,7 +130,7 @@ func (s *RunSuite) TestWaitWhileInhibitedErrorOnStartNotification(c *C) { c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil) var startCalled, finishCalled int inhibitionFlow := fakeInhibitionFlow{ @@ -166,7 +166,7 @@ func (s *RunSuite) TestWaitWhileInhibitedErrorOnFinishNotification(c *C) { c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil) var waitWhileInhibitedCalled int restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) { @@ -226,7 +226,7 @@ func (s *RunSuite) TestWaitWhileInhibitedContextCancellationOnError(c *C) { c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil) originalCtx, cancel := context.WithCancel(context.Background()) inhibitionFlow := fakeInhibitionFlow{ @@ -259,7 +259,7 @@ func (s *RunSuite) TestWaitWhileInhibitedGateRefreshNoNotification(c *C) { c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedGateRefresh, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedGateRefresh, inhibitInfo, nil), IsNil) var called int restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) { @@ -310,7 +310,7 @@ func (s *RunSuite) testWaitWhileInhibitedRemoveInhibition(c *C, svc bool) { snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{Revision: snap.R(11)}) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)} - c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo, nil), IsNil) inhibitionFlow := fakeInhibitionFlow{ start: func(ctx context.Context) error { diff --git a/cmd/snaplock/lock.go b/cmd/snaplock/lock.go index d6ac6471414..f059db5be54 100644 --- a/cmd/snaplock/lock.go +++ b/cmd/snaplock/lock.go @@ -36,6 +36,11 @@ func lockFileName(snapName string) string { } // OpenLock creates and opens a lock file associated with a particular snap. +// +// NOTE: The snap lock is only accessible to root and is only intended to +// synchronize operations between snapd and snap-confine (and snap-update-ns +// in some cases). Any process holding the snap lock must not do any +// interactions with snapd to avoid deadlocks due to locked snap state. func OpenLock(snapName string) (*osutil.FileLock, error) { if err := os.MkdirAll(dirs.SnapRunLockDir, 0700); err != nil { return nil, fmt.Errorf("cannot create lock directory: %s", err) diff --git a/cmd/snaplock/runinhibit/inhibit.go b/cmd/snaplock/runinhibit/inhibit.go index 09223f46804..3bec5a86d7f 100644 --- a/cmd/snaplock/runinhibit/inhibit.go +++ b/cmd/snaplock/runinhibit/inhibit.go @@ -124,6 +124,12 @@ func removeInhibitInfoFiles(snapName string) error { return nil } +// Unlocker functions are passed from code using runinhibit to indicate that global +// state should be unlocked during file lock operations to avoid having deadlocks where +// both inhibition hint file and state are locked and waiting for each other. Unlocker +// being nil indicates not to do this. +type Unlocker func() (relock func()) + // LockWithHint sets a persistent "snap run" inhibition lock, for the given snap, with a given hint // and saves given info that will be needed by "snap run" during inhibition (e.g. snap revision). // @@ -132,7 +138,17 @@ func removeInhibitInfoFiles(snapName string) error { // start and will block, presenting a user interface if possible. Also // info.Previous corresponding to the snap revision that was installed must be // provided and cannot be unset. -func LockWithHint(snapName string, hint Hint, info InhibitInfo) error { +// +// If unlocker is passed it indicates that the global state needs to be unlocked +// before taking the inhibition hint file lock. It is the responsibility of the +// caller to make sure state is locked if a non-nil unlocker is passed. +func LockWithHint(snapName string, hint Hint, info InhibitInfo, unlocker Unlocker) error { + if unlocker != nil { + // unlock/relock global state + relock := unlocker() + defer relock() + } + if err := hint.validate(); err != nil { return err } @@ -179,7 +195,17 @@ func LockWithHint(snapName string, hint Hint, info InhibitInfo) error { // Unlock truncates the run inhibition lock, for the given snap. // // An empty inhibition lock means uninhibited "snap run". -func Unlock(snapName string) error { +// +// If unlocker is passed it indicates that the global state needs to be unlocked +// before taking the inhibition hint file lock. It is the responsibility of the +// caller to make sure state is locked if a non-nil unlocker is passed. +func Unlock(snapName string, unlocker Unlocker) error { + if unlocker != nil { + // unlock/relock global state + relock := unlocker() + defer relock() + } + flock, err := openHintFileLock(snapName) if os.IsNotExist(err) { return nil @@ -214,7 +240,17 @@ func Unlock(snapName string) error { // // It returns the current, non-empty hint if inhibition is in place. Otherwise // it returns an empty hint. -func IsLocked(snapName string) (Hint, InhibitInfo, error) { +// +// If unlocker is passed it indicates that the global state needs to be unlocked +// before taking the inhibition hint file lock. It is the responsibility of the +// caller to make sure state is locked if a non-nil unlocker is passed. +func IsLocked(snapName string, unlocker Unlocker) (Hint, InhibitInfo, error) { + if unlocker != nil { + // unlock/relock global state + relock := unlocker() + defer relock() + } + hintFlock, err := osutil.OpenExistingLockForReading(HintFile(snapName)) if os.IsNotExist(err) { return "", InhibitInfo{}, nil @@ -254,7 +290,17 @@ func IsLocked(snapName string) (Hint, InhibitInfo, error) { // it. // // The function does not fail if the inhibition lock does not exist. -func RemoveLockFile(snapName string) error { +// +// If unlocker is passed it indicates that the global state needs to be unlocked +// before taking the inhibition hint file lock. It is the responsibility of the +// caller to make sure state is locked if a non-nil unlocker is passed. +func RemoveLockFile(snapName string, unlocker Unlocker) error { + if unlocker != nil { + // unlock/relock global state + relock := unlocker() + defer relock() + } + hintFlock, err := osutil.OpenExistingLockForReading(HintFile(snapName)) if os.IsNotExist(err) { return nil diff --git a/cmd/snaplock/runinhibit/inhibit_test.go b/cmd/snaplock/runinhibit/inhibit_test.go index 841b191d904..7a51a808b92 100644 --- a/cmd/snaplock/runinhibit/inhibit_test.go +++ b/cmd/snaplock/runinhibit/inhibit_test.go @@ -61,7 +61,7 @@ func (s *runInhibitSuite) TestLockWithEmptyHint(c *C) { _, err := os.Stat(runinhibit.InhibitDir) c.Assert(os.IsNotExist(err), Equals, true) - err = runinhibit.LockWithHint("pkg", runinhibit.HintNotInhibited, s.inhibitInfo) + err = runinhibit.LockWithHint("pkg", runinhibit.HintNotInhibited, s.inhibitInfo, nil) c.Assert(err, ErrorMatches, "lock hint cannot be empty") _, err = os.Stat(runinhibit.InhibitDir) @@ -73,7 +73,7 @@ func (s *runInhibitSuite) TestLockWithUnsetRevision(c *C) { _, err := os.Stat(runinhibit.InhibitDir) c.Assert(os.IsNotExist(err), Equals, true) - err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R(0)}) + err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R(0)}, nil) c.Assert(err, ErrorMatches, "snap revision cannot be unset") _, err = os.Stat(runinhibit.InhibitDir) @@ -95,9 +95,16 @@ func (s *runInhibitSuite) TestLockWithHint(c *C) { _, err := os.Stat(runinhibit.InhibitDir) c.Assert(os.IsNotExist(err), Equals, true) + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } expectedInfo := runinhibit.InhibitInfo{Previous: snap.R(42)} - err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo) + err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo, fakeUnlocker) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) fi, err := os.Stat(runinhibit.InhibitDir) c.Assert(err, IsNil) @@ -112,19 +119,19 @@ func (s *runInhibitSuite) TestLockWithHint(c *C) { // The lock can be re-acquired to present a different hint. func (s *runInhibitSuite) TestLockLocked(c *C) { expectedInfo := runinhibit.InhibitInfo{Previous: snap.R(42)} - err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo) + err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo, nil) c.Assert(err, IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "refresh") testInhibitInfo(c, "pkg", "refresh", expectedInfo) expectedInfo = runinhibit.InhibitInfo{Previous: snap.R(43)} - err = runinhibit.LockWithHint("pkg", runinhibit.Hint("just-testing"), expectedInfo) + err = runinhibit.LockWithHint("pkg", runinhibit.Hint("just-testing"), expectedInfo, nil) c.Assert(err, IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "just-testing") testInhibitInfo(c, "pkg", "just-testing", expectedInfo) expectedInfo = runinhibit.InhibitInfo{Previous: snap.R(44)} - err = runinhibit.LockWithHint("pkg", runinhibit.Hint("short"), expectedInfo) + err = runinhibit.LockWithHint("pkg", runinhibit.Hint("short"), expectedInfo, nil) c.Assert(err, IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "short") testInhibitInfo(c, "pkg", "short", expectedInfo) @@ -132,18 +139,25 @@ func (s *runInhibitSuite) TestLockLocked(c *C) { // Unlocking an unlocked lock doesn't break anything. func (s *runInhibitSuite) TestUnlockUnlocked(c *C) { - err := runinhibit.Unlock("pkg") + err := runinhibit.Unlock("pkg", nil) c.Assert(err, IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileAbsent) } // Unlocking an locked lock truncates the hint and removes inhibit info file. func (s *runInhibitSuite) TestUnlockLocked(c *C) { - err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo) + err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil) c.Assert(err, IsNil) - err = runinhibit.Unlock("pkg") + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + err = runinhibit.Unlock("pkg", fakeUnlocker) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "") c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FileAbsent) @@ -154,26 +168,36 @@ func (s *runInhibitSuite) TestIsLockedMissing(c *C) { _, err := os.Stat(runinhibit.InhibitDir) c.Assert(os.IsNotExist(err), Equals, true) - hint, info, err := runinhibit.IsLocked("pkg") + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + + hint, info, err := runinhibit.IsLocked("pkg", fakeUnlocker) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) err = os.MkdirAll(runinhibit.InhibitDir, 0755) c.Assert(err, IsNil) - hint, info, err = runinhibit.IsLocked("pkg") + hint, info, err = runinhibit.IsLocked("pkg", fakeUnlocker) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) + c.Check(unlockerCalled, Equals, 2) + c.Check(relockCalled, Equals, 2) } // IsLocked returns the previously set hint/info. func (s *runInhibitSuite) TestIsLockedLocked(c *C) { - err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo) + err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil) c.Assert(err, IsNil) - hint, info, err := runinhibit.IsLocked("pkg") + hint, info, err := runinhibit.IsLocked("pkg", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh) c.Check(info, Equals, s.inhibitInfo) @@ -181,27 +205,37 @@ func (s *runInhibitSuite) TestIsLockedLocked(c *C) { // IsLocked returns not-inhibited after unlocking. func (s *runInhibitSuite) TestIsLockedUnlocked(c *C) { - err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo) + err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil) c.Assert(err, IsNil) - err = runinhibit.Unlock("pkg") + err = runinhibit.Unlock("pkg", nil) c.Assert(err, IsNil) - hint, info, err := runinhibit.IsLocked("pkg") + hint, info, err := runinhibit.IsLocked("pkg", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) } func (s *runInhibitSuite) TestRemoveLockFile(c *C) { - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FilePresent) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FilePresent) - c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil) + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + + c.Assert(runinhibit.RemoveLockFile("pkg", fakeUnlocker), IsNil) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileAbsent) c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FileAbsent) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) // Removing an absent lock file is not an error. - c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil) + c.Assert(runinhibit.RemoveLockFile("pkg", fakeUnlocker), IsNil) + c.Check(unlockerCalled, Equals, 2) + c.Check(relockCalled, Equals, 2) } func checkFileLocked(c *C, path string) { @@ -219,7 +253,7 @@ func checkFileNotLocked(c *C, path string) { } func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) { - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) notInhibitedCalled := 0 inhibitedCalled := 0 @@ -232,7 +266,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) { if inhibitedCalled == 3 { // let's remove run inhibtion - c.Assert(runinhibit.Unlock("pkg"), IsNil) + c.Assert(runinhibit.Unlock("pkg", nil), IsNil) } return waitCh @@ -242,7 +276,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) { notInhibited := func(ctx context.Context) error { notInhibitedCalled++ - hint, _, err := runinhibit.IsLocked("pkg") + hint, _, err := runinhibit.IsLocked("pkg", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) @@ -279,7 +313,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) { } func (s *runInhibitSuite) TestWaitWhileInhibitedContextCancellation(c *C) { - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) ctx, cancel := context.WithCancel(context.Background()) @@ -324,7 +358,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedContextCancellation(c *C) { } func (s *runInhibitSuite) TestWaitWhileInhibitedNilCallbacks(c *C) { - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) waitCalled := 0 // closed channel returns immediately @@ -335,7 +369,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedNilCallbacks(c *C) { // lock should be released during wait interval checkFileNotLocked(c, runinhibit.HintFile("pkg")) // let's remove run inhibtion - c.Assert(runinhibit.Unlock("pkg"), IsNil) + c.Assert(runinhibit.Unlock("pkg", nil), IsNil) return waitCh } @@ -366,7 +400,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCallbackError(c *C) { // lock must be released on error checkFileNotLocked(c, runinhibit.HintFile("pkg")) - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) inhibited := func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error) { return false, fmt.Errorf("inhibited error") } @@ -378,7 +412,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCallbackError(c *C) { } func (s *runInhibitSuite) TestWaitWhileInhibitedCont(c *C) { - c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil) + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil) notInhibitedCalled := 0 inhibitedCalled := 0 @@ -406,7 +440,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCont(c *C) { c.Check(notInhibitedCalled, Equals, 0) c.Check(inhibitedCalled, Equals, 1) - hint, inhibitInfo, err := runinhibit.IsLocked("pkg") + hint, inhibitInfo, err := runinhibit.IsLocked("pkg", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh) c.Check(inhibitInfo, Equals, s.inhibitInfo) diff --git a/daemon/api.go b/daemon/api.go index f804c6156bc..ef1491f2497 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -143,6 +143,7 @@ func storeFrom(d *Daemon) snapstate.StoreService { var ( snapstateStoreInstallGoal = snapstate.StoreInstallGoal + snapstatePathUpdateGoal = snapstate.PathUpdateGoal snapstateInstallWithGoal = snapstate.InstallWithGoal snapstateInstallPath = snapstate.InstallPath snapstateInstallPathMany = snapstate.InstallPathMany @@ -150,8 +151,9 @@ var ( snapstateInstallComponents = snapstate.InstallComponents snapstateRefreshCandidates = snapstate.RefreshCandidates snapstateTryPath = snapstate.TryPath - snapstateUpdate = snapstate.Update - snapstateUpdateMany = snapstate.UpdateMany + snapstateStoreUpdateGoal = snapstate.StoreUpdateGoal + snapstateUpdateWithGoal = snapstate.UpdateWithGoal + snapstateUpdateOne = snapstate.UpdateOne snapstateRemove = snapstate.Remove snapstateRemoveMany = snapstate.RemoveMany snapstateResolveValSetsEnforcementError = snapstate.ResolveValidationSetsEnforcementError diff --git a/daemon/api_base_test.go b/daemon/api_base_test.go index 9ad98018ac1..0892b684e95 100644 --- a/daemon/api_base_test.go +++ b/daemon/api_base_test.go @@ -259,6 +259,8 @@ func (s *apiBaseSuite) SetUpTest(c *check.C) { })) s.AddCleanup(daemon.MockSnapstateStoreInstallGoal(newStoreInstallGoalRecorder)) + s.AddCleanup(daemon.MockSnapstatePathUpdateGoal(newPathUpdateGoalRecorder)) + s.AddCleanup(daemon.MockSnapstateStoreUpdateGoal(newStoreUpdateGoalRecorder)) } type storeInstallGoalRecorder struct { @@ -273,6 +275,38 @@ func newStoreInstallGoalRecorder(snaps ...snapstate.StoreSnap) snapstate.Install } } +type pathUpdateGoalRecorder struct { + snapstate.UpdateGoal + snaps []snapstate.PathSnap +} + +func newPathUpdateGoalRecorder(snaps ...snapstate.PathSnap) snapstate.UpdateGoal { + return &pathUpdateGoalRecorder{ + snaps: snaps, + UpdateGoal: snapstate.PathUpdateGoal(snaps...), + } +} + +type storeUpdateGoalRecorder struct { + snapstate.UpdateGoal + snaps []snapstate.StoreUpdate +} + +func (s *storeUpdateGoalRecorder) names() []string { + names := make([]string, 0, len(s.snaps)) + for _, snap := range s.snaps { + names = append(names, snap.InstanceName) + } + return names +} + +func newStoreUpdateGoalRecorder(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal { + return &storeUpdateGoalRecorder{ + snaps: snaps, + UpdateGoal: snapstate.StoreUpdateGoal(snaps...), + } +} + func (s *apiBaseSuite) mockModel(st *state.State, model *asserts.Model) { // realistic model setup if model == nil { @@ -402,6 +436,9 @@ func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapMana runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error { return fmt.Errorf("fake-install-snap-error errored") }, nil) + runner.AddHandler("fake-refresh-snap", func(t *state.Task, _ *tomb.Tomb) error { + return nil + }, nil) return &fakeSnapManager{} } diff --git a/daemon/api_model.go b/daemon/api_model.go index e13c7b5fb9e..540f7206c45 100644 --- a/daemon/api_model.go +++ b/daemon/api_model.go @@ -38,6 +38,7 @@ import ( "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" ) var ( @@ -128,7 +129,7 @@ func remodelJSON(c *Command, r *http.Request) Response { return AsyncResponse(nil, chg.ID()) } -func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asserts.Batch, *apiError) { +func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedContainer, *asserts.Batch, *apiError) { // New model model := form.Values["new-model"] if len(model) != 1 { @@ -141,7 +142,7 @@ func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asser } // Snap files - var snapFiles []*uploadedSnap + var snapFiles []*uploadedContainer if len(form.FileRefs["snap"]) > 0 { snaps, errRsp := form.GetSnapFiles() if errRsp != nil { @@ -164,7 +165,7 @@ func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asser } func startOfflineRemodelChange(st *state.State, newModel *asserts.Model, - snapFiles []*uploadedSnap, batch *asserts.Batch, pathsToNotRemove *[]string) ( + snapFiles []*uploadedContainer, batch *asserts.Batch, pathsToNotRemove *[]string) ( *state.Change, *apiError) { st.Lock() @@ -185,19 +186,23 @@ func startOfflineRemodelChange(st *state.State, newModel *asserts.Model, return nil, apiErr } - *pathsToNotRemove = make([]string, len(slInfo.sideInfos)) - for i, psi := range slInfo.sideInfos { + *pathsToNotRemove = make([]string, 0, len(slInfo.snaps)) + sideInfos := make([]*snap.SideInfo, 0, len(slInfo.snaps)) + paths := make([]string, 0, len(slInfo.snaps)) + for _, psi := range slInfo.snaps { // Move file to the same name of what a downloaded one would have dest := filepath.Join(dirs.SnapBlobDir, - fmt.Sprintf("%s_%s.snap", psi.RealName, psi.Revision)) - os.Rename(slInfo.tmpPaths[i], dest) + fmt.Sprintf("%s_%s.snap", psi.sideInfo.RealName, psi.sideInfo.Revision)) + os.Rename(psi.tmpPath, dest) // Avoid trying to remove a file that does not exist anymore - (*pathsToNotRemove)[i] = slInfo.tmpPaths[i] - slInfo.tmpPaths[i] = dest + *pathsToNotRemove = append(*pathsToNotRemove, psi.tmpPath) + + sideInfos = append(sideInfos, psi.sideInfo) + paths = append(paths, dest) } // Now create and start the remodel change - chg, err := devicestateRemodel(st, newModel, slInfo.sideInfos, slInfo.tmpPaths, devicestate.RemodelOptions{ + chg, err := devicestateRemodel(st, newModel, sideInfos, paths, devicestate.RemodelOptions{ // since this is the codepath that parses the form, offline is implcit // because local snaps are being provided. Offline: true, diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go index e5e4ad8c35f..55cf8ceb254 100644 --- a/daemon/api_sideload_n_try.go +++ b/daemon/api_sideload_n_try.go @@ -28,6 +28,7 @@ import ( "mime/multipart" "os" "path/filepath" + "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" @@ -79,10 +80,10 @@ func (f *Form) RemoveAllExcept(paths []string) { } } -type uploadedSnap struct { - // filename is the original name/path of the snap file. +type uploadedContainer struct { + // filename is the original name/path of the container file. filename string - // tmpPath is the location where the temp snap file is stored. + // tmpPath is the location where the temp container file is stored. tmpPath string // instanceName is optional and can only be set if only one snap was uploaded. instanceName string @@ -91,14 +92,14 @@ type uploadedSnap struct { // GetSnapFiles returns the original name and temp path for each snap file in // the form. Optionally, it might include a requested instance name, but only // if the was only one file in the form. -func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) { +func (f *Form) GetSnapFiles() ([]*uploadedContainer, *apiError) { if len(f.FileRefs["snap"]) == 0 { return nil, BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) } refs := f.FileRefs["snap"] if len(refs) == 1 && len(f.Values["snap-path"]) > 0 { - uploaded := &uploadedSnap{ + uploaded := &uploadedContainer{ filename: f.Values["snap-path"][0], tmpPath: refs[0].TmpPath, } @@ -106,12 +107,12 @@ func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) { if len(f.Values["name"]) > 0 { uploaded.instanceName = f.Values["name"][0] } - return []*uploadedSnap{uploaded}, nil + return []*uploadedContainer{uploaded}, nil } - snapFiles := make([]*uploadedSnap, len(refs)) + snapFiles := make([]*uploadedContainer, len(refs)) for i, ref := range refs { - snapFiles[i] = &uploadedSnap{ + snapFiles[i] = &uploadedContainer{ filename: ref.Filename, tmpPath: ref.TmpPath, } @@ -218,39 +219,163 @@ func sideloadOrTrySnap(ctx context.Context, c *Command, body io.ReadCloser, boun // sideloadedInfo contains information from a bunch of sideloaded snaps type sideloadedInfo struct { - sideInfos []*snap.SideInfo - names, origPaths, tmpPaths []string + // snaps contains the set of snaps that should be sideloaded. Any components + // associated with these snaps that are being sideloaded will be inside of + // the sideloadSnapInfo for that snap. + snaps []sideloadSnapInfo + // components contains the set of components that should be sideloaded, but + // their associated snaps are not being sideloaded (they must already be + // installed). + components []sideloadComponentInfo } -func sideloadInfo(st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags) (*sideloadedInfo, *apiError) { +type sideloadSnapInfo struct { + sideInfo *snap.SideInfo + components []sideloadComponentInfo + origPath string + tmpPath string +} + +type sideloadComponentInfo struct { + sideInfo *snap.ComponentSideInfo + origPath string + tmpPath string +} + +func sideloadInfo(st *state.State, uploads []*uploadedContainer, flags sideloadFlags) (*sideloadedInfo, *apiError) { deviceCtx, err := snapstate.DevicePastSeeding(st, nil) if err != nil { return nil, InternalError(err.Error()) } - names := make([]string, len(snapFiles)) - origPaths := make([]string, len(snapFiles)) - tmpPaths := make([]string, len(snapFiles)) - sideInfos := make([]*snap.SideInfo, len(snapFiles)) + var components []sideloadComponentInfo + var snaps []sideloadSnapInfo + for _, upload := range uploads { + si, snapErr := readSideInfo(st, upload.tmpPath, upload.filename, flags, deviceCtx.Model()) + if snapErr != nil { + if !flags.dangerousOK { + // TODO:COMPS: read assertions for components + return nil, snapErr + } + + ci, err := readComponentInfoFromCont(upload.tmpPath, nil) + if err != nil { + logger.Noticef("cannot sideload as a snap: %v", snapErr) + logger.Noticef("cannot sideload as a component: %v", err) - for i, snapFile := range snapFiles { - si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags, deviceCtx.Model()) - if apiError != nil { - return nil, apiError + // note that here we forward the error from reading the snap + // file, rather than the component file. this is consistent with + // what we do when installing one component from file. maybe + // something to change? + return nil, snapErr + } + + components = append(components, sideloadComponentInfo{ + sideInfo: &snap.ComponentSideInfo{ + Component: ci.Component, + Revision: snap.Revision{}, + }, + origPath: upload.filename, + tmpPath: upload.tmpPath, + }) + continue } - sideInfos[i] = si - names[i] = si.RealName - origPaths[i] = snapFile.filename - tmpPaths[i] = snapFile.tmpPath + snaps = append(snaps, sideloadSnapInfo{ + sideInfo: si, + origPath: upload.filename, + tmpPath: upload.tmpPath, + }) + } + + // we use this function here to get a pointer to an element in the snaps + // slice so that we can modify it. we can't just create a mapping in the + // above loop since the pointers could get invalidated the snaps slice + // growing. + snapByName := func(name string) (*sideloadSnapInfo, bool) { + for i := range snaps { + if snaps[i].sideInfo.RealName == name { + return &snaps[i], true + } + } + return nil, false } - return &sideloadedInfo{sideInfos: sideInfos, names: names, - origPaths: origPaths, tmpPaths: tmpPaths}, nil + onlyComponents := make([]sideloadComponentInfo, 0) + for _, ci := range components { + snapName := ci.sideInfo.Component.SnapName + + ssi, ok := snapByName(snapName) + if !ok { + onlyComponents = append(onlyComponents, ci) + continue + } + + ssi.components = append(ssi.components, ci) + } + + return &sideloadedInfo{ + components: onlyComponents, + snaps: snaps, + }, nil } -func sideloadManySnaps(ctx context.Context, st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) { - slInfo, apiErr := sideloadInfo(st, snapFiles, flags) +func sideloadTaskSets(ctx context.Context, st *state.State, sideload *sideloadedInfo, userID int, flags snapstate.Flags) ([]*state.TaskSet, *apiError) { + if flags.Transaction == client.TransactionAllSnaps && flags.Lane == 0 { + flags.Lane = st.NewLane() + } + + var tss []*state.TaskSet + + // handle all of the components whose snaps are not present in the set of + // files that are being sideloaded + for _, comp := range sideload.components { + snapName := comp.sideInfo.Component.SnapName + snapInfo, err := installedSnapInfo(st, snapName) + if err != nil { + if errors.Is(err, state.ErrNoState) { + return nil, SnapNotInstalled(snapName, fmt.Errorf("snap owning %q not installed", comp.sideInfo.Component)) + } + return nil, BadRequest("cannot retrieve information for %q: %v", snapName, err) + } + + ts, err := snapstateInstallComponentPath(st, comp.sideInfo, snapInfo, comp.tmpPath, snapstate.Options{ + Flags: flags, + }) + if err != nil { + return nil, errToResponse(err, nil, InternalError, "cannot install component %q for snap %q: %v", comp.sideInfo.Component, snapName, err) + } + tss = append(tss, ts) + } + + // handle everything else + var pathSnaps []snapstate.PathSnap + for _, sn := range sideload.snaps { + comps := make(map[*snap.ComponentSideInfo]string, len(sn.components)) + for _, ci := range sn.components { + comps[ci.sideInfo] = ci.tmpPath + } + + pathSnaps = append(pathSnaps, snapstate.PathSnap{ + Path: sn.tmpPath, + SideInfo: sn.sideInfo, + Components: comps, + }) + } + + _, uts, err := snapstateUpdateWithGoal(ctx, st, snapstatePathUpdateGoal(pathSnaps...), nil, snapstate.Options{ + UserID: userID, + Flags: flags, + }) + if err != nil { + return nil, errToResponse(err, nil, InternalError, "cannot install snap/component files: %v") + } + + return append(tss, uts.Refresh...), nil +} + +func sideloadManySnaps(ctx context.Context, st *state.State, uploads []*uploadedContainer, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) { + slInfo, apiErr := sideloadInfo(st, uploads, flags) if apiErr != nil { return nil, apiErr } @@ -260,19 +385,109 @@ func sideloadManySnaps(ctx context.Context, st *state.State, snapFiles []*upload userID = user.ID } - tss, err := snapstateInstallPathMany(ctx, st, slInfo.sideInfos, slInfo.tmpPaths, userID, &flags.Flags) + tss, err := sideloadTaskSets(ctx, st, slInfo, userID, flags.Flags) if err != nil { - return nil, errToResponse(err, slInfo.names, InternalError, "cannot install snap files: %v") + return nil, err + } + + snapNames := make([]string, 0, len(slInfo.snaps)) + snapToComps := make(map[string][]string, len(slInfo.components)) + for _, sn := range slInfo.snaps { + snapName := sn.sideInfo.RealName + snapNames = append(snapNames, snapName) + + if len(sn.components) == 0 { + continue + } + + snapToComps[snapName] = make([]string, 0, len(sn.components)) + for _, c := range sn.components { + snapToComps[snapName] = append(snapToComps[snapName], c.sideInfo.Component.ComponentName) + } + } + + for _, ci := range slInfo.components { + snapToComps[ci.sideInfo.Component.SnapName] = append(snapToComps[ci.sideInfo.Component.SnapName], ci.sideInfo.Component.ComponentName) + } + + msg := multiPathInstallMessage(slInfo) + + chg := newChange(st, "install-snap", msg, tss, snapNames) + apiData := make(map[string]interface{}, 0) + + if len(snapNames) > 0 { + apiData["snap-names"] = snapNames } - msg := fmt.Sprintf(i18n.G("Install snaps %s from files %s"), strutil.Quoted(slInfo.names), strutil.Quoted(slInfo.origPaths)) - chg := newChange(st, "install-snap", msg, tss, slInfo.names) - chg.Set("api-data", map[string][]string{"snap-names": slInfo.names}) + if len(snapToComps) > 0 { + apiData["components"] = snapToComps + } + chg.Set("api-data", apiData) return chg, nil } -func sideloadSnap(_ context.Context, st *state.State, snapFile *uploadedSnap, flags sideloadFlags) (*state.Change, *apiError) { +func multiPathInstallMessage(sli *sideloadedInfo) string { + var b strings.Builder + switch len(sli.snaps) { + case 0: + b.WriteString(i18n.G("Install")) + case 1: + b.WriteString(i18n.G("Install snap")) + default: + b.WriteString(i18n.G("Install snaps")) + } + + var paths []string + for i, sn := range sli.snaps { + fmt.Fprintf(&b, " %q", sn.sideInfo.RealName) + paths = append(paths, sn.origPath) + + comps := make([]string, 0, len(sn.components)) + for _, c := range sn.components { + comps = append(comps, c.sideInfo.Component.ComponentName) + paths = append(paths, c.origPath) + } + + if len(comps) > 0 { + b.WriteString(" (") + if len(sn.components) > 1 { + fmt.Fprintf(&b, i18n.G("with components %s"), strutil.Quoted(comps)) + } else { + fmt.Fprintf(&b, i18n.G("with component %s"), strutil.Quoted(comps)) + } + b.WriteRune(')') + } + + if i < len(sli.snaps)-1 { + b.WriteRune(',') + } + } + + compNames := make([]string, 0, len(sli.components)) + for _, c := range sli.components { + compNames = append(compNames, c.sideInfo.Component.String()) + paths = append(paths, c.origPath) + } + + if len(sli.snaps) != 0 && len(sli.components) != 0 { + b.WriteString(i18n.G(" and")) + } + + switch len(sli.components) { + case 0: + case 1: + fmt.Fprintf(&b, i18n.G(" component %s"), strutil.Quoted(compNames)) + default: + fmt.Fprintf(&b, i18n.G(" components %s"), strutil.Quoted(compNames)) + } + + fmt.Fprintf(&b, " from files %s", strutil.Quoted(paths)) + + return b.String() +} + +func sideloadSnap(_ context.Context, st *state.State, snapFile *uploadedContainer, flags sideloadFlags) (*state.Change, *apiError) { var instanceName string if snapFile.instanceName != "" { // caller has specified desired instance name diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index b97fb1a8eb0..0a9fb12ba19 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -941,21 +941,25 @@ func (s *sideloadSuite) TestSideloadCleanUpUnusedTempSnapFiles(c *check.C) { func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) { d := s.daemonWithFakeSnapManager(c) s.markSeeded(d) - expectedFlags := &snapstate.Flags{RemoveSnapPath: true, DevMode: true, Transaction: client.TransactionAllSnaps} + expectedFlags := snapstate.Flags{RemoveSnapPath: true, DevMode: true, Transaction: client.TransactionAllSnaps, Lane: 1} - restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, tmpPaths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { - c.Check(flags, check.DeepEquals, expectedFlags) - c.Check(userID, check.Not(check.Equals), 0) + restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*pathUpdateGoalRecorder) + + c.Check(opts.Flags, check.DeepEquals, expectedFlags) + c.Check(opts.UserID, check.Not(check.Equals), 0) var tss []*state.TaskSet - for i, si := range infos { - c.Check(tmpPaths[i], testutil.FileEquals, si.RealName) + var names []string + for _, sn := range goal.snaps { + c.Check(sn.Path, testutil.FileEquals, sn.SideInfo.RealName) - ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName))) tss = append(tss, ts) + names = append(names, sn.InstanceName) } - return tss, nil + return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil }) defer restore() @@ -1005,10 +1009,278 @@ func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) { c.Check(data["snap-names"], check.DeepEquals, snaps) } +type sideloadSnapsAndComponentsOpts struct { + missingSnap bool +} + +func (s *sideloadSuite) TestSideloadManySnapsAndComponentsMissingSnap(c *check.C) { + s.testSideloadManySnapsAndComponents(c, sideloadSnapsAndComponentsOpts{missingSnap: true}) +} + +func (s *sideloadSuite) TestSideloadManySnapsAndComponents(c *check.C) { + s.testSideloadManySnapsAndComponents(c, sideloadSnapsAndComponentsOpts{}) +} + +func (s *sideloadSuite) testSideloadManySnapsAndComponents(c *check.C, opts sideloadSnapsAndComponentsOpts) { + d := s.daemonWithFakeSnapManager(c) + s.markSeeded(d) + expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionAllSnaps, Lane: 1} + + restore := daemon.MockSnapstateInstallComponentPath(func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, + path string, opts snapstate.Options) (*state.TaskSet, error) { + c.Check(csi.Component.SnapName, check.Equals, "three") + c.Check(csi.Component.ComponentName, check.Equals, "comp-four") + c.Check(opts.Flags, check.DeepEquals, expectedFlags) + c.Check(path, testutil.FileEquals, "comp-four") + + t := st.NewTask("fake-install-component", "Doing a fake install") + return state.NewTaskSet(t), nil + }) + defer restore() + + snaps := []string{"one", "two"} + expectedSnapsToComps := map[string][]string{ + "one": {"comp-one"}, + "two": {"comp-two", "comp-three"}, + } + components := []string{"comp-one", "comp-two", "comp-three", "comp-four"} + + st := d.Overlord().State() + + if !opts.missingSnap { + ssi := &snap.SideInfo{ + RealName: "three", + Revision: snap.R(1), + SnapID: "three-snap-id", + } + st.Lock() + snapstate.Set(d.Overlord().State(), "three", &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{ + sequence.NewRevisionSideState(ssi, nil), + }), + Current: snap.R(1), + }) + st.Unlock() + } + + restore = daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*pathUpdateGoalRecorder) + + c.Check(opts.Flags, check.DeepEquals, expectedFlags) + c.Check(opts.UserID, check.Not(check.Equals), 0) + + var tss []*state.TaskSet + var names []string + for _, sn := range goal.snaps { + comps, ok := expectedSnapsToComps[sn.SideInfo.RealName] + c.Assert(ok, check.Equals, true, check.Commentf("unexpected snap name %q", sn.SideInfo.RealName)) + foundComps := make([]string, 0, len(comps)) + for csi := range sn.Components { + foundComps = append(foundComps, csi.Component.ComponentName) + } + c.Check(foundComps, testutil.DeepUnsortedMatches, comps) + + c.Check(sn.Path, testutil.FileEquals, sn.SideInfo.RealName) + + ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName))) + tss = append(tss, ts) + names = append(names, sn.InstanceName) + } + + return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil + }) + defer restore() + + readComponentInfoCalled := -1 + restore = daemon.MockReadComponentInfoFromCont(func(p string, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { + readComponentInfoCalled++ + var snapName string + switch readComponentInfoCalled { + case 0: + snapName = "one" + case 1, 2: + snapName = "two" + case 3: + snapName = "three" + } + return &snap.ComponentInfo{ + Component: naming.NewComponentRef(snapName, components[readComponentInfoCalled]), + Type: snap.TestComponent, + CompVersion: "1.0", + }, nil + }) + defer restore() + + readSnapInfoCalled := -1 + restore = daemon.MockUnsafeReadSnapInfo(func(p string) (*snap.Info, error) { + readSnapInfoCalled++ + switch readSnapInfoCalled { + case 0, 1: + return &snap.Info{SuggestedName: snaps[readSnapInfoCalled]}, nil + default: + return nil, errors.New("no more snaps") + } + }) + defer restore() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + body += "Content-Disposition: form-data; name=\"transaction\"\r\n" + + "\r\n" + + "all-snaps\r\n" + + "----hello--\r\n" + + containers := make([]string, len(snaps)+len(components)) + copy(containers, snaps) + copy(containers[len(snaps):], components) + for _, c := range containers { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + c + "\"\r\n" + + "\r\n" + + c + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + s.asUserAuth(c, req) + + if opts.missingSnap { + rsp := s.errorReq(c, req, s.authUser) + c.Check(rsp.Message, check.Equals, `snap owning "three+comp-four" not installed`) + + return + } + + rsp := s.asyncReq(c, req, s.authUser) + + st.Lock() + defer st.Unlock() + + expectedFileNames := []string{"file-one", "file-comp-one", "file-two", "file-comp-two", "file-comp-three", "file-comp-four"} + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps "one" (with component "comp-one"), "two" (with components "comp-two", "comp-three") and component "three+comp-four" from files %s`, strutil.Quoted(expectedFileNames))) + + var data map[string]interface{} + c.Assert(chg.Get("api-data", &data), check.IsNil) + c.Check(data, check.DeepEquals, map[string]interface{}{ + "snap-names": []interface{}{"one", "two"}, + "components": map[string]interface{}{ + "one": []interface{}{"comp-one"}, + "two": []interface{}{"comp-two", "comp-three"}, + "three": []interface{}{"comp-four"}, + }, + }) +} + +func (s *sideloadSuite) TestSideloadManyOnlyComponents(c *check.C) { + d := s.daemonWithFakeSnapManager(c) + s.markSeeded(d) + expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionAllSnaps, Lane: 1} + + components := []string{"comp-one", "comp-two", "comp-three", "comp-four"} + restore := daemon.MockSnapstateInstallComponentPath(func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, + path string, opts snapstate.Options) (*state.TaskSet, error) { + c.Check(csi.Component.SnapName, check.Equals, "one") + c.Check(components, testutil.Contains, csi.Component.ComponentName) + c.Check(opts.Flags, check.DeepEquals, expectedFlags) + c.Check(path, testutil.FileEquals, csi.Component.ComponentName) + + t := st.NewTask("fake-install-component", "Doing a fake install") + return state.NewTaskSet(t), nil + }) + defer restore() + + st := d.Overlord().State() + + ssi := &snap.SideInfo{ + RealName: "one", + Revision: snap.R(1), + SnapID: "one-snap-id", + } + st.Lock() + snapstate.Set(d.Overlord().State(), "one", &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{ + sequence.NewRevisionSideState(ssi, nil), + }), + Current: snap.R(1), + }) + st.Unlock() + + readComponentInfoCalled := -1 + restore = daemon.MockReadComponentInfoFromCont(func(p string, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { + readComponentInfoCalled++ + return &snap.ComponentInfo{ + Component: naming.NewComponentRef("one", components[readComponentInfoCalled]), + Type: snap.TestComponent, + CompVersion: "1.0", + }, nil + }) + defer restore() + + restore = daemon.MockUnsafeReadSnapInfo(func(p string) (*snap.Info, error) { + return nil, errors.New("no more snaps") + }) + defer restore() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + body += "Content-Disposition: form-data; name=\"transaction\"\r\n" + + "\r\n" + + "all-snaps\r\n" + + "----hello--\r\n" + + for _, c := range components { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + c + "\"\r\n" + + "\r\n" + + c + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + s.asUserAuth(c, req) + + rsp := s.asyncReq(c, req, s.authUser) + + st.Lock() + defer st.Unlock() + + expectedFileNames := []string{"file-comp-one", "file-comp-two", "file-comp-three", "file-comp-four"} + + fullComponentNames := make([]string, len(components)) + for i, c := range components { + fullComponentNames[i] = "one+" + c + } + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install components %s from files %s`, strutil.Quoted(fullComponentNames), strutil.Quoted(expectedFileNames))) + + var data map[string]interface{} + c.Assert(chg.Get("api-data", &data), check.IsNil) + c.Check(data, check.DeepEquals, map[string]interface{}{ + "components": map[string]interface{}{ + "one": []interface{}{"comp-one", "comp-two", "comp-three", "comp-four"}, + }, + }) +} + func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) { s.daemon(c) - restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { - return nil, errors.New("expected") + restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + return nil, nil, errors.New("expected") }) defer restore() @@ -1035,7 +1307,7 @@ func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) { apiErr := s.errorReq(c, req, nil) c.Check(apiErr.JSON().Status, check.Equals, 500) - c.Check(apiErr.Message, check.Equals, `cannot install snap files: expected`) + c.Check(apiErr.Message, check.Equals, `cannot install snap/component files: expected`) } func (s *sideloadSuite) TestSideloadManyFailUnsafeReadInfo(c *check.C) { @@ -1115,22 +1387,26 @@ func (s *sideloadSuite) TestSideloadManySnapsAsserted(c *check.C) { expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionPerSnap} - restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { - c.Check(*flags, check.DeepEquals, expectedFlags) + restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*pathUpdateGoalRecorder) + + c.Check(opts.Flags, check.DeepEquals, expectedFlags) var tss []*state.TaskSet - for i, si := range infos { - c.Check(*si, check.DeepEquals, snap.SideInfo{ + var names []string + for i, sn := range goal.snaps { + c.Check(*sn.SideInfo, check.DeepEquals, snap.SideInfo{ RealName: snaps[i], SnapID: snaps[i] + "-id", Revision: snap.R(41), }) - ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName))) tss = append(tss, ts) + names = append(names, sn.InstanceName) } - return tss, nil + return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil }) defer restore() diff --git a/daemon/api_snaps.go b/daemon/api_snaps.go index 342b38ac199..d2548a23d9e 100644 --- a/daemon/api_snaps.go +++ b/daemon/api_snaps.go @@ -416,7 +416,7 @@ func (inst *snapInstruction) validate() error { if len(inst.CompsRaw) > 0 { switch inst.Action { - case "remove", "install": + case "remove", "install", "refresh": default: return fmt.Errorf("%q action is not supported for components", inst.Action) } @@ -463,12 +463,10 @@ func snapInstall(ctx context.Context, inst *snapInstruction, st *state.State) (* return nil, errors.New(i18n.G("cannot install snap with empty name")) } - var ckey string if inst.CohortKey == "" { logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) } else { - ckey = strutil.ElliptLeft(inst.CohortKey, 10) - logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey) + logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10)) } installedSnaps, installedComponents, tss, err := installationTaskSets(ctx, st, inst) @@ -477,24 +475,29 @@ func snapInstall(ctx context.Context, inst *snapInstruction, st *state.State) (* } return &snapInstructionResult{ - Summary: installMessage(inst, ckey), + Summary: installRefreshMessage(inst.Snaps[0], inst), Tasksets: tss, Affected: installedSnaps, AffectedComponents: installedComponents, }, nil } -func installMessage(inst *snapInstruction, cohort string) string { +func installRefreshMessage(snapName string, inst *snapInstruction) string { var b strings.Builder - fmt.Fprintf(&b, i18n.G("Install %q snap"), inst.Snaps[0]) + if inst.Action == "install" { + fmt.Fprintf(&b, i18n.G("Install %q snap"), snapName) + } else { + fmt.Fprintf(&b, i18n.G("Refresh %q snap"), snapName) + } + if inst.Channel != "stable" && inst.Channel != "" { fmt.Fprintf(&b, i18n.G(" from %q channel"), inst.Channel) } if inst.CohortKey != "" { - fmt.Fprintf(&b, i18n.G(" from %q cohort"), cohort) + fmt.Fprintf(&b, i18n.G(" from %q cohort"), strutil.ElliptLeft(inst.CohortKey, 10)) } - if comps := inst.CompsForSnaps[inst.Snaps[0]]; len(comps) > 0 { + if comps := inst.CompsForSnaps[snapName]; len(comps) > 0 { if len(comps) > 1 { fmt.Fprintf(&b, i18n.G(" with components %s"), strutil.Quoted(comps)) } else { @@ -505,15 +508,19 @@ func installMessage(inst *snapInstruction, cohort string) string { return b.String() } -func multiInstallMessage(inst *snapInstruction) string { - if len(inst.Snaps) == 1 { - return installMessage(inst, "") +func multiInstallRefreshMessage(snaps []string, inst *snapInstruction) string { + if len(snaps) == 1 { + return installRefreshMessage(snaps[0], inst) } var b strings.Builder - fmt.Fprint(&b, i18n.G("Install snaps")) + if inst.Action == "install" { + fmt.Fprint(&b, i18n.G("Install snaps")) + } else { + fmt.Fprint(&b, i18n.G("Refresh snaps")) + } - for i, name := range inst.Snaps { + for i, name := range snaps { fmt.Fprintf(&b, " %q", name) if comps := inst.CompsForSnaps[name]; len(comps) > 0 { @@ -526,14 +533,14 @@ func multiInstallMessage(inst *snapInstruction) string { b.WriteRune(')') } - if i < len(inst.Snaps)-1 { + if i < len(snaps)-1 { b.WriteRune(',') } } return b.String() } -func snapUpdate(_ context.Context, inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { +func snapUpdate(ctx context.Context, inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? flags, err := inst.modeFlags() if err != nil { @@ -554,18 +561,28 @@ func snapUpdate(_ context.Context, inst *snapInstruction, st *state.State) (*sna return nil, err } - ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) - if err != nil { - return nil, err + // TODO: once we completely move away from the old snapstate API, this + // backwards compatibility bit should be removed + if flags.Transaction == "" { + flags.Transaction = client.TransactionPerSnap } - msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) - if inst.Channel != "stable" && inst.Channel != "" { - msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) + goal := snapstateStoreUpdateGoal(snapstate.StoreUpdate{ + InstanceName: inst.Snaps[0], + RevOpts: *inst.revnoOpts(), + AdditionalComponents: inst.CompsForSnaps[inst.Snaps[0]], + }) + + ts, err := snapstateUpdateOne(ctx, st, goal, nil, snapstate.Options{ + Flags: flags, + UserID: inst.userID, + }) + if err != nil { + return nil, err } return &snapInstructionResult{ - Summary: msg, + Summary: installRefreshMessage(inst.Snaps[0], inst), Tasksets: []*state.TaskSet{ts}, Affected: inst.Snaps, AffectedComponents: inst.CompsForSnaps, @@ -949,7 +966,7 @@ func snapInstallMany(ctx context.Context, inst *snapInstruction, st *state.State } return &snapInstructionResult{ - Summary: multiInstallMessage(inst), + Summary: multiInstallRefreshMessage(inst.Snaps, inst), Affected: installedSnaps, AffectedComponents: installedComponents, Tasksets: tasksets, @@ -968,10 +985,28 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State) return nil, err } - transaction := inst.Transaction - updated, tasksets, err := snapstateUpdateMany(ctx, st, inst.Snaps, nil, inst.userID, &snapstate.Flags{ + updates := make([]snapstate.StoreUpdate, 0, len(inst.Snaps)) + for _, name := range inst.Snaps { + updates = append(updates, snapstate.StoreUpdate{ + InstanceName: name, + AdditionalComponents: inst.CompsForSnaps[name], + }) + } + + flags := snapstate.Flags{ IgnoreRunning: inst.IgnoreRunning, - Transaction: transaction, + Transaction: inst.Transaction, + } + + // TODO: once we completely move away from the old snapstate API, this + // backwards compatibility bit should be removed + if flags.Transaction == "" { + flags.Transaction = client.TransactionPerSnap + } + + goal := snapstateStoreUpdateGoal(updates...) + updated, uts, err := snapstateUpdateWithGoal(ctx, st, goal, nil, snapstate.Options{ + Flags: flags, }) if err != nil { if opts.IsRefreshOfAllSnaps { @@ -981,6 +1016,7 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State) } return nil, err } + tasksets := uts.Refresh var msg string switch len(updated) { @@ -992,11 +1028,9 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State) msg = i18n.G("Refresh all snaps: no updates") } case 1: - msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) + msg = installRefreshMessage(updated[0], inst) default: - quoted := strutil.Quoted(updated) - // TRANSLATORS: the %s is a comma-separated list of quoted snap names - msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) + msg = multiInstallRefreshMessage(updated, inst) } return &snapInstructionResult{ diff --git a/daemon/api_snaps_test.go b/daemon/api_snaps_test.go index 776b8faae33..aeef7842062 100644 --- a/daemon/api_snaps_test.go +++ b/daemon/api_snaps_test.go @@ -674,10 +674,11 @@ func (s *snapsSuite) TestPostSnapsOpSystemRestartImmediate(c *check.C) { func (s *snapsSuite) testPostSnapsOp(c *check.C, extraJSON, contentType string) (systemRestartImmediate bool) { defer daemon.MockAssertstateRefreshSnapAssertions(func(*state.State, int, *assertstate.RefreshAssertionsOptions) error { return nil })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, _ int, _ *snapstate.Flags) ([]string, []*state.TaskSet, error) { - c.Check(names, check.HasLen, 0) + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 0) t := s.NewTask("fake-refresh-all", "Refreshing everything") - return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil + return []string{"fake1", "fake2"}, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() d := s.daemonWithOverlordMockAndStore() @@ -736,16 +737,17 @@ func (s *snapsSuite) TestRefreshAll(c *check.C) { msg string }{ {nil, "Refresh all snaps: no updates"}, - {[]string{"fake"}, `Refresh snap "fake"`}, + {[]string{"fake"}, `Refresh "fake" snap`}, {[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`}, } { refreshSnapAssertions = false refreshAssertionsOpts = nil - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - c.Check(names, check.HasLen, 0) + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 0) t := s.NewTask("fake-refresh-all", "Refreshing everything") - return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil + return tst.snaps, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() inst := &daemon.SnapInstruction{Action: "refresh"} @@ -768,9 +770,10 @@ func (s *snapsSuite) TestRefreshAllNoChanges(c *check.C) { return assertstate.RefreshSnapAssertions(s, userID, opts) })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - c.Check(names, check.HasLen, 0) - return nil, nil, nil + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 0) + return nil, &snapstate.UpdateTaskSets{Refresh: nil}, nil })() d := s.daemon(c) @@ -797,7 +800,7 @@ func (s *snapsSuite) TestRefreshAllRestoresValidationSets(c *check.C) { return nil })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { return nil, nil, fmt.Errorf("boom") })() @@ -824,12 +827,13 @@ func (s *snapsSuite) TestRefreshManyTransactionally(c *check.C) { return nil })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - calledFlags = flags - - c.Check(names, check.HasLen, 2) + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + calledFlags = &opts.Flags + c.Check(goal.snaps, check.HasLen, 2) t := s.NewTask("fake-refresh-2", "Refreshing two") - return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + + return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() d := s.daemon(c) @@ -861,10 +865,11 @@ func (s *snapsSuite) TestRefreshMany(c *check.C) { return nil })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - c.Check(names, check.HasLen, 2) + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 2) t := s.NewTask("fake-refresh-2", "Refreshing two") - return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() d := s.daemon(c) @@ -887,12 +892,13 @@ func (s *snapsSuite) TestRefreshManyIgnoreRunning(c *check.C) { })() var calledFlags *snapstate.Flags - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - calledFlags = flags + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + calledFlags = &opts.Flags - c.Check(names, check.HasLen, 2) + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 2) t := s.NewTask("fake-refresh-2", "Refreshing two") - return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() d := s.daemon(c) @@ -918,10 +924,11 @@ func (s *snapsSuite) TestRefreshMany1(c *check.C) { return nil })() - defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { - c.Check(names, check.HasLen, 1) + defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.HasLen, 1) t := s.NewTask("fake-refresh-1", "Refreshing one") - return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil })() d := s.daemon(c) @@ -931,7 +938,7 @@ func (s *snapsSuite) TestRefreshMany1(c *check.C) { res, err := inst.DispatchForMany()(context.Background(), inst, st) st.Unlock() c.Assert(err, check.IsNil) - c.Check(res.Summary, check.Equals, `Refresh snap "foo"`) + c.Check(res.Summary, check.Equals, `Refresh "foo" snap`) c.Check(res.Affected, check.DeepEquals, inst.Snaps) c.Check(refreshSnapAssertions, check.Equals, true) } @@ -2619,12 +2626,13 @@ func (s *snapsSuite) TestRefresh(c *check.C) { installQueue := []string{} assertstateCalledUserID := 0 - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - calledFlags = flags - calledUserID = userID - installQueue = append(installQueue, name) + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + calledFlags = opts.Flags + calledUserID = opts.UserID + installQueue = append(installQueue, goal.snaps[0].InstanceName) - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake refresh") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2646,7 +2654,9 @@ func (s *snapsSuite) TestRefresh(c *check.C) { c.Check(err, check.IsNil) c.Check(assertstateCalledUserID, check.Equals, 17) - c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{ + Transaction: client.TransactionPerSnap, + }) c.Check(calledUserID, check.Equals, 17) c.Check(err, check.IsNil) c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) @@ -2658,12 +2668,13 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) { calledUserID := 0 installQueue := []string{} - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - calledFlags = flags - calledUserID = userID - installQueue = append(installQueue, name) + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + calledFlags = opts.Flags + calledUserID = opts.UserID + installQueue = append(installQueue, goal.snaps[0].InstanceName) - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2684,8 +2695,10 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) { res, err := inst.Dispatch()(context.Background(), inst, st) c.Check(err, check.IsNil) - flags := snapstate.Flags{} - flags.DevMode = true + flags := snapstate.Flags{ + DevMode: true, + Transaction: client.TransactionPerSnap, + } c.Check(calledFlags, check.DeepEquals, flags) c.Check(calledUserID, check.Equals, 17) c.Check(err, check.IsNil) @@ -2696,8 +2709,8 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) { func (s *snapsSuite) TestRefreshClassic(c *check.C) { var calledFlags snapstate.Flags - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - calledFlags = flags + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + calledFlags = opts.Flags return nil, nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2718,7 +2731,10 @@ func (s *snapsSuite) TestRefreshClassic(c *check.C) { _, err := inst.Dispatch()(context.Background(), inst, st) c.Check(err, check.IsNil) - c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true}) + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{ + Classic: true, + Transaction: client.TransactionPerSnap, + }) } func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) { @@ -2726,12 +2742,13 @@ func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) { calledUserID := 0 installQueue := []string{} - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - calledFlags = flags - calledUserID = userID - installQueue = append(installQueue, name) + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + calledFlags = opts.Flags + calledUserID = opts.UserID + installQueue = append(installQueue, goal.snaps[0].InstanceName) - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2752,8 +2769,10 @@ func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) { res, err := inst.Dispatch()(context.Background(), inst, st) c.Check(err, check.IsNil) - flags := snapstate.Flags{} - flags.IgnoreValidation = true + flags := snapstate.Flags{ + IgnoreValidation: true, + Transaction: client.TransactionPerSnap, + } c.Check(calledFlags, check.DeepEquals, flags) c.Check(calledUserID, check.Equals, 17) @@ -2766,11 +2785,12 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) { var calledFlags snapstate.Flags installQueue := []string{} - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - calledFlags = flags - installQueue = append(installQueue, name) + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + calledFlags = opts.Flags + installQueue = append(installQueue, goal.snaps[0].InstanceName) - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2790,8 +2810,10 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) { res, err := inst.Dispatch()(context.Background(), inst, st) c.Check(err, check.IsNil) - flags := snapstate.Flags{} - flags.IgnoreRunning = true + flags := snapstate.Flags{ + IgnoreRunning: true, + Transaction: client.TransactionPerSnap, + } c.Check(calledFlags, check.DeepEquals, flags) c.Check(err, check.IsNil) @@ -2802,10 +2824,11 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) { func (s *snapsSuite) TestRefreshCohort(c *check.C) { cohort := "" - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - cohort = opts.CohortKey + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + cohort = goal.snaps[0].RevOpts.CohortKey - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -2826,16 +2849,17 @@ func (s *snapsSuite) TestRefreshCohort(c *check.C) { c.Check(err, check.IsNil) c.Check(cohort, check.Equals, "xyzzy") - c.Check(res.Summary, check.Equals, `Refresh "some-snap" snap`) + c.Check(res.Summary, check.Equals, `Refresh "some-snap" snap from "xyzzy" cohort`) } func (s *snapsSuite) TestRefreshLeaveCohort(c *check.C) { var leave *bool - defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - leave = &opts.LeaveCohort + defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + leave = &goal.snaps[0].RevOpts.LeaveCohort - t := s.NewTask("fake-refresh-snap", "Doing a fake install") + t := st.NewTask("fake-refresh-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { @@ -3765,7 +3789,7 @@ func (s *snapsSuite) TestPostRemoveComponents(c *check.C) { func (s *snapsSuite) TestPostComponentsWrongAction(c *check.C) { s.daemonWithOverlordMockAndStore() - for _, action := range []string{"refresh", "revert", "enable", "disable"} { + for _, action := range []string{"revert", "enable", "disable"} { buf := strings.NewReader(fmt.Sprintf(`{"action": %q,"components":["comp1","comp2"]}`, action)) req, err := http.NewRequest("POST", "/v2/snaps/foo", buf) @@ -3892,7 +3916,7 @@ func (s *snapsSuite) TestPostComponentsRemoveManyWithSnaps(c *check.C) { func (s *snapsSuite) TestPostComponentsManyWrongAction(c *check.C) { s.daemonWithOverlordMockAndStore() - for _, action := range []string{"refresh", "revert", "enable", "disable"} { + for _, action := range []string{"revert", "enable", "disable"} { buf := strings.NewReader(fmt.Sprintf(`{"action": %q, "snaps":["foo", "bar"], "components": { "snap1": ["comp1", "comp2"], "snap2": ["comp3", "comp4"] }}`, action)) req, err := http.NewRequest("POST", "/v2/snaps", buf) c.Assert(err, check.IsNil) @@ -3969,6 +3993,60 @@ func (s *snapsSuite) TestInstallWithComponents(c *check.C) { c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap with components "comp1", "comp2"`) } +func (s *snapsSuite) TestUpdateWithAdditionalComponents(c *check.C) { + restore := daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + return nil + }) + defer restore() + + restore = daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Check(goal.snaps, check.DeepEquals, []snapstate.StoreUpdate{{ + InstanceName: "some-snap", + AdditionalComponents: []string{"comp1", "comp2"}, + }}) + t := st.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + }) + defer restore() + + d := s.daemonWithFakeSnapManager(c) + + r := strings.NewReader(`{"action": "refresh", "components": ["comp1", "comp2"]}`) + req, err := http.NewRequest("POST", "/v2/snaps/some-snap", r) + c.Assert(err, check.IsNil) + + rsp := s.asyncReq(c, req, nil) + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + var data map[string]interface{} + err = chg.Get("api-data", &data) + c.Assert(err, check.IsNil) + c.Check(data, check.DeepEquals, map[string]interface{}{ + "snap-names": []interface{}{"some-snap"}, + "components": map[string]interface{}{ + "some-snap": []interface{}{"comp1", "comp2"}, + }, + }) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Status(), check.Equals, state.DoneStatus) + c.Check(err, check.IsNil) + c.Check(chg.Kind(), check.Equals, "refresh-snap") + c.Check(chg.Summary(), check.Equals, `Refresh "some-snap" snap with components "comp1", "comp2"`) +} + func (s *snapsSuite) TestInstallManyWithComponents(c *check.C) { defer daemon.MockSnapstateInstallWithGoal(func(ctx context.Context, st *state.State, g snapstate.InstallGoal, opts snapstate.Options) ([]*snap.Info, []*state.TaskSet, error) { goal, ok := g.(*storeInstallGoalRecorder) @@ -4018,6 +4096,60 @@ func (s *snapsSuite) TestInstallManyWithComponents(c *check.C) { c.Check(chg.Summary(), check.Equals, `Install snaps "some-snap" (with components "some-comp1", "some-comp2"), "other-snap" (with component "other-comp1")`) } +func (s *snapsSuite) TestUpdateManyWithComponents(c *check.C) { + restore := daemon.MockAssertstateRefreshSnapAssertions(func(*state.State, int, *assertstate.RefreshAssertionsOptions) error { + return nil + }) + defer restore() + + restore = daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) { + goal := g.(*storeUpdateGoalRecorder) + c.Assert(goal.snaps, check.HasLen, 2) + + c.Check(goal.snaps, check.DeepEquals, []snapstate.StoreUpdate{ + { + InstanceName: "some-snap", + AdditionalComponents: []string{"some-comp1", "some-comp2"}, + }, + { + InstanceName: "other-snap", + AdditionalComponents: []string{"other-comp1"}, + }, + }) + + t := st.NewTask("fake-refresh-snap", "Doing a fake refresh") + return []string{"some-snap", "other-snap"}, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil + }) + defer restore() + + d := s.daemonWithFakeSnapManager(c) + + r := strings.NewReader(`{"action": "refresh", "snaps": ["some-snap", "other-snap"], "components": {"some-snap": ["some-comp1", "some-comp2"], "other-snap": ["other-comp1"]}}`) + req, err := http.NewRequest("POST", "/v2/snaps", r) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "application/json") + + rsp := s.asyncReq(c, req, nil) + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Status(), check.Equals, state.DoneStatus) + c.Check(err, check.IsNil) + c.Check(chg.Kind(), check.Equals, "refresh-snap") + c.Check(chg.Summary(), check.Equals, `Refresh snaps "some-snap" (with components "some-comp1", "some-comp2"), "other-snap" (with component "other-comp1")`) +} + func (s *snapsSuite) TestInstallWithComponentsSnapAlreadyInstalled(c *check.C) { defer daemon.MockSnapstateInstallComponents(func(ctx context.Context, st *state.State, names []string, info *snap.Info, opts snapstate.Options) ([]*state.TaskSet, error) { c.Check(names, check.DeepEquals, []string{"comp1", "comp2"}) diff --git a/daemon/api_systems.go b/daemon/api_systems.go index 4ba32982c36..73f14376f5c 100644 --- a/daemon/api_systems.go +++ b/daemon/api_systems.go @@ -451,7 +451,7 @@ func postSystemActionCreateOffline(c *Command, form *Form) Response { return BadRequest("cannot parse validation sets: %v", err) } - var snapFiles []*uploadedSnap + var snapFiles []*uploadedContainer if len(form.FileRefs["snap"]) > 0 { snaps, errRsp := form.GetSnapFiles() if errRsp != nil { @@ -488,15 +488,12 @@ func postSystemActionCreateOffline(c *Command, form *Form) Response { return apiErr } - if len(slInfo.sideInfos) != len(slInfo.tmpPaths) { - return InternalError("mismatch between number of snap side infos and temporary paths") - } - - localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.sideInfos)) - for i := range slInfo.sideInfos { + // TODO:COMPS: support adding components to a recovery system + localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.snaps)) + for _, sn := range slInfo.snaps { localSnaps = append(localSnaps, devicestate.LocalSnap{ - SideInfo: slInfo.sideInfos[i], - Path: slInfo.tmpPaths[i], + SideInfo: sn.sideInfo, + Path: sn.tmpPath, }) } diff --git a/daemon/export_test.go b/daemon/export_test.go index 37bc96a2673..aa3a9019d36 100644 --- a/daemon/export_test.go +++ b/daemon/export_test.go @@ -150,6 +150,22 @@ func MockSnapstateInstallWithGoal(mock func(ctx context.Context, st *state.State } } +func MockSnapstateUpdateWithGoal(mock func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error)) (restore func()) { + return testutil.Mock(&snapstateUpdateWithGoal, mock) +} + +func MockSnapstatePathUpdateGoal(mock func(snaps ...snapstate.PathSnap) snapstate.UpdateGoal) (restore func()) { + return testutil.Mock(&snapstatePathUpdateGoal, mock) +} + +func MockSnapstateUpdateOne(mock func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error)) (restore func()) { + old := snapstateUpdateOne + snapstateUpdateOne = mock + return func() { + snapstateUpdateOne = old + } +} + func MockSnapstateInstallComponents(mock func(ctx context.Context, st *state.State, names []string, info *snap.Info, opts snapstate.Options) ([]*state.TaskSet, error)) (restore func()) { old := snapstateInstallComponents snapstateInstallComponents = mock @@ -166,19 +182,19 @@ func MockSnapstateStoreInstallGoal(mock func(snaps ...snapstate.StoreSnap) snaps } } -func MockSnapstateInstallPath(mock func(*state.State, *snap.SideInfo, string, string, string, snapstate.Flags, snapstate.PrereqTracker) (*state.TaskSet, *snap.Info, error)) (restore func()) { - oldSnapstateInstallPath := snapstateInstallPath - snapstateInstallPath = mock +func MockSnapstateStoreUpdateGoal(mock func(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal) (restore func()) { + old := snapstateStoreUpdateGoal + snapstateStoreUpdateGoal = mock return func() { - snapstateInstallPath = oldSnapstateInstallPath + snapstateStoreUpdateGoal = old } } -func MockSnapstateUpdate(mock func(*state.State, string, *snapstate.RevisionOptions, int, snapstate.Flags) (*state.TaskSet, error)) (restore func()) { - oldSnapstateUpdate := snapstateUpdate - snapstateUpdate = mock +func MockSnapstateInstallPath(mock func(*state.State, *snap.SideInfo, string, string, string, snapstate.Flags, snapstate.PrereqTracker) (*state.TaskSet, *snap.Info, error)) (restore func()) { + oldSnapstateInstallPath := snapstateInstallPath + snapstateInstallPath = mock return func() { - snapstateUpdate = oldSnapstateUpdate + snapstateInstallPath = oldSnapstateInstallPath } } @@ -214,14 +230,6 @@ func MockSnapstateRevertToRevision(mock func(*state.State, string, snap.Revision } } -func MockSnapstateUpdateMany(mock func(context.Context, *state.State, []string, []*snapstate.RevisionOptions, int, *snapstate.Flags) ([]string, []*state.TaskSet, error)) (restore func()) { - oldSnapstateUpdateMany := snapstateUpdateMany - snapstateUpdateMany = mock - return func() { - snapstateUpdateMany = oldSnapstateUpdateMany - } -} - func MockSnapstateRemove(mock func(st *state.State, name string, revision snap.Revision, flags *snapstate.RemoveFlags) (*state.TaskSet, error)) (restore func()) { oldSnapstateRemove := snapstateRemove snapstateRemove = mock @@ -325,21 +333,18 @@ func MockReboot(f func(boot.RebootAction, time.Duration, *boot.RebootInfo) error func MockSideloadSnapsInfo(sis []*snap.SideInfo) (restore func()) { r := testutil.Backup(&sideloadSnapsInfo) - sideloadSnapsInfo = func(st *state.State, snapFiles []*uploadedSnap, + sideloadSnapsInfo = func(st *state.State, snapFiles []*uploadedContainer, flags sideloadFlags) (*sideloadedInfo, *apiError) { - names := make([]string, len(snapFiles)) - sideInfos := make([]*snap.SideInfo, len(snapFiles)) - origPaths := make([]string, len(snapFiles)) - tmpPaths := make([]string, len(snapFiles)) + var snaps []sideloadSnapInfo for i, snapFile := range snapFiles { - sideInfos[i] = sis[i] - names[i] = sis[i].RealName - origPaths[i] = snapFile.filename - tmpPaths[i] = snapFile.tmpPath + snaps = append(snaps, sideloadSnapInfo{ + sideInfo: sis[i], + origPath: snapFile.filename, + tmpPath: snapFile.tmpPath, + }) } - return &sideloadedInfo{sideInfos: sideInfos, names: names, - origPaths: origPaths, tmpPaths: tmpPaths}, nil + return &sideloadedInfo{snaps: snaps}, nil } return r } diff --git a/gadget/gadget.go b/gadget/gadget.go index 502f5c0afe1..202ac0215c5 100644 --- a/gadget/gadget.go +++ b/gadget/gadget.go @@ -55,6 +55,8 @@ const ( schemaMBR = "mbr" // schemaGPT identifies a GUID Partition Table partitioning schema schemaGPT = "gpt" + // schemaEMMC identifies a schema for eMMC + schemaEMMC = "emmc" SystemBoot = "system-boot" SystemData = "system-data" @@ -1108,6 +1110,24 @@ func asOffsetPtr(offs quantity.Offset) *quantity.Offset { return &offs } +func setVolumeStructureOffset(vs *VolumeStructure, startPtr *quantity.Offset) (next *quantity.Offset) { + if vs.Offset == nil && startPtr != nil { + var start quantity.Offset + if vs.Role != schemaMBR && *startPtr < NonMBRStartOffset { + start = NonMBRStartOffset + } else { + start = *startPtr + } + vs.Offset = &start + } + // We know the end of the structure only if we could define an offset + // and the size is fixed. + if vs.Offset != nil && vs.isFixedSize() { + return asOffsetPtr(*vs.Offset + quantity.Offset(vs.Size)) + } + return nil +} + func setImplicitForVolume(vol *Volume, model Model) error { rs := whichVolRuleset(model) if vol.HasPartial(PartialSchema) { @@ -1132,41 +1152,34 @@ func setImplicitForVolume(vol *Volume, model Model) error { previousEnd := asOffsetPtr(0) for i := range vol.Structure { + vs := &vol.Structure[i] + // set the VolumeName for the structure from the volume itself - vol.Structure[i].VolumeName = vol.Name + vs.VolumeName = vol.Name // Store index as we will reorder later - vol.Structure[i].YamlIndex = i + vs.YamlIndex = i // MinSize is Size if not explicitly set - if vol.Structure[i].MinSize == 0 { - vol.Structure[i].MinSize = vol.Structure[i].Size + if vs.MinSize == 0 { + vs.MinSize = vs.Size } // Set the pointer to the volume - vol.Structure[i].EnclosingVolume = vol + vs.EnclosingVolume = vol // set other implicit data for the structure - if err := setImplicitForVolumeStructure(&vol.Structure[i], rs, knownFsLabels, knownVfatFsLabels); err != nil { + if err := setImplicitForVolumeStructure(vs, rs, knownFsLabels, knownVfatFsLabels); err != nil { return err } // Set offset if it was not set (must be after setImplicitForVolumeStructure // so roles are good). This is possible only if the previous structure had // a well-defined end. - if vol.Structure[i].Offset == nil && previousEnd != nil { - var start quantity.Offset - if vol.Structure[i].Role != schemaMBR && *previousEnd < NonMBRStartOffset { - start = NonMBRStartOffset - } else { - start = *previousEnd - } - vol.Structure[i].Offset = &start - } - // We know the end of the structure only if we could define an offset - // and the size is fixed. - if vol.Structure[i].Offset != nil && vol.Structure[i].isFixedSize() { - previousEnd = asOffsetPtr(*vol.Structure[i].Offset + - quantity.Offset(vol.Structure[i].Size)) - } else { - previousEnd = nil + switch vol.Schema { + case schemaEMMC: + // For eMMC, we do not support partition offsets. The partitions + // are hardware partitions that act more like traditional disks. + vs.Offset = asOffsetPtr(0) + default: + previousEnd = setVolumeStructureOffset(vs, previousEnd) } } @@ -1287,11 +1300,17 @@ func fmtIndexAndName(idx int, name string) string { return fmt.Sprintf("#%v", idx) } +var validSchemaNames = []string{schemaMBR, schemaGPT, schemaEMMC} + +func isValidSchema(schema string) bool { + return strutil.ListContains(validSchemaNames, schema) +} + func validateVolume(vol *Volume) error { if !validVolumeName.MatchString(vol.Name) { return errors.New("invalid name") } - if !vol.HasPartial(PartialSchema) && vol.Schema != schemaGPT && vol.Schema != schemaMBR { + if !vol.HasPartial(PartialSchema) && !isValidSchema(vol.Schema) { return fmt.Errorf("invalid schema %q", vol.Schema) } @@ -1358,6 +1377,12 @@ func isMBR(vs *VolumeStructure) bool { } func validateCrossVolumeStructure(vol *Volume) error { + // eMMC have no traditional volumes, instead eMMC has the concept + // of hardware partitions that act like separate disks. + if vol.Schema == schemaEMMC { + return nil + } + previousEnd := quantity.Offset(0) // cross structure validation: // - relative offsets that reference other structures by name @@ -1413,6 +1438,19 @@ func validateOffsetWrite(s, firstStruct *VolumeStructure, volSize quantity.Size) return nil } +func contentCheckerCreate(vs *VolumeStructure, vol *Volume) func(*VolumeContent) error { + switch { + case vol.Schema == schemaEMMC: + return validateEMMCContent + case vs.HasFilesystem(): + return validateFilesystemContent + default: + // default to bare content checker if no filesystem + // is present + return validateBareContent + } +} + func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { if !vs.hasPartialSize() { if vs.Size == 0 { @@ -1439,20 +1477,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { return fmt.Errorf("invalid filesystem %q", vs.Filesystem) } - var contentChecker func(*VolumeContent) error - - if vs.HasFilesystem() { - contentChecker = validateFilesystemContent - } else { - contentChecker = validateBareContent - } + contentChecker := contentCheckerCreate(vs, vol) for i, c := range vs.Content { if err := contentChecker(&c); err != nil { return fmt.Errorf("invalid content #%v: %v", i, err) } } - if err := validateStructureUpdate(vs, vol); err != nil { + if err := validateStructureUpdate(vs); err != nil { return err } @@ -1462,6 +1494,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { return nil } +func validateStructureTypeEMMC(s string) error { + // for eMMC we don't support the type being set + if s != "" { + return errors.New(`type is not supported for "emmc" schema`) + } + return nil +} + func validateStructureType(s string, vol *Volume) error { // Type can be one of: // - "mbr" (backwards compatible) @@ -1473,6 +1513,11 @@ func validateStructureType(s string, vol *Volume) error { // Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID // example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + // eMMC volumes we treat differently + if vol.Schema == schemaEMMC { + return validateStructureTypeEMMC(s) + } + if s == "" { return errors.New(`type is not specified`) } @@ -1582,6 +1627,20 @@ func validateBareContent(vc *VolumeContent) error { return nil } +func validateEMMCContent(vc *VolumeContent) error { + if vc.Offset != nil || vc.Size != 0 { + return fmt.Errorf("cannot specify size or offset for content") + } + + if vc.UnresolvedSource != "" || vc.Target != "" { + return fmt.Errorf("cannot use non-image content for hardware partitions") + } + if vc.Image == "" { + return fmt.Errorf("missing image file name") + } + return nil +} + func validateFilesystemContent(vc *VolumeContent) error { if vc.Image != "" || vc.Offset != nil || vc.Size != 0 { return fmt.Errorf("cannot use image content for non-bare file system") @@ -1595,7 +1654,7 @@ func validateFilesystemContent(vc *VolumeContent) error { return nil } -func validateStructureUpdate(vs *VolumeStructure, v *Volume) error { +func validateStructureUpdate(vs *VolumeStructure) error { if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 { return errors.New("preserving files during update is not supported for non-filesystem structures") } diff --git a/gadget/gadget_emmc_test.go b/gadget/gadget_emmc_test.go new file mode 100644 index 00000000000..ad97fc3144d --- /dev/null +++ b/gadget/gadget_emmc_test.go @@ -0,0 +1,387 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package gadget_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/testutil" +) + +type gadgetYamlEMMCSuite struct { + testutil.BaseTest + + dir string + gadgetYamlPath string +} + +var _ = Suite(&gadgetYamlEMMCSuite{}) + +var mockEMMCGadgetYaml = []byte(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + - source: foo + target: / + my-emmc: + schema: emmc + structure: + - name: boot0 + size: 4M + content: + - image: boot0filename + - name: boot1 + size: 4M + content: + - image: boot1filename +`) + +func (s *gadgetYamlEMMCSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + s.dir = c.MkDir() + c.Assert(os.MkdirAll(filepath.Join(s.dir, "meta"), 0755), IsNil) + s.gadgetYamlPath = filepath.Join(s.dir, "meta", "gadget.yaml") +} + +func (s *gadgetYamlEMMCSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoID(c *C) { + err := os.WriteFile(s.gadgetYamlPath, []byte(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + my-emmc: + schema: emmc + id: test +`), 0644) + c.Assert(err, IsNil) + + info, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + + err = gadget.Validate(info, nil, nil) + c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "id" for eMMC schemas`) +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoBootloader(c *C) { + err := os.WriteFile(s.gadgetYamlPath, []byte(` +volumes: + volumename: + schema: mbr + my-emmc: + schema: emmc + bootloader: u-boot +`), 0644) + c.Assert(err, IsNil) + + info, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + + err = gadget.Validate(info, nil, nil) + c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "bootloader" for eMMC schemas`) +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoPartial(c *C) { + err := os.WriteFile(s.gadgetYamlPath, []byte(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + my-emmc: + schema: emmc + partial: [size] +`), 0644) + c.Assert(err, IsNil) + + info, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + + err = gadget.Validate(info, nil, nil) + c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "partial" content for eMMC schemas`) +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlOffsetNotSupportedForBoot(c *C) { + for _, t := range []string{"boot0", "boot1"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - image: boot0filename + offset: 1000 +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, `.*cannot specify size or offset for content`) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlSourceIsNotSupported(c *C) { + for _, t := range []string{"boot0", "boot1"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - source: hello.bin +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, `.*cannot use non-image content for hardware partitions`) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlImageMustBeSet(c *C) { + for _, t := range []string{"boot0", "boot1"} { + err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(` +volumes: + volumename: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: subdir/ + target: / + unpack: false + my-emmc: + schema: emmc + structure: + - name: %s + size: 4M + content: + - unpack: true +`, t)), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, ErrorMatches, `.*missing image file name`) + } +} + +func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlHappy(c *C) { + err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644) + c.Assert(err, IsNil) + + ginfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + expected := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "volumename": { + Name: "volumename", + Schema: "mbr", + Bootloader: "u-boot", + ID: "0C", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "volumename", + Label: "system-boot", + Role: "system-boot", // implicit + Offset: asOffsetPtr(12345), + OffsetWrite: mustParseGadgetRelativeOffset(c, "777"), + Size: 88888, + MinSize: 88888, + Type: "0C", + Filesystem: "vfat", + Content: []gadget.VolumeContent{ + { + UnresolvedSource: "subdir/", + Target: "/", + Unpack: false, + }, + { + UnresolvedSource: "foo", + Target: "/", + Unpack: false, + }, + }, + }, + }, + }, + "my-emmc": { + Name: "my-emmc", + Schema: "emmc", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "my-emmc", + Name: "boot0", + Offset: asOffsetPtr(0), + Size: 4 * 1024 * 1024, + MinSize: 4 * 1024 * 1024, + Content: []gadget.VolumeContent{ + { + Image: "boot0filename", + }, + }, + YamlIndex: 0, + }, { + VolumeName: "my-emmc", + Name: "boot1", + Offset: asOffsetPtr(0), + Size: 4 * 1024 * 1024, + MinSize: 4 * 1024 * 1024, + Content: []gadget.VolumeContent{ + { + Image: "boot1filename", + }, + }, + YamlIndex: 1, + }, + }, + }, + }, + } + gadget.SetEnclosingVolumeInStructs(expected.Volumes) + + c.Check(ginfo, DeepEquals, expected) +} + +func (s *gadgetYamlEMMCSuite) TestUpdateApplyHappy(c *C) { + err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644) + c.Assert(err, IsNil) + + oldInfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "boot0filename"), 1*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "boot1filename"), 1*quantity.SizeMiB, nil) + oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newInfo, err := gadget.ReadInfo(s.dir, coreMod) + c.Assert(err, IsNil) + // pretend we have an update + newInfo.Volumes["my-emmc"].Structure[1].Update.Edition = 1 + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "boot0filename"), 1*quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "boot1filename"), 2*quantity.SizeMiB, nil) + newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir := c.MkDir() + + restore := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, gm gadget.Model, gv map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + return map[string]map[int]gadget.StructureLocation{ + "volumename": { + 0: { + Device: "/dev/emmcblk0", + Offset: quantity.OffsetMiB, + RootMountPoint: "/run/mnt/ubuntu-boot", + }, + }, + "my-emmc": { + 0: { + Device: "/dev/emmcblk0boot0", + }, + 1: { + Device: "/dev/emmcblk0boot1", + }, + }, + }, map[string]map[int]*gadget.OnDiskStructure{ + "volumename": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volumename"]), + "my-emmc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["my-emmc"]), + }, nil + }) + defer restore() + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + fmt.Println("update-for-structure", loc, ps, fromPs) + updaterForStructureCalls++ + mu := &mockUpdater{} + + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(updaterForStructureCalls, Equals, 1) +} diff --git a/gadget/install/content.go b/gadget/install/content.go index 9d72459d2ad..28a383ddfaa 100644 --- a/gadget/install/content.go +++ b/gadget/install/content.go @@ -62,16 +62,35 @@ func makeFilesystem(params mkfsParams) error { return udevTrigger(params.Device) } +type mntfsParams struct { + NoExec bool + NoDev bool + NoSuid bool +} + +func (p *mntfsParams) flags() uintptr { + var flags uintptr + if p.NoDev { + flags |= syscall.MS_NODEV + } + if p.NoExec { + flags |= syscall.MS_NOEXEC + } + if p.NoSuid { + flags |= syscall.MS_NOSUID + } + return flags +} + // mountFilesystem mounts the filesystem on a given device with // filesystem type fs under the provided mount point directory. -func mountFilesystem(fsDevice, fs, mountpoint string) error { +func mountFilesystem(fsDevice, fs, mountpoint string, params mntfsParams) error { if err := os.MkdirAll(mountpoint, 0755); err != nil { return fmt.Errorf("cannot create mountpoint: %v", err) } - if err := sysMount(fsDevice, mountpoint, fs, 0, ""); err != nil { + if err := sysMount(fsDevice, mountpoint, fs, params.flags(), ""); err != nil { return fmt.Errorf("cannot mount filesystem %q at %q: %v", fsDevice, mountpoint, err) } - return nil } diff --git a/gadget/install/content_test.go b/gadget/install/content_test.go index d4dc65ca424..05593166d5f 100644 --- a/gadget/install/content_test.go +++ b/gadget/install/content_test.go @@ -49,8 +49,11 @@ type contentTestSuite struct { gadgetRoot string - mockMountPoint string - mockMountCalls []struct{ source, target, fstype string } + mockMountPoint string + mockMountCalls []struct { + source, target, fstype string + flags uintptr + } mockUnmountCalls []string mockMountErr error @@ -76,7 +79,10 @@ func (s *contentTestSuite) SetUpTest(c *C) { s.mockMountPoint = c.MkDir() restore := install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { - s.mockMountCalls = append(s.mockMountCalls, struct{ source, target, fstype string }{source, target, fstype}) + s.mockMountCalls = append(s.mockMountCalls, struct { + source, target, fstype string + flags uintptr + }{source, target, fstype, flags}) return s.mockMountErr }) s.AddCleanup(restore) @@ -498,17 +504,52 @@ func (s *contentTestSuite) TestMountFilesystem(c *C) { defer dirs.SetRootDir("") // mount a filesystem... - err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed")) + err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), install.MntfsParams{}) c.Assert(err, IsNil) // ...and check if it was mounted at the right mount point c.Check(s.mockMountCalls, HasLen, 1) - c.Check(s.mockMountCalls, DeepEquals, []struct{ source, target, fstype string }{ - {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat"}, + c.Check(s.mockMountCalls, DeepEquals, []struct { + source, target, fstype string + flags uintptr + }{ + {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat", 0}, }) // try again with mocked error s.mockMountErr = fmt.Errorf("mock mount error") - err = install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed")) + err = install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), install.MntfsParams{}) c.Assert(err, ErrorMatches, `cannot mount filesystem "/dev/node2" at ".*/run/mnt/ubuntu-seed": mock mount error`) } + +func (s *contentTestSuite) TestMountFilesystemOptions(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + tests := []struct { + params install.MntfsParams + expectedFlags uintptr + }{ + {install.MntfsParams{}, 0}, + {install.MntfsParams{NoExec: true}, syscall.MS_NOEXEC}, + {install.MntfsParams{NoDev: true}, syscall.MS_NODEV}, + {install.MntfsParams{NoSuid: true}, syscall.MS_NOSUID}, + } + + for _, t := range tests { + // reset calls + s.mockMountCalls = nil + + err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), t.params) + c.Assert(err, IsNil) + + // .. verify flags + c.Check(s.mockMountCalls, HasLen, 1) + c.Check(s.mockMountCalls, DeepEquals, []struct { + source, target, fstype string + flags uintptr + }{ + {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat", t.expectedFlags}, + }) + } +} diff --git a/gadget/install/export_test.go b/gadget/install/export_test.go index aae52eb3f93..c138e10daa4 100644 --- a/gadget/install/export_test.go +++ b/gadget/install/export_test.go @@ -29,6 +29,7 @@ import ( ) type MkfsParams = mkfsParams +type MntfsParams = mntfsParams var ( MakeFilesystem = makeFilesystem diff --git a/gadget/install/install.go b/gadget/install/install.go index 4603162ce25..0e2c266c9b3 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -400,7 +400,7 @@ func Run(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelSnapInfo, if options.Mount && vs.Label != "" && vs.HasFilesystem() { // fs is taken from gadget, as on disk one might be displayed as // crypto_LUKS, which is not useful for formatting. - if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs)); err != nil { + if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs), mntfsParams{}); err != nil { return nil, err } } @@ -592,7 +592,7 @@ func MountVolumes(onVolumes map[string]*gadget.Volume, encSetupData *EncryptionS // Device might have been encrypted device := deviceForMaybeEncryptedVolume(&part, encSetupData) - if err := mountFilesystem(device, part.LinuxFilesystem(), mntPt); err != nil { + if err := mountFilesystem(device, part.LinuxFilesystem(), mntPt, mntfsParams{}); err != nil { defer unmount() return "", nil, fmt.Errorf("cannot mount %q at %q: %v", device, mntPt, err) } @@ -788,7 +788,7 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS if options.Mount && vs.Label != "" && vs.HasFilesystem() { // fs is taken from gadget, as on disk one might be displayed as // crypto_LUKS, which is not useful for formatting. - if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs)); err != nil { + if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs), mntfsParams{}); err != nil { return nil, err } } diff --git a/gadget/validate.go b/gadget/validate.go index 5fb5576f416..1996cc56945 100644 --- a/gadget/validate.go +++ b/gadget/validate.go @@ -141,6 +141,12 @@ func ruleValidateVolumes(vols map[string]*Volume, model Model, extra *Validation } func ruleValidateVolume(vol *Volume, hasModes bool) error { + if vol.Schema == schemaEMMC { + if err := ruleValidateEMMCVolume(vol); err != nil { + return err + } + } + for idx, s := range vol.Structure { if err := ruleValidateVolumeStructure(&s, hasModes); err != nil { return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err) @@ -150,6 +156,30 @@ func ruleValidateVolume(vol *Volume, hasModes bool) error { return nil } +func ruleValidateEMMCVolume(vol *Volume) error { + // Only content, schema and name can be set currently for eMMC + if vol.Bootloader != "" { + return fmt.Errorf(`cannot set "bootloader" for eMMC schemas`) + } + if vol.ID != "" { + return fmt.Errorf(`cannot set "id" for eMMC schemas`) + } + if len(vol.Partial) != 0 { + return fmt.Errorf(`cannot set "partial" content for eMMC schemas`) + } + return nil +} + +func validateEMMCStructureName(vs *VolumeStructure) error { + if vs.EnclosingVolume.Schema != schemaEMMC { + return nil + } + if !strutil.ListContains(validEMMCVolumeNames, vs.Name) { + return fmt.Errorf("cannot use %q as emmc name, only %q is allowed", vs.Name, validEMMCVolumeNames) + } + return nil +} + func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error { var reservedLabels []string if hasModes { @@ -160,6 +190,9 @@ func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error { if err := validateReservedLabels(vs, reservedLabels); err != nil { return err } + if err := validateEMMCStructureName(vs); err != nil { + return err + } return nil } @@ -179,6 +212,9 @@ var ( ubuntuSeedLabel, ubuntuDataLabel, } + + // valid names for volumes under an eMMC schema + validEMMCVolumeNames = []string{"boot0", "boot1"} ) func validateReservedLabels(vs *VolumeStructure, reservedLabels []string) error { diff --git a/gadget/validate_test.go b/gadget/validate_test.go index 2e92e0dd098..e1022505d97 100644 --- a/gadget/validate_test.go +++ b/gadget/validate_test.go @@ -78,6 +78,7 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C) }, }, } + gadget.SetEnclosingVolumeInStructs(gi.Volumes) err := gadget.Validate(gi, tc.model, nil) if tc.err == "" { c.Check(err, IsNil) @@ -88,6 +89,35 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C) } +func (s *validateGadgetTestSuite) TestRuleValidateStructureEmmcNames(c *C) { + for _, tc := range []struct { + name, err string + model gadget.Model + }{ + {name: "some-name", err: `cannot use "some-name" as emmc name, only \["boot0" "boot1"\] is allowed`}, + {name: "boot0", err: ""}, + {name: "boot1", err: ""}, + } { + gi := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "emmc": { + Schema: "emmc", + Structure: []gadget.VolumeStructure{{ + Name: tc.name, + }}, + }, + }, + } + gadget.SetEnclosingVolumeInStructs(gi.Volumes) + err := gadget.Validate(gi, tc.model, nil) + if tc.err == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, ".*: "+tc.err) + } + } +} + // rolesYaml produces gadget metadata with volumes with structure withs the given // role if data, seed or save are != "-", and with their label set to the value func rolesYaml(c *C, data, seed, save string) *gadget.Info { diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index 219aa60d79e..0f519dd970e 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -104,6 +104,13 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error { if strings.HasSuffix(np, "/") { return fmt.Errorf(`%q cannot end with "/"`, np) } + if strings.HasSuffix(np, "@") { + // Variables in AppArmor profiles have the form `@{FOO}`. Since we're + // going to add `{,/,/**}` to the end of the path, we cannot have a + // trailing '@', else we'll end up with a path which ends with + // `@{,/,/**}`, which looks problematically like an AppArmor variable. + return fmt.Errorf(`%q cannot end with "@"`, np) + } p := filepath.Clean(np) if p != np { return fmt.Errorf("cannot use %q: try %q", np, filepath.Clean(np)) diff --git a/interfaces/builtin/custom_device.go b/interfaces/builtin/custom_device.go index e39abedc197..218b795ffa2 100644 --- a/interfaces/builtin/custom_device.go +++ b/interfaces/builtin/custom_device.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2022 Canonical Ltd + * Copyright (C) 2022-2024 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -51,8 +51,14 @@ var ( // A cryptic, uninformative error message that we use only on impossible code paths customDeviceInternalError = errors.New(`custom-device interface internal error`) - // Validating regexp for filesystem paths - customDevicePathRegexp = regexp.MustCompile(`^/[^"@]*$`) + // Validating regexp for filesystem paths. @ can appear in paths under + // /sys/devices for devices that are defined in the device tree (of the + // form device@address), so we need to support @ characters in paths. + // However, @{foo} is the format for variables in AppArmor, so we must + // disallow `@{`. For completeness, we allow paths with a trailing @ as + // well. This is not the case for common-files-derived interfaces, since + // these append {,/,/**} pattern to the end of filepath. + customDevicePathRegexp = regexp.MustCompile(`^/([^"@]|@[^{])*@?$`) // Validating regexp for udev device names. // We forbid: diff --git a/interfaces/builtin/custom_device_test.go b/interfaces/builtin/custom_device_test.go index 19de629758d..16bd01591d2 100644 --- a/interfaces/builtin/custom_device_test.go +++ b/interfaces/builtin/custom_device_test.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2022 Canonical Ltd + * Copyright (C) 2022-2024 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -72,9 +72,11 @@ slots: read-devices: - /dev/js* files: - write: [ /bar ] + write: [ /bar, /baz@qux, /trailing@ ] read: - /dev/input/by-id/* + - /dev/dma_heap/qcom,qseecom + - /sys/devices/platform/soc@0/soc@0:bus@30000000/30350000.ocotp-ctrl/imx-ocotp0/nvmem udev-tagging: - kernel: input/mice subsystem: input @@ -209,8 +211,12 @@ apps: `custom-device "devices" path contains invalid glob pattern "\*\*"`, }, { - "devices: [/dev/@foo]", - `custom-device "devices" path must start with / and cannot contain special characters.*`, + `devices: ["/dev/@{foo}"]`, + `custom-device "devices" path must start with /dev/ and cannot contain special characters.*`, + }, + { + `devices: ["/dev/@{foo"]`, + `custom-device "devices" path must start with /dev/ and cannot contain special characters.*`, }, { "devices: [/dev/foo|bar]", @@ -405,7 +411,11 @@ func (s *CustomDeviceInterfaceSuite) TestAppArmorSpec(c *C) { c.Check(plugSnippet, testutil.Contains, `"/dev/input/mice" rwk,`) c.Check(plugSnippet, testutil.Contains, `"/dev/js*" r,`) c.Check(plugSnippet, testutil.Contains, `"/bar" rw,`) + c.Check(plugSnippet, testutil.Contains, `"/baz@qux" rw,`) + c.Check(plugSnippet, testutil.Contains, `"/trailing@" rw,`) c.Check(plugSnippet, testutil.Contains, `"/dev/input/by-id/*" r,`) + c.Check(plugSnippet, testutil.Contains, `"/dev/dma_heap/qcom,qseecom" r,`) + c.Check(plugSnippet, testutil.Contains, `"/sys/devices/platform/soc@0/soc@0:bus@30000000/30350000.ocotp-ctrl/imx-ocotp0/nvmem" r,`) c.Check(slotSnippet, HasLen, 0) } diff --git a/interfaces/builtin/desktop_legacy.go b/interfaces/builtin/desktop_legacy.go index 90a2eb0e742..d2a8ee5417e 100644 --- a/interfaces/builtin/desktop_legacy.go +++ b/interfaces/builtin/desktop_legacy.go @@ -273,6 +273,42 @@ dbus (send) member=Lookup peer=(label=unconfined), +# dbusmenu +dbus (send) + bus=session + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} + interface=com.canonical.dbusmenu + member="{LayoutUpdated,ItemsPropertiesUpdated}" + peer=(label="{plasmashell,unconfined}"), + +dbus (receive) + bus=session + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} + interface="{com.canonical.dbusmenu,org.freedesktop.DBus.Properties}" + member=Get* + peer=(label="{plasmashell,unconfined}"), + +dbus (receive) + bus=session + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} + interface=com.canonical.dbusmenu + member="{AboutTo*,Event*}" + peer=(label="{plasmashell,unconfined}"), + +dbus (receive) + bus=session + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} + interface=org.freedesktop.DBus.Introspectable + member=Introspect + peer=(label="{plasmashell,unconfined}"), + +dbus (receive) + bus=session + path=/com/canonical/dbusmenu + interface=org.freedesktop.DBus.Properties + member=Get* + peer=(label="{plasmashell,unconfined}"), + # app-indicators dbus (send) bus=session diff --git a/interfaces/builtin/fwupd.go b/interfaces/builtin/fwupd.go index 22453148a38..70f25942180 100644 --- a/interfaces/builtin/fwupd.go +++ b/interfaces/builtin/fwupd.go @@ -154,6 +154,10 @@ const fwupdPermanentSlotAppArmor = ` /sys/devices/**/psp_vbflash rw, /sys/devices/**/psp_vbflash_status r, + # Required by plugin thunderbolt + /sys/devices/**/nvm_non_active*/nvmem a, + /sys/devices/**/nvm_active*/nvmem r, + # DBus accesses #include dbus (send) diff --git a/interfaces/builtin/mount_control.go b/interfaces/builtin/mount_control.go index 6f6f4daced1..44464b5a9a1 100644 --- a/interfaces/builtin/mount_control.go +++ b/interfaces/builtin/mount_control.go @@ -125,7 +125,6 @@ var allowedFilesystemSpecificMountOptions = map[string][]string{ "jfs": {"iocharset=", "resize=", "nointegrity", "integrity", "errors=", "noquota", "quota", "usrquota", "grpquota"}, "msdos": {"blocksize=", "uid=", "gid=", "umask=", "dmask=", "fmask=", "allow_utime=", "check=", "codepage=", "conv=", "cvf_format=", "cvf_option", "debug", "discard", "dos1xfloppy", "errors=", "fat=", "iocharset=", "nfs=", "tz=", "time_offset=", "quiet", "rodir", "showexec", "sys_immutable", "flush", "usefree", "dots", "nodots", "dotsOK="}, "nfs": {"nfsvers=", "vers=", "soft", "hard", "softreval", "nosoftreval", "intr", "nointr", "timeo=", "retrans=", "rsize=", "wsize=", "ac", "noac", "acregmin=", "acregmax=", "acdirmin=", "acdirmax=", "actimeo=", "bg", "fg", "nconnect=", "max_connect=", "rdirplus", "nordirplus", "retry=", "sec=", "sharecache", "nosharecache", "revsport", "norevsport", "lookupcache=", "fsc", "nofsc", "sloppy", "proto=", "udp", "tcp", "rdma", "port=", "mountport=", "mountproto=", "mounthost=", "mountvers=", "namlen=", "lock", "nolock", "cto", "nocto", "acl", "noacl", "local_lock=", "minorversion=", "clientaddr=", "migration", "nomigration"}, - "nfs4": {"nfsvers=", "vers=", "soft", "hard", "softreval", "nosoftreval", "intr", "nointr", "timeo=", "retrans=", "rsize=", "wsize=", "ac", "noac", "acregmin=", "acregmax=", "acdirmin=", "acdirmax=", "actimeo=", "bg", "fg", "nconnect=", "max_connect=", "rdirplus", "nordirplus", "retry=", "sec=", "sharecache", "nosharecache", "revsport", "norevsport", "lookupcache=", "fsc", "nofsc", "sloppy", "proto=", "minorversion=", "port=", "cto", "nocto", "clientaddr=", "migration", "nomigration"}, "ntfs": {"iocharset=", "nls=", "utf8", "uni_xlate=", "posix=", "uid=", "gid=", "umask="}, "ntfs-3g": {"acl", "allow_other", "big_writes", "compression", "debug", "delay_mtime", "delay_mtime=", "dmask=", "efs_raw", "fmask=", "force", "hide_dot_files", "hide_hid_files", "inherit", "locale=", "max_read=", "no_def_opts", "no_detach", "nocompression", "norecover", "permissions", "posix_nlink", "recover", "remove_hiberfile", "show_sys_files", "silent", "special_files=", "streams_interface=", "uid=", "gid=", "umask=", "usermapping=", "user_xattr", "windows_names"}, "lowntfs-3g": {"acl", "allow_other", "big_writes", "compression", "debug", "delay_mtime", "delay_mtime=", "dmask=", "efs_raw", "fmask=", "force", "hide_dot_files", "hide_hid_files", "ignore_case", "inherit", "locale=", "max_read=", "no_def_opts", "no_detach", "nocompression", "norecover", "permissions", "posix_nlink", "recover", "remove_hiberfile", "show_sys_files", "silent", "special_files=", "streams_interface=", "uid=", "gid=", "umask=", "usermapping=", "user_xattr", "windows_names"}, @@ -212,6 +211,13 @@ var disallowedFSTypes = []string{ "tracefs", } +// THe filesystems which are considered deprecated and for which a better +// alternative exists. +var deprecatedFSTypes = []string{ + // use "nfs" + "nfs4", +} + // mountControlInterface allows creating transient and persistent mounts type mountControlInterface struct { commonInterface @@ -244,6 +250,11 @@ type mountControlInterface struct { // nearly any path, and due to the super-privileged nature of this interface it // is expected that sensible values of what are enforced by the store manual // review queue and security teams. +// +// Certain filesystem types impose additional restrictions on the allowed values +// for "what" attribute: +// - "tmpfs" - "what" must be set to "none" +// - "nfs" - "what" must be unset var ( whatRegexp = regexp.MustCompile(`^(none|/[^"@]*)$`) whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`) @@ -253,8 +264,16 @@ var ( // malicious string like // // auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=( +// +// The "type" attribute is an optional list of expected filesystem types. It is +// most useful in situations when it is known upfront that only a handful of +// types are accepted for a given mount. var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`) +// Because of additional rules imposed on mount attributes, some filesystems can +// only be specified as a single "type" entry. +var exclusiveFsTypes = []string{"tmpfs", "nfs"} + type MountInfo struct { what string where string @@ -298,8 +317,18 @@ func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error } for _, mount := range mounts { + types, err := parseStringList(mount, "type") + if err != nil { + return err + } + + disallowSource := false + if strutil.ListContains(types, "nfs") || strutil.ListContains(types, "nfs4") { + disallowSource = true + } + what, ok := mount["what"].(string) - if !ok { + if !ok && !disallowSource { return fmt.Errorf(`mount-control "what" must be a string`) } @@ -316,11 +345,6 @@ func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error } } - types, err := parseStringList(mount, "type") - if err != nil { - return err - } - options, err := parseStringList(mount, "options") if err != nil { return err @@ -360,6 +384,14 @@ func validateWhatAttr(mountInfo *MountInfo) error { return validateNoAppArmorRegexpWithError(`cannot use mount-control "what" attribute`, what) } + if mountInfo.isType("nfs") { + if what != "" { + return fmt.Errorf(`mount-control "what" attribute must not be specified for nfs mounts`) + } + // that's it for nfs + return nil + } + if !whatRegexp.MatchString(what) { return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`) } @@ -404,22 +436,33 @@ func validateWhereAttr(where string) error { } func validateMountTypes(types []string) error { - includesTmpfs := false + exclusiveFsType := "" + + // multiple types specified in "type" are useful when the accepted + // filesystem type is known upfront or the mount uses one of the special + // types, such as "nfs" or "tmpfs" for _, t := range types { if !typeRegexp.MatchString(t) { return fmt.Errorf(`mount-control filesystem type invalid: %q`, t) } + if strutil.ListContains(disallowedFSTypes, t) { return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t) } - if t == "tmpfs" { - includesTmpfs = true + + if strutil.ListContains(deprecatedFSTypes, t) { + return fmt.Errorf(`mount-control deprecated filesystem type: %q`, t) + } + + if exclusiveFsType == "" && strutil.ListContains(exclusiveFsTypes, t) { + exclusiveFsType = t } } - if includesTmpfs && len(types) > 1 { - return errors.New(`mount-control filesystem type "tmpfs" cannot be listed with other types`) + if exclusiveFsType != "" && len(types) > 1 { + return fmt.Errorf(`mount-control filesystem type %q cannot be listed with other types`, exclusiveFsType) } + return nil } @@ -480,15 +523,15 @@ func isAllowedFilesystemSpecificMountOption(types []string, optionName string) b } func validateMountInfo(mountInfo *MountInfo) error { - if err := validateWhatAttr(mountInfo); err != nil { + if err := validateMountTypes(mountInfo.types); err != nil { return err } - if err := validateWhereAttr(mountInfo.where); err != nil { + if err := validateWhatAttr(mountInfo); err != nil { return err } - if err := validateMountTypes(mountInfo.types); err != nil { + if err := validateWhereAttr(mountInfo.where); err != nil { return err } @@ -583,6 +626,15 @@ func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specifi target = expanded + target[variableEnd:] } + if mountInfo.isType("nfs") { + // override NFS share source, also see 'nfs-mount' interface + source = "*:**" + + // emit additional rule required by NFS + emit(" # Allow lookup of RPC program numbers (due to mount-control)\n") + emit(" /etc/rpc r,\n") + } + var typeRule string if optionIncompatibleWithFsType(mountInfo.options) != "" { // In this rule the FS type will not match unless it's empty diff --git a/interfaces/builtin/mount_control_test.go b/interfaces/builtin/mount_control_test.go index b1824a52b51..45b8681aa70 100644 --- a/interfaces/builtin/mount_control_test.go +++ b/interfaces/builtin/mount_control_test.go @@ -78,6 +78,9 @@ plugs: where: $SNAP_COMMON/mnt/** type: [aufs] options: [br:/mnt/a, add:0:/mnt/b, dirwh=1, rw] + - type: [nfs] + where: /media/foo/** + options: [rw] apps: app: plugs: [mntctl] @@ -307,6 +310,30 @@ func (s *MountControlInterfaceSuite) TestSanitizePlugUnhappy(c *C) { "mount:\n - what: diag\n where: /dev/ffs-diag\n type: [functionfs]\n options: [rw,uid=*]", `cannot use mount-control "option" attribute: "uid=\*" contains a reserved apparmor char from.*`, }, + { + "mount:\n - what: diag\n where: /media/foo\n type: [nfs]\n options: [rw]", + `mount-control "what" attribute must not be specified for nfs mounts.*`, + }, + { + "mount:\n - where: /media/foo\n type: [nfs, ext4]\n options: [rw]", + `mount-control filesystem type "nfs" cannot be listed with other types`, + }, + { + "mount:\n - where: /media/foo\n type: [tmpfs, nfs, ext4]\n options: [rw]", + `mount-control filesystem type "tmpfs" cannot be listed with other types`, + }, + { + "mount:\n - what: 123\n where: /media/foo\n type: [ext4]\n options: [rw]", + `mount-control "what" must be a string`, + }, + // the deprecated nfs4 isn't explicitly forbidden, but it is not + // possible construct a valid and useful specification using this + // type + { + // deprecated nfs4 + "mount:\n - where: /media/foo\n type: [nfs4]\n options: [rw]", + `mount-control deprecated filesystem type: "nfs4"`, + }, } for _, testData := range data { @@ -376,6 +403,14 @@ func (s *MountControlInterfaceSuite) TestAppArmorSpec(c *C) { c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine7) expectedUmountLine7 := `umount "/var/snap/consumer/common/mnt/**{,/}",` c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine7) + + expectedMountLine8 := `mount fstype=(nfs) options=(rw) ` + + `"*:**" -> "/media/foo/**{,/}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine8) + expectedUmountLine8 := `umount "/media/foo/**{,/}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine8) + expectedExtraLine8 := ` /etc/rpc r,` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedExtraLine8) } func (s *MountControlInterfaceSuite) TestStaticInfo(c *C) { diff --git a/interfaces/builtin/mpris.go b/interfaces/builtin/mpris.go index e3dc6814c20..2251c52b2d9 100644 --- a/interfaces/builtin/mpris.go +++ b/interfaces/builtin/mpris.go @@ -61,27 +61,27 @@ dbus (send) path=/org/freedesktop/DBus interface=org.freedesktop.DBus member="{Request,Release}Name" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"), dbus (send) bus=system path=/org/freedesktop/DBus interface=org.freedesktop.DBus member="GetConnectionUnix{ProcessID,User}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"), dbus (send) bus=session path=/org/mpris/MediaPlayer2 interface=org.freedesktop.DBus.Properties member="{GetAll,PropertiesChanged}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"), dbus (send) bus=session path=/org/mpris/MediaPlayer2 interface="org.mpris.MediaPlayer2{,.Player}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"), # we can always connect to ourselves dbus (receive) @@ -122,11 +122,11 @@ const mprisConnectedSlotAppArmorClassic = ` dbus (receive) bus=session path=/org/mpris/MediaPlayer2 - peer=(label=unconfined), + peer=(label="{plasmashell,unconfined}"), dbus (receive) bus=session interface=org.freedesktop.DBus.Introspectable - peer=(label=unconfined), + peer=(label="{plasmashell,unconfined}"), ` const mprisConnectedPlugAppArmor = ` @@ -139,19 +139,19 @@ dbus (send) bus=session path=/org/freedesktop/DBus interface=org.freedesktop.DBus.Introspectable - peer=(name="org.freedesktop.DBus", label="unconfined"), + peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"), dbus (send) bus=session path=/{,org,org/mpris,org/mpris/MediaPlayer2} interface=org.freedesktop.DBus.Introspectable - peer=(name="org.freedesktop.DBus", label="unconfined"), + peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"), # This reveals all names on the session bus dbus (send) bus=session path=/ interface=org.freedesktop.DBus member=ListNames - peer=(name="org.freedesktop.DBus", label="unconfined"), + peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"), # Communicate with the mpris player dbus (send) diff --git a/interfaces/builtin/opengl.go b/interfaces/builtin/opengl.go index 71aecbbd0d2..c66fe38f7a7 100644 --- a/interfaces/builtin/opengl.go +++ b/interfaces/builtin/opengl.go @@ -193,6 +193,9 @@ unix (send, receive) type=dgram peer=(addr="@var/run/nvidia-xdriver-*"), /dev/nvgpu/igpu[0-9]*/ctrl rw, /dev/nvgpu/igpu[0-9]*/prof rw, /dev/host1x-fence rw, + +# Kernel Fusion Driver for AMD GPUs +/dev/kfd rw, ` type openglInterface struct { @@ -224,6 +227,9 @@ var openglConnectedPlugUDev = []string{ // Nvidia dma barrier `SUBSYSTEM=="host1x-fence"`, + + // Kernel Fusion Driver + `SUBSYSTEM=="kfd", KERNEL=="kfd"`, } // Those two are the same, but in theory they are separate and can move (or diff --git a/interfaces/builtin/opengl_test.go b/interfaces/builtin/opengl_test.go index ebefb3322de..ba6202915d8 100644 --- a/interfaces/builtin/opengl_test.go +++ b/interfaces/builtin/opengl_test.go @@ -93,6 +93,7 @@ func (s *OpenglInterfaceSuite) TestAppArmorSpec(c *C) { spec := apparmor.NewSpecification(appSet) c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/kfd rw,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/nvidia* rw,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/dri/renderD[0-9]* rw,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/mali[0-9]* rw,`) @@ -118,7 +119,7 @@ func (s *OpenglInterfaceSuite) TestUDevSpec(c *C) { c.Assert(err, IsNil) spec := udev.NewSpecification(appSet) c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) - c.Assert(spec.Snippets(), HasLen, 19) + c.Assert(spec.Snippets(), HasLen, 20) c.Assert(spec.Snippets(), testutil.Contains, `# opengl SUBSYSTEM=="drm", KERNEL=="card[0-9]*", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# opengl @@ -143,6 +144,8 @@ KERNEL=="mali[0-9]*", TAG+="snap_consumer_app"`) KERNEL=="dma_buf_te", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# opengl KERNEL=="galcore", TAG+="snap_consumer_app"`) + c.Assert(spec.Snippets(), testutil.Contains, `# opengl +SUBSYSTEM=="kfd", KERNEL=="kfd", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", SUBSYSTEM!="module", SUBSYSTEM!="subsystem", RUN+="%v/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) } diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go index 5b8ff11fffd..3bd793b630c 100644 --- a/interfaces/builtin/personal_files_test.go +++ b/interfaces/builtin/personal_files_test.go @@ -244,6 +244,7 @@ plugs: {`read: [ "$HOME/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "\$HOME/home/\$HOME/foo"`}, {`read: [ "$HOME/sweet/$HOME" ]`, `\$HOME must only be used at the start of the path of "\$HOME/sweet/\$HOME"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, + {`read: [ "/foo/bar@" ]`, `"/foo/bar@" cannot end with "@"`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, {`read: [ "${HOME}/foo" ]`, `"\${HOME}/foo" contains a reserved apparmor char from .*`}, {`read: [ "$HOME" ]`, `"\$HOME" must start with "\$HOME/"`}, diff --git a/interfaces/builtin/screen_inhibit_control.go b/interfaces/builtin/screen_inhibit_control.go index 2509c856770..b0e263695d9 100644 --- a/interfaces/builtin/screen_inhibit_control.go +++ b/interfaces/builtin/screen_inhibit_control.go @@ -19,13 +19,27 @@ package builtin +import ( + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" +) + const screenInhibitControlSummary = `allows inhibiting the screen saver` const screenInhibitBaseDeclarationSlots = ` screen-inhibit-control: allow-installation: slot-snap-type: + - app - core + deny-auto-connection: + slot-snap-type: + - app + deny-connection: + slot-snap-type: + - app ` const screenInhibitControlConnectedPlugAppArmor = ` @@ -39,7 +53,7 @@ dbus (send) path=/org/gnome/SessionManager interface=org.gnome.SessionManager member={Inhibit,Uninhibit} - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), # unity screen API dbus (send) @@ -47,13 +61,13 @@ dbus (send) interface="org.freedesktop.DBus.Introspectable" path="/com/canonical/Unity/Screen" member="Introspect" - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), dbus (send) bus=system interface="com.canonical.Unity.Screen" path="/com/canonical/Unity/Screen" member={keepDisplayOn,removeDisplayOnRequest} - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), # freedesktop.org ScreenSaver # compatibility rule @@ -62,7 +76,7 @@ dbus (send) path=/Screensaver interface=org.freedesktop.ScreenSaver member={Inhibit,UnInhibit,SimulateUserActivity} - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), # xfce4-power-manager - # https://github.com/xfce-mirror/xfce4-power-manager/blob/0b3ad06ad4f51eae1aea3cdc26f434d8b5ce763e/src/org.freedesktop.PowerManagement.Inhibit.xml @@ -71,7 +85,13 @@ dbus (send) path=/org/freedesktop/PowerManagement/Inhibit interface=org.freedesktop.PowerManagement.Inhibit member={Inhibit,UnInhibit} - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), +dbus (receive) + bus=session + path=/org/freedesktop/PowerManagement/Inhibit + interface=org.freedesktop.PowerManagement.Inhibit + member=HasInhibitChanged + peer=(label=###SLOT_SECURITY_TAGS###), # API rule dbus (send) @@ -79,7 +99,7 @@ dbus (send) path=/{,org/freedesktop/,org/gnome/}ScreenSaver interface=org.{freedesktop,gnome}.ScreenSaver member={Inhibit,UnInhibit,SimulateUserActivity} - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), # gnome, kde and cinnamon screensaver dbus (send) @@ -87,15 +107,111 @@ dbus (send) path=/{,ScreenSaver} interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver} member=SimulateUserActivity - peer=(label=unconfined), + peer=(label=###SLOT_SECURITY_TAGS###), +` + +const screenInhibitControlConnectedSlotAppArmor = ` +# Description: Can inhibit and uninhibit screen savers in desktop sessions. +#include +#include + +# gnome-session +dbus (receive) + bus=session + path=/org/gnome/SessionManager + interface=org.gnome.SessionManager + member={Inhibit,Uninhibit} + peer=(label=###PLUG_SECURITY_TAGS###), + +# unity screen API +dbus (receive) + bus=system + interface="org.freedesktop.DBus.Introspectable" + path="/com/canonical/Unity/Screen" + member="Introspect" + peer=(label=###PLUG_SECURITY_TAGS###), +dbus (receive) + bus=system + interface="com.canonical.Unity.Screen" + path="/com/canonical/Unity/Screen" + member={keepDisplayOn,removeDisplayOnRequest} + peer=(label=###PLUG_SECURITY_TAGS###), + +# freedesktop.org ScreenSaver +# compatibility rule +dbus (receive) + bus=session + path=/Screensaver + interface=org.freedesktop.ScreenSaver + member={Inhibit,UnInhibit,SimulateUserActivity} + peer=(label=###PLUG_SECURITY_TAGS###), + +# xfce4-power-manager - +# https://github.com/xfce-mirror/xfce4-power-manager/blob/0b3ad06ad4f51eae1aea3cdc26f434d8b5ce763e/src/org.freedesktop.PowerManagement.Inhibit.xml +dbus (receive) + bus=session + path=/org/freedesktop/PowerManagement/Inhibit + interface=org.freedesktop.PowerManagement.Inhibit + member={Inhibit,UnInhibit} + peer=(label=###PLUG_SECURITY_TAGS###), +dbus (send) + bus=session + path=/org/freedesktop/PowerManagement/Inhibit + interface=org.freedesktop.PowerManagement.Inhibit + member=HasInhibitChanged + peer=(label=###PLUG_SECURITY_TAGS###), + +# API rule +dbus (receive) + bus=session + path=/{,org/freedesktop/,org/gnome/}ScreenSaver + interface=org.{freedesktop,gnome}.ScreenSaver + member={Inhibit,UnInhibit,SimulateUserActivity} + peer=(label=###PLUG_SECURITY_TAGS###), + +# gnome, kde and cinnamon screensaver +dbus (receive) + bus=session + path=/{,ScreenSaver} + interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver} + member=SimulateUserActivity + peer=(label=###PLUG_SECURITY_TAGS###), ` +type screenInhibitControlInterface struct { + commonInterface +} + +func (iface *screenInhibitControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + old := "###SLOT_SECURITY_TAGS###" + var new string + if implicitSystemConnectedSlot(slot) { + // we are running on a system that has the screen-inhibit-control slot + // provided by the OS snap and so will run unconfined + new = "unconfined" + } else { + new = slot.LabelExpression() + } + snippet := strings.Replace(screenInhibitControlConnectedPlugAppArmor, old, new, -1) + spec.AddSnippet(snippet) + return nil +} + +func (iface *screenInhibitControlInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + old := "###PLUG_SECURITY_TAGS###" + var new = plug.LabelExpression() + snippet := strings.Replace(screenInhibitControlConnectedSlotAppArmor, old, new, -1) + spec.AddSnippet(snippet) + return nil +} + func init() { - registerIface(&commonInterface{ - name: "screen-inhibit-control", - summary: screenInhibitControlSummary, - implicitOnClassic: true, - baseDeclarationSlots: screenInhibitBaseDeclarationSlots, - connectedPlugAppArmor: screenInhibitControlConnectedPlugAppArmor, + registerIface(&screenInhibitControlInterface{ + commonInterface: commonInterface{ + name: "screen-inhibit-control", + summary: screenInhibitControlSummary, + implicitOnClassic: true, + baseDeclarationSlots: screenInhibitBaseDeclarationSlots, + }, }) } diff --git a/interfaces/builtin/system_files_test.go b/interfaces/builtin/system_files_test.go index 7aa69ff954d..5ab02935498 100644 --- a/interfaces/builtin/system_files_test.go +++ b/interfaces/builtin/system_files_test.go @@ -50,7 +50,7 @@ version: 1.0 plugs: system-files: read: [/etc/read-dir2, /etc/read-file2] - write: [/etc/write-dir2, /etc/write-file2] + write: [/etc/write-dir2, /etc/write-file2, /dev/foo@bar] apps: app: command: foo @@ -83,6 +83,7 @@ func (s *systemFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { "/etc/read-file2{,/,/**}" rk, "/etc/write-dir2{,/,/**}" rwkl, "/etc/write-file2{,/,/**}" rwkl, +"/dev/foo@bar{,/,/**}" rwkl, `) } @@ -133,6 +134,7 @@ plugs: {`read: [ "$HOME/sweet/$HOME" ]`, `"\$HOME/sweet/\$HOME" must start with "/"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, + {`read: [ "/foo/bar@" ]`, `"/foo/bar@" cannot end with "@"`}, } for _, t := range testCases { diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index b4fbd7ec2ca..290e4be879d 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -143,19 +143,20 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) { // these have more complex or in flux policies and have their // own separate tests snowflakes := map[string]bool{ - "content": true, - "core-support": true, - "desktop": true, - "home": true, - "lxd-support": true, - "microstack-support": true, - "multipass-support": true, - "packagekit-control": true, - "pkcs11": true, - "remoteproc": true, - "snapd-control": true, - "upower-observe": true, - "empty": true, + "content": true, + "core-support": true, + "desktop": true, + "home": true, + "lxd-support": true, + "microstack-support": true, + "multipass-support": true, + "packagekit-control": true, + "pkcs11": true, + "remoteproc": true, + "screen-inhibit-control": true, + "snapd-control": true, + "upower-observe": true, + "empty": true, } // these simply auto-connect, anything else doesn't @@ -173,7 +174,6 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) { "opengl": true, "optical-drive": true, "ros-opt-data": true, - "screen-inhibit-control": true, "ubuntu-download-manager": true, "unity7": true, "unity8": true, @@ -205,8 +205,9 @@ func (s *baseDeclSuite) TestAutoConnectionImplicitSlotOnly(c *C) { // these auto-connect only with an implicit slot autoconnect := map[string]bool{ - "desktop": true, - "upower-observe": true, + "desktop": true, + "screen-inhibit-control": true, + "upower-observe": true, } for _, iface := range all { @@ -852,6 +853,7 @@ var ( "sd-control": {"core"}, "serial-port": {"core", "gadget"}, "spi": {"core", "gadget"}, + "screen-inhibit-control": {"core", "app"}, "steam-support": {"core"}, "storage-framework-service": {"app"}, "thumbnailer-service": {"app"}, @@ -1109,6 +1111,7 @@ func (s *baseDeclSuite) TestConnection(c *C) { "posix-mq": true, "qualcomm-ipc-router": true, "raw-volume": true, + "screen-inhibit-control": true, "shared-memory": true, "storage-framework-service": true, "thumbnailer-service": true, @@ -1139,9 +1142,10 @@ func (s *baseDeclSuite) TestConnectionImplicitSlotOnly(c *C) { // these allow connect only with an implicit slot autoconnect := map[string]bool{ - "desktop": true, - "qualcomm-ipc-router": true, - "upower-observe": true, + "desktop": true, + "qualcomm-ipc-router": true, + "screen-inhibit-control": true, + "upower-observe": true, } for _, iface := range all { @@ -1334,6 +1338,7 @@ func (s *baseDeclSuite) TestValidity(c *C) { "polkit-agent": true, "remoteproc": true, "qualcomm-ipc-router": true, + "screen-inhibit-control": true, "sd-control": true, "shutdown": true, "shared-memory": true, @@ -1884,3 +1889,35 @@ plugs: err = ic.Check() c.Assert(err, IsNil) } + +func (s *baseDeclSuite) TestConnectionScreenInhibitControl(c *C) { + cand := s.connectCand(c, "screen-inhibit-control", "", "") + err := cand.Check() + c.Assert(err, ErrorMatches, `connection denied by slot rule of interface "screen-inhibit-control"`) + + plugsSlots := ` +plugs: + screen-inhibit-control: + allow-connection: true +` + snapDecl := s.mockSnapDecl(c, "some-snap", "some-snap", "canonical", plugsSlots) + cand.PlugSnapDeclaration = snapDecl + err = cand.Check() + c.Assert(err, IsNil) +} + +func (s *baseDeclSuite) TestAutoConnectionScreenInhibitControl(c *C) { + cand := s.connectCand(c, "screen-inhibit-control", "", "") + _, err := cand.CheckAutoConnect() + c.Assert(err, ErrorMatches, "auto-connection denied by slot rule of interface \"screen-inhibit-control\"") + + plugsSlots := ` +plugs: + screen-inhibit-control: + allow-auto-connection: true +` + snapDecl := s.mockSnapDecl(c, "some-snap", "some-snap", "canonical", plugsSlots) + cand.PlugSnapDeclaration = snapDecl + _, err = cand.CheckAutoConnect() + c.Check(err, IsNil) +} diff --git a/overlord/hookstate/ctlcmd/mount.go b/overlord/hookstate/ctlcmd/mount.go index 65cb8437ce8..91daf84cb68 100644 --- a/overlord/hookstate/ctlcmd/mount.go +++ b/overlord/hookstate/ctlcmd/mount.go @@ -72,17 +72,29 @@ func matchMountPathAttribute(path string, attribute interface{}, snapInfo *snap. return err == nil && pp.Matches(path) } -// matchConnection checks whether the given mount connection attributes give -// the snap permission to execute the mount command -func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool { - if !matchMountPathAttribute(m.Positional.What, attributes["what"], m.snapInfo) { - return false - } +func matchMountSourceAttribute(path string, attribute interface{}, fsType string, snapInfo *snap.Info) bool { + if fsType == "nfs" { + // NFS mount source AppArmor profiles expects a match for "*:**", so + // make sure that the attribute is unset, and the path matches the + // format + if _, ok := attribute.(string); ok { + return false + } - if !matchMountPathAttribute(m.Positional.Where, attributes["where"], m.snapInfo) { - return false + host, share, found := strings.Cut(path, ":") + if !found || host == "" || strings.Contains(host, "/") || share == "" { + return false + } + + return true } + return matchMountPathAttribute(path, attribute, snapInfo) +} + +// matchConnection checks whether the given mount connection attributes give +// the snap permission to execute the mount command +func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool { if m.Type != "" { if types, ok := attributes["type"].([]interface{}); ok { found := false @@ -106,6 +118,14 @@ func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool { } } + if !matchMountSourceAttribute(m.Positional.What, attributes["what"], m.Type, m.snapInfo) { + return false + } + + if !matchMountPathAttribute(m.Positional.Where, attributes["where"], m.snapInfo) { + return false + } + if optionsIfaces, ok := attributes["options"].([]interface{}); ok { var allowedOptions []string for _, iface := range optionsIfaces { diff --git a/overlord/hookstate/ctlcmd/mount_test.go b/overlord/hookstate/ctlcmd/mount_test.go index ff7783c3463..44f24e55738 100644 --- a/overlord/hookstate/ctlcmd/mount_test.go +++ b/overlord/hookstate/ctlcmd/mount_test.go @@ -143,6 +143,11 @@ func (s *mountSuite) SetUpTest(c *C) { "options": []string{"ro"}, "persistent": false, }, + map[string]interface{}{ + "where": "/nfs-dest", + "options": []string{"rw"}, + "type": []string{"nfs"}, + }, }, }, } @@ -270,6 +275,15 @@ func (s *mountSuite) TestMissingProperPlug(c *C) { _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "--persistent", "-o", "bind,rw", "/src", "/dest"}, 0) c.Check(err, ErrorMatches, `.*no matching mount-control connection found`) c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, HasLen, 0) + + // bad NFS source format + _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "/src", "/dest"}, 0) + c.Check(err, ErrorMatches, `.*no matching mount-control connection found`) + _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "/host:/src", "/dest"}, 0) + c.Check(err, ErrorMatches, `.*no matching mount-control connection found`) + _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", ":/share", "/dest"}, 0) + c.Check(err, ErrorMatches, `.*no matching mount-control connection found`) + c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, HasLen, 0) } func (s *mountSuite) TestUnitCreationFailure(c *C) { @@ -353,6 +367,27 @@ func (s *mountSuite) TestHappyWithCommasInPath(c *C) { }) } +func (s *mountSuite) TestHappyNFS(c *C) { + s.injectSnapWithProperPlug(c) + + s.sysd.EnsureMountUnitFileWithOptionsResult = ResultForEnsureMountUnitFileWithOptions{"/path/unit.mount", nil} + + // Now try with commas in the paths + _, _, err := ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "localhost:/var/share", "/nfs-dest"}, 0) + c.Check(err, IsNil) + c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, DeepEquals, []*systemd.MountUnitOptions{ + { + Lifetime: systemd.Transient, + Description: "Mount unit for snap1, revision 1 via mount-control", + What: "localhost:/var/share", + Where: "/nfs-dest", + Fstype: "nfs", + Options: []string{"rw"}, + Origin: "mount-control", + }, + }) +} + func (s *mountSuite) TestEnsureMountUnitFailed(c *C) { s.injectSnapWithProperPlug(c) diff --git a/overlord/hookstate/ctlcmd/refresh.go b/overlord/hookstate/ctlcmd/refresh.go index 91af2d8ec31..8c33a75a93c 100644 --- a/overlord/hookstate/ctlcmd/refresh.go +++ b/overlord/hookstate/ctlcmd/refresh.go @@ -365,7 +365,7 @@ func (c *refreshCommand) printInhibitLockHint() error { } defer lock.Unlock() - hint, _, err := runinhibit.IsLocked(snapName) + hint, _, err := runinhibit.IsLocked(snapName, nil) if err != nil { return err } diff --git a/overlord/hookstate/ctlcmd/refresh_test.go b/overlord/hookstate/ctlcmd/refresh_test.go index 0e72badf4e6..f9713ed162e 100644 --- a/overlord/hookstate/ctlcmd/refresh_test.go +++ b/overlord/hookstate/ctlcmd/refresh_test.go @@ -511,7 +511,7 @@ func (s *refreshSuite) TestRefreshPrintInhibitHint(c *C) { err = lock.Lock() c.Assert(err, IsNil) inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(1)} - c.Check(runinhibit.LockWithHint("snap1", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil) + c.Check(runinhibit.LockWithHint("snap1", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil) lock.Unlock() stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--show-lock"}, 0) diff --git a/overlord/hookstate/hooks.go b/overlord/hookstate/hooks.go index b2dd9844f00..d13a0298538 100644 --- a/overlord/hookstate/hooks.go +++ b/overlord/hookstate/hooks.go @@ -177,7 +177,7 @@ func (h *gateAutoRefreshHookHandler) Before() error { defer lock.Unlock() inhibitInfo := runinhibit.InhibitInfo{Previous: snapRev} - if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedGateRefresh, inhibitInfo); err != nil { + if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedGateRefresh, inhibitInfo, st.Unlocker()); err != nil { return err } @@ -214,7 +214,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) { // invoking --hold/--proceed; this means proceed (except for respecting // refresh inhibit). if h.refreshAppAwareness { - if err := runinhibit.Unlock(snapName); err != nil { + if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil { return fmt.Errorf("cannot unlock inhibit lock for snap %s: %v", snapName, err) } } @@ -232,7 +232,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) { case snapstate.GateAutoRefreshHold: // for action=hold the ctlcmd calls HoldRefresh; only unlock runinhibit. if h.refreshAppAwareness { - if err := runinhibit.Unlock(snapName); err != nil { + if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil { return fmt.Errorf("cannot unlock inhibit lock of snap %s: %v", snapName, err) } } @@ -243,14 +243,18 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) { return err } if h.refreshAppAwareness { + // Unlock global state once for IsLocked and LockWithHint instead + // of unlocking/locking state twice quickly. + st.Unlock() + defer st.Lock() // we have HintInhibitedGateRefresh lock already when running the hook, change // it to HintInhibitedForRefresh. // Also let's reuse inhibit info that was saved in Before(). - _, inhibitInfo, err := runinhibit.IsLocked(snapName) + _, inhibitInfo, err := runinhibit.IsLocked(snapName, nil) if err != nil { return err } - if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRefresh, inhibitInfo); err != nil { + if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRefresh, inhibitInfo, nil); err != nil { return fmt.Errorf("cannot set inhibit lock for snap %s: %v", snapName, err) } } @@ -284,7 +288,7 @@ func (h *gateAutoRefreshHookHandler) Error(hookErr error) (ignoreHookErr bool, e } defer lock.Unlock() - if err := runinhibit.Unlock(snapName); err != nil { + if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil { return false, fmt.Errorf("cannot release inhibit lock of snap %s: %v", snapName, err) } } diff --git a/overlord/hookstate/hooks_test.go b/overlord/hookstate/hooks_test.go index 6530634d062..d162d5777b4 100644 --- a/overlord/hookstate/hooks_test.go +++ b/overlord/hookstate/hooks_test.go @@ -145,7 +145,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock( defer ctx.Unlock() // check that runinhibit hint has been set by Before() hook handler. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) @@ -178,7 +178,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock( c.Assert(change.Err(), IsNil) c.Assert(change.Status(), Equals, state.DoneStatus) - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh) c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) @@ -192,7 +192,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit( defer ctx.Unlock() // check that runinhibit hint has been set by Before() hook handler. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) @@ -226,7 +226,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit( c.Assert(change.Status(), Equals, state.DoneStatus) // runinhibit lock is released. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -237,7 +237,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit( func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRuninhibit(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // validity, refresh is inhibited for snap-a. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) @@ -279,7 +279,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRunin checkIsNotHeld(c, st, "snap-a") // runinhibit lock is released. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -290,7 +290,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRunin func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -327,7 +327,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) { checkIsNotHeld(c, st, "snap-b") // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -338,7 +338,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) { func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -373,7 +373,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) { checkIsHeld(c, st, "snap-a", "snap-a") // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -384,7 +384,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) { func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -425,7 +425,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C checkIsHeld(c, st, "snap-a", "snap-a") // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -436,7 +436,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) @@ -476,7 +476,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock( checkIsHeld(c, st, "snap-a", "snap-a") // inhibit lock is unlocked - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -485,7 +485,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock( func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c *C) { hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) @@ -532,7 +532,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c c.Check(held, HasLen, 0) // no runinhibit because the refresh-app-awareness feature is disabled. - hint, info, err := runinhibit.IsLocked("snap-a") + hint, info, err := runinhibit.IsLocked("snap-a", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(info, Equals, runinhibit.InhibitInfo{}) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 08bb6142049..10fbf56272f 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -314,7 +314,28 @@ func (m *InterfaceManager) setupProfilesForAppSet(task *state.Task, appSet *inte return err } - appSet, err := appSetForSnapRevision(st, snapInfo) + var appSet *interfaces.SnapAppSet + if snapst.PendingSecurity != nil { + // a content plug/slot may have already updated in this change, so the appSet + // should reflect in the revision (otherwise, we may regenerate the + // profile for the wrong revision) + snapInfo.SideInfo = *snapst.PendingSecurity.SideInfo + + var comps []*snap.ComponentInfo + for _, csi := range snapst.PendingSecurity.Components { + ci, err := snapstate.ReadComponentInfo(snapInfo, csi) + if err != nil { + return fmt.Errorf("cannot read component info when building app set %q: %v", name, err) + } + + comps = append(comps, ci) + } + + appSet, err = interfaces.NewSnapAppSet(snapInfo, comps) + } else { + appSet, err = appSetForSnapRevision(st, snapInfo) + } + if err != nil { return fmt.Errorf("building app set for snap %q: %v", name, err) } diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index b439e466f3c..98a78bf7297 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -43,7 +43,6 @@ import ( "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/snap/snapdir" "github.com/snapcore/snapd/systemd" "github.com/snapcore/snapd/timings" ) @@ -1104,17 +1103,12 @@ func snapsWithSecurityProfiles(st *state.State) ([]*interfaces.SnapAppSet, error components := make([]*snap.ComponentInfo, 0, len(snapst.PendingSecurity.Components)) for _, csi := range snapst.PendingSecurity.Components { - cpi := snap.MinimalComponentContainerPlaceInfo( - csi.Component.ComponentName, - csi.Revision, - instanceName, - ) - container := snapdir.New(cpi.MountDir()) - ci, err := snap.ReadComponentInfoFromContainer(container, snapInfo, csi) + ci, err := snapstate.ReadComponentInfo(snapInfo, csi) if err != nil { logger.Noticef("cannot read component info for snap %q: %s", instanceName, err) continue } + components = append(components, ci) } diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index b633af53d24..c4abb9e66aa 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -10696,3 +10696,136 @@ func (s *interfaceManagerSuite) TestOnSnapLinkageChanged(c *C) { Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&info.SideInfo}), }) } + +func (s *interfaceManagerSuite) TestSetupProfilesAffectedSnapRegenUsesMostRecentRevision(c *C) { + // this test checks a bug fix where a consumer and producer snap (content interface) + // were refreshed together and sometimes the producing snap lost access to its own + // file. The issue was that, if the slot (producer) was refreshed first, then, + // when the plugging snap was refreshed it would regenerate the slot's profile + // but with the previous revision, not the revision that the current change was + // updating to. + s.mockIfaces(&ifacetest.TestInterface{InterfaceName: "test"}) + + const componentYaml = ` +component: producer2+comp1 +type: standard +version: 1.0 +` + compProducerYaml := producer2Yaml + + `components: + comp1: + type: standard +` + + mgr := s.manager(c) + repo := mgr.Repository() + + consumer := s.mockSnap(c, consumer2Yaml) + producer := s.mockSnap(c, compProducerYaml) + + s.state.Lock() + var snapst snapstate.SnapState + c.Assert(snapstate.Get(s.state, "producer2", &snapst), IsNil) + + compInfo := snaptest.MockComponent(c, componentYaml, producer, snap.ComponentSideInfo{ + Revision: snap.R(1), + }) + + err := snapst.Sequence.AddComponentForRevision(snap.R(1), &sequence.ComponentState{ + SideInfo: &compInfo.ComponentSideInfo, + CompType: snap.StandardComponent, + }) + c.Assert(err, IsNil) + snapstate.Set(s.state, "producer2", &snapst) + + for _, info := range []*snap.Info{consumer, producer} { + appSet, err := interfaces.NewSnapAppSet(info, nil) + c.Assert(err, IsNil) + + err = repo.AddAppSet(appSet) + c.Assert(err, IsNil) + } + + connRef := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{ + Snap: "consumer2", + Name: "plug", + }, SlotRef: interfaces.SlotRef{ + Snap: "producer2", + Name: "slot", + }} + _, err = repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + s.state.Set("conns", map[string]interface{}{"consumer2:plug producer2:slot": map[string]interface{}{"interface": "test"}}) + + // mock new snaps for the refresh + snaptest.MockSnap(c, consumer2Yaml, &snap.SideInfo{Revision: snap.R(2)}) + snaptest.MockSnap(c, compProducerYaml, &snap.SideInfo{Revision: snap.R(2)}) + snaptest.MockComponent(c, componentYaml, producer, snap.ComponentSideInfo{ + Revision: snap.R(2), + }) + + // need to mark snap as inactive (which it would be during unlink) + for _, sn := range []string{"consumer2", "producer2"} { + var snapst snapstate.SnapState + err = snapstate.Get(s.state, sn, &snapst) + c.Assert(err, IsNil) + + snapst.Active = false + snapstate.Set(s.state, sn, &snapst) + } + + chg := s.state.NewChange("test", "") + slotTask := s.state.NewTask("setup-profiles", "") + slotTask.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "producer2", + Revision: snap.R(2), + }}) + slotTask.Set("component-setup", &snapstate.ComponentSetup{ + CompSideInfo: &snap.ComponentSideInfo{ + Component: compInfo.Component, + Revision: snap.R(2), + }}) + chg.AddTask(slotTask) + + plugTask := s.state.NewTask("setup-profiles", "") + plugTask.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "consumer2", + Revision: snap.R(2), + }}) + chg.AddTask(plugTask) + plugTask.WaitFor(slotTask) + s.state.Unlock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // ensure that the task succeeded. + c.Assert(chg.Err(), IsNil) + c.Check(chg.Status(), Equals, state.DoneStatus) + + calls := s.secBackend.SetupCalls + c.Assert(calls, HasLen, 4) + + // we run setup-profiles for the slot first + c.Assert(calls[0].AppSet.InstanceName(), Equals, "producer2") + c.Assert(calls[0].AppSet.Info().Revision, Equals, snap.R(2)) + + // the connected plug is regenerated (but revision as we haven't setup its new profile yet) + c.Assert(calls[1].AppSet.InstanceName(), Equals, "consumer2") + c.Assert(calls[1].AppSet.Info().Revision, Equals, snap.R(1)) + + // then we run setup-profiles for the plug + c.Assert(calls[2].AppSet.InstanceName(), Equals, "consumer2") + c.Assert(calls[2].AppSet.Info().Revision, Equals, snap.R(2)) + + // the connected slot is also setup but we use the new revision + c.Assert(calls[3].AppSet.InstanceName(), Equals, "producer2") + c.Assert(calls[3].AppSet.Info().Revision, Equals, snap.R(2)) + c.Assert(calls[3].AppSet.Components(), HasLen, 1) + c.Assert(calls[3].AppSet.Components()[0].Revision, Equals, snap.R(2)) +} diff --git a/overlord/install/install_test.go b/overlord/install/install_test.go index eb2ca305e47..709ece7bcf2 100644 --- a/overlord/install/install_test.go +++ b/overlord/install/install_test.go @@ -568,6 +568,7 @@ func (s *installSuite) TestEncryptionSupportInfoGadgetIncompatibleWithEncryption "storage-safety": tc.storageSafety, }) + gadget.SetEnclosingVolumeInStructs(tc.gadgetInfo.Volumes) res, err := install.GetEncryptionSupportInfo(mockModel, secboot.TPMProvisionFull, kernelInfo, tc.gadgetInfo, nil) c.Assert(err, IsNil) c.Check(res, DeepEquals, tc.expected, Commentf("%v", tc)) diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index 5dc7dd5d647..118b1e90902 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -79,11 +79,12 @@ type managerBackend interface { SetupKernelModulesComponents(currentComps, finalComps []*snap.ComponentSideInfo, ksnapName string, ksnapRev snap.Revision, meter progress.Meter) (err error) CopySnapData(newSnap, oldSnap *snap.Info, opts *dirs.SnapDirOptions, meter progress.Meter) error SetupSnapSaveData(info *snap.Info, dev snap.Device, meter progress.Meter) error - LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootInfo boot.RebootInfo, err error) + LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) error LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error StartServices(svcs []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, meter progress.Meter, tm timings.Measurer) error StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error QueryDisabledServices(info *snap.Info, pb progress.Meter) (*wrappers.DisabledServices, error) + MaybeSetNextBoot(info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error) // the undoers for install UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev snap.Device, meter progress.Meter) error @@ -106,7 +107,7 @@ type managerBackend interface { RemoveComponentDir(cpi snap.ContainerPlaceInfo) error RemoveContainerMountUnits(cpi snap.ContainerPlaceInfo, meter progress.Meter) error DiscardSnapNamespace(snapName string) error - RemoveSnapInhibitLock(snapName string) error + RemoveSnapInhibitLock(snapName string, stateUnlocker runinhibit.Unlocker) error RemoveAllSnapAppArmorProfiles() error RemoveKernelSnapSetup(instanceName string, rev snap.Revision, meter progress.Meter) error @@ -119,7 +120,7 @@ type managerBackend interface { Candidate(sideInfo *snap.SideInfo) // refresh related - RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (*osutil.FileLock, error) + RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (*osutil.FileLock, error) // (not a backend method because doInstall cannot access the backend) // WithSnapLock(info *snap.Info, action func() error) error diff --git a/overlord/snapstate/backend/link.go b/overlord/snapstate/backend/link.go index 443f62b40ed..0d24e43ddfb 100644 --- a/overlord/snapstate/backend/link.go +++ b/overlord/snapstate/backend/link.go @@ -48,10 +48,6 @@ type LinkContext struct { // installed FirstInstall bool - // IsUndo is set when we are installing the previous snap while - // performing a revert of the latest one that was installed - IsUndo bool - // ServiceOptions is used to configure services. ServiceOptions *wrappers.SnapServiceOptions @@ -59,6 +55,9 @@ type LinkContext struct { // establish run inhibition lock for refresh operations. RunInhibitHint runinhibit.Hint + // StateUnlocker is passed to inhibition lock operations. + StateUnlocker runinhibit.Unlocker + // RequireMountedSnapdSnap indicates that the apps and services // generated when linking need to use tooling from the snapd snap mount. RequireMountedSnapdSnap bool @@ -153,10 +152,28 @@ func updateCurrentSymlinks(info *snap.Info) (revert func(), e error) { return revertFunc, nil } +// MaybeSetNextBoot configures the system for a reboot if necesssary because +// of a snap refresh. isUndo must be set when we are installing the previous +// snap while performing a revert of the latest one that was installed +func (b Backend) MaybeSetNextBoot(info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error) { + if b.preseed { + return boot.RebootInfo{}, nil + } + + bootCtx := boot.NextBootContext{BootWithoutTry: isUndo} + return boot.Participant(info, info.Type(), dev).SetNextBoot(bootCtx) +} + // LinkSnap makes the snap available by generating wrappers and setting the current symlinks. -func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, tm timings.Measurer) (rebootRequired boot.RebootInfo, e error) { +func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, tm timings.Measurer) (e error) { + // explicitly prevent passing nil state unlocker to avoid internal errors of + // forgeting to pass the unlocker leading to deadlocks. + if linkCtx.StateUnlocker == nil { + return errors.New("internal error: LinkContext.StateUnlocker cannot be nil") + } + if info.Revision.Unset() { - return boot.RebootInfo{}, fmt.Errorf("cannot link snap %q with unset revision", info.InstanceName()) + return fmt.Errorf("cannot link snap %q with unset revision", info.InstanceName()) } osutil.MaybeInjectFault("link-snap") @@ -167,7 +184,7 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, restart, err = b.generateWrappers(info, linkCtx) }) if err != nil { - return boot.RebootInfo{}, err + return err } defer func() { if e == nil { @@ -178,21 +195,11 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, }) }() - var rebootInfo boot.RebootInfo - if !b.preseed { - bootCtx := boot.NextBootContext{BootWithoutTry: linkCtx.IsUndo} - rebootInfo, err = boot.Participant( - info, info.Type(), dev).SetNextBoot(bootCtx) - if err != nil { - return boot.RebootInfo{}, err - } - } - // only after link snap it will be possible to execute snap // applications, so ensure that the shared snap directory exists for // parallel installed snaps if err := createSharedSnapDirForParallelInstance(info); err != nil { - return boot.RebootInfo{}, err + return err } cleanupSharedParallelInstanceDir := func() { if !linkCtx.HasOtherInstances { @@ -203,7 +210,7 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, revertSymlinks, err := updateCurrentSymlinks(info) if err != nil { cleanupSharedParallelInstanceDir() - return boot.RebootInfo{}, err + return err } // if anything below here could return error, you need to // somehow clean up whatever updateCurrentSymlinks did @@ -214,17 +221,17 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, revertSymlinks() cleanupSharedParallelInstanceDir() - return boot.RebootInfo{}, err + return err } } // Stop inhibiting application startup by removing the inhibitor file. - if err := runinhibit.Unlock(info.InstanceName()); err != nil { - return boot.RebootInfo{}, err + if err := runinhibit.Unlock(info.InstanceName(), linkCtx.StateUnlocker); err != nil { + return err } - return rebootInfo, nil + return nil } func (b Backend) LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error { @@ -379,9 +386,14 @@ func removeGeneratedSnapdWrappers(s *snap.Info, firstInstall bool, meter progres func (b Backend) UnlinkSnap(info *snap.Info, linkCtx LinkContext, meter progress.Meter) error { var err0 error if hint := linkCtx.RunInhibitHint; hint != runinhibit.HintNotInhibited { + // explicitly prevent passing nil state unlocker to avoid internal errors of + // forgeting to pass the unlocker leading to deadlocks. + if linkCtx.StateUnlocker == nil { + return errors.New("internal error: LinkContext.StateUnlocker cannot be nil if LinkContext.RunInhibitHint is set") + } // inhibit startup of new programs inhibitInfo := runinhibit.InhibitInfo{Previous: info.SnapRevision()} - err0 = runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo) + err0 = runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo, linkCtx.StateUnlocker) } // remove generated services, binaries etc diff --git a/overlord/snapstate/backend/link_test.go b/overlord/snapstate/backend/link_test.go index bb705e64397..754de204bb7 100644 --- a/overlord/snapstate/backend/link_test.go +++ b/overlord/snapstate/backend/link_test.go @@ -78,6 +78,13 @@ type linkSuite struct { var _ = Suite(&linkSuite{}) +func mockLinkContextWithStateUnlocker() backend.LinkContext { + return backend.LinkContext{ + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, + } +} + func (s *linkSuite) TestLinkDoUndoGenerateWrappers(c *C) { const yaml = `name: hello version: 1.0 @@ -110,7 +117,7 @@ apps: ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) @@ -177,7 +184,7 @@ Exec=foo c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.png"), []byte(""), 0644), IsNil) c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.svg"), []byte(""), 0644), IsNil) - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) @@ -232,7 +239,7 @@ Exec=foo c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.png"), []byte(""), 0644), IsNil) c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.svg"), []byte(""), 0644), IsNil) - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) @@ -269,9 +276,12 @@ version: 1.0 ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) + c.Assert(err, IsNil) c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: false}) mountDir := info.MountDir() @@ -301,11 +311,16 @@ version: 1.0 ` info := snaptest.MockSnapInstance(c, "hello_foo", yaml, &snap.SideInfo{Revision: snap.R(11)}) - reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{ + err := s.be.LinkSnap(info, mockDev, backend.LinkContext{ HasOtherInstances: false, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, }, s.perfTimings) c.Assert(err, IsNil) + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) + c.Assert(err, IsNil) c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: false}) mountDir := info.MountDir() @@ -360,7 +375,11 @@ type: base ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - reboot, err := s.be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings) + c.Assert(err, IsNil) + + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, coreDev, isUndo) c.Assert(err, IsNil) c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: true}) } @@ -382,7 +401,11 @@ type: kernel ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - reboot, err := be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings) + err := be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings) + c.Assert(err, IsNil) + + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) c.Assert(err, IsNil) c.Check(reboot, DeepEquals, boot.RebootInfo{}) } @@ -412,7 +435,7 @@ type: snapd ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - _, err := be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings) + err := be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) c.Assert(called, Equals, true) } @@ -433,11 +456,23 @@ apps: ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + linkCtx := backend.LinkContext{StateUnlocker: fakeUnlocker} + + err := s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings) c.Assert(err, IsNil) + // no hint file, no locking needed + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) - _, err = s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 2) + c.Check(relockCalled, Equals, 2) l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) c.Assert(err, IsNil) @@ -475,7 +510,7 @@ apps: ` info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, IsNil) err = s.be.UnlinkSnap(info, backend.LinkContext{}, progress.Null) @@ -506,7 +541,7 @@ func (s *linkSuite) TestLinkFailsForUnsetRevision(c *C) { info := &snap.Info{ SuggestedName: "foo", } - _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, ErrorMatches, `cannot link snap "foo" with unset revision`) } @@ -545,7 +580,11 @@ func (s *linkSuite) TestLinkSnapdSnapOnCore(c *C) { info, _ := mockSnapdSnapForLink(c) - reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) + c.Assert(err, IsNil) + + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) c.Assert(err, IsNil) c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false}) @@ -654,7 +693,7 @@ func (s *linkCleanupSuite) testLinkCleanupDirOnFail(c *C, dir string) { c.Assert(os.Chmod(dir, 0555), IsNil) defer os.Chmod(dir, 0755) - _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, NotNil) _, isPathError := err.(*os.PathError) _, isLinkError := err.(*os.LinkError) @@ -699,7 +738,7 @@ func (s *linkCleanupSuite) TestLinkCleanupOnSystemctlFail(c *C) { }) defer r() - _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, ErrorMatches, "ouchie") for _, d := range []string{dirs.SnapBinariesDir, dirs.SnapDesktopFilesDir, dirs.SnapServicesDir} { @@ -719,7 +758,7 @@ func (s *linkCleanupSuite) TestLinkCleansUpDataDirAndSymlinksOnSymlinkFail(c *C) c.Assert(os.Chmod(d, 0555), IsNil) defer os.Chmod(d, 0755) - _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings) + err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, ErrorMatches, `(?i).*symlink.*permission denied.*`) c.Check(s.info.DataDir(), testutil.FileAbsent) @@ -756,9 +795,15 @@ func (s *linkCleanupSuite) testLinkCleanupFailedSnapdSnapOnCorePastWrappers(c *C linkCtx := backend.LinkContext{ FirstInstall: firstInstall, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, } - reboot, err := s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings) c.Assert(err, ErrorMatches, fmt.Sprintf("symlink %s /.*/snapd/current.*: permission denied", info.Revision)) + + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) + c.Assert(err, IsNil) c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false}) checker := testutil.FilePresent @@ -814,9 +859,11 @@ version: 1.0 snapinstance := snaptest.MockSnapInstance(c, "instance-snap_foo", yaml, &snap.SideInfo{Revision: snap.R(11)}) - _, err := s.be.LinkSnap(snapinstance, mockDev, + err := s.be.LinkSnap(snapinstance, mockDev, backend.LinkContext{ HasOtherInstances: tc.otherInstances, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, }, s.perfTimings) c.Assert(err, NotNil) @@ -871,7 +918,11 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) { return filepath.Join(dirs.SnapServicesDir, filepath.Base(p)) } - reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings) + c.Assert(err, IsNil) + + isUndo := false + reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo) c.Assert(err, IsNil) c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false}) @@ -885,12 +936,20 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) { // linked snaps do not have a run inhibition lock c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FileAbsent) + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } linkCtx := backend.LinkContext{ FirstInstall: true, RunInhibitHint: runinhibit.HintInhibitedForRefresh, + StateUnlocker: fakeUnlocker, } err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) // generated wrappers should be gone now for _, entry := range generatedSnapdUnits { @@ -903,6 +962,8 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) { // unlink is idempotent err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 2) + c.Check(relockCalled, Equals, 2) c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FilePresent) c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.refresh"), testutil.FilePresent) } @@ -928,12 +989,20 @@ func (s *snapdOnCoreUnlinkSuite) TestUnlinkNonFirstSnapdOnCoreDoesNothing(c *C) } // content list uses absolute paths already snaptest.PopulateDir("/", units) + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } linkCtx := backend.LinkContext{ FirstInstall: false, RunInhibitHint: runinhibit.HintInhibitedForRefresh, + StateUnlocker: fakeUnlocker, } err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) for _, unit := range units { c.Check(unit[0], testutil.FileEquals, "precious") } @@ -963,8 +1032,10 @@ apps: linkCtxWithTooling := backend.LinkContext{ RequireMountedSnapdSnap: true, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, } - _, err := s.be.LinkSnap(info, mockDev, linkCtxWithTooling, s.perfTimings) + err := s.be.LinkSnap(info, mockDev, linkCtxWithTooling, s.perfTimings) c.Assert(err, IsNil) c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), testutil.FileContains, `Wants=usr-lib-snapd.mount @@ -977,8 +1048,10 @@ After=usr-lib-snapd.mount`) linkCtxNoTooling := backend.LinkContext{ RequireMountedSnapdSnap: false, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, } - _, err = s.be.LinkSnap(info, mockDev, linkCtxNoTooling, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, linkCtxNoTooling, s.perfTimings) c.Assert(err, IsNil) c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), Not(testutil.FileContains), `usr-lib-snapd.mount`) } @@ -1001,8 +1074,10 @@ apps: ServiceOptions: &wrappers.SnapServiceOptions{ QuotaGroup: grp, }, + // This is required for LinkSnap + StateUnlocker: func() (relock func()) { return func() {} }, } - _, err = s.be.LinkSnap(info, mockDev, linkCtxWithGroup, s.perfTimings) + err = s.be.LinkSnap(info, mockDev, linkCtxWithGroup, s.perfTimings) c.Assert(err, IsNil) c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), testutil.FileContains, "\nSlice=snap.foogroup.slice\n") @@ -1055,7 +1130,7 @@ type: snapd be := backend.NewForPreseedMode() coreDev := boottest.MockUC20Device("run", nil) - _, err = be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings) + err = be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings) c.Assert(err, ErrorMatches, `BROKEN`) c.Assert(restartDone, Equals, true) @@ -1194,3 +1269,13 @@ func (s *linkSuite) TestKillSnapApps(c *C) { c.Assert(err, IsNil) c.Assert(called, Equals, 1) } + +func (s *linkSuite) TestLinkSnapNilStateUnlockerError(c *C) { + err := s.be.LinkSnap(nil, nil, backend.LinkContext{}, nil) + c.Assert(err, ErrorMatches, "internal error: LinkContext.StateUnlocker cannot be nil") +} + +func (s *linkSuite) TestUnlinkSnapNilStateUnlockerError(c *C) { + err := s.be.UnlinkSnap(nil, backend.LinkContext{RunInhibitHint: "not-nil"}, nil) + c.Assert(err, ErrorMatches, "internal error: LinkContext.StateUnlocker cannot be nil if LinkContext.RunInhibitHint is set") +} diff --git a/overlord/snapstate/backend/locking.go b/overlord/snapstate/backend/locking.go index 0bdb128e199..95064e45a8a 100644 --- a/overlord/snapstate/backend/locking.go +++ b/overlord/snapstate/backend/locking.go @@ -19,13 +19,19 @@ package backend import ( + "errors" + "github.com/snapcore/snapd/cmd/snaplock" "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) -func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) { +func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (lock *osutil.FileLock, err error) { + if stateUnlocker == nil { + return nil, errors.New("internal error: stateUnlocker cannot be nil") + } + // A process may be created after the soft refresh done upon // the request to refresh a snap. If such process is alive by // the time this code is reached the refresh process is stopped. @@ -65,7 +71,7 @@ func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, // and hard checks, as it would effectively make hard check a no-op, // but it might provide a nicer user experience. inhibitInfo := runinhibit.InhibitInfo{Previous: info.SnapRevision()} - if err := runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo); err != nil { + if err := runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo, stateUnlocker); err != nil { return nil, err } return lock, nil @@ -81,6 +87,7 @@ func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, // Note that this is not a method of the Backend type, so that it can be // invoked from doInstall, which does not have access to a backend object. func WithSnapLock(info *snap.Info, action func() error) error { + // XXX: Should we unlock state while holding snap lock? (ie. pass runinhibit.Unlocker) lock, err := snaplock.OpenLock(info.InstanceName()) if err != nil { return err diff --git a/overlord/snapstate/backend/locking_test.go b/overlord/snapstate/backend/locking_test.go index 350b5b9def0..982c9dc40f1 100644 --- a/overlord/snapstate/backend/locking_test.go +++ b/overlord/snapstate/backend/locking_test.go @@ -48,18 +48,27 @@ func (s *lockingSuite) TestRunInhibitSnapForUnlinkPositiveDecision(c *C) { version: 1 ` info := snaptest.MockInfo(c, yaml, &snap.SideInfo{Revision: snap.R(1)}) - lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error { + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", fakeUnlocker, func() error { // This decision function returns nil so the lock is established and // the inhibition hint is set. return nil }) c.Assert(err, IsNil) c.Assert(lock, NotNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) lock.Close() - hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName()) + hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), fakeUnlocker) c.Assert(err, IsNil) c.Check(string(hint), Equals, "hint") c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)}) + c.Check(unlockerCalled, Equals, 2) + c.Check(relockCalled, Equals, 2) } func (s *lockingSuite) TestRunInhibitSnapForUnlinkNegativeDecision(c *C) { @@ -67,17 +76,31 @@ func (s *lockingSuite) TestRunInhibitSnapForUnlinkNegativeDecision(c *C) { version: 1 ` info := snaptest.MockInfo(c, yaml, nil) - lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error { + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", fakeUnlocker, func() error { // This decision function returns an error so the lock is not // established and the inhibition hint is not set. return errors.New("propagated") }) c.Assert(err, ErrorMatches, "propagated") c.Assert(lock, IsNil) - hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName()) + c.Check(unlockerCalled, Equals, 0) + c.Check(relockCalled, Equals, 0) + hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), fakeUnlocker) c.Assert(err, IsNil) c.Check(string(hint), Equals, "") c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{}) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) +} + +func (s *linkSuite) TestRunInhibitSnapForUnlinkNilStateUnlockerError(c *C) { + _, err := s.be.RunInhibitSnapForUnlink(nil, "not-nil", nil, nil) + c.Assert(err, ErrorMatches, "internal error: stateUnlocker cannot be nil") } func (s *lockingSuite) TestWithSnapLock(c *C) { diff --git a/overlord/snapstate/backend/setup.go b/overlord/snapstate/backend/setup.go index e845694f955..a37949565ee 100644 --- a/overlord/snapstate/backend/setup.go +++ b/overlord/snapstate/backend/setup.go @@ -20,6 +20,7 @@ package backend import ( + "errors" "fmt" "os" "path/filepath" @@ -302,8 +303,11 @@ func (b Backend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, installRecord * } // RemoveSnapInhibitLock removes the file controlling inhibition of "snap run". -func (b Backend) RemoveSnapInhibitLock(instanceName string) error { - return runinhibit.RemoveLockFile(instanceName) +func (b Backend) RemoveSnapInhibitLock(instanceName string, stateUnlocker runinhibit.Unlocker) error { + if stateUnlocker == nil { + return errors.New("internal error: stateUnlocker cannot be nil") + } + return runinhibit.RemoveLockFile(instanceName, stateUnlocker) } // SetupKernelModulesComponents changes kernel-modules configuration by diff --git a/overlord/snapstate/backend/setup_test.go b/overlord/snapstate/backend/setup_test.go index 63a4f6634f4..eed8d07b896 100644 --- a/overlord/snapstate/backend/setup_test.go +++ b/overlord/snapstate/backend/setup_test.go @@ -1057,3 +1057,20 @@ func (s *setupSuite) TestRemoveKernelModulesComponentsFails(c *C) { s.testRemoveKernelModulesComponents(c, newComps, firstInstalled, ksnap, kernRev, "cannot remove mount in .*: cannot disable comp3-32") } + +func (s *linkSuite) TestRemoveSnapInhibitLock(c *C) { + var unlockerCalled, relockCalled int + fakeUnlocker := func() (relock func()) { + unlockerCalled++ + return func() { relockCalled++ } + } + err := s.be.RemoveSnapInhibitLock("some-snap", fakeUnlocker) + c.Assert(err, IsNil) + c.Check(unlockerCalled, Equals, 1) + c.Check(relockCalled, Equals, 1) +} + +func (s *linkSuite) TestRemoveSnapInhibitLockNilStateUnlockerError(c *C) { + err := s.be.RemoveSnapInhibitLock("some-snap", nil) + c.Assert(err, ErrorMatches, "internal error: stateUnlocker cannot be nil") +} diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 58ab52ea88b..e638024dd52 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -101,6 +101,7 @@ type fakeOp struct { containerFileName string snapLocked bool + isUndo bool } type fakeOps []fakeOp @@ -1266,7 +1267,25 @@ func (f *fakeSnappyBackend) SetupSnapSaveData(info *snap.Info, _ snap.Device, me return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootInfo boot.RebootInfo, err error) { +func (f *fakeSnappyBackend) MaybeSetNextBoot( + info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error) { + + op := &fakeOp{ + op: "maybe-set-next-boot", + isUndo: isUndo, + } + f.appendOp(op) + + reboot := false + if f.linkSnapMaybeReboot { + reboot = info.InstanceName() == dev.Base() || + (f.linkSnapRebootFor != nil && f.linkSnapRebootFor[info.InstanceName()]) + } + + return boot.RebootInfo{RebootRequired: reboot}, nil +} + +func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (err error) { if info.MountDir() == f.linkSnapWaitTrigger { f.linkSnapWaitCh <- 1 <-f.linkSnapWaitCh @@ -1290,18 +1309,12 @@ func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx b if info.MountDir() == f.linkSnapFailTrigger { op.op = "link-snap.failed" f.appendOp(op) - return boot.RebootInfo{RebootRequired: false}, errors.New("fail") + return errors.New("fail") } f.appendOp(op) - reboot := false - if f.linkSnapMaybeReboot { - reboot = info.InstanceName() == dev.Base() || - (f.linkSnapRebootFor != nil && f.linkSnapRebootFor[info.InstanceName()]) - } - - return boot.RebootInfo{RebootRequired: reboot}, nil + return nil } func (f *fakeSnappyBackend) LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error { @@ -1551,7 +1564,7 @@ func (f *fakeSnappyBackend) RemoveAllSnapAppArmorProfiles() error { return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string) error { +func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string, stateUnlocker runinhibit.Unlocker) error { f.appendOp(&fakeOp{ op: "remove-inhibit-lock", name: snapName, @@ -1632,7 +1645,7 @@ func (f *fakeSnappyBackend) RemoveSnapAliases(snapName string) error { return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) { +func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (lock *osutil.FileLock, err error) { f.appendOp(&fakeOp{ op: "run-inhibit-snap-for-unlink", name: info.InstanceName(), diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index 168e3a21da8..428b3d75945 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -73,9 +73,9 @@ func MockSnapReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, err } func MockReadComponentInfo(mock func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error)) (restore func()) { - old := readComponentInfo - readComponentInfo = mock - return func() { readComponentInfo = old } + old := readComponentInfoAt + readComponentInfoAt = mock + return func() { readComponentInfoAt = old } } func MockMountPollInterval(intv time.Duration) (restore func()) { diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index e3b387c9870..4a23e626477 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -1341,11 +1341,12 @@ func (m *SnapManager) restoreUnlinkOnError(t *state.Task, info *snap.Info, other } linkCtx := backend.LinkContext{ FirstInstall: false, - IsUndo: true, ServiceOptions: opts, HasOtherInstances: otherInstances, + // passed state must be locked + StateUnlocker: st.Unlocker(), } - _, err = m.backend.LinkSnap(info, deviceCtx, linkCtx, tm) + err = m.backend.LinkSnap(info, deviceCtx, linkCtx, tm) return err } @@ -1461,6 +1462,7 @@ func (m *SnapManager) doUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) (err erro // This task is only used for unlinking a snap during refreshes so we // can safely hard-code this condition here. RunInhibitHint: runinhibit.HintInhibitedForRefresh, + StateUnlocker: st.Unlocker(), SkipBinaries: skipBinaries, HasOtherInstances: otherInstances, } @@ -1604,11 +1606,16 @@ func (m *SnapManager) undoUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error { } linkCtx := backend.LinkContext{ FirstInstall: false, - IsUndo: true, ServiceOptions: opts, HasOtherInstances: otherInstances, + StateUnlocker: st.Unlocker(), } - reboot, err := m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) + err = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) + if err != nil { + return err + } + isUndo := true + reboot, err := m.backend.MaybeSetNextBoot(oldInfo, deviceCtx, isUndo) if err != nil { return err } @@ -2213,6 +2220,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) { FirstInstall: firstInstall, ServiceOptions: opts, HasOtherInstances: otherInstances, + StateUnlocker: st.Unlocker(), } // on UC18+, snap tooling comes from the snapd snap so we need generated // mount units to depend on the snapd snap mount units @@ -2247,7 +2255,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) { // links the new revision to current and ensures a shared base prefix // directory for parallel installed snaps - rebootInfo, err := m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings) + err = m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings) // defer a cleanup helper which will unlink the snap if anything fails after // this point defer func() { @@ -2261,7 +2269,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) { // snapd snap is special in the sense that we always // need the current symlink, so we restore the link to // the old revision - _, backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) + backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) } else { // snapd during first install and all other snaps backendErr = m.backend.UnlinkSnap(newInfo, linkCtx, pb) @@ -2274,6 +2282,19 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) { if err != nil { return err } + // Prepare for rebooting when needed + // TODO we have to revert changes in bootloader config/modeenv if an + // error happens later in this method. This is not likely as possible + // errors after this would happen only due to internal errors or not + // being able to write to the filesystem, but still. There is also the + // question of what would happen if a restart happens when the boot + // configuration has been already written but DoneStatus in the state + // has not. + isUndo := false + rebootInfo, err := m.backend.MaybeSetNextBoot(newInfo, deviceCtx, isUndo) + if err != nil { + return err + } // Restore configuration of the target revision (if available) on revert if isInstalled { @@ -2837,8 +2858,8 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error { pb := NewTaskProgressAdapterLocked(t) linkCtx := backend.LinkContext{ FirstInstall: firstInstall, - IsUndo: true, HasOtherInstances: otherInstances, + StateUnlocker: st.Unlocker(), } var backendErr error @@ -2853,7 +2874,7 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error { // the snapd snap is special in the sense that we need to make // sure that a sensible version is always linked as current, // also we never reboot when updating snapd snap - _, backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) + backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings) } else { // snapd during first install and all other snaps backendErr = m.backend.UnlinkSnap(newInfo, linkCtx, pb) @@ -3388,20 +3409,6 @@ func (m *SnapManager) doKillSnapApps(t *state.Task, _ *tomb.Tomb) (err error) { lock.Lock() defer lock.Unlock() - inhibitInfo := runinhibit.InhibitInfo{Previous: snapsup.Revision()} - if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRemove, inhibitInfo); err != nil { - return err - } - // Note: The snap hint lock file is completely removed in “discard-snap” - // so we only need to unlock it in case of an error here or during undo. - defer func() { - // Unlock snap inhibition if anything goes wrong afterwards to - // avoid keeping the snap stuck at this inhibited state. - if err != nil { - runinhibit.Unlock(snapName) - } - }() - var reason snap.AppKillReason if err := t.Get("kill-reason", &reason); err != nil && !errors.Is(err, state.ErrNoState) { return err @@ -3410,11 +3417,27 @@ func (m *SnapManager) doKillSnapApps(t *state.Task, _ *tomb.Tomb) (err error) { perfTimings := state.TimingsForTask(t) defer perfTimings.Save(st) + inhibitInfo := runinhibit.InhibitInfo{Previous: snapsup.Revision()} + if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRemove, inhibitInfo, st.Unlocker()); err != nil { + return err + } + // State lock is not needed for killing apps or stopping services and since those // can take some time, let's unlock the state st.Unlock() defer st.Lock() + // Note: The snap hint lock file is completely removed in “discard-snap” + // so we only need to unlock it in case of an error here or during undo. + defer func() { + // Unlock snap inhibition if anything goes wrong afterwards to + // avoid keeping the snap stuck at this inhibited state. + if err != nil { + // state is unlocked, it is okay to pass nil here + runinhibit.Unlock(snapName, nil) + } + }() + if err := m.backend.KillSnapApps(snapName, reason, perfTimings); err != nil { // Snap processes termination is best-effort and task should continue // without returning an error. This is to avoid a maliciously crafted snap @@ -3455,7 +3478,7 @@ func (m *SnapManager) undoKillSnapApps(t *state.Task, _ *tomb.Tomb) error { return err } - if err := runinhibit.Unlock(snapsup.InstanceName()); err != nil { + if err := runinhibit.Unlock(snapsup.InstanceName(), st.Unlocker()); err != nil { return err } @@ -3565,11 +3588,17 @@ func (m *SnapManager) undoUnlinkSnap(t *state.Task, _ *tomb.Tomb) error { } linkCtx := backend.LinkContext{ FirstInstall: false, - IsUndo: true, ServiceOptions: opts, HasOtherInstances: otherInstances, + StateUnlocker: st.Unlocker(), } - reboot, err := m.backend.LinkSnap(info, deviceCtx, linkCtx, perfTimings) + err = m.backend.LinkSnap(info, deviceCtx, linkCtx, perfTimings) + if err != nil { + return err + } + + isUndo := true + reboot, err := m.backend.MaybeSetNextBoot(info, deviceCtx, isUndo) if err != nil { return err } @@ -3720,7 +3749,7 @@ func (m *SnapManager) doDiscardSnap(t *state.Task, _ *tomb.Tomb) error { t.Errorf("cannot discard snap namespace %q, will retry in 3 mins: %s", snapsup.InstanceName(), err) return &state.Retry{After: 3 * time.Minute} } - err = m.backend.RemoveSnapInhibitLock(snapsup.InstanceName()) + err = m.backend.RemoveSnapInhibitLock(snapsup.InstanceName(), st.Unlocker()) if err != nil { return err } diff --git a/overlord/snapstate/handlers_components.go b/overlord/snapstate/handlers_components.go index f956cff2749..3c7181a97f0 100644 --- a/overlord/snapstate/handlers_components.go +++ b/overlord/snapstate/handlers_components.go @@ -296,7 +296,7 @@ func (m *SnapManager) doMountComponent(t *state.Task, _ *tomb.Tomb) (err error) var readInfoErr error for i := 0; i < 10; i++ { compMntDir := cpi.MountDir() - _, readInfoErr = readComponentInfo(compMntDir, nil, csi) + _, readInfoErr = readComponentInfoAt(compMntDir, nil, csi) if readInfoErr == nil { logger.Debugf("component %q (%v) available at %q", csi.Component, compSetup.Revision(), compMntDir) @@ -342,8 +342,15 @@ func (m *SnapManager) doMountComponent(t *state.Task, _ *tomb.Tomb) (err error) return nil } +// ReadComponentInfo reads the snap's component and returns a ComponentInfo. +func ReadComponentInfo(snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { + compName, compRev := csi.Component.ComponentName, csi.Revision + mountDir := snap.ComponentMountDir(compName, compRev, snapInfo.InstanceName()) + return readComponentInfoAt(mountDir, snapInfo, csi) +} + // Maybe we will need flags as in readInfo -var readComponentInfo = func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { +var readComponentInfoAt = func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { cont := snapdir.New(compMntDir) return snap.ReadComponentInfoFromContainer(cont, snapInfo, csi) } diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go index 4a1361de39f..63a37b46d46 100644 --- a/overlord/snapstate/handlers_link_test.go +++ b/overlord/snapstate/handlers_link_test.go @@ -714,6 +714,10 @@ func (s *linkSnapSuite) TestDoUndoUnlinkCurrentSnapWithVitalityScore(c *C) { path: filepath.Join(dirs.SnapMountDir, "foo/11"), vitalityRank: 2, }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, } c.Check(s.fakeBackend.ops, DeepEquals, expected) } @@ -924,6 +928,9 @@ func (s *linkSnapSuite) TestDoLinkSnapWithVitalityScore(c *C) { path: filepath.Join(dirs.SnapMountDir, "foo/33"), vitalityRank: 2, }, + { + op: "maybe-set-next-boot", + }, } c.Check(s.fakeBackend.ops, DeepEquals, expected) } @@ -1302,6 +1309,65 @@ func (s *linkSnapSuite) TestDoLinkSnapSuccessGadgetDoesRequestsRestart(c *C) { c.Check(t.Log(), HasLen, 1) } +func (s *linkSnapSuite) TestDoLinkSnapFailGadgetDoesRequestsRestart(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.state.Lock() + si := &snap.SideInfo{ + RealName: "pc", + SnapID: "pc-snap-id", + Revision: snap.R(1), + } + t := s.state.NewTask("link-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + Type: snap.TypeGadget, + }) + chg := s.state.NewChange("sample", "...") + chg.AddTask(t) + + // Force failure in a contrieved way by setting + // "gadget-restart-required" to a string (we want to make sure that we + // unlink on an error after setting next boot) + chg.Set("gadget-restart-required", "not-bool") + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + defer s.state.Unlock() + + expected := fakeOps{ + { + op: "candidate", + sinfo: *si, + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "pc/1"), + }, + { + op: "maybe-set-next-boot", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "pc/1"), + + unlinkFirstInstallUndo: true, + }, + } + c.Check(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // Error, no reboot has been requested + c.Check(t.Status(), Equals, state.ErrorStatus) + c.Check(s.restartRequested, HasLen, 0) + c.Check(t.Log(), HasLen, 2) +} + func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreAndSnapdNoCoreRestart(c *C) { restore := release.MockOnClassic(true) defer restore() @@ -1536,6 +1602,9 @@ func (s *linkSnapSuite) TestDoLinkSnapdDiscardsNsOnDowngrade(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/41"), }, + { + op: "maybe-set-next-boot", + }, } // start with an easier-to-read error if this fails: @@ -1621,6 +1690,9 @@ func (s *linkSnapSuite) TestDoLinkSnapdRemovesAppArmorProfilesOnSnapdDowngrade(c op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/41"), }, + { + op: "maybe-set-next-boot", + }, } // start with an easier-to-read error if this fails: @@ -2272,6 +2344,9 @@ func (s *linkSnapSuite) TestUndoLinkSnapdFirstInstall(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/22"), }, + { + op: "maybe-set-next-boot", + }, { op: "discard-namespace", name: "snapd", @@ -2350,6 +2425,9 @@ func (s *linkSnapSuite) TestUndoLinkSnapdNthInstall(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/22"), }, + { + op: "maybe-set-next-boot", + }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/20"), @@ -2602,6 +2680,9 @@ func (s *linkSnapSuite) testDoLinkSnapWithToolingDependency(c *C, classicOrBase path: filepath.Join(dirs.SnapMountDir, "services-snap/11"), requireSnapdTooling: needsTooling, }, + { + op: "maybe-set-next-boot", + }, } // start with an easier-to-read error if this fails: @@ -2683,7 +2764,7 @@ func (s *linkSnapSuite) testDoKillSnapApps(c *C, svc bool) { c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Check(s.fakeBackend.ops, DeepEquals, expected) - hint, _, err := runinhibit.IsLocked("some-snap") + hint, _, err := runinhibit.IsLocked("some-snap", nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintInhibitedForRemove) @@ -2737,7 +2818,7 @@ func (s *linkSnapSuite) TestDoKillSnapAppsUnlocksOnError(c *C) { c.Assert(task.Status(), Equals, state.ErrorStatus) - hint, _, err := runinhibit.IsLocked("some-snap") + hint, _, err := runinhibit.IsLocked("some-snap", nil) c.Assert(err, IsNil) // On error hint inhibition file is unlocked c.Check(hint, Equals, runinhibit.HintNotInhibited) @@ -2784,7 +2865,7 @@ func (s *linkSnapSuite) TestDoKillSnapAppsTerminateBestEffort(c *C) { c.Assert(task.Status(), Equals, state.DoneStatus) - hint, _, err := runinhibit.IsLocked("some-snap") + hint, _, err := runinhibit.IsLocked("some-snap", nil) c.Assert(err, IsNil) // Error is ignored, inhibition lock is held c.Check(hint, Equals, runinhibit.HintInhibitedForRemove) @@ -2863,7 +2944,7 @@ func (s *linkSnapSuite) testDoUndoKillSnapApps(c *C, svc bool) { c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Check(s.fakeBackend.ops, DeepEquals, expected) - hint, _, err := runinhibit.IsLocked("some-snap") + hint, _, err := runinhibit.IsLocked("some-snap", nil) c.Assert(err, IsNil) // On undo hint inhibition file is unlocked c.Check(hint, Equals, runinhibit.HintNotInhibited) diff --git a/overlord/snapstate/refresh.go b/overlord/snapstate/refresh.go index 86663ee1eeb..c722070419c 100644 --- a/overlord/snapstate/refresh.go +++ b/overlord/snapstate/refresh.go @@ -175,7 +175,7 @@ func (err BusySnapError) Pids() []int { // the refresh change and continue running existing app processes. func hardEnsureNothingRunningDuringRefresh(backend managerBackend, st *state.State, snapst *SnapState, snapsup *SnapSetup, info *snap.Info) (bool, *osutil.FileLock, error) { var inhibitionTimeout bool - lock, err := backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, func() error { + lock, err := backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, st.Unlocker(), func() error { // In case of successful refresh inhibition the snap state is modified // to indicate when the refresh was first inhibited. If the first // refresh inhibition is outside of a grace period then refresh diff --git a/overlord/snapstate/refresh_test.go b/overlord/snapstate/refresh_test.go index 7fab8ce2bb4..8bddfd12afa 100644 --- a/overlord/snapstate/refresh_test.go +++ b/overlord/snapstate/refresh_test.go @@ -177,7 +177,7 @@ func (s *refreshSuite) TestDoSoftRefreshCheckAllowed(c *C) { c.Assert(err, IsNil) // In addition, the inhibition lock is not set. - hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName()) + hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{}) @@ -201,7 +201,7 @@ func (s *refreshSuite) TestDoSoftRefreshCheckDisallowed(c *C) { c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks, pids: 123`) // Validity check: the inhibition lock was not set. - hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName()) + hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), nil) c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{}) diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 13efae7a2e4..dc96cecc2d7 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -665,10 +665,7 @@ func (snapst *SnapState) ComponentInfosForRevision(rev snap.Revision) ([]*snap.C compInfos := make([]*snap.ComponentInfo, 0, len(revState.Components)) for _, comp := range revState.Components { - cpi := snap.MinimalComponentContainerPlaceInfo(comp.SideInfo.Component.ComponentName, - comp.SideInfo.Revision, si.InstanceName()) - - compInfo, err := readComponentInfo(cpi.MountDir(), si, comp.SideInfo) + compInfo, err := ReadComponentInfo(si, comp.SideInfo) if err != nil { return nil, err } @@ -693,9 +690,7 @@ func (snapst *SnapState) CurrentComponentInfo(cref naming.ComponentRef) (*snap.C return nil, err } - cpi := snap.MinimalComponentContainerPlaceInfo(csi.Component.ComponentName, - csi.Revision, si.InstanceName()) - return readComponentInfo(cpi.MountDir(), si, csi) + return ReadComponentInfo(si, csi) } func (snapst *SnapState) InstanceName() string { diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index d4fab1e5166..8ec4d555b97 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -1815,7 +1815,7 @@ func keys[K comparable, V any](m map[K]V) []K { // If the filter returns true, the update for that snap proceeds. If // it returns false, the snap is removed from the list of updates to // consider. -type updateFilter func(*snap.Info, *SnapState) bool +type updateFilter = func(*snap.Info, *SnapState) bool func updateManyFiltered(ctx context.Context, st *state.State, names []string, revOpts []*RevisionOptions, userID int, filter updateFilter, flags *Flags, fromChange string) ([]string, *UpdateTaskSets, error) { if flags == nil { diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go index c220b95cb71..ea67f17d277 100644 --- a/overlord/snapstate/snapstate_install_test.go +++ b/overlord/snapstate/snapstate_install_test.go @@ -1383,6 +1383,9 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1583,6 +1586,9 @@ func (s *snapmgrTestSuite) testParallelInstanceInstallRunThrough(c *C, inputFlag op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap_instance", @@ -1795,6 +1801,9 @@ func (s *snapmgrTestSuite) TestInstallUndoRunThroughJustOneSnap(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1951,6 +1960,9 @@ func (s *snapmgrTestSuite) TestInstallWithCohortRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/666"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -2150,6 +2162,9 @@ func (s *snapmgrTestSuite) testInstallWithRevisionRunThrough(c *C, snapName, req op: "link-snap", path: filepath.Join(dirs.SnapMountDir, filepath.Join(snapName, "42")), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: snapName, @@ -2350,6 +2365,9 @@ version: 1.0`) op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x1"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "mock", @@ -2482,6 +2500,9 @@ epoch: 1* op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x3"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "mock", @@ -2621,6 +2642,9 @@ epoch: 1* op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "mock/x1"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "mock", @@ -2677,7 +2701,7 @@ version: 1.0`) s.settle(c) // ensure only local install was run, i.e. first actions are pseudo-action current - c.Assert(s.fakeBackend.ops.Ops(), HasLen, 10) + c.Assert(s.fakeBackend.ops.Ops(), HasLen, 11) c.Check(s.fakeBackend.ops[0].op, Equals, "current") c.Check(s.fakeBackend.ops[0].old, Equals, "") // and setup-snap @@ -2690,6 +2714,7 @@ version: 1.0`) c.Check(s.fakeBackend.ops[5].sinfo, DeepEquals, *si) c.Check(s.fakeBackend.ops[6].op, Equals, "link-snap") c.Check(s.fakeBackend.ops[6].path, Equals, filepath.Join(dirs.SnapMountDir, "some-snap/42")) + c.Check(s.fakeBackend.ops[7].op, Equals, "maybe-set-next-boot") // verify snapSetup info var snapsup snapstate.SnapSetup @@ -2864,6 +2889,9 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "core/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "core", @@ -2929,6 +2957,9 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/42"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -3323,6 +3354,8 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snap-content-slot/11"), + }, { + op: "maybe-set-next-boot", }, { op: "auto-connect:Doing", name: "snap-content-slot", @@ -3375,6 +3408,8 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) { }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snap-content-plug/42"), + }, { + op: "maybe-set-next-boot", }, { op: "auto-connect:Doing", name: "snap-content-plug", @@ -6436,10 +6471,16 @@ func undoOps(instanceName string, newSequence, prevSequence *sequence.RevisionSi path: filepath.Join(dirs.SnapDataDir, instanceName), }) } else { - ops = append(ops, fakeOp{ - op: "link-snap", - path: filepath.Join(dirs.SnapMountDir, instanceName, prevRevision.String()), - }) + ops = append(ops, + fakeOp{ + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, instanceName, prevRevision.String()), + }, + fakeOp{ + op: "maybe-set-next-boot", + isUndo: true, + }, + ) } for i := len(newComponents) - 1; i >= 0; i-- { @@ -6702,6 +6743,8 @@ func (s *snapmgrTestSuite) testInstallComponentsRunThrough(c *C, snapName, insta }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, filepath.Join(instanceName, snapRevision.String())), + }, { + op: "maybe-set-next-boot", }}...) // ops for linking components @@ -7030,6 +7073,8 @@ components: }, { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, filepath.Join(instanceName, snapRevision.String())), + }, { + op: "maybe-set-next-boot", }}...) for _, compName := range compNames { diff --git a/overlord/snapstate/snapstate_remove_test.go b/overlord/snapstate/snapstate_remove_test.go index 746805cfc59..1b9e0bccd41 100644 --- a/overlord/snapstate/snapstate_remove_test.go +++ b/overlord/snapstate/snapstate_remove_test.go @@ -1571,6 +1571,10 @@ func (s *snapmgrTestSuite) TestRemoveManyUndoRestoresCurrent(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, { op: "update-aliases", }, diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 5a6cbd87bd1..f4444e912a1 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -1639,6 +1639,9 @@ func (s *snapmgrTestSuite) testRevertRunThrough(c *C, refreshAppAwarenessUX bool op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -1871,6 +1874,9 @@ func (s *snapmgrTestSuite) TestRevertWithBaseRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap-with-base/2"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap-with-base", @@ -1961,6 +1967,9 @@ func (s *snapmgrTestSuite) TestParallelInstanceRevertRunThrough(c *C) { path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/2"), otherInstances: true, }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap_instance", @@ -2030,7 +2039,7 @@ func (s *snapmgrTestSuite) TestRevertWithLocalRevisionRunThrough(c *C) { s.settle(c) - c.Assert(s.fakeBackend.ops.Ops(), HasLen, 8) + c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9) // verify that LocalRevision is still -7 var snapst snapstate.SnapState @@ -2098,6 +2107,9 @@ func (s *snapmgrTestSuite) TestRevertToRevisionNewVersion(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -2189,6 +2201,9 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -2220,6 +2235,10 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, { op: "update-aliases", }, @@ -2311,6 +2330,10 @@ func (s *snapmgrTestSuite) TestRevertUndoRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, { op: "update-aliases", }, @@ -2996,6 +3019,9 @@ func (s *snapmgrTestSuite) TestEnableRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -3158,6 +3184,9 @@ func (s *snapmgrTestSuite) TestParallelInstanceEnableRunThrough(c *C) { path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), otherInstances: true, }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap_instance", @@ -5629,6 +5658,9 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "core/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "core", @@ -8133,6 +8165,9 @@ func (s *snapmgrTestSuite) TestSnapdRefreshTasks(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "snapd/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "snapd", diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index 1d8df6e0891..0605579c72a 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -129,6 +129,9 @@ func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -270,6 +273,7 @@ func (s *snapmgrTestSuite) testUpdateScenario(c *C, desc string, t switchScenari "setup-profiles:Doing", "candidate", "link-snap", + "maybe-set-next-boot", "auto-connect:Doing", "update-aliases", "cleanup-trash", @@ -415,6 +419,9 @@ func (s *snapmgrTestSuite) testUpdateCanDoBackwards(c *C, refreshAppAwarenessUX op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -932,6 +939,7 @@ func (s *snapmgrTestSuite) testUpdateAmendRunThrough(c *C, tryMode bool, compone "setup-profiles:Doing", "candidate", "link-snap", + "maybe-set-next-boot", }...) for range components { ops = append(ops, "link-component") @@ -1190,6 +1198,9 @@ func (s *snapmgrTestSuite) testUpdateRunThrough(c *C, refreshAppAwarenessUX bool op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "services-snap/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "services-snap", @@ -1565,6 +1576,9 @@ func (s *snapmgrTestSuite) testParallelInstanceUpdateRunThrough(c *C, refreshApp op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "services-snap_instance", @@ -2501,6 +2515,10 @@ func (s *snapmgrTestSuite) testUpdateUndoRunThrough(c *C, refreshAppAwarenessUX op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, }...) // aliases removal undo is skipped when refresh-app-awareness-ux is enabled if !refreshAppAwarenessUX { @@ -2816,6 +2834,9 @@ func (s *snapmgrTestSuite) testUpdateTotalUndoRunThrough(c *C, refreshAppAwarene op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: "some-snap", @@ -2866,6 +2887,10 @@ func (s *snapmgrTestSuite) testUpdateTotalUndoRunThrough(c *C, refreshAppAwarene op: "link-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, }...) // aliases removal undo is skipped when refresh-app-awareness-ux is enabled if !refreshAppAwarenessUX { @@ -14382,6 +14407,9 @@ func (s *snapmgrTestSuite) TestUpdateBackToPrevRevision(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: instanceName, @@ -14603,6 +14631,9 @@ func (s *snapmgrTestSuite) testRevertWithComponents(c *C, undo bool) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, { op: "auto-connect:Doing", name: instanceName, @@ -14647,6 +14678,10 @@ func (s *snapmgrTestSuite) testRevertWithComponents(c *C, undo bool) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), }, + { + op: "maybe-set-next-boot", + isUndo: true, + }, { op: "update-aliases", }, @@ -14999,6 +15034,9 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevision(c *C) { op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, }...) for i, compName := range components { @@ -15339,6 +15377,9 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevisionAddComponen op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, { op: "link-component", path: snap.ComponentMountDir("kernel-modules-component", snap.R(2), instanceName), @@ -15886,6 +15927,9 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateW op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, }...) for _, cs := range expectedComponentStates { @@ -16531,6 +16575,9 @@ components: op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, }...) for _, cs := range expectedComponentStates { @@ -16977,6 +17024,9 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsRunThroughOnlyComponentUpdate op: "link-snap", path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()), }, + { + op: "maybe-set-next-boot", + }, }...) for _, cs := range expectedComponentStates { diff --git a/packaging/fedora-39 b/packaging/fedora-41 similarity index 100% rename from packaging/fedora-39 rename to packaging/fedora-41 diff --git a/randutil/rand.go b/randutil/rand.go index 1757afd571d..73d39abbe2b 100644 --- a/randutil/rand.go +++ b/randutil/rand.go @@ -83,6 +83,7 @@ func RandomString(length int) string { var ( Intn = rand.Intn Int63n = rand.Int63n + Perm = rand.Perm ) // RandomDuration returns a random duration up to the given length. diff --git a/spread.yaml b/spread.yaml index 4741d84d588..5b75d1a5bff 100644 --- a/spread.yaml +++ b/spread.yaml @@ -245,6 +245,7 @@ backends: workers: 6 - opensuse-tumbleweed-64: workers: 6 + storage: 12G google-arm: type: google diff --git a/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh b/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh index 73ab37f07be..0f4a8ce3a72 100755 --- a/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh +++ b/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh @@ -4,4 +4,4 @@ LAST_CHANGE_ID=$1 CHANGES_COUNT=$2 #shellcheck disable=SC2086,SC2046 -test $(snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT +test $(snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT diff --git a/tests/core/auto-refresh-backoff-after-reboot/task.yaml b/tests/core/auto-refresh-backoff-after-reboot/task.yaml index a280de50e3a..ce01dc863b8 100644 --- a/tests/core/auto-refresh-backoff-after-reboot/task.yaml +++ b/tests/core/auto-refresh-backoff-after-reboot/task.yaml @@ -21,15 +21,11 @@ prepare: | exit fi - # Needed by make-snap-installable, Install before switching to fakestore - snap install jq - snap install remarshal - # Prevent refreshes until we have right snap revisions snap set system refresh.hold=forever # Record last change id before we start to avoid flakiness due to auto-refreshes in other tests - snap debug api /v2/changes?select=all | jq '.result | sort_by(.id|tonumber) | .[-1].id' > last-change-id + snap debug api /v2/changes?select=all | gojq '.result | sort_by(.id|tonumber) | .[-1].id' > last-change-id # Record current snap revision for reference readlink "/snap/$SNAP_NAME/current" > snap.rev @@ -58,7 +54,7 @@ restore: | snap set system refresh.hold! debug: | - snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)" + snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -169,17 +165,18 @@ execute: | retry -n 50 --wait 1 "$(pwd)"/check_auto_refresh_count.sh "$LAST_CHANGE_ID" 2 echo "Check auto-refresh behaviour matches expectations for backoff algorithm" - snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json + snap debug api /v2/changes?select=ready | \ + gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json # 1st auto-refresh - jq '.[0].status' < changes.json | MATCH "Error" - jq '.[0].data."snap-names" | length' < changes.json | MATCH "1" - jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_NAME" - jq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_NAME" + gojq '.[0].status' < changes.json | MATCH "Error" + gojq '.[0].data."snap-names" | length' < changes.json | MATCH "1" + gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_NAME" + gojq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_NAME" # 2nd auto-refresh - jq '.[1].status' < changes.json | MATCH "Done" - jq '.[1].data."snap-names" | length' < changes.json | MATCH "1" - jq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_NAME" - jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_NAME" + gojq '.[1].status' < changes.json | MATCH "Done" + gojq '.[1].data."snap-names" | length' < changes.json | MATCH "1" + gojq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_NAME" + gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_NAME" fi diff --git a/tests/core/basic20plus/task.yaml b/tests/core/basic20plus/task.yaml index 51225b939ff..38f67d792d9 100644 --- a/tests/core/basic20plus/task.yaml +++ b/tests/core/basic20plus/task.yaml @@ -8,17 +8,23 @@ details: | properly systems: - - ubuntu-core-20-* - - ubuntu-core-22-* + - -ubuntu-core-16-* + - -ubuntu-core-18-* execute: | case "$SPREAD_SYSTEM" in + ubuntu-core-24-*) + base_snap=core24 + ;; ubuntu-core-22-*) base_snap=core22 ;; ubuntu-core-20-*) base_snap=core20 ;; + *) + echo "Unsupported ubuntu core system, add missing case here" + exit 1 esac echo "Check that the system snaps are there" snap list "${base_snap}" @@ -32,12 +38,9 @@ execute: | snap changes | MATCH "Done.*Initialize system state" echo "Check that a simple shell snap" - if os.query is-core22; then - snap install --edge "test-snapd-sh-${base_snap}" - else - snap install "test-snapd-sh-${base_snap}" - fi - "test-snapd-sh-${base_snap}.sh" -c 'echo hello' | MATCH hello + SHELL_SNAP="test-snapd-sh-${base_snap}" + snap install "$SHELL_SNAP" + "${SHELL_SNAP}.sh" -c 'echo hello' | MATCH hello if python3 -m json.tool < /var/lib/snapd/system-key | grep '"build-id": ""'; then echo "The build-id of snapd must not be empty." @@ -45,10 +48,10 @@ execute: | fi echo "Ensure passwd/group is available for snaps" - "test-snapd-sh-${base_snap}.sh" -c 'cat /var/lib/extrausers/passwd' | MATCH test + "${SHELL_SNAP}.sh" -c 'cat /var/lib/extrausers/passwd' | MATCH test # rpi devices don't use grub - if ( os.query is-core20 || os.query is-core22 ) && not snap list pi-kernel &>/dev/null; then + if not snap list pi-kernel &>/dev/null; then echo "Ensure extracted kernel.efi exists" kernel_name="$(snaps.name kernel)" test -e /boot/grub/"$kernel_name"*/kernel.efi @@ -70,9 +73,13 @@ execute: | # ensure that our the-tool (and thus our snap-bootstrap ran) # for external backend the initramfs is not rebuilt - echo "Check that we booted with the rebuilt initramfs in the kernel snap" - if [ "$SPREAD_BACKEND" != "external" ] && [ "$SPREAD_BACKEND" != "testflinger" ]; then - test -e /writable/system-data/the-tool-ran + # On core24+ we repack the kernel snap differently, and thus do not + # touch the /writable/system-data/the-tool-ran + if os.query is-core20 || os.query is-core22; then + echo "Check that we booted with the rebuilt initramfs in the kernel snap" + if [ "$SPREAD_BACKEND" != "external" ] && [ "$SPREAD_BACKEND" != "testflinger" ]; then + test -e /writable/system-data/the-tool-ran + fi fi # ensure we handled cloud-init, either we have: @@ -117,6 +124,15 @@ execute: | losetup -O ro -n --raw "${loop}" | MATCH "1" done + # make sure that ubuntu-save is mounted with appropriate flags + findmnt -T /run/mnt/ubuntu-save > save-mount.info + + # print it for debug purposes before we match flags + cat save-mount.info + MATCH nosuid < save-mount.info + MATCH noexec < save-mount.info + MATCH nodev < save-mount.info + # ensure apparmor works, see LP: 2024637 systemctl status apparmor.service diff --git a/tests/core/kernel-snap-refresh-on-core/task.yaml b/tests/core/kernel-snap-refresh-on-core/task.yaml index 95e889bbab4..d55a1382ce7 100644 --- a/tests/core/kernel-snap-refresh-on-core/task.yaml +++ b/tests/core/kernel-snap-refresh-on-core/task.yaml @@ -5,8 +5,7 @@ details: | revision to a new one. It expects to find a new snap revision in the channel pointed by the NEW_CORE_CHANNEL env var. -# TODO:UC20 enable for UC20/UC18 -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-*-64] manual: true diff --git a/tests/core/snap-repair/task.yaml b/tests/core/snap-repair/task.yaml index b3330741f12..5f768dc35fa 100644 --- a/tests/core/snap-repair/task.yaml +++ b/tests/core/snap-repair/task.yaml @@ -8,10 +8,6 @@ environment: BLOB_DIR: $(pwd)/fake-store-blobdir STORE_ADDR: localhost:11028 -prepare: | - snap install jq - tests.cleanup defer snap remove jq - restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" @@ -93,7 +89,7 @@ execute: | # note that the value "2" here _must_ be a string, otherwise we can't # sign it as all values must be strings or lists of strings, etc. - jq '."repair-id" = "2"' < "$PWD/$REPAIR_JSON" > "$PWD/$REPAIR_JSON.tmp" + gojq '."repair-id" = "2"' < "$PWD/$REPAIR_JSON" > "$PWD/$REPAIR_JSON.tmp" mv "$PWD/$REPAIR_JSON.tmp" "$PWD/2-$REPAIR_JSON" fakestore new-repair --dir "$BLOB_DIR" retry.sh --repair-json="$PWD/2-$REPAIR_JSON" @@ -108,7 +104,7 @@ execute: | echo "Add a new repair ID 2 revision that completes successfully" - jq '."revision" = "1"' < "$PWD/2-$REPAIR_JSON" > "$PWD/2-$REPAIR_JSON.tmp" + gojq '."revision" = "1"' < "$PWD/2-$REPAIR_JSON" > "$PWD/2-$REPAIR_JSON.tmp" mv "$PWD/2-$REPAIR_JSON.tmp" "$PWD/2-$REPAIR_JSON" fakestore new-repair --dir "$BLOB_DIR" "$REPAIR_SCRIPT" --repair-json="$PWD/2-$REPAIR_JSON" diff --git a/tests/core/snapd-maintenance-msg/task.yaml b/tests/core/snapd-maintenance-msg/task.yaml index d882cd3f627..18dba16cd93 100644 --- a/tests/core/snapd-maintenance-msg/task.yaml +++ b/tests/core/snapd-maintenance-msg/task.yaml @@ -7,16 +7,12 @@ details: | systems: [ubuntu-core-20-64] prepare: | - snap install jq - # make sure that the snapd daemon gives us time for comms before # closing the socket echo "SNAPD_SHUTDOWN_DELAY=1" >> /etc/environment systemctl restart snapd restore: | - snap remove jq - # remove SNAPD_SHUTDOWN_DELAY from /etc/environment again #shellcheck disable=SC2005 echo "$(grep -v 'SNAPD_SHUTDOWN_DELAY=1' /etc/environment)" > /etc/environment @@ -31,7 +27,7 @@ execute: | # closed so we need to catch it in that timeframe. echo "Testing maintenance message for daemon restarts" snap install --dangerous "$SNAPD_SNAP" & - retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | jq ".maintenance" | MATCH "daemon is restarting"' + retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | gojq ".maintenance" | MATCH "daemon is restarting"' wait echo "Restoring the snapd snap" @@ -39,7 +35,7 @@ execute: | echo "Testing maintenance message for system reboots" snap refresh core20 --channel=stable --amend & - retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | jq ".maintenance" | MATCH "system is restarting"' + retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | gojq ".maintenance" | MATCH "system is restarting"' wait REBOOT diff --git a/tests/core/snapd-refresh-vs-services/task.yaml b/tests/core/snapd-refresh-vs-services/task.yaml index e81b35b280c..a1b16a8a613 100644 --- a/tests/core/snapd-refresh-vs-services/task.yaml +++ b/tests/core/snapd-refresh-vs-services/task.yaml @@ -42,11 +42,8 @@ environment: SNAPD_2_49_2_ARMHF: https://storage.googleapis.com/snapd-spread-tests/snaps/snapd_2.49.2_11586.snap prepare: | - # install http snap to download files, jq + remarshal to simplify the check if - # stable == 2.49.2 so we can skip that case automatically until a new version - # is released to stable + # install http snap to download files snap install test-snapd-curl --edge --devmode # devmode so it can save to any dir - snap install jq remarshal # save the current version of snapd for later INITIAL_REV=$(snap list snapd | tail -n +2 | awk '{print $3}') cp "/var/lib/snapd/snaps/snapd_$INITIAL_REV.snap" snapd-pr.snap @@ -62,7 +59,7 @@ prepare: | execute: | # check if snapd 2.49.2 is the current latest/stable release as it simplifies # some of the logic below - if snap info snapd | yaml2json | jq -r '.channels."latest/stable"' | grep -q -Po '2.49.2\s+'; then + if snap info snapd | gojq --yaml-input -r '.channels."latest/stable"' | grep -q -Po '2.49.2\s+'; then # skip the stable variant of the test if [ "${SNAPD_VERSION_UNDER_TEST}" = "stable" ]; then echo "Skipping duplicated test case" diff --git a/tests/core/remodel-kernel/task.yaml b/tests/core/uc16-remodel-kernel/task.yaml similarity index 98% rename from tests/core/remodel-kernel/task.yaml rename to tests/core/uc16-remodel-kernel/task.yaml index f34e72d70fd..e8c4f6c519b 100644 --- a/tests/core/remodel-kernel/task.yaml +++ b/tests/core/uc16-remodel-kernel/task.yaml @@ -7,8 +7,6 @@ details: | the new kernel snap. Finally check it is possible to remodel back to the initial model. -# FIXME: add core18 test as well -# TODO:UC20: enable for UC20 systems: [ubuntu-core-16-64] environment: diff --git a/tests/core/uc20-recovery/task.yaml b/tests/core/uc20-recovery/task.yaml index 411fd9f1a82..87bbe5200e4 100644 --- a/tests/core/uc20-recovery/task.yaml +++ b/tests/core/uc20-recovery/task.yaml @@ -34,22 +34,20 @@ execute: | if [ "$SPREAD_REBOOT" == "0" ]; then echo "In run mode" - snap install --edge jq - MATCH 'snapd_recovery_mode=run' < /proc/cmdline # verify we are in run mode via the API snap debug api '/v2/system-info' > system-info - jq -r '.result["system-mode"]' < system-info | MATCH 'run' + gojq -r '.result["system-mode"]' < system-info | MATCH 'run' echo "Obtain available systems" snap debug api '/v2/systems' > systems.json # TODO:UC20: there is only one system for now - jq .result.systems[0].current < systems.json | MATCH 'true' - label="$(jq -r .result.systems[0].label < systems.json)" + gojq .result.systems[0].current < systems.json | MATCH 'true' + label="$(gojq -r .result.systems[0].label < systems.json)" test -n "$label" # make sure that the seed exists test -d "/var/lib/snapd/seed/systems/$label" - jq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install recover run' + gojq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install recover run' # keep a copy of the systems dump for later reference cp systems.json /writable/systems.json.run @@ -66,7 +64,7 @@ execute: | test -e /host/ubuntu-data/systems.json.run snap debug api '/v2/systems' > systems.json - jq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install run' + gojq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install run' label="$(cat /host/ubuntu-data/systems.label)" test -n "$label" @@ -79,11 +77,11 @@ execute: | elif [ "$SPREAD_REBOOT" == "2" ]; then echo "In run mode again" snap debug api '/v2/system-info' > system-info - jq -r '.result["system-mode"]' < system-info | MATCH 'run' + gojq -r '.result["system-mode"]' < system-info | MATCH 'run' # now go back to recover mode so we can test that a simple reboot # works to transition us back to run mode - label="$(jq -r .result.systems[0].label < systems.json)" + label="$(gojq -r .result.systems[0].label < systems.json)" transition_to_recover_mode "$label" elif [ "$SPREAD_REBOOT" == "3" ]; then echo "In recover mode again" @@ -107,5 +105,5 @@ execute: | elif [ "$SPREAD_REBOOT" == "4" ]; then echo "In run mode again again" snap debug api '/v2/system-info' > system-info - jq -r '.result["system-mode"]' < system-info | MATCH 'run' + gojq -r '.result["system-mode"]' < system-info | MATCH 'run' fi diff --git a/tests/lib/external/snapd-testing-tools/spread.yaml b/tests/lib/external/snapd-testing-tools/spread.yaml index 9282cc06ed4..1d966071faf 100644 --- a/tests/lib/external/snapd-testing-tools/spread.yaml +++ b/tests/lib/external/snapd-testing-tools/spread.yaml @@ -20,7 +20,6 @@ backends: - debian-11-64: - debian-12-64: - debian-sid-64: - - fedora-39-64: - arch-linux-64: - amazon-linux-2-64: storage: preserve-size diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index bf0c0ab14f6..dcd810e00f9 100755 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -299,7 +299,7 @@ nested_get_snap_rev_for_channel() { -H "Content-Type: application/json" \ --data "{\"context\": [], \"actions\": [{\"action\": \"install\", \"name\": \"$SNAP\", \"channel\": \"$CHANNEL\", \"instance-key\": \"1\"}]}" \ https://api.snapcraft.io/v2/snaps/refresh | \ - jq '.results[0].snap.revision' + gojq '.results[0].snap.revision' } nested_is_nested_system() { diff --git a/tests/lib/pkgdb.sh b/tests/lib/pkgdb.sh index 9bad51a6723..ecb46753415 100755 --- a/tests/lib/pkgdb.sh +++ b/tests/lib/pkgdb.sh @@ -589,7 +589,6 @@ pkg_dependencies_ubuntu_classic(){ echo " dbus-user-session gccgo-8 - gperf evolution-data-server fwupd packagekit @@ -617,6 +616,7 @@ pkg_dependencies_ubuntu_classic(){ dbus-user-session fwupd golang + gperf libvirt-daemon-system linux-tools-$(uname -r) lz4 diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh index c9331bd4336..4b187aca9bb 100755 --- a/tests/lib/prepare-restore.sh +++ b/tests/lib/prepare-restore.sh @@ -608,6 +608,15 @@ prepare_project() { disable_journald_rate_limiting disable_journald_start_limiting fi + + # native jq replacement, but still with some incompatibilies, see + # https://github.com/itchyny/gojq + # major differences: + # - map keys are sorted by default + # - with --yaml-input, can parse YAML + GOBIN=$PROJECT_PATH/tests/bin \ + CGO_ENABLED=0 \ + go install github.com/itchyny/gojq/cmd/gojq@v0.12.16 } prepare_project_each() { @@ -772,6 +781,16 @@ restore_suite_each() { "$TESTSLIB"/reset.sh --reuse-core fi + # The ntp service randomly fails to create a socket on virbr0-nic, + # generating issues in actions like the auto-refresh (in Xenial). + # The errror lines are: + # ntpd: bind(23) AF_INET6 ... flags 0x11 failed: Cannot assign requested address + # ntpd: unable to create socket on virbr0-nic + # ntpd: kernel reports TIME_ERROR: 0x41: Clock Unsynchronized + if os.query is-xenial && systemctl status ntp | MATCH TIME_ERROR; then + systemctl restart ntp + fi + # Check for invariants late, in order to detect any bugs in the code above. if [[ "$variant" = full ]]; then "$TESTSTOOLS"/cleanup-state pre-invariant diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 799fb8f2c1f..3177c649247 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -97,9 +97,6 @@ ensure_jq() { } disable_refreshes() { - echo "Ensure jq is available" - ensure_jq - echo "Modify state to make it look like the last refresh just happened" systemctl stop snapd.socket snapd.service "$TESTSTOOLS"/snapd-state prevent-autorefresh @@ -108,13 +105,6 @@ disable_refreshes() { echo "Minimize risk of hitting refresh schedule" snap set core refresh.schedule=00:00-23:59 snap refresh --time --abs-time | MATCH "last: 2[0-9]{3}" - - echo "Ensure jq is gone" - snap remove --purge jq - snap remove --purge jq-core18 - snap remove --purge jq-core20 - snap remove --purge jq-core22 - snap remove --purge test-snapd-jq-core24 } setup_systemd_snapd_overrides() { @@ -613,9 +603,10 @@ build_snapd_snap_with_run_mode_firstboot_tweaks() { mv "${PROJECT_PATH}/snapd_from_snapcraft.snap" "/tmp/snapd_from_snapcraft.snap" fi - local UNPACK_DIR="/tmp/snapd-unpack" - rm -rf "${UNPACK_DIR}" - unsquashfs -no-progress -d "$UNPACK_DIR" /tmp/snapd_from_snapcraft.snap + # TODO set up a trap to clean this up properly? + local UNPACK_DIR + UNPACK_DIR="$(mktemp -d /tmp/snapd-unpack.XXXXXXXX)" + unsquashfs -no-progress -f -d "$UNPACK_DIR" /tmp/snapd_from_snapcraft.snap # now install a unit that sets up enough so that we can connect cat > "$UNPACK_DIR"/lib/systemd/system/snapd.spread-tests-run-mode-tweaks.service <<'EOF' @@ -697,8 +688,10 @@ repack_core_snap_with_tweaks() { local CORESNAP="$1" local TARGET="$2" - local UNPACK_DIR="/tmp/core-unpack" - unsquashfs -no-progress -d "$UNPACK_DIR" "$CORESNAP" + local UNPACK_DIR + # TODO set up a trap to clean this up properly? + UNPACK_DIR="$(mktemp -d /tmp/core-unpack.XXXXXXXX)" + unsquashfs -no-progress -f -d "$UNPACK_DIR" "$CORESNAP" mkdir -p "$UNPACK_DIR"/etc/systemd/journald.conf.d cat < "$UNPACK_DIR"/etc/systemd/journald.conf.d/to-console.conf @@ -736,9 +729,10 @@ repack_kernel_snap() { fi echo "Repacking kernel snap" - UNPACK_DIR=/tmp/kernel-unpack + # TODO set up a trap to clean this up properly? + UNPACK_DIR="$(mktemp -d /tmp/kernel-unpack.XXXXXXXX)" snap download --basename=pc-kernel --channel="$CHANNEL/${KERNEL_CHANNEL}" pc-kernel - unsquashfs -no-progress -d "$UNPACK_DIR" pc-kernel.snap + unsquashfs -no-progress -f -d "$UNPACK_DIR" pc-kernel.snap snap pack --filename="$TARGET" "$UNPACK_DIR" rm -rf pc-kernel.snap "$UNPACK_DIR" @@ -975,7 +969,7 @@ uc24_build_initramfs_kernel_snap() { chmod +x ./initrd/main/usr/lib/snapd/snap-bootstrap if [ "$injectKernelPanic" = "true" ]; then # add a kernel panic to the end of the-tool execution - echo "echo 'forcibly panicking'; echo c > /proc/sysrq-trigger" >> ./initrd/main/usr/lib/snapd/snap-bootstrap + echo "echo 'forcibly panicking'; echo c > /proc/sysrq-trigger" > ./initrd/main/usr/lib/snapd/snap-bootstrap fi (cd ./initrd/early; find . | cpio --create --quiet --format=newc --owner=0:0) >initrd.img @@ -1170,8 +1164,10 @@ setup_reflash_magic() { snap model --verbose # remove the above debug lines once the mentioned bug is fixed snap install "--channel=${CORE_CHANNEL}" "$core_name" - UNPACK_DIR="/tmp/$core_name-snap" - unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap + # TODO set up a trap to clean this up properly? + local UNPACK_DIR + UNPACK_DIR="$(mktemp -d "/tmp/$core_name-unpack.XXXXXXXX")" + unsquashfs -no-progress -f -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap if os.query is-arm; then snap install ubuntu-image --channel="$UBUNTU_IMAGE_SNAP_CHANNEL" --classic @@ -1242,7 +1238,7 @@ EOF # Make /var/lib/systemd writable so that we can get linger enabled. # This only applies to Ubuntu Core 16 where individual directories were # writable. In Core 18 and beyond all of /var/lib/systemd is writable. - mkdir -p $UNPACK_DIR/var/lib/systemd/{catalog,coredump,deb-systemd-helper-enabled,rfkill,linger} + mkdir -p "$UNPACK_DIR"/var/lib/systemd/{catalog,coredump,deb-systemd-helper-enabled,rfkill,linger} touch "$UNPACK_DIR"/var/lib/systemd/random-seed # build new core snap for the image diff --git a/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd b/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd new file mode 100755 index 00000000000..b49a7080b24 --- /dev/null +++ b/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd @@ -0,0 +1,4 @@ +#!/bin/sh +PS1='$ ' + +exec "$@" diff --git a/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml b/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml new file mode 100644 index 00000000000..3eea84c6242 --- /dev/null +++ b/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml @@ -0,0 +1,34 @@ +name: test-snapd-mount-control-nfs +summary: Snap for testing mount-control with NFS +description: Snap for testing mount-control with NFS +version: "1.0" +base: core22 +confinement: strict + +apps: + cmd: + command: bin/cmd + plugs: + - mntctl + - network + - removable-media + +plugs: + mntctl: + interface: mount-control + mount: + - type: [nfs] + where: /media/** + options: [rw] + +parts: + apps: + plugin: dump + source: . + + network-shares: + plugin: nil + stage-packages: + - nfs-common + stage: + - -lib/systemd/system/nfs-common.service diff --git a/tests/lib/spread/backend.openstack.yaml b/tests/lib/spread/backend.openstack.yaml index 45fb4f522fa..9f6b60667ec 100644 --- a/tests/lib/spread/backend.openstack.yaml +++ b/tests/lib/spread/backend.openstack.yaml @@ -1,6 +1,6 @@ openstack: key: '$(HOST: echo "$SPREAD_OPENSTACK_ENV")' - plan: staging-cpu2-ram4-disk50 + plan: staging-cpu2-ram4-disk20 halt-timeout: 2h groups: [default] environment: @@ -22,6 +22,9 @@ - fedora-40-64: image: snapd-spread/fedora-40-64 workers: 6 + - fedora-41-64: + image: snapd-spread/fedora-41-64 + workers: 6 - opensuse-15.5-64: image: snapd-spread/opensuse-15.5-64 diff --git a/tests/lib/spread/backend.testflinger.beta.yaml b/tests/lib/spread/backend.testflinger.beta.yaml index 3203d4f7b18..651da380f40 100644 --- a/tests/lib/spread/backend.testflinger.beta.yaml +++ b/tests/lib/spread/backend.testflinger.beta.yaml @@ -1,4 +1,4 @@ - #this backend is used for edge validation + #this backend is used for beta validation testflinger: environment: TRUST_TEST_KEYS: "false" diff --git a/tests/lib/spread/rules/nested.yaml b/tests/lib/spread/rules/nested.yaml index a4341187eef..71d463fbb70 100644 --- a/tests/lib/spread/rules/nested.yaml +++ b/tests/lib/spread/rules/nested.yaml @@ -4,6 +4,12 @@ rules: - tests/nested/.* to: [$SELF] - rest: - from: [.*] + nestedlib: + from: + - tests/lib/nested.sh + to: [tests/nested/] + + assertions: + from: + - tests/lib/assertions/.* to: [tests/nested/] diff --git a/tests/lib/tools/snapd-state b/tests/lib/tools/snapd-state index 4a5a118b5f0..9e7ce1b76ba 100755 --- a/tests/lib/tools/snapd-state +++ b/tests/lib/tools/snapd-state @@ -19,7 +19,7 @@ print_state() { echo "snapd-state: jq-filter is a required parameter" exit 1 fi - jq -r "$JQ_FILTER" < /var/lib/snapd/state.json + gojq -r "$JQ_FILTER" < /var/lib/snapd/state.json } check_state() { @@ -56,17 +56,17 @@ change_snap_channel() { echo "snapd-state: snap and channel are required parameters" exit 1 fi - jq ".data.snaps[\"$SNAP\"].channel = \"$CHANNEL\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".data.snaps[\"$SNAP\"].channel = \"$CHANNEL\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json } force_autorefresh() { - jq ".data[\"last-refresh\"] = \"2007-08-22T09:30:44.449455783+01:00\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".data[\"last-refresh\"] = \"2007-08-22T09:30:44.449455783+01:00\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json } prevent_autorefresh() { - jq ".data[\"last-refresh\"] = \"$(date +%Y-%m-%dT%H:%M:%S%:z)\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".data[\"last-refresh\"] = \"$(date +%Y-%m-%dT%H:%M:%S%:z)\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json } diff --git a/tests/lib/tools/store-state b/tests/lib/tools/store-state index a4009e7140f..d49e59f72fa 100755 --- a/tests/lib/tools/store-state +++ b/tests/lib/tools/store-state @@ -1,5 +1,7 @@ #!/bin/bash +set -e + STORE_CONFIG=/etc/systemd/system/snapd.service.d/store.conf show_help() { @@ -71,21 +73,9 @@ make_snap_installable(){ local snap_id="${3:-}" if [ -n "$snap_id" ]; then - if ! command -v yaml2json; then - # FIXME: When fakestore is setup this snap cannot be installed - # TODO: Install remarshal in setup_fake_store - snap install remarshal - fi - if ! command -v jq; then - # FIXME: When fakestore is setup this snap cannot be installed - # TODO: Install jq in setup_fake_store or don't use snapped jq - SUFFIX="$(snaps.name snap-suffix)" - snap install "jq$SUFFIX" - fi - # unsquash the snap to get its name unsquashfs -d /tmp/snap-squashfs "$snap_path" meta/snap.yaml - snap_name=$(yaml2json < /tmp/snap-squashfs/meta/snap.yaml | jq -r .name) + snap_name=$(gojq --yaml-input -r '.name' < /tmp/snap-squashfs/meta/snap.yaml) rm -rf /tmp/snap-squashfs cat >> /tmp/snap-decl.json << EOF @@ -100,7 +90,7 @@ EOF if [ -n "$extra_decl_json_file" ]; then # then we need to combine the extra snap declaration json with the one # we just wrote - jq -s '.[0] * .[1]' <(cat /tmp/snap-decl.json) <(cat "$extra_decl_json_file") > /tmp/snap-decl.json.tmp + gojq -s '.[0] * .[1]' <(cat /tmp/snap-decl.json) <(cat "$extra_decl_json_file") > /tmp/snap-decl.json.tmp mv /tmp/snap-decl.json.tmp /tmp/snap-decl.json fi @@ -141,8 +131,9 @@ setup_fake_store(){ return 1 fi - # before switching make sure we have a session macaroon - snap find test-snapd-tools + # before switching make sure we have a session macaroon, but keep it best + # effort + snap find test-snapd-tools || true mkdir -p "$top_dir/asserts" # debugging diff --git a/tests/main/api-get-systems-label/task.yaml b/tests/main/api-get-systems-label/task.yaml index 4423320e3e3..2ea1611b68e 100644 --- a/tests/main/api-get-systems-label/task.yaml +++ b/tests/main/api-get-systems-label/task.yaml @@ -8,27 +8,25 @@ systems: - ubuntu-core-2* execute: | - snap install --edge jq - echo "Find what systems are available" snap debug api /v2/systems > systems - current_label=$(jq -r '.result.systems[0]["label"]' < systems) + current_label=$(gojq -r '.result.systems[0]["label"]' < systems) echo "Get details for a specific system" snap debug api "/v2/systems/$current_label" > current-system echo "Ensure the result contains a model assertion" - jq -r '.result.model.type' < current-system | MATCH model - jq -r '.result.model.series' < current-system | MATCH 16 - jq -r '.result.model.base' < current-system | MATCH "core[0-9][0-9]" + gojq -r '.result.model.type' < current-system | MATCH model + gojq -r '.result.model.series' < current-system | MATCH 16 + gojq -r '.result.model.base' < current-system | MATCH "core[0-9][0-9]" echo "Ensure the result looks like a systems reply" - jq -r '.result.brand.id' < current-system | MATCH "$(snap model --verbose|awk '/brand-id:/ {print $2}')" - jq -r '.result.brand.validation' < current-system | MATCH '(verified|unproven|starred)' - jq -r '.result.label' < current-system | MATCH "$current_label" - jq -r '.result.current' < current-system | MATCH '(true|false)' + gojq -r '.result.brand.id' < current-system | MATCH "$(snap model --verbose|awk '/brand-id:/ {print $2}')" + gojq -r '.result.brand.validation' < current-system | MATCH '(verified|unproven|starred)' + gojq -r '.result.label' < current-system | MATCH "$current_label" + gojq -r '.result.current' < current-system | MATCH '(true|false)' # we expect at least one current action to be available and # each action always has a mode - jq -r '.result.actions[0]' < current-system | MATCH 'mode' + gojq -r '.result.actions[0]' < current-system | MATCH 'mode' echo "Ensure the result contains the gadget volumes" - jq -r '.result.volumes' < current-system | MATCH bootloader + gojq -r '.result.volumes' < current-system | MATCH bootloader # internal fields are not exported - jq -r '.result.volumes' < current-system | NOMATCH VolumeName + gojq -r '.result.volumes' < current-system | NOMATCH VolumeName diff --git a/tests/main/apparmor-prompting-flag-restart/task.yaml b/tests/main/apparmor-prompting-flag-restart/task.yaml index 2dbb6b0db47..d2fe74115b9 100644 --- a/tests/main/apparmor-prompting-flag-restart/task.yaml +++ b/tests/main/apparmor-prompting-flag-restart/task.yaml @@ -14,7 +14,6 @@ systems: - ubuntu-core-* prepare: | - snap install jq # prerequisite for having a prompts handler service snap set system experimental.user-daemons=true "$TESTSTOOLS"/snaps-state install-local test-snapd-prompt-handler @@ -93,9 +92,9 @@ execute: | echo "Check that snap CLI reports prompting flag set correctly" snap get system experimental.apparmor-prompting | MATCH "$1" echo "Check that /v2/snaps/system/conf reports prompting flag set correctly" - snap debug api /v2/snaps/system/conf | jq -r '.result.experimental."apparmor-prompting"' | MATCH "$1" + snap debug api /v2/snaps/system/conf | gojq -r '.result.experimental."apparmor-prompting"' | MATCH "$1" echo "Check that /v2/system-info reports prompting correctly" - snap debug api /v2/system-info | jq -r '.result.features."apparmor-prompting".enabled' | MATCH "$1" + snap debug api /v2/system-info | gojq -r '.result.features."apparmor-prompting".enabled' | MATCH "$1" } echo "Precondition check that snapd is active" @@ -140,14 +139,18 @@ execute: | echo "Enable prompting via API request" - echo '{"experimental.apparmor-prompting": true}' | snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | jq -r '.status' | MATCH "Accepted" || reset_start_limit + echo '{"experimental.apparmor-prompting": true}' | \ + snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | \ + gojq -r '.status' | MATCH "Accepted" || reset_start_limit echo "Check that snapd restarted after prompting set to true via api" check_snapd_restarted check_prompting_setting "true" echo "Disable prompting via API request" - echo '{"experimental.apparmor-prompting": false}' | snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | jq -r '.status' | MATCH "Accepted" || reset_start_limit + echo '{"experimental.apparmor-prompting": false}' | \ + snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | \ + gojq -r '.status' | MATCH "Accepted" || reset_start_limit echo "Check that snapd restarted after prompting set to false via api" check_snapd_restarted diff --git a/tests/main/apparmor-prompting-snapd-startup/task.yaml b/tests/main/apparmor-prompting-snapd-startup/task.yaml index 92befc7fd82..9d7ee489e51 100644 --- a/tests/main/apparmor-prompting-snapd-startup/task.yaml +++ b/tests/main/apparmor-prompting-snapd-startup/task.yaml @@ -33,7 +33,8 @@ execute: | echo "Write two rules to disk, one of which is expired" mkdir -p "$(dirname $RULES_PATH)" - echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"},{"id":"0000000000000003","timestamp":"2004-10-20T16:47:32.138415627-05:00","user":1000,"snap":"firefox","interface":"home","constraints":{"path-pattern":"/home/test/Downloads/**","permissions":["read","write"]},"outcome":"allow","lifespan":"timespan","expiration":"2005-04-08T00:00:00Z"}]}' | tee "$RULES_PATH" + echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"},{"id":"0000000000000003","timestamp":"2004-10-20T16:47:32.138415627-05:00","user":1000,"snap":"firefox","interface":"home","constraints":{"path-pattern":"/home/test/Downloads/**","permissions":["read","write"]},"outcome":"allow","lifespan":"timespan","expiration":"2005-04-08T00:00:00Z"}]}' | \ + tee "$RULES_PATH" # Prompting is unsupported everywhere but the Ubuntu non-core systems with # kernels which support apparmor prompting @@ -65,25 +66,28 @@ execute: | retry --wait 1 -n 60 systemctl is-active snapd echo "Check that apparmor prompting is supported and enabled" - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true # Write expected rules after the expired rule has been removed - echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"}]}' | jq | tee expected.json - # Parse existing rules through jq so they can be compared - jq < "$RULES_PATH" > current.json + echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"}]}' | \ + gojq | tee expected.json + # Parse existing rules through (go)jq so they can be compared + gojq < "$RULES_PATH" > current.json echo "Check that rules on disk match what is expected" diff expected.json current.json echo "Check that we received two notices" - snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 2 - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result' | grep -c '"removed": "expired"' | MATCH 1 + snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result | length' | MATCH 2 + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result' | grep -c '"removed": "expired"' | MATCH 1 echo "Check that only the former rule is still valid (must be done with UID 1000)" - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1 - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002" + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1 + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002" echo "Stop snapd and ensure it is not in failure mode" systemctl stop snapd.service snapd.socket @@ -98,20 +102,22 @@ execute: | retry --wait 1 -n 60 systemctl is-active snapd.service snapd.socket echo "Check that apparmor prompting is supported and enabled" - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true echo "Check that rules on disk still match what is expected" diff expected.json current.json echo "Check that we received one notices for the non-expired rule" - snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 1 - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result[0].key' | MATCH "0000000000000002" + snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result | length' | MATCH 1 + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result[0].key' | MATCH "0000000000000002" echo "Check that only the non-expired rule is still valid (must be done with UID 1000)" - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1 - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002" + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1 + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002" echo '### Simulate failure to open interfaces requests manager ###' @@ -134,15 +140,16 @@ execute: | echo "Check that apparmor prompting is supported and enabled" # XXX: in the future, we should set enabled to be false if m.AppArmorPromptingRunning() is false, # such as because creating the interfaces requests manager failed. - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true echo "Check that rules on disk still match what is expected" diff expected.json current.json echo "Check that accessing a prompting endpoint results in an expected error" - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '."status-code"' | MATCH 500 - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result.message' | MATCH -i "Apparmor Prompting is not running" + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '."status-code"' | MATCH 500 + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | \ + gojq '.result.message' | MATCH -i "Apparmor Prompting is not running" echo '### Remove the corrupted max prompt ID file and check that prompting backends can start again ###' @@ -162,17 +169,19 @@ execute: | retry --wait 1 -n 60 systemctl is-active snapd.service snapd.socket echo "Check that apparmor prompting is supported and enabled" - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true - snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true + snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true echo "Check that rules on disk still match what is expected" diff expected.json current.json echo "Check that we received one notices for the non-expired rule" - snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 1 - snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result[0].key' | MATCH "0000000000000002" + snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result | length' | MATCH 1 + snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \ + gojq '.result[0].key' | MATCH "0000000000000002" echo "Check that the non-expired rule is still valid (must be done with UID 1000)" - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1 - sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002" + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1 + sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002" diff --git a/tests/main/appstream-id/task.yaml b/tests/main/appstream-id/task.yaml index d91606c3cc0..6f4c5174583 100644 --- a/tests/main/appstream-id/task.yaml +++ b/tests/main/appstream-id/task.yaml @@ -7,9 +7,6 @@ details: | # ubuntu-core: no jq systems: [-ubuntu-core*] -prepare: | - snap install --edge jq - debug: | cat response || true @@ -17,7 +14,7 @@ execute: | echo "Verify that search results contain common-ids" timeout 5 snap debug api /v2/find?name=test-snapd-appstreamid > response # shellcheck disable=SC2002 - cat response | jq -r ' .result[0]["common-ids"] | sort | join (",")' | \ + cat response | gojq -r ' .result[0]["common-ids"] | sort | join (",")' | \ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo' snap install --edge test-snapd-appstreamid @@ -25,11 +22,11 @@ execute: | echo "Verify that installed snap info contains common-ids" timeout 5 snap debug api /v2/snaps/test-snapd-appstreamid > response # shellcheck disable=SC2002 - cat response | jq -r ' .result["common-ids"] | sort | join(",")' | \ + cat response | gojq -r ' .result["common-ids"] | sort | join(",")' | \ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo' echo "Verify that apps have their common-id set" timeout 5 snap debug api /v2/apps?names=test-snapd-appstreamid > response # shellcheck disable=SC2002 - cat response | jq -r ' .result | sort_by(.name) | [.[]."common-id"] | join(",")' | \ + cat response | gojq -r ' .result | sort_by(.name) | [.[]."common-id"] | join(",")' | \ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,,io.snapcraft.test-snapd-appstreamid.foo' diff --git a/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh b/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh index 73ab37f07be..0f4a8ce3a72 100755 --- a/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh +++ b/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh @@ -4,4 +4,4 @@ LAST_CHANGE_ID=$1 CHANGES_COUNT=$2 #shellcheck disable=SC2086,SC2046 -test $(snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT +test $(snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT diff --git a/tests/main/auto-refresh-backoff/task.yaml b/tests/main/auto-refresh-backoff/task.yaml index f308ffe1b42..a513a8759a4 100644 --- a/tests/main/auto-refresh-backoff/task.yaml +++ b/tests/main/auto-refresh-backoff/task.yaml @@ -18,10 +18,6 @@ prepare: | exit fi - # Needed by make-snap-installable, Install before switching to fakestore - snap install jq - snap install remarshal - # Install snaps as baseline since we want to test what happens in refreshes not installs echo "Given installed snaps" snap install "$SNAP_ONE" "$SNAP_TWO" @@ -50,7 +46,7 @@ restore: | rm -rf "$BLOB_DIR" debug: | - snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)" + snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)" execute: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -94,7 +90,7 @@ execute: | } # Record last change id before we start to avoid flakiness due to auto-refreshes in other tests - LAST_CHANGE_ID=$(snap debug api /v2/changes?select=all | jq '.result | sort_by(.id|tonumber) | .[-1].id') + LAST_CHANGE_ID=$(snap debug api /v2/changes?select=all | gojq '.result | sort_by(.id|tonumber) | .[-1].id') # -------- FIRST AUTO REFRESH -------- @@ -162,26 +158,26 @@ execute: | readlink "$SNAP_MOUNT_DIR/$SNAP_TWO/current" | MATCH 33 echo "Check auto-refresh behaviour matches expectations for backoff algorithm" - snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json + snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json # 1st auto-refresh - jq '.[0].status' < changes.json | MATCH "Error" - jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_ONE" - jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" - jq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_ONE" - jq '.[0].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" + gojq '.[0].status' < changes.json | MATCH "Error" + gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_ONE" + gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" + gojq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_ONE" + gojq '.[0].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" # 2nd auto-refresh - jq '.[1].status' < changes.json | MATCH "Done" + gojq '.[1].status' < changes.json | MATCH "Done" # Broken SNAP_ONE should have been skipped this time - jq '.[1].data."snap-names"' < changes.json | NOMATCH "$SNAP_ONE" - jq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" - jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE" - jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" + gojq '.[1].data."snap-names"' < changes.json | NOMATCH "$SNAP_ONE" + gojq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" + gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE" + gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" # 3rd auto-refresh - jq '.[2].status' < changes.json | MATCH "Done" - jq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_ONE" - jq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" - jq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE" - jq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" + gojq '.[2].status' < changes.json | MATCH "Done" + gojq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_ONE" + gojq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_TWO" + gojq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE" + gojq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO" diff --git a/tests/main/auto-refresh-gating-from-snap/task.yaml b/tests/main/auto-refresh-gating-from-snap/task.yaml index 4641971043c..72c7c53753c 100644 --- a/tests/main/auto-refresh-gating-from-snap/task.yaml +++ b/tests/main/auto-refresh-gating-from-snap/task.yaml @@ -16,7 +16,6 @@ environment: DEBUG_LOG_FILE: /var/snap/test-snapd-refresh-control/common/debug.log prepare: | - snap install --devmode jq snap set system experimental.gate-auto-refresh-hook=true echo "Install test snaps" @@ -25,7 +24,7 @@ prepare: | "$TESTSTOOLS"/snaps-state install-local "$NOHOOK_SNAP" debug: | - jq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true + gojq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true execute: | LAST_REFRESH_CHANGE_ID=1 diff --git a/tests/main/auto-refresh-gating/task.yaml b/tests/main/auto-refresh-gating/task.yaml index de3a18c905a..dea535b473f 100644 --- a/tests/main/auto-refresh-gating/task.yaml +++ b/tests/main/auto-refresh-gating/task.yaml @@ -14,11 +14,10 @@ environment: DEBUG_LOG_FILE: /var/snap/test-snapd-refresh-control/common/debug.log prepare: | - snap install --devmode jq snap set system experimental.gate-auto-refresh-hook=true debug: | - jq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true + gojq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true snap changes || true snap refresh --time || true diff --git a/tests/main/auto-refresh-pre-download/task.yaml b/tests/main/auto-refresh-pre-download/task.yaml index 7c198541a61..69833520218 100644 --- a/tests/main/auto-refresh-pre-download/task.yaml +++ b/tests/main/auto-refresh-pre-download/task.yaml @@ -22,11 +22,9 @@ environment: prepare: | # ensure no other refreshes interfere with the test snap refresh - snap install --devmode jq snap install test-snapd-sh restore: | - snap remove --purge jq || true snap remove --purge test-snapd-sh || true debug: | diff --git a/tests/main/auto-refresh-private/task.yaml b/tests/main/auto-refresh-private/task.yaml index 67350081b68..10dd684cb3c 100644 --- a/tests/main/auto-refresh-private/task.yaml +++ b/tests/main/auto-refresh-private/task.yaml @@ -85,7 +85,8 @@ execute: | systemctl stop snapd.{service,socket} - jq ".data.auth.users[0][\"store-macaroon\"] = \"$M\"|.data.auth.users[0][\"store-discharges\"][0] = \"$D\"" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".data.auth.users[0][\"store-macaroon\"] = \"$M\"|.data.auth.users[0][\"store-discharges\"][0] = \"$D\"" \ + /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json "$TESTSTOOLS"/snapd-state force-autorefresh systemctl start snapd.{service,socket} diff --git a/tests/main/auto-refresh/task.yaml b/tests/main/auto-refresh/task.yaml index 66f48baa71f..5c7979af617 100644 --- a/tests/main/auto-refresh/task.yaml +++ b/tests/main/auto-refresh/task.yaml @@ -10,7 +10,6 @@ environment: SNAP_NAME/parallel: test-snapd-tools_instance prepare: | - snap install --devmode jq if [[ "$SPREAD_VARIANT" =~ parallel ]]; then snap set system experimental.parallel-instances=true fi @@ -66,7 +65,7 @@ execute: | snap list|MATCH "$SNAP_NAME +[0-9]+\\.[0-9]+\\+fake1" echo "Ensure refresh.last is set" - jq ".data[\"last-refresh\"]" /var/lib/snapd/state.json | MATCH "$(date +%Y)" + gojq ".data[\"last-refresh\"]" /var/lib/snapd/state.json | MATCH "$(date +%Y)" echo "No refresh hold at this point" snap refresh --time | NOMATCH "^hold:" diff --git a/tests/main/aux-info/task.yaml b/tests/main/aux-info/task.yaml index 443754a616e..ff176b4e8ae 100644 --- a/tests/main/aux-info/task.yaml +++ b/tests/main/aux-info/task.yaml @@ -9,17 +9,16 @@ systems: [ubuntu-18.04-64, ubuntu-2*, ubuntu-core-*, fedora-*] prepare: | snap install snap-store - snap install jq execute: | - snap_id=$(snap info snap-store | grep snap-id | awk '{ print $2 }') - jq --sort-keys .media < "/var/cache/snapd/aux/${snap_id}.json" > media.json + snap_id=$(snap info snap-store | gojq -r --yaml-input '.["snap-id"]') + gojq .media < "/var/cache/snapd/aux/${snap_id}.json" > media.json # don't depend on the exact number of media files, but there should be # something here - media_length=$(jq '. | length' < media.json) + media_length=$(gojq '. | length' < media.json) test "${media_length}" -gt 0 - timeout 5 snap debug api /v2/snaps/snap-store | jq --sort-keys .result.media > snapd-media.json + timeout 5 snap debug api /v2/snaps/snap-store | gojq .result.media > snapd-media.json diff media.json snapd-media.json diff --git a/tests/main/cgroup-devices-v2/task.yaml b/tests/main/cgroup-devices-v2/task.yaml index 07743752c4e..165bcf7ebd3 100644 --- a/tests/main/cgroup-devices-v2/task.yaml +++ b/tests/main/cgroup-devices-v2/task.yaml @@ -59,7 +59,7 @@ execute: | dump_cgroup_device_progs() { # dump all progs but those that are assigned to systemd, in which case # they have the pids list non empty - bpftool prog list -j | jq -r '.[] | select(.type == "cgroup_device") | select(.pids == null) | .id' + bpftool prog list -j | gojq -r '.[] | select(.type == "cgroup_device") | select(.pids == null) | .id' } echo "Dump BPF programs that are of type cgroup_device" dump_cgroup_device_progs > cgroup_device.progs-1 diff --git a/tests/main/cloud-init/task.yaml b/tests/main/cloud-init/task.yaml index 41454b51772..aca43d75930 100644 --- a/tests/main/cloud-init/task.yaml +++ b/tests/main/cloud-init/task.yaml @@ -8,11 +8,6 @@ details: | # GCE backend sets instance data backends: [google] -prepare: | - if ! command -v jq; then - snap install --devmode jq - fi - execute: | if [[ ! -e /run/cloud-init/instance-data.json ]]; then echo "cloud-init instance data is required to execute the test" @@ -35,7 +30,7 @@ execute: | kname=${kname/_/-} fi - jq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json + gojq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json } # GCE sets the following in Ubuntu images: # { diff --git a/tests/main/component-local-installs/comp-four/meta/component.yaml b/tests/main/component-local-installs/comp-four/meta/component.yaml new file mode 100644 index 00000000000..7b00171df07 --- /dev/null +++ b/tests/main/component-local-installs/comp-four/meta/component.yaml @@ -0,0 +1,5 @@ +component: test-snap-two+comp-four +type: standard +version: '1.0' +summary: test component +description: test component diff --git a/tests/main/component-local-installs/comp-one/meta/component.yaml b/tests/main/component-local-installs/comp-one/meta/component.yaml new file mode 100644 index 00000000000..165fdb3b8e2 --- /dev/null +++ b/tests/main/component-local-installs/comp-one/meta/component.yaml @@ -0,0 +1,5 @@ +component: test-snap-one+comp-one +type: standard +version: '1.0' +summary: test component +description: test component diff --git a/tests/main/component-local-installs/comp-three/meta/component.yaml b/tests/main/component-local-installs/comp-three/meta/component.yaml new file mode 100644 index 00000000000..c4897d8cc51 --- /dev/null +++ b/tests/main/component-local-installs/comp-three/meta/component.yaml @@ -0,0 +1,5 @@ +component: test-snap-two+comp-three +type: standard +version: '1.0' +summary: test component +description: test component diff --git a/tests/main/component-local-installs/comp-two/meta/component.yaml b/tests/main/component-local-installs/comp-two/meta/component.yaml new file mode 100644 index 00000000000..c4772ef15ee --- /dev/null +++ b/tests/main/component-local-installs/comp-two/meta/component.yaml @@ -0,0 +1,5 @@ +component: test-snap-one+comp-two +type: standard +version: '1.0' +summary: test component +description: test component diff --git a/tests/main/component-local-installs/task.yaml b/tests/main/component-local-installs/task.yaml new file mode 100644 index 00000000000..8f7d6d0b9dd --- /dev/null +++ b/tests/main/component-local-installs/task.yaml @@ -0,0 +1,81 @@ +summary: Test installing components and snaps from local files. + +details: | + Test that we can install components and snaps from local files. + We test various combinations, like: + - installing a snap and some components for that snap + - installing a snap and some components for another snap (that is already installed) + - failing to install a snap and some components for another snap (that is not installed) + - installing multiple components for different snaps + +systems: [ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-2*, ubuntu-core-*, fedora-*] + +execute: | + for container in test-snap-one/ test-snap-two/ comp-one/ comp-two/ comp-three/ comp-four/; do + snap pack $container + done + + # install everything at once + snap install --dangerous \ + ./test-snap-one_1.0_all.snap \ + ./test-snap-two_1.0_all.snap \ + ./test-snap-one+comp-one_1.0.comp \ + ./test-snap-one+comp-two_1.0.comp \ + ./test-snap-two+comp-three_1.0.comp \ + ./test-snap-two+comp-four_1.0.comp + + snap run test-snap-one comp-one + snap run test-snap-one comp-two + snap run test-snap-two comp-three + snap run test-snap-two comp-four + + snap remove test-snap-one test-snap-two + + # install only the snaps + snap install --dangerous \ + ./test-snap-one_1.0_all.snap \ + ./test-snap-two_1.0_all.snap + not snap run test-snap-one comp-one + not snap run test-snap-one comp-two + not snap run test-snap-two comp-three + not snap run test-snap-two comp-four + + # install only the components + snap install --dangerous \ + ./test-snap-one+comp-one_1.0.comp \ + ./test-snap-one+comp-two_1.0.comp \ + ./test-snap-two+comp-three_1.0.comp \ + ./test-snap-two+comp-four_1.0.comp + snap run test-snap-one comp-one + snap run test-snap-one comp-two + snap run test-snap-two comp-three + snap run test-snap-two comp-four + + snap remove test-snap-one test-snap-two + + # install mixture of components for already installed snaps and a new snap + # with components + snap install --dangerous \ + ./test-snap-one_1.0_all.snap \ + ./test-snap-one+comp-one_1.0.comp + snap run test-snap-one comp-one + not snap run test-snap-one comp-two + snap install --dangerous \ + ./test-snap-one+comp-two_1.0.comp \ + ./test-snap-two_1.0_all.snap \ + ./test-snap-two+comp-three_1.0.comp \ + ./test-snap-two+comp-four_1.0.comp + snap run test-snap-one comp-one + snap run test-snap-one comp-two + snap run test-snap-two comp-three + snap run test-snap-two comp-four + + snap remove test-snap-one test-snap-two + + # fail to install a component for a snap that isn't installed. note that this + # isn't impacted by the separate lanes that the snaps are installed in, since + # the failure here doesn't occur from inside of a task. + not snap install --dangerous \ + ./test-snap-one_1.0_all.snap \ + ./test-snap-one+comp-one_1.0.comp \ + ./test-snap-two+comp-three_1.0.comp diff --git a/tests/main/component-local-installs/test-snap-one/meta/snap.yaml b/tests/main/component-local-installs/test-snap-one/meta/snap.yaml new file mode 100644 index 00000000000..a757b367983 --- /dev/null +++ b/tests/main/component-local-installs/test-snap-one/meta/snap.yaml @@ -0,0 +1,22 @@ +name: test-snap-one +version: '1.0' +summary: A snap with components +description: | + A snap with components used for testing snapd. +architectures: +- all +base: core24 +apps: + test-snap-one: + command: test +confinement: strict +grade: stable +components: + comp-one: + summary: test component + description: test component + type: standard + comp-two: + summary: test component + description: test component + type: standard diff --git a/tests/main/component-local-installs/test-snap-one/test b/tests/main/component-local-installs/test-snap-one/test new file mode 100755 index 00000000000..18106872b61 --- /dev/null +++ b/tests/main/component-local-installs/test-snap-one/test @@ -0,0 +1,15 @@ +#!/bin/sh -e + +if [ $# -ne 1 ]; then + echo "pass in a component name to check if it is installed" + exit 1 +fi + +if [ ! -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}/meta/component.yaml" ]; then + echo "component ${1} is not installed!" + exit 1 +fi + +comp_rev="$(basename "$(readlink -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}")")" + +echo "component ${1} is installed at revision ${comp_rev}" diff --git a/tests/main/component-local-installs/test-snap-two/meta/snap.yaml b/tests/main/component-local-installs/test-snap-two/meta/snap.yaml new file mode 100644 index 00000000000..8544f1e900f --- /dev/null +++ b/tests/main/component-local-installs/test-snap-two/meta/snap.yaml @@ -0,0 +1,22 @@ +name: test-snap-two +version: '1.0' +summary: A snap with components +description: | + A snap with components used for testing snapd. +architectures: +- all +base: core24 +apps: + test-snap-two: + command: test +confinement: strict +grade: stable +components: + comp-three: + summary: test component + description: test component + type: standard + comp-four: + summary: test component + description: test component + type: standard diff --git a/tests/main/component-local-installs/test-snap-two/test b/tests/main/component-local-installs/test-snap-two/test new file mode 100755 index 00000000000..18106872b61 --- /dev/null +++ b/tests/main/component-local-installs/test-snap-two/test @@ -0,0 +1,15 @@ +#!/bin/sh -e + +if [ $# -ne 1 ]; then + echo "pass in a component name to check if it is installed" + exit 1 +fi + +if [ ! -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}/meta/component.yaml" ]; then + echo "component ${1} is not installed!" + exit 1 +fi + +comp_rev="$(basename "$(readlink -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}")")" + +echo "component ${1} is installed at revision ${comp_rev}" diff --git a/tests/main/component-refresh-and-revert/task.yaml b/tests/main/component-refresh-and-revert/task.yaml index 82373959980..0064f14ebb0 100644 --- a/tests/main/component-refresh-and-revert/task.yaml +++ b/tests/main/component-refresh-and-revert/task.yaml @@ -70,3 +70,15 @@ execute: | snap revert test-snap-component-refreshes --revision=5 test-snap-component-refreshes one | MATCH '.*revision 7$' test-snap-component-refreshes two | MATCH '.*revision 6$' + + snap remove test-snap-component-refreshes + + snap install test-snap-component-refreshes+one --revision=3 --edge + test-snap-component-refreshes one | MATCH '.*revision 3$' + not test-snap-component-refreshes two + + # this should refresh the snap to the latest in the edge channel, update the + # "one" component, and install the "two" component + snap refresh test-snap-component-refreshes+two + test-snap-component-refreshes one | MATCH '.*revision 5$' + test-snap-component-refreshes two | MATCH '.*revision 5$' diff --git a/tests/main/document-portal-activation/task.yaml b/tests/main/document-portal-activation/task.yaml index 398eae5cfbe..1ef25e3e063 100644 --- a/tests/main/document-portal-activation/task.yaml +++ b/tests/main/document-portal-activation/task.yaml @@ -102,6 +102,9 @@ execute: | echo "No output on stderr when running without a session bus" # NOTE: lack of session bus is emulated by unsetting DBUS_SESSION_BUS address # and stopping dbus.socket - tests.session -u test exec systemctl --user stop dbus.socket - tests.session -u test exec sh -c "DBUS_SESSION_BUS_ADDRESS= test-snapd-desktop.check-dirs /home/test/snap/test-snapd-desktop/current" 2>stderr.log - check_stderr stderr.log + # In opensuse tumbleweed the dbus.socket is configured to refuse manual start/stop + if ! os.query is-opensuse tumbleweed; then + tests.session -u test exec systemctl --user stop dbus.socket + tests.session -u test exec sh -c "DBUS_SESSION_BUS_ADDRESS= test-snapd-desktop.check-dirs /home/test/snap/test-snapd-desktop/current" 2>stderr.log + check_stderr stderr.log + fi diff --git a/tests/main/interfaces-calendar-service/task.yaml b/tests/main/interfaces-calendar-service/task.yaml index aa251061612..9abbe5ca706 100644 --- a/tests/main/interfaces-calendar-service/task.yaml +++ b/tests/main/interfaces-calendar-service/task.yaml @@ -25,9 +25,8 @@ systems: - -arch-linux-* # test-snapd-eds is incompatible with eds version shipped with the distro - -centos-* - -debian-* - - -fedora-38-* # test-snapd-eds is incompatible with eds version shipped with the distro - - -fedora-39-* # test-snapd-eds is incompatible with eds version shipped with the distro - -fedora-40-* # test-snapd-eds is incompatible with eds version shipped with the distro + - -fedora-41-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.5-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.6-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro diff --git a/tests/main/interfaces-contacts-service/task.yaml b/tests/main/interfaces-contacts-service/task.yaml index a54199c84ef..af7fda64a12 100644 --- a/tests/main/interfaces-contacts-service/task.yaml +++ b/tests/main/interfaces-contacts-service/task.yaml @@ -20,9 +20,8 @@ systems: - -arch-linux-* # test-snapd-eds is incompatible with eds version shipped with the distro - -centos-* - -debian-* - - -fedora-38-* # test-snapd-eds is incompatible with eds version shipped with the distro - - -fedora-39-* # test-snapd-eds is incompatible with eds version shipped with the distro - -fedora-40-* # test-snapd-eds is incompatible with eds version shipped with the distro + - -fedora-41-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.5-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.6-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro diff --git a/tests/main/interfaces-custom-device-app-slot/task.yaml b/tests/main/interfaces-custom-device-app-slot/task.yaml index da763e1e964..2f614849238 100644 --- a/tests/main/interfaces-custom-device-app-slot/task.yaml +++ b/tests/main/interfaces-custom-device-app-slot/task.yaml @@ -25,9 +25,22 @@ prepare: | snap install core fi + # Install store-state dependencies + echo "Ensure jq is installed" + if ! command -v jq; then + snap install --devmode jq + fi + + echo "Ensure yaml2json is installed" + if ! command -v yaml2json; then + snap install --devmode remarshal + fi + snap debug can-manage-refreshes | MATCH false snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + snap ack "$TESTSLIB/assertions/developer1.account" + snap ack "$TESTSLIB/assertions/developer1.account-key" "$TESTSTOOLS"/store-state setup-fake-store "$BLOB_DIR" diff --git a/tests/main/interfaces-gpio-memory-control/task.yaml b/tests/main/interfaces-gpio-memory-control/task.yaml index 7746a845868..36792f3b50b 100644 --- a/tests/main/interfaces-gpio-memory-control/task.yaml +++ b/tests/main/interfaces-gpio-memory-control/task.yaml @@ -3,7 +3,7 @@ summary: Ensure that the gpio physical memory control interface works. details: | The gpio-memory-control interface allows read/write access to all gpio memory. -systems: [ubuntu-core-16-arm-*] +systems: [ubuntu-core-18-arm-32*] prepare: | echo "Given the test-snapd-gpio-memory-control snap is installed" diff --git a/tests/main/interfaces-hooks/task.yaml b/tests/main/interfaces-hooks/task.yaml index 9446bb13f06..0899696c9cc 100644 --- a/tests/main/interfaces-hooks/task.yaml +++ b/tests/main/interfaces-hooks/task.yaml @@ -11,8 +11,6 @@ environment: PRODUCER_DATA: /var/snap/basic-iface-hooks-producer/common prepare: | - snap install --devmode jq - echo "Install test hooks snaps" "$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-consumer "$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-producer @@ -36,13 +34,19 @@ execute: | } check_attributes(){ # static values should have the values defined in snap's yaml - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-1"]' /var/lib/snapd/state.json | MATCH "consumer-value-1" - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-2"]' /var/lib/snapd/state.json | MATCH "consumer-value-2" - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-1"]' /var/lib/snapd/state.json | MATCH "producer-value-1" - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-2"]' /var/lib/snapd/state.json | MATCH "producer-value-2" + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-1"]' \ + /var/lib/snapd/state.json | MATCH "consumer-value-1" + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-2"]' \ + /var/lib/snapd/state.json | MATCH "consumer-value-2" + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-1"]' \ + /var/lib/snapd/state.json | MATCH "producer-value-1" + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-2"]' \ + /var/lib/snapd/state.json | MATCH "producer-value-2" # dynamic attributes have values created by the hooks, the "-validated" suffix is added by our test interface - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-dynamic"]["before-connect"]' /var/lib/snapd/state.json | MATCH 'plug-changed\(consumer-value\)' - jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-dynamic"]["before-connect"]' /var/lib/snapd/state.json | MATCH 'slot-changed\(producer-value\)' + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-dynamic"]["before-connect"]' \ + /var/lib/snapd/state.json | MATCH 'plug-changed\(consumer-value\)' + gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-dynamic"]["before-connect"]' \ + /var/lib/snapd/state.json | MATCH 'slot-changed\(producer-value\)' } check_hooks_were_run(){ diff --git a/tests/main/interfaces-mount-control-nfs/task.yaml b/tests/main/interfaces-mount-control-nfs/task.yaml new file mode 100644 index 00000000000..dc96b0aaa13 --- /dev/null +++ b/tests/main/interfaces-mount-control-nfs/task.yaml @@ -0,0 +1,85 @@ +summary: Test that with mount-control NFS shares can be mounted + +details: | + Verify that NFS mounts can be performed with mount-control + +# limit to systems where we know NFS works without problems +# ubuntu-core: no required dependencies to export NFS shares +systems: + - ubuntu-22.04-* + - ubuntu-24.04-* + +prepare: | + # Mount should not leak + tests.cleanup defer NOMATCH 'nfs-share' /proc/self/mountinfo + + # Create directory which the test will share + if [ ! -d /var/nfs-share ]; then + mkdir /var/nfs-share + tests.cleanup defer rm -r /var/nfs-share + fi + echo 'hello from NFS share' > /var/nfs-share/hello + + # Install a package with additional kernel modules + if ! tests.pkgs install "linux-modules-extra-$(uname -r)"; then + echo "SKIP: Kernel version and extras module mismatch" + exit 1 + fi + + # Install nfs with some precautions to undo the side-effects if we are + # really installing it and it was not pre-installed. If /proc/fs/nfsd + # is not initially mounted then ask the test to unmount it later + # without checking if it is mounted (hence okfail wrapper). + if not mountinfo.query /proc/fs/nfsd .fs_type=nfsd; then + tests.cleanup defer okfail umount /proc/fs/nfsd + fi + # If /var/lib/nfs/rpc_pipefs is not initially mounted then ask the test + # to unmount it later. + if not mountinfo.query /var/lib/nfs/rpc_pipefs .fs_type=rpc_pipefs; then + tests.cleanup defer okfail umount /proc/nfs/rpc_pipefs + fi + + tests.pkgs install nfs-kernel-server + + # Export /var/home/test-remote over NFS. + mkdir -p /etc/exports.d/ + echo '/var/nfs-share localhost(rw,no_subtree_check)' > /etc/exports.d/test.exports + tests.cleanup defer rm -f /etc/exports.d/test.exports + retry -n 10 exportfs -r + + # Later on remove the exports file and reload exported filesystems. + tests.cleanup defer retry -n 10 exportfs -r + +restore: | + # Run cleanup handlers registered earlier. + tests.cleanup restore + +execute: | + snap install test-snapd-mount-control-nfs + mkdir -p /media/mounted + tests.cleanup defer rm -rf /media/mounted + + # Connect removable media first so that we can 'read' files. + snap connect test-snapd-mount-control-nfs:removable-media + + # Blocked by seccomp, hence EPERM rather than EACCESS + test-snapd-mount-control-nfs.cmd mount.nfs localhost:/var/nfs-share /media/mounted 2>&1 | \ + MATCH 'Operation not permitted' + + test-snapd-mount-control-nfs.cmd snapctl mount -t nfs localhost:/var/nfs-share /media/mounted 2>&1 | \ + MATCH 'no matching mount-control connection found' + + echo "When the mount-control interface which lists NFS is connected" + snap connect test-snapd-mount-control-nfs:mntctl + + echo "It is possible to mount the share" + test-snapd-mount-control-nfs.cmd mount.nfs localhost:/var/nfs-share /media/mounted + echo "Read the contents under the mount point" + test-snapd-mount-control-nfs.cmd cat /media/mounted/hello | MATCH 'hello from NFS share' + echo "And unmount it" + test-snapd-mount-control-nfs.cmd umount /media/mounted + + echo "Same thing works through snapctl" + test-snapd-mount-control-nfs.cmd snapctl mount -t nfs localhost:/var/nfs-share /media/mounted + test-snapd-mount-control-nfs.cmd cat /media/mounted/hello | MATCH 'hello from NFS share' + test-snapd-mount-control-nfs.cmd snapctl umount /media/mounted diff --git a/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh new file mode 100755 index 00000000000..0f845e07c5a --- /dev/null +++ b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh @@ -0,0 +1,3 @@ +#!/bin/sh +PS1='$ ' +exec /bin/sh "$@" diff --git a/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml new file mode 100644 index 00000000000..7aa65c2db55 --- /dev/null +++ b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml @@ -0,0 +1,16 @@ +name: test-snapd-mount-control-nfs +summary: A snap for testing mount-control with NFS +version: 1.0 + +apps: + cmd: + command: bin/sh + plugs: [mount-control] + +plugs: + mntctl: + interface: mount-control + mount: + - where: /media/** + type: [nfs] + options: [rw] diff --git a/tests/main/interfaces-snap-interfaces-requests-control/task.yaml b/tests/main/interfaces-snap-interfaces-requests-control/task.yaml index 74e044cc485..f360c152e51 100644 --- a/tests/main/interfaces-snap-interfaces-requests-control/task.yaml +++ b/tests/main/interfaces-snap-interfaces-requests-control/task.yaml @@ -14,11 +14,10 @@ details: | environment: # not all terminals support UTF-8, but Python tries to be smart and attempts # to guess the encoding as if the output would go to the terminal, but in - # fact all the test does is pipe the output to jq + # fact all the test does is pipe the output to (go)jq PYTHONIOENCODING: utf-8 prepare: | - snap install --edge jq # prerequisite for having a prompts handler service snap set system experimental.user-daemons=true @@ -42,14 +41,17 @@ execute: | snap connect api-client:snap-interfaces-requests-control echo "Check snap can access interfaces-requests-prompt and interfaces-requests-rule-update notices under /v2/notices" - api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | jq '."status-code"' | MATCH '^200$' - api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | jq '."status-code"' | MATCH '^200$' - api-client --socket /run/snapd-snap.socket "/v2/notices" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | \ + gojq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | \ + gojq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices" | gojq '."status-code"' | MATCH '^200$' echo "But not other notice types" - api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | jq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | \ + gojq '."status-code"' | MATCH '^403$' echo "Check snap can access system info via /v2/system-info" - api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/system-info" | gojq '."status-code"' | MATCH '^200$' SNAP_NAME="snapd" if os.query is-core16; then @@ -57,7 +59,7 @@ execute: | fi echo "Check snap can access snap info via /v2/snaps/{name}" - api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | gojq '."status-code"' | MATCH '^200$' echo "Ensure AppArmor Prompting experimental feature can be enabled where possible" # Prompting is unsupported everywhere but the Ubuntu non-core systems with @@ -79,10 +81,12 @@ execute: | snap set system experimental.apparmor-prompting=true echo 'Check "apparmor-prompting" is shown as enabled in /v2/system-info' - api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."result"."features"."apparmor-prompting"."enabled"' | MATCH '^true$' + api-client --socket /run/snapd-snap.socket "/v2/system-info" | \ + gojq '."result"."features"."apparmor-prompting"."enabled"' | MATCH '^true$' EXPECTED_HTTP_CODE="200" - if api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."result"."features"."apparmor-prompting"."supported"' | MATCH '^false$' ; then + if api-client --socket /run/snapd-snap.socket "/v2/system-info" | \ + gojq '."result"."features"."apparmor-prompting"."supported"' | MATCH '^false$' ; then # AppArmor prompting isn't supported, so rules and prompts backends are # not active, and will return InternalError (500). We can at least check # that we receive an InternalError instead of Forbidden (403). @@ -91,29 +95,36 @@ execute: | fi echo "Check snap can access prompts via /v2/interfaces/requests/prompts" - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | \ + gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' # echo "Check snap can access a single prompt via /v2/interfaces/requests/prompts/" # TODO: include the "home" interface and create a request prompt by attempting to list contents of $HOME # PROMPT_ID=FIXME - # api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/$PROMPT_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + # api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/$PROMPT_ID" | \ + # gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' # echo "Check snap can reply to a prompt via /v2/interfaces/requests/prompts/ - # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"allow","lifespan":"forever","constraints":{"path-pattern":"/**","permissions":["read"]}}' "/v2/interfaces/requests/prompts/$PROMPT_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + # TODO: split this line more + # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"allow","lifespan":"forever","constraints":{"path-pattern":"/**","permissions":["read"]}}' "/v2/interfaces/requests/prompts/$PROMPT_ID" | \ + # gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' # TODO: check that thread which triggered request completed successfully echo "Check snap can access rules via /v2/interfaces/requests/rules" - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | \ + gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' # XXX: creating rules requires polkit authentication, so for now, use snap debug api instead of api-client # echo "Check snap can create rule via /v2/interfaces/requests/rules" # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' "/v2/interfaces/requests/rules" > result.json - echo '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' | snap debug api -X POST -H 'Content-Type: application/json' "/v2/interfaces/requests/rules" | tee result.json - jq '."status-code"' < result.json | MATCH '^'"$EXPECTED_HTTP_CODE"'$' - RULE_ID=$(jq '."result"."id"' < result.json | tr -d '"') + echo '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' | snap debug api -X POST -H 'Content-Type: application/json' "/v2/interfaces/requests/rules" | \ + tee result.json + gojq '."status-code"' < result.json | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + RULE_ID=$(gojq '."result"."id"' < result.json | tr -d '"') echo "Check snap can view a single rule via /v2/interfaces/requests/rules/" - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | \ + gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' # XXX: modifying rules requires polkit authentication # echo "Check snap can modify a single rule via /v2/interfaces/requests/rules/" - # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"remove"}' "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' + # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"remove"}' "/v2/interfaces/requests/rules/$RULE_ID" | gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$' echo "Without snap-interfaces-requests-control the snap cannot access those API endpoints" snap disconnect api-client:snap-interfaces-requests-control @@ -122,12 +133,17 @@ execute: | # the prerequisite of there being a snap with snap-interfaces-requests-control # connected and a handler service running is no longer true. Otherwise, the error # code would be 500 instead of 403. - api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | jq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | \ + gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | \ + gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/system-info" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | \ + gojq '."status-code"' | MATCH '^403$' # Try to access an arbitrary prompt ID, should fail with 403 rather than 404 - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/1234123412341234" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/1234123412341234" | \ + gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | \ + gojq '."status-code"' | MATCH '^403$' diff --git a/tests/main/interfaces-snap-refresh-observe/task.yaml b/tests/main/interfaces-snap-refresh-observe/task.yaml index 2f311d96f32..7e801157441 100644 --- a/tests/main/interfaces-snap-refresh-observe/task.yaml +++ b/tests/main/interfaces-snap-refresh-observe/task.yaml @@ -12,12 +12,9 @@ details: | environment: # not all terminals support UTF-8, but Python tries to be smart and attempts # to guess the encoding as if the output would go to the terminal, but in - # fact all the test does is pipe the output to jq + # fact all the test does is pipe the output to (go)jq PYTHONIOENCODING: utf-8 -prepare: | - snap install --edge jq - execute: | "$TESTSTOOLS"/snaps-state install-local api-client echo "The snap-refresh-observe plug on the api-client snap is initially disconnected" @@ -26,29 +23,29 @@ execute: | snap connect api-client:snap-refresh-observe echo "Check snap can access change-update and refresh-inhibit notices under /v2/notices" - api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | jq '."status-code"' | MATCH '^200$' - api-client --socket /run/snapd-snap.socket "/v2/notices?types=refresh-inhibit" | jq '."status-code"' | MATCH '^200$' - api-client --socket /run/snapd-snap.socket "/v2/notices" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | gojq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=refresh-inhibit" | gojq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/notices" | gojq '."status-code"' | MATCH '^200$' echo "But not other notice types" - api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | jq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | gojq '."status-code"' | MATCH '^403$' echo "Check snap can access changes /v2/changes" - api-client --socket /run/snapd-snap.socket "/v2/changes" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/changes" | gojq '."status-code"' | MATCH '^200$' echo "Check snap can access a single change /v2/changes/" CHANGE_ID=$(snap changes | tr -s '\n' | awk 'END{ print $1 }') - api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | gojq '."status-code"' | MATCH '^200$' # TODO: Check it can only access /v2/snaps?select=refresh-inhibited echo "Check snap can access snaps /v2/snaps" - api-client --socket /run/snapd-snap.socket "/v2/snaps" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/snaps" | gojq '."status-code"' | MATCH '^200$' echo "And also a specific snap /v2/snaps/" - api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | jq '."status-code"' | MATCH '^200$' + api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | gojq '."status-code"' | MATCH '^200$' echo "Without snap-refresh-observe the snap cannot access those API endpoints" snap disconnect api-client:snap-refresh-observe - api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/changes" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/snaps" | jq '."status-code"' | MATCH '^403$' - api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | jq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/changes" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/snaps" | gojq '."status-code"' | MATCH '^403$' + api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | gojq '."status-code"' | MATCH '^403$' diff --git a/tests/main/microk8s-smoke/task.yaml b/tests/main/microk8s-smoke/task.yaml index 1b5aa6a5a41..77247279768 100644 --- a/tests/main/microk8s-smoke/task.yaml +++ b/tests/main/microk8s-smoke/task.yaml @@ -13,9 +13,8 @@ systems: - -amazon-linux-2-* # fails to start service daemon-containerd - -amazon-linux-2023-* # fails to start service daemon-containerd - -centos-9-* # fails to start service daemon-containerd - - -fedora-38-* # fails to start service daemon-containerd - - -fedora-39-* # fails to start service daemon-containerd - -fedora-40-* # fails to start service daemon-containerd + - -fedora-41-* # fails to start service daemon-containerd - -ubuntu-14.04-* # doesn't have libseccomp >= 2.4 - -arch-linux-* # XXX: no curl to the pod for unknown reasons - -ubuntu-*-arm* # not available on arm diff --git a/tests/main/mounts-persist-refresh-content-snap/task.yaml b/tests/main/mounts-persist-refresh-content-snap/task.yaml index 2fc2ba1f712..5b96e6ed5d2 100644 --- a/tests/main/mounts-persist-refresh-content-snap/task.yaml +++ b/tests/main/mounts-persist-refresh-content-snap/task.yaml @@ -18,7 +18,8 @@ kill-timeout: 10m prepare: | # make a font directory and restart snapd so it will see it when it goes to # connect the desktop interface for the snap - mkdir /usr/share/fonts/foo-font + mkdir -p /usr/share/fonts/foo-font + tests.cleanup defer rmdir /usr/share/fonts/foo-font systemctl restart snapd # install a snap which exposes some files via a content slot @@ -35,26 +36,28 @@ prepare: | tests.cleanup defer tests.session -u test restore execute: | - touch /run/keep-running - # read a file continuously in the background until it fails - note that the - # while loop here has to be inside the snap run shell, since the process must - # persist during the refresh, if it is on the outside the mount namespace will - # be rebuilt and the crash will not be reproduced - tests.session -u test exec snap run --shell test-snapd-desktop-layout-with-content.cmd -c 'while test -f /run/keep-running && test -d /usr/share/fonts/foo-font; do true; done' & + # Construct the mount namespace once so that we are not racing construction + # from background job of snap run with update with snap install of the + # test-snapd-content-slot below. + test-snapd-desktop-layout-with-content.sh -c 'true' + # Read a file continuously in the background until it fails. Note that we are + # not using a service but a regular app running in the background since the + # process must persist during the refresh. + tests.session -u test exec test-snapd-desktop-layout-with-content.crash-foo-font & pid=$! - # refresh the content slot snap + # Wait for the script to start. + retry grep -xF 'started' ~test/snap/test-snapd-desktop-layout-with-content/common/status + + # Refresh the content slot snap. # TODO: when refresh app awareness is enabled, this will need to ignore running processes to # check the behavior "$TESTSTOOLS"/snaps-state install-local test-snapd-content-slot - # ensure the process is still running - if not ps -p "$pid"; then - echo "process died, test failed" - exit 1 - fi - - # signal to kill the loop - rm /run/keep-running + # Signal to kill the loop + rm ~test/snap/test-snapd-desktop-layout-with-content/common/keep-running + wait "$pid" - wait "$pid" || true + # Ensure that /usr/share/fonts/foo-font was never missing. + MATCH 'exited' ~test/snap/test-snapd-desktop-layout-with-content/common/status + NOMATCH 'foo-font missing' ~test/snap/test-snapd-desktop-layout-with-content/common/status diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh deleted file mode 100755 index 901d49b98c7..00000000000 --- a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -/bin/bash "$@" diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font new file mode 100755 index 00000000000..b4ca932de54 --- /dev/null +++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu +touch "$SNAP_USER_COMMON"/keep-running +echo "started" > "$SNAP_USER_COMMON"/status +while test -f "$SNAP_USER_COMMON"/keep-running; do + if ! [ -d /usr/share/fonts/foo-font ] ; then + echo "foo-font missing" >> "$SNAP_USER_COMMON"/status + exit 1 + fi +done +echo "exited" >> "$SNAP_USER_COMMON"/status + diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh new file mode 100755 index 00000000000..0f845e07c5a --- /dev/null +++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh @@ -0,0 +1,3 @@ +#!/bin/sh +PS1='$ ' +exec /bin/sh "$@" diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml index d45a2e71520..53cfdc5a2eb 100644 --- a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml +++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml @@ -2,8 +2,10 @@ name: test-snapd-desktop-layout-with-content version: 0.1 apps: - cmd: - cmd: bin.sh + sh: + command: bin/sh + crash-foo-font: + command: bin/crash-foo-font plugs: desktop: diff --git a/tests/main/quota-groups-systemd-accounting/task.yaml b/tests/main/quota-groups-systemd-accounting/task.yaml index a18e01a0877..5dda4364f44 100644 --- a/tests/main/quota-groups-systemd-accounting/task.yaml +++ b/tests/main/quota-groups-systemd-accounting/task.yaml @@ -22,7 +22,7 @@ systems: - -ubuntu-core-*-arm-* prepare: | - snap install hello-world go-example-webserver remarshal jq + snap install hello-world go-example-webserver execute: | # the bug mainly happens when we create a quota group with nothing in it, @@ -48,12 +48,12 @@ execute: | # that systemd returns # TODO: change this to use the no unit flag when that is a thing so we can # compare to actual value - snap quota sub | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB" - snap quota top | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB" + snap quota sub | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB" + snap quota top | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB" # now trigger the bug by removing the sub21 group snap remove-quota sub21 # usage should still be sensible - snap quota sub | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB" - snap quota top | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB" + snap quota sub | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB" + snap quota top | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB" diff --git a/tests/main/services-socket-activation/task.yaml b/tests/main/services-socket-activation/task.yaml index 8508be36e67..5440e5559de 100644 --- a/tests/main/services-socket-activation/task.yaml +++ b/tests/main/services-socket-activation/task.yaml @@ -13,56 +13,68 @@ restore: | execute: | [ -f /etc/systemd/system/snap.socket-activation.sleep-daemon.sock.socket ] [ -S /var/snap/socket-activation/common/socket ] + + verify_status() { + local ENABLED="$1" + local MAIN_ACTIVE="$2" + local ACT_ACTIVE="$3" + + echo "Checking that services are listed correctly" + snap services | cat -n > svcs.txt + MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt + MATCH " 2\s+socket-activation.sleep-daemon\s+${ENABLED}\s+${MAIN_ACTIVE}\s+socket-activated$" < svcs.txt + + echo "Check that systemctl for the main unit is as expected" + systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.service | grep "static" + systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.service | grep "ActiveState=${MAIN_ACTIVE}" + + echo "Check that systemctl for the socket is looking correct too" + systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "${ENABLED}" + systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=${ACT_ACTIVE}" + } + + # verify default behavior on install is that the main service + # is inactive but enabled, and socket is active + verify_status "enabled" "inactive" "active" + + # this will fail, but still start the service + echo "Start the primary unit, emulate that the trigger has run" + systemctl start snap.socket-activation.sleep-daemon.service + + # verify that the main service is now active + verify_status "enabled" "active" "active" + + # test normal restart + snap restart socket-activation - echo "Checking that services are listed correctly" - snap services | cat -n > svcs.txt - MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt - MATCH " 2\s+socket-activation.sleep-daemon\s+enabled\s+inactive\s+socket-activated$" < svcs.txt + verify_status "enabled" "active" "active" - echo "Checking that the service is reported as static" - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.service | grep "static" + # test --reload restart, with --reload we expect different behavior + # because of systemd. Verify that systemd is acting like we expect + # as well + snap restart --reload socket-activation - echo "Checking that service activation unit is reported as enabled and running" - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled" - systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active" + verify_status "enabled" "active" "active" + + systemctl reload-or-restart snap.socket-activation.sleep-daemon.sock.socket 2>&1 | MATCH "failed" echo "Testing that we can stop will not disable the service" snap stop socket-activation.sleep-daemon - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled" - systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=inactive" + + verify_status "enabled" "inactive" "inactive" echo "Testing that we can correctly disable activations" snap stop --disable socket-activation.sleep-daemon echo "Verifying that service is now listed as disabled" - snap services | cat -n > svcs.txt - MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt - MATCH " 2\s+socket-activation.sleep-daemon\s+disabled\s+inactive\s+socket-activated$" < svcs.txt - - echo "Checking that service activation unit is reported as disabled and inactive" - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "disabled" - systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=inactive" + verify_status "disabled" "inactive" "inactive" echo "Starting the service will start the socket unit, but not enable" snap start socket-activation.sleep-daemon - - echo "Checking that services are listed as expected" - snap services | cat -n > svcs.txt - MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt - MATCH " 2\s+socket-activation.sleep-daemon\s+disabled\s+inactive\s+socket-activated$" < svcs.txt - - echo "Checking that service activation unit is reported as disabled and active" - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "disabled" - systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active" + + verify_status "disabled" "inactive" "active" echo "Enable service and verify its listed as enabled" snap start --enable socket-activation.sleep-daemon - echo "Checking that services are listed correctly" - snap services | cat -n > svcs.txt - MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt - MATCH " 2\s+socket-activation.sleep-daemon\s+enabled\s+inactive\s+socket-activated$" < svcs.txt - - echo "Checking that service activation unit is reported as enabled and active again" - systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled" - systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active" + verify_status "enabled" "inactive" "active" diff --git a/tests/main/set-proxy-store/task.yaml b/tests/main/set-proxy-store/task.yaml index 182d18f7312..980379b331f 100644 --- a/tests/main/set-proxy-store/task.yaml +++ b/tests/main/set-proxy-store/task.yaml @@ -65,10 +65,10 @@ execute: | systemctl stop snapd snapd.socket - jq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|MATCH null + gojq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|MATCH null # XXX the fakestore currently does not support faking session creation - jq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json systemctl start snapd.socket @@ -81,14 +81,14 @@ execute: | snap refresh --list | not grep -Pzq "$expected" echo "Ensure changing the proxy.store will clear out the session-macaroon" - jq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|NOMATCH fake-session + gojq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|NOMATCH fake-session echo "Configure back to use fakestore" snap set core proxy.store=fake # XXX the fakestore currently does not support faking session creation systemctl stop snapd snapd.socket - jq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json systemctl start snapd.socket diff --git a/tests/main/snap-disconnect/task.yaml b/tests/main/snap-disconnect/task.yaml index 6e2e74d812e..a8423c2ca9a 100644 --- a/tests/main/snap-disconnect/task.yaml +++ b/tests/main/snap-disconnect/task.yaml @@ -13,13 +13,12 @@ prepare: | echo "Install a test snap" snap pack "$TESTSLIB"/snaps/home-consumer snap install --dangerous "$SNAP_FILE" - snap install --edge jq execute: | inspect_connection() { CONN="$1" - # shellcheck disable=SC2002 - cat /var/lib/snapd/state.json | jq --arg CONN "$CONN" -r '.data["conns"] | has($CONN)' + # shellcheck disable=SC2002,SC2016 + cat /var/lib/snapd/state.json | gojq --arg CONN "$CONN" -r '.data["conns"] | has($CONN)' } DISCONNECTED_PATTERN='-\s+home-consumer:home' diff --git a/tests/main/snap-get/task.yaml b/tests/main/snap-get/task.yaml index 56a1dbe5472..18c8784cfc9 100644 --- a/tests/main/snap-get/task.yaml +++ b/tests/main/snap-get/task.yaml @@ -10,8 +10,6 @@ details: | the json output format and error scenarios. prepare: | - snap install --devmode jq - echo "Build basic test package (without hooks)" snap pack "$TESTSLIB"/snaps/basic snap install --dangerous basic_1.0_all.snap @@ -105,7 +103,7 @@ execute: | echo "$output" | MATCH ".*\"a\": 9876543210.*" echo "Ensure config value has correct format" - jq ".data[\"config\"][\"snapctl-hooks\"].intnumber" /var/lib/snapd/state.json | MATCH "1234567890" + gojq ".data[\"config\"][\"snapctl-hooks\"].intnumber" /var/lib/snapd/state.json | MATCH "1234567890" echo "Test unsetting of root.key2 with exclamation mark via snapctl" # precondition check diff --git a/tests/main/snap-quota-memory/task.yaml b/tests/main/snap-quota-memory/task.yaml index 51a7f5acf10..a6f7450d484 100644 --- a/tests/main/snap-quota-memory/task.yaml +++ b/tests/main/snap-quota-memory/task.yaml @@ -18,7 +18,7 @@ systems: - -ubuntu-core-*-arm-* prepare: | - snap install go-example-webserver jq remarshal hello-world + snap install go-example-webserver hello-world execute: | echo "Create a group with a snap in it" @@ -80,7 +80,7 @@ execute: | exit 1 fi - snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-one | jq -r '.result.current.memory')" + snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-one | gojq -r '.result.current.memory')" kernelSaysMemUsage="$(cat "$cgroupMemFile")" pyCmd="import math; print(math.ceil(abs($snapdSaysMemUsage - $kernelSaysMemUsage) / $snapdSaysMemUsage * 100))" @@ -163,9 +163,9 @@ execute: | echo "Removing a snap ensures that the snap is not in the quota group anymore" snap set-quota group-three --memory=100MB go-example-webserver - snap quota group-three | yaml2json | jq -r '.snaps | .[]' | MATCH go-example-webserver + snap quota group-three | gojq --yaml-input -r '.snaps | .[]' | MATCH go-example-webserver snap remove go-example-webserver - snap quota group-three | yaml2json | jq -r '.snaps' | MATCH null + snap quota group-three | gojq --yaml-input -r '.snaps' | MATCH null snap remove-quota group-three echo "Creating a quota group with no actual services in it still has logical memory usage reported for it" @@ -175,7 +175,7 @@ execute: | # in reporting it's memory usage on old systemd versions snap set-quota group-five --memory=10MB --parent=group-four hello-world - snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-five | jq -r '.result.current.memory')" + snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-five | gojq -r '.result.current.memory')" # both 0 and up to 12KiB values are expected here, 0 is for older systemd/kernels # where an empty cgroup has exactly 0, but on newer systems there is some # minimum amount of accounting memory for an empty cgroup, which is observed @@ -189,7 +189,7 @@ execute: | exit 1 esac - snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-four | jq -r '.result.current.memory')" + snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-four | gojq -r '.result.current.memory')" case "$snapdSaysMemUsage" in null|0|4096|8192|12288) # expected diff --git a/tests/main/snap-seccomp-syscalls/task.yaml b/tests/main/snap-seccomp-syscalls/task.yaml index fd19bbf4a14..1b5a1d7e492 100644 --- a/tests/main/snap-seccomp-syscalls/task.yaml +++ b/tests/main/snap-seccomp-syscalls/task.yaml @@ -8,7 +8,7 @@ details: | more details. # one system is enough -systems: [ubuntu-18.04-64] +systems: [ubuntu-24.04-64] # Start early as it takes a long time. priority: 100 diff --git a/tests/main/snapctl-from-snap/task.yaml b/tests/main/snapctl-from-snap/task.yaml index 15fd2885213..32a4965bd61 100644 --- a/tests/main/snapctl-from-snap/task.yaml +++ b/tests/main/snapctl-from-snap/task.yaml @@ -12,13 +12,12 @@ environment: SNAP/wcore18: snapctl-from-snap-core18 prepare: | - snap install --devmode jq echo "Build basic test package" snap pack snapctl-from-snap execute: | check_single_cookie() { - cnt=$(jq -r '.data["snap-cookies"]' /var/lib/snapd/state.json | grep -c "$1" || true) + cnt=$(gojq -r '.data["snap-cookies"]' /var/lib/snapd/state.json | grep -c "$1" || true) if [ "$cnt" -ne 1 ]; then echo "Expected single cookie for snap $1, found $cnt" exit 1 @@ -61,7 +60,7 @@ execute: | echo "Simulate upgrade from old snapd with no cookie support" systemctl stop snapd.{service,socket} rm -f "$COOKIE_FILE" - jq -c 'del(.data["snap-cookies"])' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq -c 'del(.data["snap-cookies"])' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json systemctl start snapd.{service,socket} diff --git a/tests/main/snapd-reexec/task.yaml b/tests/main/snapd-reexec/task.yaml index 76a9bb2d596..8e0c5b81f69 100644 --- a/tests/main/snapd-reexec/task.yaml +++ b/tests/main/snapd-reexec/task.yaml @@ -27,7 +27,7 @@ prepare: | # without .data.snaps.snapd.sequence and .data.snaps.snapd.current="unset" # snapd is not considered installed and core install will request restart cp -a /var/lib/snapd/state.json /tmp/backup_state.json - jq 'del(.data.snaps.snapd)' /tmp/backup_state.json > /tmp/modified_state.json + gojq 'del(.data.snaps.snapd)' /tmp/backup_state.json > /tmp/modified_state.json cp /tmp/modified_state.json /var/lib/snapd/state.json && rm /tmp/modified_state.json systemctl start snapd.service fi diff --git a/tests/main/snapshot-exclusions-dynamic/task.yaml b/tests/main/snapshot-exclusions-dynamic/task.yaml index ed7ad471d13..4832db10d28 100644 --- a/tests/main/snapshot-exclusions-dynamic/task.yaml +++ b/tests/main/snapshot-exclusions-dynamic/task.yaml @@ -16,7 +16,6 @@ environment: prepare: | "$TESTSTOOLS"/snaps-state install-local test-snap - snap install jq debug: | snap saved || true @@ -52,9 +51,9 @@ execute: | # Create snapshot that will apply dynamic exclusions, and grab the set ID #shellcheck disable=SC2016 RESPONSE=$( echo '{"action": "snapshot", "snaps": ["test-snap"], "snapshot-options": {"test-snap":{"exclude":["$SNAP_DATA/dynamic-exclude.txt", "$SNAP_COMMON/dynamic-exclude.txt", "$SNAP_USER_COMMON/dynamic-exclude.txt", "$SNAP_USER_DATA/dynamic-exclude.txt"]}}}' | timeout 5 snap debug api -X POST -H 'Content-Type: application/json' /v2/snaps ) - SET_ID=$( echo "$RESPONSE" | jq '.result."set-id"' ) + SET_ID=$( echo "$RESPONSE" | gojq '.result."set-id"' ) echo "$SET_ID" | MATCH "^[0-9]+$" - CHANGE=$( echo "$RESPONSE" | jq ".change" | grep -o "[0-9]*" ) + CHANGE=$( echo "$RESPONSE" | gojq ".change" | grep -o "[0-9]*" ) # Wait for completion of async change retry -n 20 sh -c "snap change \"$CHANGE\" | tail -n2 | MATCH \"Done\".*" @@ -78,7 +77,7 @@ execute: | "\$SNAP_USER_COMMON/dynamic-exclude.txt" "\$SNAP_USER_DATA/dynamic-exclude.txt" EOF - timeout 5 snap debug api /v2/snapshots?set="$SET_ID" | jq .result[0].snapshots[0].options.exclude[] > actual_options_entry + timeout 5 snap debug api /v2/snapshots?set="$SET_ID" | gojq .result[0].snapshots[0].options.exclude[] > actual_options_entry diff -u expected_options_entry actual_options_entry # Remove the canaries to test restore diff --git a/tests/main/store-state/snap/meta/snap.yaml.in b/tests/main/store-state/snap/meta/snap.yaml.in index 3bea62b9d08..e71c9488903 100644 --- a/tests/main/store-state/snap/meta/snap.yaml.in +++ b/tests/main/store-state/snap/meta/snap.yaml.in @@ -1,6 +1,7 @@ name: SNAPNAME summary: generic snap version: '1.0' +base: core22 apps: test-app: diff --git a/tests/main/store-state/task.yaml b/tests/main/store-state/task.yaml index dd61ccf3bfa..f847ae17d93 100644 --- a/tests/main/store-state/task.yaml +++ b/tests/main/store-state/task.yaml @@ -12,15 +12,8 @@ backends: [-external] systems: [-ubuntu-14.04-64] prepare: | - echo "Ensure jq is installed" - if ! command -v jq; then - snap install --devmode jq - fi - - echo "Ensure yaml2json is installed" - if ! command -v yaml2json; then - snap install --devmode remarshal - fi + # acquire session macaroon + snap find core execute: | # Check help @@ -40,6 +33,10 @@ execute: | snap info core | MATCH "store-url:.*https://snapcraft.io" fi + # install test snap dependency before switching to fake store + base_dep="$(gojq -r --yaml-input '.base' < snap/meta/snap.yaml.in)" + snap install "$base_dep" + # Setup fakestore STORE_DIR="$(pwd)/fake-store-blobdir" snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" @@ -50,8 +47,8 @@ execute: | # Check make-snap-installable command with snap-id create_snap() { - yaml2json -i snap/meta/snap.yaml.in > snap/meta/snap.json - jq ".name = \"$1\"" snap/meta/snap.json | json2yaml -o snap/meta/snap.yaml + gojq --yaml-input --yaml-output \ + ".name = \"$1\"" snap/meta/snap.yaml.in > snap/meta/snap.yaml "$TESTSTOOLS"/snaps-state pack-local snap } diff --git a/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in b/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in index d40315d6245..eb83a56ce73 100644 --- a/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in +++ b/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in @@ -1,6 +1,7 @@ name: SNAPNAME summary: Snap requesting snap-scoped system users version: '1.0' +base: core22 apps: test-app: diff --git a/tests/main/system-usernames-snap-scoped/task.yaml b/tests/main/system-usernames-snap-scoped/task.yaml index d395ccd2673..cc3e46bf33d 100644 --- a/tests/main/system-usernames-snap-scoped/task.yaml +++ b/tests/main/system-usernames-snap-scoped/task.yaml @@ -34,18 +34,12 @@ prepare: | exit fi - echo "Ensure jq is installed" - if ! command -v jq; then - snap install --devmode jq - fi - - echo "Ensure yaml2json is installed" - if ! command -v yaml2json; then - snap install --devmode remarshal - fi - snap debug can-manage-refreshes | MATCH false + # install test snap dependencies before switching to fake store + base_dep="$(gojq -r --yaml-input '.base' < snap/meta/snap.yaml.in)" + snap install "$base_dep" + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" "$TESTSTOOLS"/store-state setup-fake-store "$STORE_DIR" @@ -57,13 +51,14 @@ prepare: | snap ack "$TESTSLIB/assertions/developer1.account-key" create_snap() { - yaml2json -i snap/meta/snap.yaml.in > snap/meta/snap.json + gojq --yaml-input --yaml-output ".name = \"$1\"" < snap/meta/snap.yaml.in > snap/meta/snap.yaml for user in $TESTED_USERS do - jq ".\"system-usernames\" += { \"$user\" : \"shared\"}" snap/meta/snap.json > snap/meta/snap.json.tmp - mv snap/meta/snap.json.tmp snap/meta/snap.json + gojq --yaml-input --yaml-output \ + ".\"system-usernames\" += { \"$user\" : \"shared\"}" \ + snap/meta/snap.yaml > snap/meta/snap.yaml.tmp + mv snap/meta/snap.yaml.tmp snap/meta/snap.yaml done - jq ".name = \"$1\"" snap/meta/snap.json | json2yaml -o snap/meta/snap.yaml "$TESTSTOOLS"/snaps-state pack-local snap } diff --git a/tests/main/theme-install/task.yaml b/tests/main/theme-install/task.yaml index c1baaf08a30..14e0f14672f 100644 --- a/tests/main/theme-install/task.yaml +++ b/tests/main/theme-install/task.yaml @@ -16,12 +16,9 @@ details: | environment: # not all terminals support UTF-8, but Python tries to be smart and attempts # to guess the encoding as if the output would go to the terminal, but in - # fact all the test does is pipe the output to jq + # fact all the test does is pipe the output to gojq PYTHONIOENCODING: utf-8 -prepare: | - snap install --edge jq - execute: | "$TESTSTOOLS"/snaps-state install-local api-client echo "The snapd*-control plugs on the api-client snap are initially disconnected" @@ -35,21 +32,21 @@ execute: | echo "Check for presence of a collection of themes" api-client '/v2/accessories/themes?gtk-theme=Yaru>k-theme=TraditionalHumanized&icon-theme=Yaru&icon-theme=Adwaita&sound-theme=Yaru&sound-theme=No-Such-Theme' > response.txt - jq . < response.txt + gojq . < response.txt - jq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed' - jq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^available' - jq -r '.result."icon-themes".Yaru' < response.txt | MATCH '^installed' - jq -r '.result."icon-themes".Adwaita' < response.txt | MATCH '^installed' - jq -r '.result."sound-themes".Yaru' < response.txt | MATCH '^installed' - jq -r '.result."sound-themes"."No-Such-Theme"' < response.txt | MATCH '^unavailable' + gojq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed' + gojq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^available' + gojq -r '.result."icon-themes".Yaru' < response.txt | MATCH '^installed' + gojq -r '.result."icon-themes".Adwaita' < response.txt | MATCH '^installed' + gojq -r '.result."sound-themes".Yaru' < response.txt | MATCH '^installed' + gojq -r '.result."sound-themes"."No-Such-Theme"' < response.txt | MATCH '^unavailable' echo "We can request installation of a snap to satisfy a theme" api-client --method=POST /v2/accessories/themes '{"gtk-themes":["TraditionalHumanized"]}' > response.txt - jq . < response.txt + gojq . < response.txt echo "Wait for change to complete" - change_id="$(jq -r .change < response.txt)" + change_id="$(gojq -r .change < response.txt)" snap watch "$change_id" echo "The snap providing the theme is now installed" @@ -57,23 +54,23 @@ execute: | echo "The theme now reports as installed" api-client '/v2/accessories/themes?gtk-theme=TraditionalHumanized' > response.txt - jq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^installed' + gojq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^installed' echo "The API is also available to snaps via snapd-snap.socket, provided they have snap-themes-control plugged" snap disconnect api-client:snapd-control not api-client --socket /run/snapd-snap.socket '/v2/accessories/themes?gtk-theme=Yaru' > response.txt - jq -r '."status-code"' < response.txt | MATCH '^403$' + gojq -r '."status-code"' < response.txt | MATCH '^403$' snap connect api-client:snap-themes-control api-client --socket /run/snapd-snap.socket '/v2/accessories/themes?gtk-theme=Yaru' > response.txt - jq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed' + gojq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed' echo "POST requests are also accepted on snapd-snap.socket" not api-client --socket /run/snapd-snap.socket --method=POST /v2/accessories/themes '{"gtk-themes":["TraditionalHumanized"]}' > response.txt - jq -r '.result.message' < response.txt | MATCH '^no snaps to install' + gojq -r '.result.message' < response.txt | MATCH '^no snaps to install' echo "Information about install-themes changes can also be accessed" api-client --socket /run/snapd-snap.socket "/v2/accessories/changes/$change_id" > response.txt - jq -r .result.status < response.txt | MATCH '^Done$' - jq -r .result.kind < response.txt | MATCH '^install-themes$' + gojq -r .result.status < response.txt | MATCH '^Done$' + gojq -r .result.kind < response.txt | MATCH '^install-themes$' diff --git a/tests/main/uc20-create-partitions-encrypt/task.yaml b/tests/main/uc20-create-partitions-encrypt/task.yaml index 2eb81067518..681ae645560 100644 --- a/tests/main/uc20-create-partitions-encrypt/task.yaml +++ b/tests/main/uc20-create-partitions-encrypt/task.yaml @@ -306,75 +306,75 @@ execute: | LOOP_BASENAME="$(basename "$LOOP")" # disk things - jq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 10000000000 - jq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512 - jq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME" - jq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP" - jq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt - jq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5 - jq -r '.pc."structure-encryption" | length' < "$DISK_MAPPING_JSON" | MATCH 2 - jq -r '.pc."structure-encryption"."ubuntu-data" | length' < "$DISK_MAPPING_JSON" | MATCH 1 - jq -r '.pc."structure-encryption"."ubuntu-save" | length' < "$DISK_MAPPING_JSON" | MATCH 1 - jq -r '.pc."structure-encryption"."ubuntu-data"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS - jq -r '.pc."structure-encryption"."ubuntu-save"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS + gojq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 10000000000 + gojq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512 + gojq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME" + gojq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP" + gojq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt + gojq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5 + gojq -r '.pc."structure-encryption" | length' < "$DISK_MAPPING_JSON" | MATCH 2 + gojq -r '.pc."structure-encryption"."ubuntu-data" | length' < "$DISK_MAPPING_JSON" | MATCH 1 + gojq -r '.pc."structure-encryption"."ubuntu-save" | length' < "$DISK_MAPPING_JSON" | MATCH 1 + gojq -r '.pc."structure-encryption"."ubuntu-data"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS + gojq -r '.pc."structure-encryption"."ubuntu-save"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS # note: no partition "id" for gpt disks # first structure - "BIOS Boot" # note: no filesystem for the BIOS Boot structure - jq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1" - jq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1" - jq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot" - jq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576 - jq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576 + gojq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1" + gojq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1" + gojq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot" + gojq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576 + gojq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576 # second structure - ubuntu-seed # TODO: for some reason udev does not identify ubuntu-seed as having a # filesystem label, I think this has something to do with how we create it # artificially above - jq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2" - jq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2" - jq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed - jq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152 - jq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200 + gojq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2" + gojq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2" + gojq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed + gojq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152 + gojq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200 # third structure - ubuntu-boot - jq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3" - jq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3" - jq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot - jq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot - jq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352 - jq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000 + gojq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3" + gojq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3" + gojq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot + gojq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot + gojq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352 + gojq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000 # fourth structure - ubuntu-save-enc - jq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4" - jq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4" - jq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save-enc - jq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save - jq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4" + gojq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4" + gojq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save-enc + gojq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save + gojq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH "" if [ "$channel" = "20" ]; then - jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 - jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216 + gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 + gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216 else - jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 - jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432 + gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 + gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432 fi # fifth structure - ubuntu-data-enc - jq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5" - jq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5" - jq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data-enc - jq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data - jq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5" + gojq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5" + gojq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data-enc + gojq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data + gojq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH "" if [ "$channel" = "20" ]; then - jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568 - jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7936385536 + gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568 + gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7936385536 else - jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784 - jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7919608320 + gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784 + gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7919608320 fi diff --git a/tests/main/uc20-create-partitions/task.yaml b/tests/main/uc20-create-partitions/task.yaml index 4d84b709f6d..87013d05634 100644 --- a/tests/main/uc20-create-partitions/task.yaml +++ b/tests/main/uc20-create-partitions/task.yaml @@ -217,70 +217,70 @@ execute: | LOOP_BASENAME="$(basename "$LOOP")" # disk things - jq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 20000000000 - jq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512 - jq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME" - jq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP" - jq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt - jq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5 + gojq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 20000000000 + gojq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512 + gojq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME" + gojq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP" + gojq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt + gojq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5 # note: no partition "id" for gpt disks # first structure - "BIOS Boot" # note: no filesystem for the BIOS Boot structure - jq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1" - jq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1" - jq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot" - jq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576 - jq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576 + gojq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1" + gojq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1" + gojq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot" + gojq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576 + gojq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576 # second structure - ubuntu-seed # TODO: for some reason udev does not identify ubuntu-seed as having a # filesystem label, I think this has something to do with how we create it # artificially above - jq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2" - jq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2" - jq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed - jq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152 - jq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200 + gojq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2" + gojq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2" + gojq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed + gojq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152 + gojq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200 # third structure - ubuntu-boot - jq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3" - jq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3" - jq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot - jq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot - jq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH "" - jq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352 - jq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000 + gojq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3" + gojq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3" + gojq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot + gojq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot + gojq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352 + gojq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000 # fourth structure - ubuntu-save - jq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4" - jq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4" - jq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save - jq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save - jq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4" + gojq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4" + gojq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save + gojq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save + gojq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH "" if [ "$channel" = "20" ]; then - jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 - jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216 + gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 + gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216 else - jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 - jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432 + gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352 + gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432 fi # fifth structure - ubuntu-data - jq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5" - jq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5" - jq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data - jq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data - jq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH "" + gojq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5" + gojq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5" + gojq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data + gojq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data + gojq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH "" if [ "$channel" = "20" ]; then - jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568 - jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17936385536 + gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568 + gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17936385536 else - jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784 - jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17919608320 + gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784 + gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17919608320 fi diff --git a/tests/main/unhandled-task/task.yaml b/tests/main/unhandled-task/task.yaml index dccb907c82c..ac4583db1f4 100644 --- a/tests/main/unhandled-task/task.yaml +++ b/tests/main/unhandled-task/task.yaml @@ -8,23 +8,20 @@ details: | unknown handler name in order to check that such tasks do not cause snapd to malfunction entirely, which would make rollback impossible. -prepare: | - snap install --devmode jq - execute: | echo "Stop snapd" systemctl stop snapd.{service,socket} - LAST_LANE_ID=$(jq ".[\"last-lane-id\"]" /var/lib/snapd/state.json) + LAST_LANE_ID=$(gojq ".[\"last-lane-id\"]" /var/lib/snapd/state.json) TASK_SNIPPET="{\"id\":\"90999\",\"kind\":\"alien-task\",\"summary\":\"alien task\",\"status\":0,\"data\":{},\"wait-tasks\":[],\"lanes\":[$LAST_LANE_ID],\"change\":\"80999\",\"spawn-time\":\"2010-11-09T22:04:10.320985653Z\"}" CHANGE_SNIPPET="{\"id\":\"80999\",\"kind\":\"some-change\",\"summary\":\"...\",\"status\":0,\"clean\":true,\"data\":{},\"task-ids\":[\"90999\"],\"spawn-time\":\"2010-11-09T22:04:10.320985653Z\"}" echo "Add unknown task to the state" - jq ".changes[\"80999\"]=$CHANGE_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".changes[\"80999\"]=$CHANGE_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json - jq ".tasks[\"90999\"]=$TASK_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + gojq ".tasks[\"90999\"]=$TASK_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json systemctl start snapd.{service,socket} diff --git a/tests/nested/manual/core20-create-recovery/task.yaml b/tests/nested/manual/core20-create-recovery/task.yaml index 3a8857da6a3..4caa4562bf4 100644 --- a/tests/nested/manual/core20-create-recovery/task.yaml +++ b/tests/nested/manual/core20-create-recovery/task.yaml @@ -21,7 +21,7 @@ execute: | boot_id="$( tests.nested boot-id )" echo '{"action":"create-recovery-system","params":{"recovery-system-label":"1234"}}' | \ remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/debug" > change.out - REMOTE_CHG_ID=$(jq -r .change < change.out) + REMOTE_CHG_ID=$(gojq -r .change < change.out) remote.wait-for reboot "${boot_id}" remote.exec sudo snap watch "${REMOTE_CHG_ID}" @@ -37,7 +37,7 @@ execute: | boot_id="$( tests.nested boot-id )" echo '{"action":"create-recovery-system","params":{"recovery-system-label":"1234-1"}}' | \ remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/debug" > change.out - REMOTE_CHG_ID=$(jq -r .change < change.out) + REMOTE_CHG_ID=$(gojq -r .change < change.out) remote.wait-for reboot "${boot_id}" remote.exec sudo snap watch "${REMOTE_CHG_ID}" diff --git a/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml b/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml index 8d535c90a77..68d8e8bca65 100644 --- a/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml +++ b/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml @@ -41,11 +41,6 @@ prepare: | exit fi - # install pre-reqs which we need to adjust various bits - snap install jq remarshal - tests.cleanup defer snap remove remarshal - tests.cleanup defer snap remove jq - # Setup the fake-store for ubuntu-image to use when creating our core image. # We immediately tear down the staging store, to make sure snapd is not pointed # towards this once we invoke ubuntu-image. @@ -78,12 +73,11 @@ prepare: | cp prepare-device pc-gadget/meta/hooks/prepare-device echo "Add the extra hooks definition to the snap.yaml" - # convert our yaml files to json (yaml is easier to write and maintain in VCS) - yaml2json < pc-gadget/meta/snap.yaml | tee pc-gadget/meta/snap.json > /dev/null - yaml2json < snap-yaml-extras.yaml | tee snap-yaml-extras.json > /dev/null - # slurp our two json files together into one json document, then convert - # back to yaml and write out to the snap.yaml in the unpacked gadget snap - jq -s '.[0] * .[1]' <(cat snap-yaml-extras.json) <(cat pc-gadget/meta/snap.json) | json2yaml | tee pc-gadget/meta/snap.yaml + # slurp our two yaml files together into one document, then convert write + # out to the snap.yaml in the unpacked gadget snap + gojq -s --yaml-input --yaml-output '.[0] * .[1]' <(cat snap-yaml-extras.yaml) <(cat pc-gadget/meta/snap.yaml) | \ + tee snap.yaml.tmp + cp -v snap.yaml.tmp pc-gadget/meta/snap.yaml # delay all refreshes for a week from now, as otherwise refreshes for our # snaps (which are asserted by the testrootorg authority-id) may happen, which diff --git a/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml b/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml index 0366cb40f61..050fc038c18 100644 --- a/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml +++ b/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml @@ -42,11 +42,6 @@ prepare: | exit fi - # install pre-reqs which we need to adjust various bits - snap install jq remarshal - tests.cleanup defer snap remove remarshal - tests.cleanup defer snap remove jq - # setup the fakestore, but don't use it for our snapd here on the host VM, so # tear down the staging_store immediately afterwards so that only the SAS is # running and our snapd is not pointed at it, ubuntu-image is the only thing @@ -79,12 +74,11 @@ prepare: | cp prepare-device pc-gadget/meta/hooks/prepare-device echo "Add the extra hooks definition to the snap.yaml" - # convert our yaml files to json (yaml is easier to write and maintain in VCS) - yaml2json < pc-gadget/meta/snap.yaml | tee pc-gadget/meta/snap.json > /dev/null - yaml2json < snap-yaml-extras.yaml | tee snap-yaml-extras.json > /dev/null - # slurp our two json files together into one json document, then convert - # back to yaml and write out to the snap.yaml in the unpacked gadget snap - jq -s '.[0] * .[1]' <(cat snap-yaml-extras.json) <(cat pc-gadget/meta/snap.json) | json2yaml | tee pc-gadget/meta/snap.yaml + # slurp our two yaml files together into one document, then convert write + # out to the snap.yaml in the unpacked gadget snap + gojq -s --yaml-output --yaml-input '.[0] * .[1]' <(cat snap-yaml-extras.yaml) <(cat pc-gadget/meta/snap.yaml) | \ + tee snap.yaml.tmp + cp -v snap.yaml.tmp pc-gadget/meta/snap.yaml # delay all refreshes for a week from now, as otherwise refreshes for our # snaps (which are asserted by the testrootorg authority-id) may happen, which diff --git a/tests/nested/manual/core20-validation-sets/task.yaml b/tests/nested/manual/core20-validation-sets/task.yaml index d76abd7abab..96733df879c 100644 --- a/tests/nested/manual/core20-validation-sets/task.yaml +++ b/tests/nested/manual/core20-validation-sets/task.yaml @@ -40,11 +40,8 @@ prepare: | } # install pre-reqs which we need to adjust various bits - snap install jq remarshal snap install test-snapd-swtpm --edge tests.cleanup defer snap remove test-snapd-swtpm - tests.cleanup defer snap remove remarshal - tests.cleanup defer snap remove jq # download snaps for the model snap download core diff --git a/tests/nested/manual/split-refresh/task.yaml b/tests/nested/manual/split-refresh/task.yaml index 0ea0aec1210..68846fc57af 100644 --- a/tests/nested/manual/split-refresh/task.yaml +++ b/tests/nested/manual/split-refresh/task.yaml @@ -24,8 +24,6 @@ prepare: | echo "This test needs test keys to be trusted" exit fi - echo "Install used snaps" - snap install jq --devmode --edge if [ -d /var/lib/snapd/seed ]; then mv /var/lib/snapd/seed /var/lib/snapd/seed.orig fi @@ -100,9 +98,9 @@ execute: | cp -a ./classic-seed/system-seed/ /var/lib/snapd/seed # do some light checking that the system is valid - snap debug api /v2/systems | jq '.result.systems[0].label' | MATCH "$LABEL" + snap debug api /v2/systems | gojq '.result.systems[0].label' | MATCH "$LABEL" snap debug api "/v2/systems/$LABEL" > system - jq '.result.model.distribution' system | MATCH "ubuntu" + gojq '.result.model.distribution' system | MATCH "ubuntu" # build muinstaller and put in place go build -o muinstaller "$TESTSLIB"/muinstaller/main.go diff --git a/tests/regression/lp-2084730/task.yaml b/tests/regression/lp-2084730/task.yaml new file mode 100644 index 00000000000..b40dec348a1 --- /dev/null +++ b/tests/regression/lp-2084730/task.yaml @@ -0,0 +1,63 @@ +summary: Ensure that snapd state doesn't get stuck if inhibition hint file is locked. + +details: | + Check that snapd state will not stay locked when inhibition hint file lock is held. + For context: In LP #2084730, A problem was discovered related to refresh app awareness + feature which, in a very specific scenario, can cause snapd to enter a deadlock state + because snapd was tring to hold an exclusive lock on the inhibition hint lock file + while locking global state which was already locked by snap run. This results in all API + calls hanging which was made worse by snap run trying to call the snapd API causing + a deadlock. + +environment: + INHIBITION_LOCK_FILE: /var/lib/snapd/inhibit/test-snapd-sh.lock + +prepare: | + snap install --stable test-snapd-sh + mkdir -p /var/lib/snapd/inhibit + touch "$INHIBITION_LOCK_FILE" + + snap set core experimental.parallel-instances=true + +restore: | + # release inhibition hint lock + systemctl stop inhibition-file-locker.service + + # Wait for refresh to finish to be able to remove the snap + snap debug ensure-state-soon + retry -n 10 sh -c "snap changes | NOMATCH Doing" + + snap set core experimental.parallel-instances! + +execute: | + systemd-run --unit inhibition-file-locker.service flock "$INHIBITION_LOCK_FILE" --command "sleep 10000" + # Wait for the inhibition file lock to be held + retry -n 10 not flock --timeout 0 "$INHIBITION_LOCK_FILE" --command "true" + + # This refresh will block because it tries to hold the inhibition file lock for test-snapd-sh + snap refresh --no-wait --edge test-snapd-sh > locked-change-id + + # wait until snapd is blocked in link-snap + # avoid using the API directly to not take the state lock + locked_id="$(cat locked-change-id)" + retry -n 50 --wait 1 sh -c "snap debug state /var/lib/snapd/state.json --change=$locked_id | MATCH 'Doing .* Make current revision for snap .* unavailable'" + + # and still waiting + snap debug state /var/lib/snapd/state.json --change="$locked_id" | MATCH 'Doing .* Make current revision for snap .* unavailable' + + # Check snapd has the inhibition hint lock file open + if command -v lsof; then + lsof "$INHIBITION_LOCK_FILE" | MATCH "snapd" + fi + + # Check that snapd state is not locked when trying to hold the inhibition file lock above + timeout 5 snap debug api /v2/snaps + + # Check that operations changing the state of the snap (remove, or another refresh) fail the conflict check + snap refresh --beta test-snapd-sh 2>&1 | MATCH 'snap "test-snapd-sh" has "refresh-snap" change in progress' + snap remove test-snapd-sh 2>&1 | MATCH 'snap "test-snapd-sh" has "refresh-snap" change in progress' + + # Check that parallel instance of the blocked snap is not affected + snap install --stable test-snapd-sh_instance + snap refresh --edge test-snapd-sh_instance + snap remove test-snapd-sh_instance diff --git a/tests/upgrade/basic/task.yaml b/tests/upgrade/basic/task.yaml index 8854ac92726..bd3325bcb39 100644 --- a/tests/upgrade/basic/task.yaml +++ b/tests/upgrade/basic/task.yaml @@ -173,7 +173,7 @@ execute: | snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-" echo "Check migrating to types in state" - coreType=$(jq -r '.data.snaps["core"].type' /var/lib/snapd/state.json) - testSnapType=$(jq -r '.data.snaps["test-snapd-sh"].type' /var/lib/snapd/state.json) + coreType=$(gojq -r '.data.snaps["core"].type' /var/lib/snapd/state.json) + testSnapType=$(gojq -r '.data.snaps["test-snapd-sh"].type' /var/lib/snapd/state.json) [ "$coreType" = "os" ] [ "$testSnapType" = "app" ] diff --git a/testtime/export_test.go b/testtime/export_test.go new file mode 100644 index 00000000000..acabafc55b3 --- /dev/null +++ b/testtime/export_test.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package testtime + +import ( + "time" +) + +func (t *TestTimer) SetCChan(c chan time.Time) { + t.c = c +} diff --git a/testtime/testtime.go b/testtime/testtime.go new file mode 100644 index 00000000000..cfb943e92d3 --- /dev/null +++ b/testtime/testtime.go @@ -0,0 +1,213 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package testtime provides a mocked version of time.Timer for use in tests. +package testtime + +import ( + "sync" + "time" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/timeutil" +) + +// TestTimer is a mocked version of time.Timer for which the passage of time or +// the direct expiration of the timer is controlled manually. +// +// TestTimer implements timeutil.Timer. +// +// TestTimer also provides methods to introspect whether the timer is active or +// how many times it has fired. +type TestTimer struct { + mu sync.Mutex + duration time.Duration + elapsed time.Duration + active bool + fireCount int + callback func() + c chan time.Time +} + +var _ timeutil.Timer = (*TestTimer)(nil) + +// AfterFunc waits for the timer to fire and then calls f in its own goroutine. +// It returns a Timer that can be used to cancel the call using its Stop method. +// The returned Timer's C field is not used and will be nil. +// +// AfterFunc returns a TestTimer which simulates the behavior of a timer which +// was created via time.AfterFunc. +// +// See here for more details: https://pkg.go.dev/time#AfterFunc +func AfterFunc(d time.Duration, f func()) *TestTimer { + osutil.MustBeTestBinary("testtime timers cannot be used outside of tests") + timer := &TestTimer{ + duration: d, + active: true, + callback: f, + } + // If duration is 0 or negative, ensure timer fires + defer timer.maybeFire() + return timer +} + +// NewTimer creates a new Timer that will send the current time on its channel +// after the timer fires. +// +// NewTimer returns a TestTimer which simulates the behavior of a timer which +// was created via time.NewTimer. +// +// See here for more details: https://pkg.go.dev/time#NewTimer +func NewTimer(d time.Duration) *TestTimer { + osutil.MustBeTestBinary("testtime timers cannot be used outside of tests") + c := make(chan time.Time, 1) + timer := &TestTimer{ + duration: d, + active: true, + c: c, + } + // If duration is 0 or negative, ensure timer fires + defer timer.maybeFire() + return timer +} + +// ExpiredC returns the underlying C channel of the timer. +func (t *TestTimer) ExpiredC() <-chan time.Time { + return t.c +} + +// Reset changes the timer to expire after duration d. It returns true if the +// timer had been active, false if the timer had expired or been stopped. +// +// As the test timer does not actually count down, Reset sets the timer's +// elapsed time to 0 and set its duration to the given duration. The elapsed +// time must be advanced manually using Elapse. +// +// This simulates the behavior of Timer.Reset() from the time package. +// See here fore more details: https://pkg.go.dev/time#Timer.Reset +func (t *TestTimer) Reset(d time.Duration) bool { + t.mu.Lock() + defer t.mu.Unlock() + active := t.active + t.active = true + t.duration = d + t.elapsed = 0 + if t.c != nil { + // Drain the channel, guaranteeing that a receive after Reset will + // block until the timer fires again, and not receive a time value + // from the timer firing before the reset occurred. + // This complies with the new behavior of Reset as of Go 1.23. + // See: https://pkg.go.dev/time#Timer.Reset + select { + case <-t.c: + default: + } + } + // If duration is 0 or negative, ensure timer fires + defer t.maybeFire() + return active +} + +// Stop prevents the timer from firing. It returns true if the call stops the +// timer, false if the timer has already expired or been stopped. +// +// This simulates the behavior of Timer.Stop() from the time package. +// See here for more details: https://pkg.go.dev/time#Timer.Stop +func (t *TestTimer) Stop() bool { + t.mu.Lock() + defer t.mu.Unlock() + wasActive := t.active + t.active = false + if t.c != nil { + // Drain the channel, guaranteeing that a receive after Stop will block + // and not receive a time value from the timer firing before the stop + // occurred. This complies with the new behavior of Stop as of Go 1.23. + // See: https://pkg.go.dev/time#Timer.Stop + select { + case <-t.c: + default: + } + } + return wasActive +} + +// Active returns true if the timer is active, false if the timer has expired +// or been stopped. +func (t *TestTimer) Active() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.active +} + +// FireCount returns the number of times the timer has fired. +func (t *TestTimer) FireCount() int { + t.mu.Lock() + defer t.mu.Unlock() + return t.fireCount +} + +// Elapse simulates time advancing by the given duration, which potentially +// causes the timer to fire. +// +// The timer will fire if the total elapsed time since the timer was created +// or reset is greater than the timer's duration and the timer has not yet +// fired. +func (t *TestTimer) Elapse(duration time.Duration) { + t.mu.Lock() + defer t.mu.Unlock() + t.elapsed += duration + t.maybeFire() +} + +// maybeFire fires the timer if the elapsed time is greater than the timer's +// duration. The caller must hold the timer lock. +func (t *TestTimer) maybeFire() { + if t.elapsed >= t.duration { + t.doFire(time.Now()) + } +} + +// Fire causes the timer to fire. If the timer was created via NewTimer, then +// sends the given current time over the C channel. +// +// To avoid accidental misuse, panics if the timer is not active (if it has +// already fired or been stopped). +func (t *TestTimer) Fire(currTime time.Time) { + t.mu.Lock() + defer t.mu.Unlock() + if !t.active { + panic("cannot fire timer which is not active") + } + t.doFire(currTime) +} + +// doFire carries out the timer firing. The caller must hold the timer lock. +func (t *TestTimer) doFire(currTime time.Time) { + if !t.active { + return + } + t.active = false + t.fireCount++ + // Either t.callback or t.C should be non-nil, and the other should be nil. + if t.callback != nil { + go t.callback() + } else if t.c != nil { + t.c <- currTime + } +} diff --git a/testtime/testtime_test.go b/testtime/testtime_test.go new file mode 100644 index 00000000000..df31fadca67 --- /dev/null +++ b/testtime/testtime_test.go @@ -0,0 +1,761 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package testtime_test + +import ( + "errors" + "runtime" + "sync" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/randutil" + "github.com/snapcore/snapd/testtime" +) + +func Test(t *testing.T) { TestingT(t) } + +type testtimeSuite struct{} + +var _ = Suite(&testtimeSuite{}) + +func (s *testtimeSuite) TestTimerInterfaceCompatibility(c *C) { + t := testtime.NewTimer(time.Second) + active := t.Reset(time.Second) + c.Check(active, Equals, true) + active = t.Stop() + c.Check(active, Equals, true) + c.Check(t.ExpiredC(), NotNil) + t = testtime.AfterFunc(time.Second, func() { return }) + active = t.Reset(time.Second) + c.Check(active, Equals, true) + active = t.Stop() + c.Check(active, Equals, true) + c.Check(t.ExpiredC(), IsNil) +} + +func (s *testtimeSuite) TestAfterFunc(c *C) { + // Create a non-buffered channel on which a message will be sent when the + // callback is called. Use a non-buffered channel so that we ensure that + // the callback runs in its own goroutine. + callbackChan := make(chan string) + + timer := testtime.AfterFunc(time.Hour, func() { + callbackChan <- "called" + }) + + c.Check(timer.ExpiredC(), IsNil) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-callbackChan: + c.Fatal("callback fired early") + default: + } + + // Manually advance the timer so that it will fire + timer.Elapse(time.Hour) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 1) + select { + case msg := <-callbackChan: + c.Assert(msg, Equals, "called") + case <-time.NewTimer(time.Minute).C: + // Goroutine may not start immediately, so allow some grace period + c.Fatal("callback did not complete") + } + + // Reset timer to check that if it fires again, the callback will be called again + active := timer.Reset(time.Nanosecond) + c.Check(active, Equals, false) + + c.Check(timer.ExpiredC(), IsNil) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-callbackChan: + c.Fatal("callback fired early") + default: + } + + // Manually fire the timer with the current time, though the time doesn't matter here + timer.Fire(time.Now()) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 2) + select { + case msg := <-callbackChan: + c.Assert(msg, Equals, "called") + case <-time.NewTimer(time.Minute).C: + // Goroutine may not start immediately, so allow some grace period + c.Fatal("callback did not complete") + } + + // Firing inactive timer panics + c.Check(func() { timer.Fire(time.Now()) }, PanicMatches, "cannot fire timer which is not active") +} + +func (s *testtimeSuite) TestNewTimer(c *C) { + timer := testtime.NewTimer(time.Second) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + // Manually advance the timer so that it will fire + timer.Elapse(time.Second) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + default: + c.Fatal("timer did not fire") + } + + // Reset timer to check that if it fires again, the callback will be called again + active := timer.Reset(time.Nanosecond) + c.Check(active, Equals, false) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + // Manually fire the timer with the current time + currTime := time.Now() + timer.Fire(currTime) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 2) + select { + case t := <-timer.ExpiredC(): + c.Assert(t.Equal(currTime), Equals, true) + default: + c.Fatal("timer did not fire") + } + + // Firing inactive timer panics + c.Check(func() { timer.Fire(currTime) }, PanicMatches, "cannot fire timer which is not active") +} + +func (s *testtimeSuite) TestReset(c *C) { + timer := testtime.NewTimer(time.Millisecond) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + timer.Fire(time.Now()) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 1) + + active := timer.Reset(time.Millisecond) + c.Check(active, Equals, false) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + // Check that receiving from the timer channel blocks after reset, even + // though the timer previously fired and write time to channel. + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired after reset") + default: + } + + // Reset the timer + active = timer.Reset(3 * time.Second) + c.Check(active, Equals, true) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + // Elapse more than half the time + timer.Elapse(2 * time.Second) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + // Reset the timer + active = timer.Reset(3 * time.Second) + c.Check(active, Equals, true) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired after reset") + default: + } + + // Elapse more than half the time again + timer.Elapse(2 * time.Second) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired after time elapsed following reset") + default: + } + + // Elapse the remaining time + timer.Elapse(time.Second) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 2) + select { + case <-timer.ExpiredC(): + default: + c.Fatal("timer did not fire") + } + + active = timer.Reset(time.Second) + c.Check(active, Equals, false) + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 2) +} + +func (s *testtimeSuite) TestStop(c *C) { + timer := testtime.NewTimer(time.Millisecond) + + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired early") + default: + } + + active := timer.Stop() + c.Check(active, Equals, true) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-timer.ExpiredC(): + c.Fatal("timer fired after Stop") + default: + } + + // Elapse time so the timer would have fired if it were not stopped + timer.Elapse(time.Millisecond) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 0) + select { + case <-timer.ExpiredC(): + c.Fatal("received from timer chan after Stop and Elapse") + default: + } + + // Reset the timer, and check that the timer was not previously active + active = timer.Reset(time.Second) + c.Check(active, Equals, false) + c.Check(timer.Active(), Equals, true) + c.Check(timer.FireCount(), Equals, 0) + + // Elapse time so that the timer fires + timer.Elapse(1500 * time.Millisecond) + + c.Check(active, Equals, false) + + // Stop the timer after it has fired + active = timer.Stop() + c.Check(active, Equals, false) + + c.Check(timer.Active(), Equals, false) + c.Check(timer.FireCount(), Equals, 1) + select { + case <-timer.ExpiredC(): + c.Fatal("received from timer chan after Stop called after firing") + default: + } +} + +// Tests from the Go standard library which relate to timers + +// Adapted from src/time/time_test.go as of go1.23.3. +// +// Issue 25686: hard crash on concurrent timer access. +// Issue 37400: panic with "racy use of timers" +// This test deliberately invokes a race condition. +// We are testing that we don't crash with "fatal error: panic holding locks", +// and that we also don't panic. +func (s *testtimeSuite) TestStdlibConcurrentTimerReset(c *C) { + const goroutines = 8 + const tries = 1000 + var wg sync.WaitGroup + wg.Add(goroutines) + timer := testtime.NewTimer(time.Hour) + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + for j := 0; j < tries; j++ { + timer.Reset(time.Hour + time.Duration(i*j)) + } + }(i) + } + wg.Wait() +} + +// Adapted from src/time/time_test.go as of go1.23.3. +// +// Issue 37400: panic with "racy use of timers". +func (s *testtimeSuite) TestStdlibConcurrentTimerResetStop(c *C) { + const goroutines = 8 + const tries = 1000 + var wg sync.WaitGroup + wg.Add(goroutines * 2) + timer := testtime.NewTimer(time.Hour) + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + for j := 0; j < tries; j++ { + timer.Reset(time.Hour + time.Duration(i*j)) + } + }(i) + go func(i int) { + defer wg.Done() + timer.Stop() + }(i) + } + wg.Wait() +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// newTimerFunc simulates NewTimer using AfterFunc, +// but this version will not hit the special cases for channels +// that are used when calling NewTimer. +// This makes it easy to test both paths. +func newTimerFunc(d time.Duration) *testtime.TestTimer { + c := make(chan time.Time, 1) + t := testtime.AfterFunc(d, func() { c <- time.Now() }) + t.SetCChan(c) + return t +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibAfterStopNewTimer(c *C) { + testAfterStop(c, testtime.NewTimer) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibAfterStopAfterFunc(c *C) { + testAfterStop(c, newTimerFunc) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func testAfterStop(c *C, newTimer func(time.Duration) *testtime.TestTimer) { + // We want to test that we stop a timer before it runs. + // We also want to test that it didn't run after a longer timer. + // Since we don't want the test to run for too long, we don't + // want to use lengthy times. That makes the test inherently flaky. + // So only report an error if it fails five times in a row. + + var errs []string + logErrs := func() { + for _, e := range errs { + c.Log(e) + } + } + + for i := 0; i < 5; i++ { + tInitial := testtime.AfterFunc(100*time.Millisecond, func() {}) + t0 := newTimer(50 * time.Millisecond) + c1 := make(chan bool, 1) + t1 := testtime.AfterFunc(150*time.Millisecond, func() { c1 <- true }) + if !t0.Stop() { + errs = append(errs, "failed to stop event 0") + continue + } + if !t1.Stop() { + errs = append(errs, "failed to stop event 1") + continue + } + for _, timer := range []*testtime.TestTimer{tInitial, t0, t1} { + timer.Elapse(200 * time.Millisecond) + } + select { + case <-t0.ExpiredC(): + errs = append(errs, "event 0 was not stopped") + continue + case <-c1: + errs = append(errs, "event 1 was not stopped") + continue + default: + } + if t1.Stop() { + errs = append(errs, "Stop returned true twice") + continue + } + + // Test passed, so all done. + if len(errs) > 0 { + c.Logf("saw %d errors, ignoring to avoid flakiness", len(errs)) + logErrs() + } + + return + } + + c.Errorf("saw %d errors", len(errs)) + logErrs() +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibTimerStopStress(c *C) { + for i := 0; i < 100; i++ { + go func(i int) { + timer := testtime.AfterFunc(2*time.Second, func() { + c.Errorf("timer %d was not stopped", i) + }) + timer.Elapse(1 * time.Second) + timer.Stop() + timer.Elapse(1 * time.Second) + }(i) + } +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func testReset(d time.Duration) error { + t0 := testtime.NewTimer(2 * d) + t0.Elapse(d) + if !t0.Reset(3 * d) { + return errors.New("resetting unfired timer returned false") + } + t0.Elapse(2 * d) + select { + case <-t0.ExpiredC(): + return errors.New("timer fired early") + default: + } + t0.Elapse(2 * d) + select { + case <-t0.ExpiredC(): + default: + return errors.New("reset timer did not fire") + } + + if t0.Reset(50 * time.Millisecond) { + return errors.New("resetting expired timer returned true") + } + return nil +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibReset(c *C) { + // We try to run this test with increasingly larger multiples + // until one works so slow, loaded hardware isn't as flaky, + // but without slowing down fast machines unnecessarily. + // + // (maxDuration is several orders of magnitude longer than we + // expect this test to actually take on a fast, unloaded machine.) + d := 1 * time.Millisecond + const maxDuration = 10 * time.Second + for { + err := testReset(d) + if err == nil { + break + } + d *= 2 + if d > maxDuration { + c.Error(err) + } + c.Logf("%v; trying duration %v", err, d) + } +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868. +func (s *testtimeSuite) TestStdlibZeroTimerNewTimer(c *C) { + testZeroTimer(c, testtime.NewTimer) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868. +func (s *testtimeSuite) TestStdlibZeroTimerAfterFunc(c *C) { + testZeroTimer(c, newTimerFunc) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868. +func (s *testtimeSuite) TestStdlibZeroTimerAfterFuncReset(c *C) { + timer := newTimerFunc(time.Hour) + testZeroTimer(c, func(d time.Duration) *testtime.TestTimer { + timer.Reset(d) + return timer + }) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func testZeroTimer(c *C, newTimer func(time.Duration) *testtime.TestTimer) { + // XXX: stdlib does 1000000, but that's really slow, so do 1/10 that + for i := 0; i < 100000; i++ { + s := time.Now() + ti := newTimer(0) + <-ti.ExpiredC() + if diff := time.Since(s); diff > 2*time.Second { + c.Errorf("Expected time to get value from Timer channel in less than 2 sec, took %v", diff) + } + } +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that rapidly moving a timer earlier doesn't cause it to get dropped. +// Issue 47329. +func (s *testtimeSuite) TestStdlibTimerModifiedEarlier(c *C) { + past := time.Until(time.Unix(0, 0)) + count := 1000 + fail := 0 + for i := 0; i < count; i++ { + timer := newTimerFunc(time.Hour) + for j := 0; j < 10; j++ { + if !timer.Stop() { + select { + case <-timer.ExpiredC(): + // This shouldn't be necessary since we comply with 1.23 + // behavior: + // "as of Go 1.23, any receive from t.C after Stop has + // returned is guaranteed to block rather than receive a + // stale time value from before the Stop" + // + // See: https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/time/sleep.go;l=105 + default: + } + } + timer.Reset(past) + } + + deadline := time.NewTimer(10 * time.Second) + defer deadline.Stop() + now := time.Now() + select { + case <-timer.ExpiredC(): + if since := time.Since(now); since > 8*time.Second { + c.Errorf("timer took too long (%v)", since) + fail++ + } + case <-deadline.C: + c.Error("deadline expired") + } + } + + if fail > 0 { + c.Errorf("%d failures", fail) + } +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that rapidly moving timers earlier and later doesn't cause +// some of the sleep times to be lost. +// Issue 47762 +func (s *testtimeSuite) TestStdlibAdjustTimers(c *C) { + timers := make([]*testtime.TestTimer, 100) + states := make([]int, len(timers)) + indices := randutil.Perm(len(timers)) + + for len(indices) != 0 { + var ii = randutil.Intn(len(indices)) + var i = indices[ii] + + var timer = timers[i] + var state = states[i] + states[i]++ + + switch state { + case 0: + timers[i] = newTimerFunc(0) + + case 1: + <-timer.ExpiredC() // Timer is now idle. + + // Reset to various long durations, which we'll cancel. + case 2: + if timer.Reset(1 * time.Minute) { + panic("shouldn't be active (1)") + } + case 4: + if timer.Reset(3 * time.Minute) { + panic("shouldn't be active (3)") + } + case 6: + if timer.Reset(2 * time.Minute) { + panic("shouldn't be active (2)") + } + + // Stop and drain a long-duration timer. + case 3, 5, 7: + if !timer.Stop() { + c.Logf("timer %d state %d Stop returned false", i, state) + <-timer.ExpiredC() + } + + // Start a short-duration timer we expect to select without blocking. + case 8: + if timer.Reset(0) { + c.Fatal("timer.Reset returned true") + } + case 9: + now := time.Now() + <-timer.ExpiredC() + dur := time.Since(now) + if dur > 750*time.Millisecond { + c.Errorf("timer %d took %v to complete", i, dur) + } + + // Timer is done. Swap with tail and remove. + case 10: + indices[ii] = indices[len(indices)-1] + indices = indices[:len(indices)-1] + } + } +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibStopResult(c *C) { + testStopResetResult(c, true) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +func (s *testtimeSuite) TestStdlibResetResult(c *C) { + testStopResetResult(c, false) +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test that when racing between running a timer and stopping a timer Stop +// consistently indicates whether a value can be read from the channel. +// Issue #69312. +func testStopResetResult(c *C, testStop bool) { + stopOrReset := func(timer *testtime.TestTimer) bool { + if testStop { + return timer.Stop() + } else { + return timer.Reset(1 * time.Hour) + } + } + + start := make(chan struct{}) + var wg sync.WaitGroup + const N = 1000 + wg.Add(N) + for i := 0; i < N; i++ { + go func() { + defer wg.Done() + <-start + for j := 0; j < 100; j++ { + timer1 := testtime.NewTimer(1 * time.Millisecond) + timer2 := testtime.NewTimer(1 * time.Millisecond) + if randutil.Intn(2) == 0 { + timer1.Elapse(time.Millisecond) + } else { + timer2.Elapse(time.Millisecond) + } + select { + case <-timer1.ExpiredC(): + if !stopOrReset(timer2) { + // The test fails if this + // channel read times out. + <-timer2.ExpiredC() + } + case <-timer2.ExpiredC(): + if !stopOrReset(timer1) { + // The test fails if this + // channel read times out. + <-timer1.ExpiredC() + } + } + } + }() + } + close(start) + wg.Wait() +} + +// Adapted from src/time/sleep_test.go as of go1.23.3. +// +// Test having a large number of goroutines wake up a timer simultaneously. +// This used to trigger a crash when run under x/tools/cmd/stress. +func (s *testtimeSuite) TestStdlibMultiWakeupTimer(c *C) { + goroutines := runtime.GOMAXPROCS(0) + timer := testtime.NewTimer(time.Nanosecond) + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + for j := 0; j < 10000; j++ { + select { + case <-timer.ExpiredC(): + default: + } + timer.Reset(time.Nanosecond) + } + }() + } + doneChan := make(chan struct{}) + go func() { + // Time won't elapse on its own, so we do it manually + for { + select { + case <-doneChan: + return + default: + timer.Elapse(time.Nanosecond) + } + } + }() + wg.Wait() + close(doneChan) +} diff --git a/timeutil/timer.go b/timeutil/timer.go new file mode 100644 index 00000000000..0332f22dccd --- /dev/null +++ b/timeutil/timer.go @@ -0,0 +1,69 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package timeutil + +import ( + "time" +) + +// Timer is an interface which wraps time.Timer so that it may be mocked. +// +// Timer is fully compatible with time.Timer, except that since interfaces +// cannot have instance variables, we must expose the C channel as a method. +// Therefore, when replacing a time.Timer with timeutil.Timer, any accesses of C +// must be replaced with ExpireC(). +// +// For more information about time.Timer, see: https://pkg.go.dev/time#Timer +type Timer interface { + Reset(d time.Duration) bool + Stop() bool + // ExpiredC is equivalent to t.C for StdlibTimer and time.Timer. + ExpiredC() <-chan time.Time +} + +type StdlibTimer struct { + *time.Timer +} + +// AfterFunc waits for the duration to elapse and then calls f in its own +// goroutine. It returns a Timer that can be used to cancel the call using its +// Stop method. The returned Timer's C field is not used and will be nil. +// +// See here for more information: https://pkg.go.dev/time#AfterFunc +func AfterFunc(d time.Duration, f func()) StdlibTimer { + return StdlibTimer{time.AfterFunc(d, f)} +} + +// NewTimer creates a new Timer that will send the current time on its channel +// after at least duration d. +// +// See here for more information: https://pkg.go.dev/time#NewTimer +func NewTimer(d time.Duration) StdlibTimer { + return StdlibTimer{time.NewTimer(d)} +} + +// ExpiredC returns the channel t.C over which the current time will be sent +// when the timer expires, assuming the timer was created via NewTimer. +// +// If the timer was created via AfterFunc, then t.C is nil, so this function +// returns nil. +func (t StdlibTimer) ExpiredC() <-chan time.Time { + return t.C +} diff --git a/timeutil/timer_test.go b/timeutil/timer_test.go new file mode 100644 index 00000000000..0c916f43b5f --- /dev/null +++ b/timeutil/timer_test.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package timeutil_test + +import ( + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/timeutil" +) + +type timerSuite struct{} + +var _ = Suite(&timerSuite{}) + +var _ timeutil.Timer = timeutil.StdlibTimer{} + +func (s *timerSuite) TestAfterFuncExpiredC(c *C) { + var timer timeutil.Timer = timeutil.AfterFunc(time.Second, func() {}) + c.Assert(timer, NotNil) + c.Assert(timer.ExpiredC(), IsNil) + active := timer.Stop() + c.Assert(active, Equals, true) +} + +func (s *timerSuite) TestNewTimerExpiredC(c *C) { + before := time.Now() + var timer timeutil.Timer = timeutil.NewTimer(time.Nanosecond) + c.Assert(timer, NotNil) + c.Assert(timer.ExpiredC(), NotNil) + fired := <-timer.ExpiredC() + after := time.Now() + c.Check(before.Before(fired), Equals, true) + c.Check(after.After(fired), Equals, true) + active := timer.Reset(time.Second) + c.Check(active, Equals, false) + active = timer.Stop() + c.Check(active, Equals, true) +} diff --git a/wrappers/services.go b/wrappers/services.go index 8db7f1a1a67..0d526478f69 100644 --- a/wrappers/services.go +++ b/wrappers/services.go @@ -1231,7 +1231,10 @@ func restartServicesByStatus(svcsSts []*internal.ServiceStatus, explicitServices var unitsToRestart []string // If the service is activated, then we must also consider it's activators - if len(st.ActivatorUnitStatuses()) != 0 { + // as long as we are not requesting a reload. For activated units reload + // is not a supported action. In that case treat it like a non-activated + // service. + if len(st.ActivatorUnitStatuses()) != 0 && !opts.Reload { // Restart any activators first and operate normally on these for _, act := range st.ActivatorUnitStatuses() { // Use the primary name here for shouldRestart, as the caller diff --git a/wrappers/services_test.go b/wrappers/services_test.go index 8f78ecde167..ca5c1eb8bed 100644 --- a/wrappers/services_test.go +++ b/wrappers/services_test.go @@ -5352,6 +5352,19 @@ apps: {"show", "--property=ActiveState", srvFile2Sock2}, {"start", srvFile2Sock1, srvFile2Sock2}, }) + + // Restart but also reload. When reloading we expect to see different behaviour. + // Reloading activated units is not supported by systemd, and for that reason they must + // not appear in the list of systemctl calls. + // The only call we expect to appear here is the primary service of svc1 as + // that one is the only one reported as active here. + s.sysdLog = nil + c.Assert(wrappers.RestartServices(services, nil, &wrappers.RestartServicesOptions{Reload: true}, progress.Null, s.perfTimings), IsNil) + c.Check(s.sysdLog, DeepEquals, [][]string{ + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names,NeedDaemonReload", srvFile1, srvFile2}, + {"show", "--property=Id,ActiveState,UnitFileState,Names", srvFile2Sock1, srvFile2Sock2}, + {"reload-or-restart", srvFile1}, + }) } func (s *servicesTestSuite) TestRestartWithActivatedServicesActive(c *C) { @@ -5428,6 +5441,17 @@ apps: {"show", "--property=ActiveState", srvFile2Sock2}, {"start", srvFile2Sock1, srvFile2Sock2}, }) + + // Restart but also reload. We do not expect any services to be restarted here as + // both the primary units are reported inactive. (Only the activation units are + // reported active). The reason we don't expect to see the activation units restarted + // when reloading, is that these units do not support reloading by systemd. + s.sysdLog = nil + c.Assert(wrappers.RestartServices(services, nil, &wrappers.RestartServicesOptions{Reload: true}, progress.Null, s.perfTimings), IsNil) + c.Check(s.sysdLog, DeepEquals, [][]string{ + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names,NeedDaemonReload", srvFile1, srvFile2}, + {"show", "--property=Id,ActiveState,UnitFileState,Names", srvFile2Sock1, srvFile2Sock2}, + }) } func (s *servicesTestSuite) TestRestartWithActivatedServicesActivePrimaryUnit(c *C) {