diff --git a/.github/workflows/build-rpm-packages.yml b/.github/workflows/build-rpm-packages.yml index 4b1ec05ddcbc..f8145457582d 100644 --- a/.github/workflows/build-rpm-packages.yml +++ b/.github/workflows/build-rpm-packages.yml @@ -27,7 +27,7 @@ jobs: - aarch64 container: - image: ghcr.io/saltstack/salt-ci-containers/packaging:centosstream-9 + image: ghcr.io/saltstack/salt-ci-containers/packaging:centos-7 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bd27c52252d..f79b749b9351 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -276,6 +276,296 @@ jobs: self-hosted-runners: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} github-hosted-runners: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['github-hosted-runners'] }} +# <-------------------------------- PACKAGE TESTS --------------------------------> +# TODO: Extract these out later + + amazonlinux-2-pkg-tests: + name: Amazon Linux 2 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: amazonlinux-2 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: rpm + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + centos-7-pkg-tests: + name: CentOS 7 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: centos-7 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: rpm + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + centosstream-8-pkg-tests: + name: CentOS 8 Stream Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: centosstream-8 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: rpm + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + centosstream-9-pkg-tests: + name: CentOS 9 Stream Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: centosstream-9 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: rpm + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + debian-10-pkg-tests: + name: Debian 10 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: debian-10 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + debian-11-pkg-tests: + name: Debian 11 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: debian-11 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + debian-11-arm64-pkg-tests: + name: Debian 11 Arm64 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: debian-11-arm64 + platform: linux + arch: aarch64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + + ubuntu-1804-pkg-tests: + name: Ubuntu 18.04 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: ubuntu-18.04 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + ubuntu-2004-pkg-tests: + name: Ubuntu 20.04 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: ubuntu-20.04 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + ubuntu-2004-arm64-pkg-tests: + name: Ubuntu 20.04 Arm64 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: ubuntu-20.04-arm64 + platform: linux + arch: aarch64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + ubuntu-2204-pkg-tests: + name: Ubuntu 22.04 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: ubuntu-22.04 + platform: linux + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + ubuntu-2204-arm64-pkg-tests: + name: Ubuntu 22.04 Arm64 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: ubuntu-22.04-arm64 + platform: linux + arch: aarch64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: deb + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + macos-12-pkg-tests: + name: macOS 12 Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['github-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action-macos.yml + with: + distro-slug: macos-12 + platform: darwin + arch: x86_64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: macos + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2016-nsis-pkg-tests: + name: Windows 2016 NSIS Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2016 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: NSIS + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2016-msi-pkg-tests: + name: Windows 2016 MSI Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2016 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: MSI + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2019-nsis-pkg-tests: + name: Windows 2019 NSIS Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2019 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: NSIS + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2019-msi-pkg-tests: + name: Windows 2019 MSI Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2019 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: MSI + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2022-nsis-pkg-tests: + name: Windows 2022 NSIS Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2022 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: NSIS + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} + + windows-2022-msi-pkg-tests: + name: Windows 2022 MSI Package Tests + if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} + needs: + - prepare-workflow + - build-pkgs + uses: ./.github/workflows/test-packages-action.yml + with: + distro-slug: windows-2022 + platform: windows + arch: amd64 + salt-version: "${{ needs.prepare-workflow.outputs.salt-version }}" + pkg-type: MSI + cache-seed: ${{ needs.prepare-workflow.outputs.cache-seed }} +# <-------------------------------- PACKAGE TESTS --------------------------------> + windows-2016: name: Windows 2016 if: ${{ fromJSON(needs.prepare-workflow.outputs.jobs)['self-hosted-runners'] }} diff --git a/.github/workflows/test-packages-action-macos.yml b/.github/workflows/test-packages-action-macos.yml new file mode 100644 index 000000000000..749577494089 --- /dev/null +++ b/.github/workflows/test-packages-action-macos.yml @@ -0,0 +1,327 @@ +name: Test Artifact + +on: + workflow_call: + inputs: + distro-slug: + required: true + type: string + description: The OS slug to run tests against + platform: + required: true + type: string + description: The platform being tested + arch: + required: true + type: string + description: The platform arch being tested + pkg-type: + required: true + type: string + description: The platform arch being tested + salt-version: + type: string + required: true + description: The Salt version of the packages to install and test + cache-seed: + required: true + type: string + description: Seed used to invalidate caches + python-version: + required: false + type: string + description: The python version to run tests with + default: "3.9" + package-name: + required: false + type: string + description: The onedir package name to use + default: salt + + +env: + NOX_VERSION: "2022.8.7" + COLUMNS: 160 + AWS_MAX_ATTEMPTS: "10" + AWS_RETRY_MODE: "adaptive" + PIP_INDEX_URL: https://pypi-proxy.saltstack.net/root/local/+simple/ + PIP_EXTRA_INDEX_URL: https://pypi.org/simple + +jobs: + + generate-matrix: + name: Generate Package Test Matrix + runs-on: ubuntu-latest + outputs: + pkg-matrix-include: ${{ steps.generate-pkg-matrix.outputs.matrix }} + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Setup Python Tools Scripts + uses: ./.github/actions/setup-python-tools-scripts + + - name: Generate Package Test Matrix + id: generate-pkg-matrix + run: | + PKG_MATRIX=$(tools ci pkg-matrix ${{ inputs.distro-slug }}) + echo "$PKG_MATRIX" + echo "matrix=$PKG_MATRIX" >> "$GITHUB_OUTPUT" + + dependencies: + name: Setup Test Dependencies + needs: + - generate-matrix + runs-on: ${{ inputs.distro-slug }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Cache nox.${{ inputs.distro-slug }}.tar.* for session ${{ matrix.nox-session }} + id: nox-dependencies-cache + uses: actions/cache@v3 + with: + path: nox.${{ inputs.distro-slug }}.tar.* + key: ${{ inputs.cache-seed }}|testrun-deps|${{ inputs.distro-slug }}|${{ matrix.nox-session }}|${{ hashFiles('requirements/**/*.txt', 'cicd/golden-images.json') }} + + # Skip jobs if nox.*.tar.* is already cached + - name: Download Onedir Tarball as an Artifact + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.package-name }}-${{ inputs.salt-version }}-onedir-${{ inputs.platform }}-${{ inputs.arch }}.tar.xz + path: artifacts/ + + - name: Decompress Onedir Tarball + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + shell: bash + run: | + python3 -c "import os; os.makedirs('artifacts', exist_ok=True)" + cd artifacts + tar xvf ${{ inputs.package-name }}-${{ inputs.salt-version }}-onedir-${{ inputs.platform }}-${{ inputs.arch }}.tar.xz + + - name: Set up Python ${{ inputs.python-version }} + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + uses: actions/setup-python@v4 + with: + python-version: "${{ inputs.python-version }}" + + - name: Install System Dependencies + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + brew install openssl@3 + + - name: Install Nox + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + python3 -m pip install 'nox==${{ env.NOX_VERSION }}' + + - name: Install Dependencies + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + env: + PRINT_TEST_SELECTION: "0" + PRINT_SYSTEM_INFO: "0" + run: | + export PYCURL_SSL_LIBRARY=openssl + export LDFLAGS="-L/usr/local/opt/openssl@3/lib" + export CPPFLAGS="-I/usr/local/opt/openssl@3/include" + export PKG_CONFIG_PATH="/usr/local/opt/openssl@3/lib/pkgconfig" + nox --install-only -e ${{ matrix.nox-session }} + + - name: Cleanup .nox Directory + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + nox -e "pre-archive-cleanup(pkg=False)" + + - name: Compress .nox Directory + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + nox -e compress-dependencies -- ${{ inputs.distro-slug }} + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-deps + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error + + test: + name: Test + runs-on: ${{ inputs.distro-slug }} + timeout-minutes: 120 # 2 Hours - More than this and something is wrong + needs: + - dependencies + - generate-matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Download Packages + uses: actions/download-artifact@v3 + with: + name: salt-${{ inputs.salt-version }}-${{ inputs.arch }}-${{ inputs.pkg-type }} + path: pkg/artifacts/ + + - name: Install System Dependencies + run: | + brew install tree + + - name: List Packages + run: | + tree pkg/artifacts + + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v4 + with: + python-version: "${{ inputs.python-version }}" + + - name: Install Nox + run: | + python3 -m pip install 'nox==${{ env.NOX_VERSION }}' + + - name: Download cached nox.${{ inputs.distro-slug }}.tar.* for session ${{ matrix.nox-session }} + uses: actions/cache@v3 + with: + path: nox.${{ inputs.distro-slug }}.tar.* + key: ${{ inputs.cache-seed }}|testrun-deps|${{ inputs.distro-slug }}|${{ matrix.nox-session }}|${{ hashFiles('requirements/**/*.txt', 'cicd/golden-images.json') }} + + - name: Decompress .nox Directory + run: | + nox -e decompress-dependencies -- ${{ inputs.distro-slug }} + + - name: Show System Info & Test Plan + env: + SKIP_REQUIREMENTS_INSTALL: "1" + PRINT_TEST_SELECTION: "1" + PRINT_TEST_PLAN_ONLY: "1" + PRINT_SYSTEM_INFO: "1" + GITHUB_ACTIONS_PIPELINE: "1" + SKIP_INITIAL_GH_ACTIONS_FAILURES: "1" + run: | + sudo -E nox -e ${{ matrix.nox-session }} + + - name: Run Package Tests + env: + SKIP_REQUIREMENTS_INSTALL: "1" + PRINT_TEST_SELECTION: "0" + PRINT_TEST_PLAN_ONLY: "0" + PRINT_SYSTEM_INFO: "0" + RERUN_FAILURES: "1" + GITHUB_ACTIONS_PIPELINE: "1" + SKIP_INITIAL_GH_ACTIONS_FAILURES: "1" + run: | + sudo -E nox -e ${{ matrix.nox-session }} + + - name: Fix file ownership + run: | + sudo chown -R "$(id -un)" . + + - name: Prepare Test Run Artifacts + id: download-artifacts-from-vm + if: always() && job.status != 'cancelled' + run: | + # Delete the salt onedir, we won't need it anymore and it will prevent + # from it showing in the tree command below + rm -rf artifacts/salt* + tree -a artifacts + + - name: Upload Test Run Artifacts + if: always() && job.status != 'cancelled' + uses: actions/upload-artifact@v3 + with: + name: pkg-testrun-artifacts-${{ inputs.distro-slug }}-${{ matrix.nox-session }} + path: | + artifacts + !artifacts/salt/* + !artifacts/salt-*.tar.* + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-tests + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error + + report: + name: Reports for ${{ inputs.distro-slug }}(${{ matrix.nox-session }}) + runs-on: ubuntu-latest + if: always() && needs.test.result != 'cancelled' && needs.test.result != 'skipped' + needs: + - test + - generate-matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Download Test Run Artifacts + id: download-test-run-artifacts + uses: actions/download-artifact@v3 + with: + name: pkg-testrun-artifacts-${{ inputs.distro-slug }}-${{ matrix.nox-session }} + path: artifacts + + - name: Show Test Run Artifacts + if: always() && steps.download-test-run-artifacts.outcome == 'success' + run: | + tree -a artifacts + + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Install Nox + run: | + python3 -m pip install 'nox==${{ env.NOX_VERSION }}' + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + # always run even if the previous steps fails + if: always() && github.event_name == 'push' && steps.download-test-run-artifacts.outcome == 'success' + with: + check_name: Overall Test Results(${{ inputs.distro-slug }} ${{ matrix.nox-session }}) + report_paths: 'artifacts/xml-unittests-output/*.xml' + annotate_only: true + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-report + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error diff --git a/.github/workflows/test-packages-action.yml b/.github/workflows/test-packages-action.yml new file mode 100644 index 000000000000..6aeb27f159c9 --- /dev/null +++ b/.github/workflows/test-packages-action.yml @@ -0,0 +1,324 @@ +name: Test Artifact + +on: + workflow_call: + inputs: + distro-slug: + required: true + type: string + description: The OS slug to run tests against + platform: + required: true + type: string + description: The platform being tested + arch: + required: true + type: string + description: The platform arch being tested + pkg-type: + required: true + type: string + description: The platform arch being tested + salt-version: + type: string + required: true + description: The Salt version of the packages to install and test + cache-seed: + required: true + type: string + description: Seed used to invalidate caches + package-name: + required: false + type: string + description: The onedir package name to use + default: salt + + +env: + NOX_VERSION: "2022.8.7" + COLUMNS: 160 + AWS_MAX_ATTEMPTS: "10" + AWS_RETRY_MODE: "adaptive" + PIP_INDEX_URL: https://pypi-proxy.saltstack.net/root/local/+simple/ + PIP_EXTRA_INDEX_URL: https://pypi.org/simple + +jobs: + + generate-matrix: + name: Generate Package Test Matrix + runs-on: + - self-hosted + - linux + - x86_64 + outputs: + pkg-matrix-include: ${{ steps.generate-pkg-matrix.outputs.matrix }} + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Setup Python Tools Scripts + uses: ./.github/actions/setup-python-tools-scripts + + - name: Generate Package Test Matrix + id: generate-pkg-matrix + run: | + PKG_MATRIX=$(tools ci pkg-matrix ${{ inputs.distro-slug }}) + echo "$PKG_MATRIX" + echo "matrix=$PKG_MATRIX" >> "$GITHUB_OUTPUT" + + dependencies: + name: Setup Test Dependencies + needs: + - generate-matrix + runs-on: + - self-hosted + - linux + - bastion + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Cache nox.${{ inputs.distro-slug }}.tar.* for session ${{ matrix.nox-session }} + id: nox-dependencies-cache + uses: actions/cache@v3 + with: + path: nox.${{ inputs.distro-slug }}.tar.* + key: ${{ inputs.cache-seed }}|testrun-deps|${{ inputs.distro-slug }}|${{ matrix.nox-session }}|${{ hashFiles('requirements/**/*.txt', 'cicd/golden-images.json') }} + + # Skip jobs if nox.*.tar.* is already cached + - name: Download Onedir Tarball as an Artifact + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.package-name }}-${{ inputs.salt-version }}-onedir-${{ inputs.platform }}-${{ inputs.arch }}.tar.xz + path: artifacts/ + + - name: Decompress Onedir Tarball + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + shell: bash + run: | + python3 -c "import os; os.makedirs('artifacts', exist_ok=True)" + cd artifacts + tar xvf ${{ inputs.package-name }}-${{ inputs.salt-version }}-onedir-${{ inputs.platform }}-${{ inputs.arch }}.tar.xz + + - name: Setup Python Tools Scripts + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-python-tools-scripts + + - name: Start VM + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + id: spin-up-vm + run: | + tools --timestamps vm create --retries=2 ${{ inputs.distro-slug }} + + - name: List Free Space + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm ssh ${{ inputs.distro-slug }} -- df -h || true + + - name: Upload Checkout To VM + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm rsync ${{ inputs.distro-slug }} + + - name: Install Dependencies + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm install-dependencies --nox-session=${{ matrix.nox-session }} ${{ inputs.distro-slug }} + + - name: Cleanup .nox Directory + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm pre-archive-cleanup ${{ inputs.distro-slug }} + + - name: Compress .nox Directory + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm compress-dependencies ${{ inputs.distro-slug }} + + - name: Download Compressed .nox Directory + if: steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm download-dependencies ${{ inputs.distro-slug }} + + - name: Destroy VM + if: always() && steps.nox-dependencies-cache.outputs.cache-hit != 'true' + run: | + tools --timestamps vm destroy ${{ inputs.distro-slug }} + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-deps + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error + + test: + name: Test + runs-on: + - self-hosted + - linux + - bastion + timeout-minutes: 120 # 2 Hours - More than this and something is wrong + needs: + - generate-matrix + - dependencies + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Download Packages + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.package-name }}-${{ inputs.salt-version }}-${{ inputs.arch }}-${{ inputs.pkg-type }} + path: pkg/artifacts/ + + - name: List Packages + run: | + tree pkg/artifacts + + - name: Download cached nox.${{ inputs.distro-slug }}.tar.* for session ${{ matrix.nox-session }} + uses: actions/cache@v3 + with: + path: nox.${{ inputs.distro-slug }}.tar.* + key: ${{ inputs.cache-seed }}|testrun-deps|${{ inputs.distro-slug }}|${{ matrix.nox-session }}|${{ hashFiles('requirements/**/*.txt', 'cicd/golden-images.json') }} + + - name: Setup Python Tools Scripts + uses: ./.github/actions/setup-python-tools-scripts + + - name: Start VM + id: spin-up-vm + run: | + tools --timestamps vm create --retries=2 ${{ inputs.distro-slug }} + + - name: List Free Space + run: | + tools --timestamps vm ssh ${{ inputs.distro-slug }} -- df -h || true + + - name: Upload Checkout To VM + run: | + tools --timestamps vm rsync ${{ inputs.distro-slug }} + + - name: Decompress .nox Directory + run: | + tools --timestamps vm decompress-dependencies ${{ inputs.distro-slug }} + + - name: Show System Info & Test Plan + run: | + tools --timestamps --timeout-secs=1800 vm testplan --skip-requirements-install \ + --nox-session=${{ matrix.nox-session }} ${{ inputs.distro-slug }} + + - name: Run Package Tests + run: | + tools --timestamps --no-output-timeout-secs=1800 --timeout-secs=14400 vm test --skip-requirements-install\ + --nox-session=${{ matrix.nox-session }} --rerun-failures ${{ inputs.distro-slug }} + + - name: Download Test Run Artifacts + id: download-artifacts-from-vm + if: always() && steps.spin-up-vm.outcome == 'success' + run: | + tools --timestamps vm download-artifacts ${{ inputs.distro-slug }} + # Delete the salt onedir, we won't need it anymore and it will prevent + # from it showing in the tree command below + rm -rf artifacts/salt* + tree -a artifacts + + - name: Destroy VM + if: always() + run: | + tools --timestamps vm destroy ${{ inputs.distro-slug }} || true + + - name: Upload Test Run Artifacts + if: always() && steps.download-artifacts-from-vm.outcome == 'success' + uses: actions/upload-artifact@v3 + with: + name: pkg-testrun-artifacts-${{ inputs.distro-slug }}-${{ matrix.nox-session }} + path: | + artifacts + !artifacts/salt/* + !artifacts/salt-*.tar.* + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-tests + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error + + report: + name: Reports for ${{ inputs.distro-slug }}(${{ matrix.nox-session }}) + runs-on: + - self-hosted + - linux + - x86_64 + if: always() && needs.test.result != 'cancelled' && needs.test.result != 'skipped' + needs: + - test + - generate-matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.generate-matrix.outputs.pkg-matrix-include) }} + + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Download Test Run Artifacts + id: download-test-run-artifacts + uses: actions/download-artifact@v3 + with: + name: pkg-testrun-artifacts-${{ inputs.distro-slug }}-${{ matrix.nox-session }} + path: artifacts + + - name: Show Test Run Artifacts + if: always() && steps.download-test-run-artifacts.outcome == 'success' + run: | + tree -a artifacts + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + # always run even if the previous steps fails + if: always() && github.event_name == 'push' && steps.download-test-run-artifacts.outcome == 'success' + with: + check_name: Overall Test Results(${{ inputs.distro-slug }} ${{ matrix.nox-session }}) + report_paths: 'artifacts/xml-unittests-output/*.xml' + annotate_only: true + + - name: Set Exit Status + if: always() + run: | + python3 -c "import os; os.makedirs('exitstatus', exist_ok=True)" + echo "${{ job.status }}" > exitstatus/${{ github.job }}-${{ inputs.distro-slug }}-${{ matrix.nox-session }}-report + + - name: Upload Exit Status + if: always() + uses: actions/upload-artifact@v3 + with: + name: exitstatus + path: exitstatus + if-no-files-found: error diff --git a/.gitignore b/.gitignore index b5ec74906444..f4076ae84be1 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ kitchen.local.yml .bundle/ Gemfile.lock /artifacts/ +/pkg/artifacts/ requirements/static/*/py*/*.log # Vim's default session file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 896c54b0a062..1e21397fe7ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1035,6 +1035,70 @@ repos: - requirements/static/ci/invoke.in # <---- Invoke ----------------------------------------------------------------------------------------------------- + # <---- PKG ci requirements----------------------------------------------------------------------------------------- + - id: pip-tools-compile + alias: compile-ci-pkg-3.6-requirements + name: PKG tests CI Py3.6 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkgtests|common)\.in|py3\.6/pkgtests\.in)))$ + pass_filenames: false + args: + - -v + - --py-version=3.6 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - requirements/static/ci/pkgtests.in + + - id: pip-tools-compile + alias: compile-ci-pkg-3.7-requirements + name: PKG tests CI Py3.7 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkgtests|common)\.in|py3\.7/pkgtests\.in)))$ + pass_filenames: false + args: + - -v + - --py-version=3.7 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - requirements/static/ci/pkgtests.in + + - id: pip-tools-compile + alias: compile-ci-pkg-3.8-requirements + name: PKG tests CI Py3.8 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkgtests|common)\.in|py3\.8/pkgtests\.in)))$ + pass_filenames: false + args: + - -v + - --py-version=3.8 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - requirements/static/ci/pkgtests.in + + - id: pip-tools-compile + alias: compile-ci-pkg-3.9-requirements + name: PKG tests CI Py3.9 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkgtests|common)\.in|py3\.9/pkgtests\.in)))$ + pass_filenames: false + args: + - -v + - --py-version=3.9 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - requirements/static/ci/pkgtests.in + + + - id: pip-tools-compile + alias: compile-ci-pkg-3.10-requirements + name: PKG tests CI Py3.10 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkgtests|common)\.in|py3\.10/pkgtests\.in)))$ + pass_filenames: false + args: + - -v + - --py-version=3.10 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - requirements/static/ci/pkgtests.in + # <---- PKG ci requirements----------------------------------------------------------------------------------------- + + # ----- Tools ----------------------------------------------------------------------------------------------------> - id: pip-tools-compile alias: compile-ci-tools-3.9-requirements diff --git a/noxfile.py b/noxfile.py index 24511b067196..9b4ca937a3ff 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,6 +17,8 @@ import tarfile import tempfile +import nox.command + # fmt: off if __name__ == "__main__": sys.stderr.write( @@ -1755,3 +1757,51 @@ def build(session): ] session.run("sha256sum", *packages, external=True) session.run("python", "-m", "twine", "check", "dist/*") + + +@nox.session(python=_PYTHON_VERSIONS, name="test-pkgs") +def test_pkgs(session): + """ + pytest pkg tests session + """ + pydir = _get_pydir(session) + # Install requirements + if _upgrade_pip_setuptools_and_wheel(session): + requirements_file = os.path.join( + "requirements", "static", "ci", _get_pydir(session), "pkgtests.txt" + ) + + install_command = ["--progress-bar=off", "-r", requirements_file] + session.install(*install_command, silent=PIP_INSTALL_SILENT) + + cmd_args = ["pkg/tests/"] + session.posargs + _pytest(session, False, cmd_args) + + +@nox.session(python=_PYTHON_VERSIONS, name="test-upgrade-pkgs") +@nox.parametrize("classic", [False, True]) +def test_upgrade_pkgs(session, classic): + """ + pytest pkg upgrade tests session + """ + pydir = _get_pydir(session) + # Install requirements + if _upgrade_pip_setuptools_and_wheel(session): + requirements_file = os.path.join( + "requirements", "static", "ci", _get_pydir(session), "pkgtests.txt" + ) + + install_command = ["--progress-bar=off", "-r", requirements_file] + session.install(*install_command, silent=PIP_INSTALL_SILENT) + + cmd_args = [ + "pkg/tests/upgrade/test_salt_upgrade.py::test_salt_upgrade", + "--upgrade", + "--no-uninstall", + ] + session.posargs + if classic: + cmd_args = cmd_args + ["--classic"] + try: + _pytest(session, False, cmd_args) + except nox.command.CommandFailed: + sys.exit(0) diff --git a/pkg/debian/control b/pkg/debian/control index 5a0c0c75ecec..88c1d1025ff2 100644 --- a/pkg/debian/control +++ b/pkg/debian/control @@ -15,7 +15,7 @@ Vcs-Git: git://github.com/saltstack/salt.git Package: salt-common -Architecture: all +Architecture: amd64 arm64 Depends: ${misc:Depends} Suggests: ifupdown Recommends: lsb-release @@ -41,7 +41,7 @@ Description: shared libraries that salt requires for all packages Package: salt-master -Architecture: all +Architecture: amd64 arm64 Depends: salt-common (= ${source:Version}), ${misc:Depends} Description: remote manager to administer servers via salt @@ -65,7 +65,7 @@ Description: remote manager to administer servers via salt Package: salt-minion -Architecture: all +Architecture: amd64 arm64 Depends: bsdmainutils, dctrl-tools, salt-common (= ${source:Version}), @@ -92,7 +92,7 @@ Description: client package for salt, the distributed remote execution system Package: salt-syndic -Architecture: all +Architecture: amd64 arm64 Depends: salt-master (= ${source:Version}), ${misc:Depends} Description: master-of-masters for salt, the distributed remote execution system @@ -117,7 +117,7 @@ Description: master-of-masters for salt, the distributed remote execution system Package: salt-ssh -Architecture: all +Architecture: amd64 arm64 Depends: salt-common (= ${source:Version}), openssh-client, ${misc:Depends} @@ -145,7 +145,7 @@ Description: remote manager to administer servers via Salt SSH Package: salt-cloud -Architecture: all +Architecture: amd64 arm64 Depends: salt-common (= ${source:Version}), ${misc:Depends} Description: public cloud VM management system @@ -154,7 +154,7 @@ Description: public cloud VM management system Package: salt-api -Architecture: all +Architecture: amd64 arm64 Depends: salt-master, ${misc:Depends} Description: Generic, modular network access system diff --git a/pkg/debian/salt-common.install b/pkg/debian/salt-common.install index 1a624246301e..4b612bd3aa6c 100644 --- a/pkg/debian/salt-common.install +++ b/pkg/debian/salt-common.install @@ -7,3 +7,8 @@ pkg/common/fish-completions/salt-call.fish /usr/share/fish/vendor_completions.d pkg/common/fish-completions/salt-syndic.fish /usr/share/fish/vendor_completions.d pkg/common/fish-completions/salt_common.fish /usr/share/fish/vendor_completions.d pkg/common/salt.bash /usr/share/bash-completions/completions/salt-common.bash +pkg/common/fish-completions/salt-minion.fish /usr/share/fish/vendor_completions.d +pkg/common/fish-completions/salt-key.fish /usr/share/fish/vendor_completions.d +pkg/common/fish-completions/salt-master.fish /usr/share/fish/vendor_completions.d +pkg/common/fish-completions/salt-run.fish /usr/share/fish/vendor_completions.d +pkg/common/fish-completions/salt.fish /usr/share/fish/vendor_completions.d diff --git a/pkg/debian/salt-common.links b/pkg/debian/salt-common.links index f1a5039416b1..a1f03163f767 100644 --- a/pkg/debian/salt-common.links +++ b/pkg/debian/salt-common.links @@ -1,2 +1,3 @@ opt/saltstack/salt/spm /usr/bin/spm opt/saltstack/salt/salt-pip /usr/bin/salt-pip +opt/saltstack/salt/salt-call /usr/bin/salt-call diff --git a/pkg/debian/salt-master.install b/pkg/debian/salt-master.install index 0ef6940970f9..1dc8a04ef55e 100644 --- a/pkg/debian/salt-master.install +++ b/pkg/debian/salt-master.install @@ -1,6 +1,2 @@ conf/master /etc/salt pkg/common/salt-master.service /lib/systemd/system -pkg/common/fish-completions/salt-master.fish /usr/share/fish/vendor_completions.d -pkg/common/fish-completions/salt-key.fish /usr/share/fish/vendor_completions.d -pkg/common/fish-completions/salt.fish /usr/share/fish/vendor_completions.d -pkg/common/fish-completions/salt-run.fish /usr/share/fish/vendor_completions.d diff --git a/pkg/debian/salt-minion.install b/pkg/debian/salt-minion.install index c6fc5d5e8c7f..4fc4633bda82 100644 --- a/pkg/debian/salt-minion.install +++ b/pkg/debian/salt-minion.install @@ -1,3 +1,2 @@ conf/minion /etc/salt pkg/common/salt-minion.service /lib/systemd/system -pkg/common/fish-completions/salt-minion.fish /usr/share/fish/vendor_completions.d diff --git a/pkg/debian/salt-minion.links b/pkg/debian/salt-minion.links index 9d9b990f53bc..9dae19eb1d3a 100644 --- a/pkg/debian/salt-minion.links +++ b/pkg/debian/salt-minion.links @@ -1,3 +1,2 @@ opt/saltstack/salt/salt-minion /usr/bin/salt-minion opt/saltstack/salt/salt-proxy /usr/bin/salt-proxy -opt/saltstack/salt/salt-call /usr/bin/salt-call diff --git a/pkg/tests/__init__.py b/pkg/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/tests/conftest.py b/pkg/tests/conftest.py new file mode 100644 index 000000000000..50f30c01caad --- /dev/null +++ b/pkg/tests/conftest.py @@ -0,0 +1,364 @@ +import logging +import pathlib +import re +import shutil + +import pytest +from pytestskipmarkers.utils import platform +from saltfactories.utils import random_string +from saltfactories.utils.tempfiles import SaltPillarTree, SaltStateTree + +from tests.support.helpers import ( + ARTIFACTS_DIR, + CODE_DIR, + TESTS_DIR, + ApiRequest, + SaltMaster, + SaltPkgInstall, + TestUser, +) + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def version(): + """ + get version number from artifact + """ + _version = "" + for artifact in ARTIFACTS_DIR.glob("**/*.*"): + _version = re.search( + r"([0-9].*)(\-[0-9].fc|\-[0-9].el|\+ds|\_all|\_any|\_amd64|\_arm64|\-[0-9].am|(\-[0-9]-[a-z]*-[a-z]*[0-9_]*.|\-[0-9]*.*)(tar.gz|tar.xz|zip|exe|pkg|rpm|deb))", + artifact.name, + ) + if _version: + _version = _version.groups()[0].replace("_", "-").replace("~", "") + _version = _version.split("-")[0] + break + return _version + + +def pytest_addoption(parser): + """ + register argparse-style options and ini-style config values. + """ + test_selection_group = parser.getgroup("Tests Runtime Selection") + test_selection_group.addoption( + "--system-service", + default=False, + action="store_true", + help="Run the daemons as system services", + ) + test_selection_group.addoption( + "--upgrade", + default=False, + action="store_true", + help="Install previous version and then upgrade then run tests", + ) + test_selection_group.addoption( + "--no-install", + default=False, + action="store_true", + help="Do not install salt and use a previous install Salt package", + ) + test_selection_group.addoption( + "--no-uninstall", + default=False, + action="store_true", + help="Do not uninstall salt packages after test run is complete", + ) + test_selection_group.addoption( + "--classic", + default=False, + action="store_true", + help="Test an upgrade from the classic packages.", + ) + test_selection_group.addoption( + "--prev-version", + action="store", + help="Test an upgrade from the version specified.", + ) + + +@pytest.fixture(scope="session") +def salt_factories_root_dir(request, tmp_path_factory): + root_dir = SaltPkgInstall.salt_factories_root_dir( + request.config.getoption("--system-service") + ) + if root_dir is not None: + yield root_dir + else: + if platform.is_darwin(): + root_dir = pathlib.Path("/tmp/salt-tests-tmpdir") + root_dir.mkdir(mode=0o777, parents=True, exist_ok=True) + else: + root_dir = tmp_path_factory.mktemp("salt-tests") + try: + yield root_dir + finally: + shutil.rmtree(str(root_dir), ignore_errors=True) + + +@pytest.fixture(scope="session") +def salt_factories_config(salt_factories_root_dir): + return { + "code_dir": CODE_DIR, + "root_dir": salt_factories_root_dir, + "system_install": True, + } + + +@pytest.fixture(scope="session") +def install_salt(request, salt_factories_root_dir): + with SaltPkgInstall( + conf_dir=salt_factories_root_dir / "etc" / "salt", + system_service=request.config.getoption("--system-service"), + upgrade=request.config.getoption("--upgrade"), + no_uninstall=request.config.getoption("--no-uninstall"), + no_install=request.config.getoption("--no-install"), + classic=request.config.getoption("--classic"), + prev_version=request.config.getoption("--prev-version"), + ) as fixture: + yield fixture + + +@pytest.fixture(scope="session") +def salt_factories(salt_factories, salt_factories_root_dir): + salt_factories.root_dir = salt_factories_root_dir + return salt_factories + + +@pytest.fixture(scope="session") +def state_tree(): + if platform.is_windows(): + file_root = pathlib.Path("C:/salt/srv/salt") + elif platform.is_darwin(): + file_root = pathlib.Path("/opt/srv/salt") + else: + file_root = pathlib.Path("/srv/salt") + envs = { + "base": [ + str(file_root), + str(TESTS_DIR / "files"), + ], + } + tree = SaltStateTree(envs=envs) + test_sls_contents = """ + test_foo: + test.succeed_with_changes: + - name: foo + """ + states_sls_contents = """ + update: + pkg.installed: + - name: bash + salt_dude: + user.present: + - name: dude + - fullname: Salt Dude + """ + win_states_sls_contents = """ + create_empty_file: + file.managed: + - name: C://salt/test/txt + salt_dude: + user.present: + - name: dude + - fullname: Salt Dude + """ + with tree.base.temp_file("test.sls", test_sls_contents), tree.base.temp_file( + "states.sls", states_sls_contents + ), tree.base.temp_file("win_states.sls", win_states_sls_contents): + yield tree + + +@pytest.fixture(scope="session") +def pillar_tree(): + """ + Add pillar files + """ + if platform.is_windows(): + pillar_root = pathlib.Path("C:/salt/srv/pillar") + elif platform.is_darwin(): + pillar_root = pathlib.Path("/opt/srv/pillar") + else: + pillar_root = pathlib.Path("/srv/pillar") + pillar_root.mkdir(mode=0o777, parents=True, exist_ok=True) + tree = SaltPillarTree( + envs={ + "base": [ + str(pillar_root), + ] + }, + ) + top_file_contents = """ + base: + '*': + - test + """ + test_file_contents = """ + info: test + """ + with tree.base.temp_file("top.sls", top_file_contents), tree.base.temp_file( + "test.sls", test_file_contents + ): + yield tree + + +@pytest.fixture(scope="module") +def sls(state_tree): + """ + Add an sls file + """ + test_sls_contents = """ + test_foo: + test.succeed_with_changes: + - name: foo + """ + states_sls_contents = """ + update: + pkg.installed: + - name: bash + salt_dude: + user.present: + - name: dude + - fullname: Salt Dude + """ + win_states_sls_contents = """ + create_empty_file: + file.managed: + - name: C://salt/test/txt + salt_dude: + user.present: + - name: dude + - fullname: Salt Dude + """ + with state_tree.base.temp_file( + "tests.sls", test_sls_contents + ), state_tree.base.temp_file( + "states.sls", states_sls_contents + ), state_tree.base.temp_file( + "win_states.sls", win_states_sls_contents + ): + yield + + +@pytest.fixture(scope="session") +def salt_master(salt_factories, install_salt, state_tree, pillar_tree): + """ + Start up a master + """ + start_timeout = None + # Since the daemons are "packaged" with tiamat, the salt plugins provided + # by salt-factories won't be discovered. Provide the required `*_dirs` on + # the configuration so that they can still be used. + config_defaults = { + "engines_dirs": [ + str(salt_factories.get_salt_engines_path()), + ], + "log_handlers_dirs": [ + str(salt_factories.get_salt_log_handlers_path()), + ], + } + if platform.is_darwin(): + config_defaults["enable_fqdns_grains"] = False + config_overrides = { + "timeout": 30, + "file_roots": state_tree.as_dict(), + "pillar_roots": pillar_tree.as_dict(), + "rest_cherrypy": {"port": 8000, "disable_ssl": True}, + "netapi_enable_clients": ["local"], + "external_auth": {"auto": {"saltdev": [".*"]}}, + } + if (platform.is_windows() or platform.is_darwin()) and install_salt.singlebin: + start_timeout = 240 + # For every minion started we have to accept it's key. + # On windows, using single binary, it has to decompress it and run the command. Too slow. + # So, just in this scenario, use open mode + config_overrides["open_mode"] = True + factory = salt_factories.salt_master_daemon( + random_string("master-"), + defaults=config_defaults, + overrides=config_overrides, + factory_class=SaltMaster, + salt_pkg_install=install_salt, + ) + factory.after_terminate(pytest.helpers.remove_stale_master_key, factory) + with factory.started(start_timeout=start_timeout): + yield factory + + +@pytest.fixture(scope="session") +def salt_minion(salt_master, install_salt): + """ + Start up a minion + """ + start_timeout = None + if (platform.is_windows() or platform.is_darwin()) and install_salt.singlebin: + start_timeout = 240 + minion_id = random_string("minion-") + # Since the daemons are "packaged" with tiamat, the salt plugins provided + # by salt-factories won't be discovered. Provide the required `*_dirs` on + # the configuration so that they can still be used. + config_defaults = { + "engines_dirs": salt_master.config["engines_dirs"].copy(), + "log_handlers_dirs": salt_master.config["log_handlers_dirs"].copy(), + } + if platform.is_darwin(): + config_defaults["enable_fqdns_grains"] = False + config_overrides = { + "id": minion_id, + "file_roots": salt_master.config["file_roots"].copy(), + "pillar_roots": salt_master.config["pillar_roots"].copy(), + } + factory = salt_master.salt_minion_daemon( + minion_id, + overrides=config_overrides, + defaults=config_defaults, + ) + factory.after_terminate( + pytest.helpers.remove_stale_minion_key, salt_master, factory.id + ) + with factory.started(start_timeout=start_timeout): + yield factory + + +@pytest.fixture(scope="module") +def salt_cli(salt_master): + return salt_master.salt_cli() + + +@pytest.fixture(scope="module") +def salt_key_cli(salt_master): + return salt_master.salt_key_cli() + + +@pytest.fixture(scope="module") +def salt_call_cli(salt_minion): + return salt_minion.salt_call_cli() + + +@pytest.fixture(scope="module") +def test_account(salt_call_cli): + with TestUser(salt_call_cli=salt_call_cli) as account: + yield account + + +@pytest.fixture(scope="module") +def salt_api(salt_master, install_salt): + """ + start up and configure salt_api + """ + start_timeout = None + if platform.is_windows() and install_salt.singlebin: + start_timeout = 240 + factory = salt_master.salt_api_daemon() + with factory.started(start_timeout=start_timeout): + yield factory + + +@pytest.fixture(scope="module") +def api_request(test_account, salt_api): + with ApiRequest(salt_api=salt_api, test_account=test_account) as session: + yield session diff --git a/pkg/tests/files/check_imports.sls b/pkg/tests/files/check_imports.sls new file mode 100644 index 000000000000..0dde9d6ad332 --- /dev/null +++ b/pkg/tests/files/check_imports.sls @@ -0,0 +1,53 @@ +#!py +import importlib + +def run(): + config = {} + for test_import in [ + 'templates', 'platform', 'cli', 'executors', 'config', 'wheel', 'netapi', + 'cache', 'proxy', 'transport', 'metaproxy', 'modules', 'tokens', 'matchers', + 'acl', 'auth', 'log', 'engines', 'client', 'returners', 'runners', 'tops', + 'output', 'daemons', 'thorium', 'renderers', 'states', 'cloud', 'roster', + 'beacons', 'pillar', 'spm', 'utils', 'sdb', 'fileserver', 'defaults', + 'ext', 'queues', 'grains', 'serializers' + ]: + try: + import_name = "salt.{}".format(test_import) + importlib.import_module(import_name) + config['test_imports_succeeded'] = { + 'test.succeed_without_changes': [ + { + 'name': import_name + }, + ], + } + except ModuleNotFoundError as err: + config['test_imports_failed'] = { + 'test.fail_without_changes': [ + { + 'name': import_name, + 'comment': "The imports test failed. The error was: {}".format(err) + }, + ], + } + + for stdlib_import in ["telnetlib"]: + try: + importlib.import_module(stdlib_import) + config['stdlib_imports_succeeded'] = { + 'test.succeed_without_changes': [ + { + 'name': stdlib_import + }, + ], + } + except ModuleNotFoundError as err: + config['stdlib_imports_failed'] = { + 'test.fail_without_changes': [ + { + 'name': stdlib_import, + 'comment': "The stdlib imports test failed. The error was: {}".format(err) + }, + ], + } + return config diff --git a/pkg/tests/files/check_python.py b/pkg/tests/files/check_python.py new file mode 100644 index 000000000000..f1d46b76df7b --- /dev/null +++ b/pkg/tests/files/check_python.py @@ -0,0 +1,13 @@ +import sys + +import salt.utils.data + +user_arg = sys.argv + +if user_arg[1] == "raise": + raise Exception("test") + +if salt.utils.data.is_true(user_arg[1]): + sys.exit(0) +else: + sys.exit(1) diff --git a/pkg/tests/integration/__init__.py b/pkg/tests/integration/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/tests/integration/test_check_imports.py b/pkg/tests/integration/test_check_imports.py new file mode 100644 index 000000000000..742b08c39179 --- /dev/null +++ b/pkg/tests/integration/test_check_imports.py @@ -0,0 +1,17 @@ +import logging + +from saltfactories.utils.functional import MultiStateResult + +log = logging.getLogger(__name__) + + +def test_check_imports(salt_cli, salt_minion): + """ + Test imports + """ + ret = salt_cli.run("state.sls", "check_imports", minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data + result = MultiStateResult(raw=ret.data) + for state_ret in result: + assert state_ret.result is True diff --git a/pkg/tests/integration/test_enabled_disabled.py b/pkg/tests/integration/test_enabled_disabled.py new file mode 100644 index 000000000000..6257766e2d54 --- /dev/null +++ b/pkg/tests/integration/test_enabled_disabled.py @@ -0,0 +1,26 @@ +import pytest +from saltfactories.utils.functional import MultiStateResult + + +@pytest.mark.skip_on_windows(reason="Linux test only") +def test_services(install_salt, salt_cli, salt_minion): + """ + Check if Services are enabled/disabled + """ + if install_salt.compressed: + pytest.skip("Skip test on single binary and onedir package") + + if install_salt.distro_id in ("ubuntu", "debian"): + services_enabled = ["salt-master", "salt-minion", "salt-syndic", "salt-api"] + services_disabled = [] + elif install_salt.distro_id in ("centos", "redhat", "amzn", "fedora"): + services_enabled = [] + services_disabled = ["salt-master", "salt-minion", "salt-syndic", "salt-api"] + else: + pytest.fail(f"Don't know how to handle os_family={os_family}") + + for service in services_enabled: + assert salt_cli.run("service.enabled") + + for service in services_disabled: + assert salt_cli.run("service.disabled") diff --git a/pkg/tests/integration/test_help.py b/pkg/tests/integration/test_help.py new file mode 100644 index 000000000000..2f701c624943 --- /dev/null +++ b/pkg/tests/integration/test_help.py @@ -0,0 +1,14 @@ +def test_help(install_salt): + """ + Test --help works for all salt cmds + """ + for cmd in install_salt.binary_paths.values(): + # TODO: add back salt-cloud and salt-ssh when its fixed + cmd = [str(x) for x in cmd] + if "python" in cmd[0]: + ret = install_salt.proc.run(*cmd, "--version") + assert "Python" in ret.stdout + else: + ret = install_salt.proc.run(*cmd, "--help") + assert "Usage" in ret.stdout + assert ret.returncode == 0 diff --git a/pkg/tests/integration/test_pip.py b/pkg/tests/integration/test_pip.py new file mode 100644 index 000000000000..b72477370314 --- /dev/null +++ b/pkg/tests/integration/test_pip.py @@ -0,0 +1,137 @@ +import os +import pathlib +import shutil +import subprocess + +import pytest +from pytestskipmarkers.utils import platform + + +@pytest.fixture +def pypath(): + if platform.is_windows(): + return pathlib.Path(os.getenv("LocalAppData"), "salt", "bin") + elif platform.is_darwin(): + return pathlib.Path(f"{os.sep}opt", "salt", "bin") + else: + return pathlib.Path(f"{os.sep}opt", "saltstack", "salt", "bin") + + +@pytest.fixture(autouse=True) +def wipe_pydeps(pypath, install_salt): + try: + yield + finally: + for dep in ["pep8", "PyGithub"]: + subprocess.run( + install_salt.binary_paths["pip"] + ["uninstall", dep], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + + +def test_pip_install(salt_call_cli): + """ + Test pip.install and ensure + module can use installed library + """ + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + + try: + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + finally: + ret = salt_call_cli.run("--local", "pip.uninstall", dep) + assert ret.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "The github execution module cannot be loaded" in use_lib.stderr + + +def demote(user_uid, user_gid): + def result(): + os.setgid(user_gid) + os.setuid(user_uid) + + return result + + +@pytest.mark.skip_on_windows(reason="We can't easily demote users on Windows") +def test_pip_non_root(install_salt, test_account, pypath): + check_path = pypath / "pep8" + # Lets make sure pep8 is not currently installed + subprocess.run( + install_salt.binary_paths["pip"] + ["uninstall", "pep8"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + + assert not check_path.exists() + # We should be able to issue a --help without being root + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--help"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 0, ret.stderr + assert "Usage" in ret.stdout + assert not check_path.exists() + + # Try to pip install something, should fail + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 1, ret.stderr + assert "Could not install packages due to an OSError" in ret.stderr + assert not check_path.exists() + + # Let tiamat-pip create the pypath directory for us + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "-h"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 0, ret.stderr + + # Now, we should still not be able to install as non-root + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode != 0, ret.stderr + # But we should be able to install as root + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + + assert check_path.exists() + + assert ret.returncode == 0, ret.stderr diff --git a/pkg/tests/integration/test_pip_upgrade.py b/pkg/tests/integration/test_pip_upgrade.py new file mode 100644 index 000000000000..20f6cd08218a --- /dev/null +++ b/pkg/tests/integration/test_pip_upgrade.py @@ -0,0 +1,92 @@ +import logging +import subprocess + +import pytest + +log = logging.getLogger(__name__) + + +def test_pip_install(install_salt, salt_call_cli): + """ + Test pip.install and ensure that a package included in the tiamat build can be upgraded + """ + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--versions-report"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + shell=False, + ) + assert ret.returncode == 0 + + possible_upgrades = [ + "docker-py", + "msgpack", + "pycparser", + "python-gnupg", + "pyyaml", + "pyzmq", + "jinja2", + ] + found_new = False + for dep in possible_upgrades: + get_latest = salt_call_cli.run("--local", "pip.list_all_versions", dep) + if not get_latest.data: + # No information available + continue + dep_version = get_latest.data[-1] + installed_version = None + for line in ret.stdout.splitlines(): + if dep in line.lower(): + installed_version = line.lower().strip().split(":")[-1].strip() + break + else: + pytest.fail(f"Failed to find {dep} in the versions report output") + + if dep_version == installed_version: + log.warning(f"The {dep} dependency is already latest") + else: + found_new = True + break + + if found_new: + try: + install = salt_call_cli.run( + "--local", "pip.install", f"{dep}=={dep_version}" + ) + assert install + log.warning(install) + # The assert is commented out because pip will actually trigger a failure since + # we're breaking the dependency tree, but, for the purpose of this test, we can + # ignore it. + # + # assert install.returncode == 0 + + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--versions-report"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + shell=False, + ) + assert ret.returncode == 0 + for line in ret.stdout.splitlines(): + if dep in line.lower(): + new_version = line.lower().strip().split(":")[-1].strip() + if new_version == installed_version: + pytest.fail( + f"The newly installed version of {dep} does not show in the versions report" + ) + assert new_version == dep_version + break + else: + pytest.fail(f"Failed to find {dep} in the versions report output") + finally: + log.info(f"Uninstalling {dep_version}") + assert salt_call_cli.run( + "--local", "pip.uninstall", f"{dep}=={dep_version}" + ) + else: + pytest.skip("Did not find an upgrade version for any of the dependencies") diff --git a/pkg/tests/integration/test_pkg.py b/pkg/tests/integration/test_pkg.py new file mode 100644 index 000000000000..2913ba6fc784 --- /dev/null +++ b/pkg/tests/integration/test_pkg.py @@ -0,0 +1,32 @@ +import sys + +import pytest + +pytestmark = [ + pytest.mark.skip_unless_on_linux, +] + + +@pytest.fixture(scope="module") +def grains(salt_call_cli): + ret = salt_call_cli.run("--local", "grains.items") + assert ret.data, ret + return ret.data + + +@pytest.fixture(scope="module") +def pkgname(grains): + if sys.platform.startswith("win"): + return "putty" + elif grains["os_family"] == "RedHat": + if grains["os"] == "VMware Photon OS": + return "snoopy" + return "units" + elif grains["os_family"] == "Debian": + return "ifenslave" + return "figlet" + + +def test_pkg_install(salt_call_cli, pkgname): + ret = salt_call_cli.run("--local", "state.single", "pkg.installed", pkgname) + assert ret.returncode == 0 diff --git a/pkg/tests/integration/test_python.py b/pkg/tests/integration/test_python.py new file mode 100644 index 000000000000..e6ed5c2c34f7 --- /dev/null +++ b/pkg/tests/integration/test_python.py @@ -0,0 +1,31 @@ +import subprocess + +import pytest + +from tests.support.helpers import TESTS_DIR + + +@pytest.mark.parametrize("exp_ret,user_arg", [(1, "false"), (0, "true")]) +def test_python_script(install_salt, exp_ret, user_arg): + ret = subprocess.run( + install_salt.binary_paths["python"] + + [str(TESTS_DIR / "files" / "check_python.py"), user_arg], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + + assert ret.returncode == exp_ret, ret.stderr + + +def test_python_script_exception(install_salt): + ret = subprocess.run( + install_salt.binary_paths["python"] + + [str(TESTS_DIR / "files" / "check_python.py"), "raise"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert "Exception: test" in ret.stderr diff --git a/pkg/tests/integration/test_salt_api.py b/pkg/tests/integration/test_salt_api.py new file mode 100644 index 000000000000..0c9485038c14 --- /dev/null +++ b/pkg/tests/integration/test_salt_api.py @@ -0,0 +1,14 @@ +def test_salt_api(api_request): + """ + Test running a command against the salt api + """ + ret = api_request.post( + "/run", + data={ + "client": "local", + "tgt": "*", + "fun": "test.arg", + "arg": ["foo", "bar"], + }, + ) + assert ret["args"] == ["foo", "bar"] diff --git a/pkg/tests/integration/test_salt_call.py b/pkg/tests/integration/test_salt_call.py new file mode 100644 index 000000000000..13af02bb394b --- /dev/null +++ b/pkg/tests/integration/test_salt_call.py @@ -0,0 +1,59 @@ +import pytest + + +def test_salt_call_local(salt_call_cli): + """ + Test salt-call --local test.ping + """ + ret = salt_call_cli.run("--local", "test.ping") + assert ret.data is True + assert ret.returncode == 0 + + +def test_salt_call(salt_call_cli): + """ + Test salt-call test.ping + """ + ret = salt_call_cli.run("test.ping") + assert ret.data is True + assert ret.returncode == 0 + + +def test_sls(salt_call_cli): + """ + Test calling a sls file + """ + ret = salt_call_cli.run("state.apply", "test") + assert ret.data, ret + sls_ret = ret.data[next(iter(ret.data))] + assert sls_ret["changes"]["testing"]["new"] == "Something pretended to change" + assert ret.returncode == 0 + + +def test_salt_call_local_sys_doc_none(salt_call_cli): + """ + Test salt-call --local sys.doc none + """ + ret = salt_call_cli.run("--local", "sys.doc", "none") + assert not ret.data + assert ret.returncode == 0 + + +def test_salt_call_local_sys_doc_aliasses(salt_call_cli): + """ + Test salt-call --local sys.doc aliasses + """ + ret = salt_call_cli.run("--local", "sys.doc", "aliases.list_aliases") + assert "aliases.list_aliases" in ret.data + assert ret.returncode == 0 + + +@pytest.mark.skip_on_windows() +def test_salt_call_cmd_run_id_runas(salt_call_cli, test_account, caplog): + """ + Test salt-call --local cmd_run id with runas + """ + ret = salt_call_cli.run("--local", "cmd.run", "id", runas=test_account.username) + assert "Environment could not be retrieved for user" not in caplog.text + assert str(test_account.uid) in ret.stdout + assert str(test_account.gid) in ret.stdout diff --git a/pkg/tests/integration/test_salt_exec.py b/pkg/tests/integration/test_salt_exec.py new file mode 100644 index 000000000000..9b7d7fc7f682 --- /dev/null +++ b/pkg/tests/integration/test_salt_exec.py @@ -0,0 +1,25 @@ +from sys import platform + + +def test_salt_cmd_run(salt_cli, salt_minion): + """ + Test salt cmd.run 'ipconfig' or 'ls -lah /' + """ + ret = None + if platform.startswith("win"): + ret = salt_cli.run("cmd.run", "ipconfig", minion_tgt=salt_minion.id) + else: + ret = salt_cli.run("cmd.run", "ls -lah /", minion_tgt=salt_minion.id) + assert ret + assert ret.stdout + + +def test_salt_list_users(salt_cli, salt_minion): + """ + Test salt user.list_users + """ + ret = salt_cli.run("user.list_users", minion_tgt=salt_minion.id) + if platform.startswith("win"): + assert "Administrator" in ret.stdout + else: + assert "root" in ret.stdout diff --git a/pkg/tests/integration/test_salt_grains.py b/pkg/tests/integration/test_salt_grains.py new file mode 100644 index 000000000000..e42dbb1c1c8f --- /dev/null +++ b/pkg/tests/integration/test_salt_grains.py @@ -0,0 +1,34 @@ +def test_grains_items(salt_cli, salt_minion): + """ + Test grains.items + """ + ret = salt_cli.run("grains.items", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "osrelease" in ret.data + + +def test_grains_item_os(salt_cli, salt_minion): + """ + Test grains.item os + """ + ret = salt_cli.run("grains.item", "os", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "os" in ret.data + + +def test_grains_item_pythonversion(salt_cli, salt_minion): + """ + Test grains.item pythonversion + """ + ret = salt_cli.run("grains.item", "pythonversion", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "pythonversion" in ret.data + + +def test_grains_setval_key_val(salt_cli, salt_minion): + """ + Test grains.setval key val + """ + ret = salt_cli.run("grains.setval", "key", "val", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "key" in ret.data diff --git a/pkg/tests/integration/test_salt_key.py b/pkg/tests/integration/test_salt_key.py new file mode 100644 index 000000000000..5a2db4cddea1 --- /dev/null +++ b/pkg/tests/integration/test_salt_key.py @@ -0,0 +1,7 @@ +def test_salt_key(salt_key_cli, salt_minion): + """ + Test running salt-key -L + """ + ret = salt_key_cli.run("-L") + assert ret.data + assert salt_minion.id in ret.data["minions"] diff --git a/pkg/tests/integration/test_salt_minion.py b/pkg/tests/integration/test_salt_minion.py new file mode 100644 index 000000000000..1c9e743dad53 --- /dev/null +++ b/pkg/tests/integration/test_salt_minion.py @@ -0,0 +1,19 @@ +def test_salt_minion_ping(salt_cli, salt_minion): + """ + Test running a command against a targeted minion + """ + ret = salt_cli.run("test.ping", minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data is True + + +def test_salt_minion_setproctitle(salt_cli, salt_minion): + """ + Test that setproctitle is working + for the running Salt minion + """ + ret = salt_cli.run( + "ps.pgrep", "MinionProcessManager", full=True, minion_tgt=salt_minion.id + ) + assert ret.returncode == 0 + assert ret.data != "" diff --git a/pkg/tests/integration/test_salt_output.py b/pkg/tests/integration/test_salt_output.py new file mode 100644 index 000000000000..953618b2dfb4 --- /dev/null +++ b/pkg/tests/integration/test_salt_output.py @@ -0,0 +1,15 @@ +import pytest + + +@pytest.mark.parametrize("output_fmt", ["yaml", "json"]) +def test_salt_output(salt_cli, salt_minion, output_fmt): + """ + Test --output + """ + ret = salt_cli.run( + f"--output={output_fmt}", "test.fib", "7", minion_tgt=salt_minion.id + ) + if output_fmt == "json": + assert 13 in ret.data + else: + ret.stdout.matcher.fnmatch_lines(["*- 13*"]) diff --git a/pkg/tests/integration/test_salt_pillar.py b/pkg/tests/integration/test_salt_pillar.py new file mode 100644 index 000000000000..43656fce4e50 --- /dev/null +++ b/pkg/tests/integration/test_salt_pillar.py @@ -0,0 +1,6 @@ +def test_salt_pillar(salt_cli, salt_minion): + """ + Test pillar.items + """ + ret = salt_cli.run("pillar.items", minion_tgt=salt_minion.id) + assert "info" in ret.data diff --git a/pkg/tests/integration/test_salt_state_file.py b/pkg/tests/integration/test_salt_state_file.py new file mode 100644 index 000000000000..585167a7e550 --- /dev/null +++ b/pkg/tests/integration/test_salt_state_file.py @@ -0,0 +1,16 @@ +import sys + + +def test_salt_state_file(salt_cli, salt_minion): + """ + Test state file + """ + if sys.platform.startswith("win"): + ret = salt_cli.run("state.apply", "win_states", minion_tgt=salt_minion.id) + else: + ret = salt_cli.run("state.apply", "states", minion_tgt=salt_minion.id) + + assert ret.data, ret + sls_ret = ret.data[next(iter(ret.data))] + assert "changes" in sls_ret + assert "name" in sls_ret diff --git a/pkg/tests/integration/test_systemd_config.py b/pkg/tests/integration/test_systemd_config.py new file mode 100644 index 000000000000..c8f1312526d6 --- /dev/null +++ b/pkg/tests/integration/test_systemd_config.py @@ -0,0 +1,43 @@ +import subprocess + +import pytest + + +@pytest.mark.skip_on_windows(reason="Linux test only") +def test_system_config(salt_cli, salt_minion): + """ + Test system config + """ + get_family = salt_cli.run("grains.get", "os_family", minion_tgt=salt_minion.id) + assert get_family.returncode == 0 + get_finger = salt_cli.run("grains.get", "osfinger", minion_tgt=salt_minion.id) + assert get_finger.returncode == 0 + + if get_family.data == "RedHat": + if get_finger.data in ( + "CentOS Stream-8", + "CentOS Linux-8", + "CentOS Stream-9", + "Fedora Linux-36", + ): + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 0 + else: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 1 + + elif "Debian" in get_family.stdout: + if "Debian-9" in get_finger.stdout: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 1 + else: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 0 diff --git a/pkg/tests/integration/test_version.py b/pkg/tests/integration/test_version.py new file mode 100644 index 000000000000..aef2b0158f3b --- /dev/null +++ b/pkg/tests/integration/test_version.py @@ -0,0 +1,123 @@ +import pathlib +import subprocess +import sys + +import pytest +from pytestskipmarkers.utils import platform + + +def test_salt_version(version, install_salt): + """ + Test version outputed from salt --version + """ + ret = install_salt.proc.run(*install_salt.binary_paths["salt"], "--version") + assert ret.stdout.strip() == f"salt {version}" + + +def test_salt_versions_report_master(install_salt): + """ + Test running --versions-report on master + """ + ret = install_salt.proc.run( + *install_salt.binary_paths["master"], "--versions-report" + ) + ret.stdout.matcher.fnmatch_lines(["*Salt Version:*"]) + if sys.platform == "win32": + python_executable = pathlib.Path( + r"C:\Program Files\Salt Project\Salt\Scripts\python.exe" + ) + elif sys.platform == "darwin": + python_executable = pathlib.Path("/opt/salt/bin/python3") + else: + python_executable = pathlib.Path("/opt/saltstack/salt/bin/python3") + py_version = subprocess.run( + [str(python_executable), "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ).stdout + py_version = py_version.decode().strip().replace(" ", ": ") + ret.stdout.matcher.fnmatch_lines([f"*{py_version}*"]) + + +def test_salt_versions_report_minion(salt_cli, salt_minion): + """ + Test running test.versions_report on minion + """ + ret = salt_cli.run("test.versions_report", minion_tgt=salt_minion.id) + ret.stdout.matcher.fnmatch_lines(["*Salt Version:*"]) + + +@pytest.mark.parametrize( + "binary", ["master", "cloud", "syndic", "minion", "call", "api"] +) +def test_compare_versions(version, binary, install_salt): + """ + Test compare versions + """ + if platform.is_windows() and install_salt.singlebin: + pytest.skip( + "Already tested in `test_salt_version`. No need to repeat " + "for windows single binary installs." + ) + if binary in ["master", "cloud", "syndic"]: + if sys.platform.startswith("win"): + pytest.skip(f"{binary} not installed on windows") + + ret = install_salt.proc.run(*install_salt.binary_paths[binary], "--version") + ret.stdout.matcher.fnmatch_lines([f"*{version}*"]) + + +@pytest.mark.skip_unless_on_darwin() +@pytest.mark.parametrize( + "symlink", + [ + # We can't create a salt symlink because there is a salt directory + "salt", + "salt-api", + "salt-call", + "salt-cloud", + "salt-cp", + "salt-key", + "salt-master", + "salt-minion", + "salt-proxy", + "salt-run", + "spm", + "salt-ssh", + "salt-syndic", + ], +) +def test_symlinks_created(version, symlink, install_salt): + """ + Test symlinks created + """ + if not install_salt.installer_pkg: + pytest.skip( + "This test is for the installer package only (pkg). It does not " + "apply to the tarball" + ) + ret = install_salt.proc.run(pathlib.Path("/usr/local/sbin") / symlink, "--version") + ret.stdout.matcher.fnmatch_lines([f"*{version}*"]) + + +def test_compare_pkg_versions_redhat_rc(version, install_salt): + """ + Test compare pkg versions for redhat RC packages. + A tilde should be included in RC Packages and it + should test to be a lower version than a non RC package + of the same version. For example, v3004~rc1 should be + less than v3004. + """ + if install_salt.distro_id not in ("centos", "redhat", "amzn", "fedora"): + pytest.skip("Only tests rpm packages") + + pkg = [x for x in install_salt.pkgs if "rpm" in x] + if not pkg: + pytest.skip("Not testing rpm packages") + pkg = pkg[0].split("/")[-1] + if "rc" not in pkg: + pytest.skip("Not testing an RC package") + assert "~" in pkg + comp_pkg = pkg.split("~")[0] + ret = install_salt.proc.run("rpmdev-vercmp", pkg, comp_pkg) + ret.stdout.matcher.fnmatch_lines([f"{pkg} < {comp_pkg}"]) diff --git a/pkg/tests/support/__init__.py b/pkg/tests/support/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/tests/support/coverage/sitecustomize.py b/pkg/tests/support/coverage/sitecustomize.py new file mode 100644 index 000000000000..bee2ff80f2f5 --- /dev/null +++ b/pkg/tests/support/coverage/sitecustomize.py @@ -0,0 +1,11 @@ +""" +Python will always try to import sitecustomize. +We use that fact to try and support code coverage for sub-processes +""" + +try: + import coverage + + coverage.process_startup() +except ImportError: + pass diff --git a/pkg/tests/support/helpers.py b/pkg/tests/support/helpers.py new file mode 100644 index 000000000000..11941599cbd5 --- /dev/null +++ b/pkg/tests/support/helpers.py @@ -0,0 +1,1555 @@ +import atexit +import contextlib +import logging +import os +import pathlib +import pprint +import re +import shutil +import tarfile +import textwrap +import time +from typing import TYPE_CHECKING, Any, Dict, List +from zipfile import ZipFile + +import attr +import distro +import packaging +import psutil +import pytest +import requests +from pytestshellutils.shell import DaemonImpl, Subprocess +from pytestshellutils.utils.processes import ( + ProcessResult, + _get_cmdline, + terminate_process, +) +from pytestskipmarkers.utils import platform +from saltfactories.bases import SystemdSaltDaemonImpl +from saltfactories.cli import call, key, salt +from saltfactories.daemons import api, master, minion + +try: + import crypt + + HAS_CRYPT = True +except ImportError: + HAS_CRYPT = False +try: + import pwd + + HAS_PWD = True +except ImportError: + HAS_PWD = False + + +TESTS_DIR = pathlib.Path(__file__).resolve().parent.parent +CODE_DIR = TESTS_DIR.parent +ARTIFACTS_DIR = CODE_DIR / "artifacts" + +log = logging.getLogger(__name__) + + +@attr.s(kw_only=True, slots=True) +class SaltPkgInstall: + conf_dir: pathlib.Path = attr.ib() + system_service: bool = attr.ib(default=False) + proc: Subprocess = attr.ib(init=False) + pkgs: List[str] = attr.ib(factory=list) + onedir: bool = attr.ib(default=False) + singlebin: bool = attr.ib(default=False) + compressed: bool = attr.ib(default=False) + hashes: Dict[str, Dict[str, Any]] = attr.ib() + root: pathlib.Path = attr.ib(default=None) + run_root: pathlib.Path = attr.ib(default=None) + ssm_bin: pathlib.Path = attr.ib(default=None) + bin_dir: pathlib.Path = attr.ib(default=None) + # The artifact is an installer (exe, pkg, rpm, deb) + installer_pkg: bool = attr.ib(default=False) + upgrade: bool = attr.ib(default=False) + # install salt or not. This allows someone + # to test a currently installed version of salt + no_install: bool = attr.ib(default=False) + no_uninstall: bool = attr.ib(default=False) + + distro_id: str = attr.ib(init=False) + pkg_mngr: str = attr.ib(init=False) + rm_pkg: str = attr.ib(init=False) + salt_pkgs: List[str] = attr.ib(init=False) + install_dir: pathlib.Path = attr.ib(init=False) + binary_paths: List[pathlib.Path] = attr.ib(init=False) + classic: bool = attr.ib(default=False) + prev_version: str = attr.ib() + pkg_version: str = attr.ib(default="1") + repo_data: str = attr.ib(init=False) + major: str = attr.ib(init=False) + minor: str = attr.ib(init=False) + + @proc.default + def _default_proc(self): + return Subprocess() + + @hashes.default + def _default_hashes(self): + return { + "BLAKE2B": {"file": None, "tool": "-blake2b512"}, + "SHA3_512": {"file": None, "tool": "-sha3-512"}, + "SHA512": {"file": None, "tool": "-sha512"}, + } + + @distro_id.default + def _default_distro_id(self): + return distro.id().lower() + + @pkg_mngr.default + def _default_pkg_mngr(self): + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + return "yum" + elif self.distro_id in ("ubuntu", "debian"): + ret = self.proc.run("apt-get", "update") + self._check_retcode(ret) + return "apt-get" + + @rm_pkg.default + def _default_rm_pkg(self): + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + return "remove" + elif self.distro_id in ("ubuntu", "debian"): + return "purge" + + @salt_pkgs.default + def _default_salt_pkgs(self): + salt_pkgs = [ + "salt-api", + "salt-syndic", + "salt-ssh", + "salt-master", + "salt-cloud", + "salt-minion", + ] + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + salt_pkgs.append("salt") + elif self.distro_id in ("ubuntu", "debian"): + salt_pkgs.append("salt-common") + return salt_pkgs + + @install_dir.default + def _default_install_dir(self): + if platform.is_windows(): + install_dir = pathlib.Path( + os.getenv("ProgramFiles"), "Salt Project", "Salt" + ).resolve() + elif platform.is_darwin(): + # TODO: Add mac install dir path + install_dir = pathlib.Path("/opt", "salt") + else: + install_dir = pathlib.Path("/opt", "saltstack", "salt") + return install_dir + + @repo_data.default + def _default_repo_data(self): + """ + Query to see the published Salt artifacts + from repo.json + """ + url = "https://repo.saltproject.io/salt/onedir/repo.json" + ret = requests.get(url) + data = ret.json() + return data + + def relenv(self, version): + """ + Detects if we are using relenv + onedir build + """ + relenv = False + if packaging.version.parse(version) >= packaging.version.parse("3006.0"): + relenv = True + return relenv + + def get_version(self): + """ + Return the version information + needed to install a previous version + of Salt. + """ + prev_version = self.prev_version + pkg_version = None + if not prev_version: + # We did not pass in a version, lets detect the latest + # version information of a Salt artifact. + latest = list(self.repo_data["latest"].keys())[0] + version = self.repo_data["latest"][latest]["version"] + if "-" in version: + prev_version, pkg_version = version.split("-") + else: + prev_version, pkg_version = version, None + else: + # We passed in a version, but lets check if the pkg_version + # is defined. Relenv pkgs do not define a pkg build number + if "-" not in prev_version and not self.relenv(version=prev_version): + pkg_numbers = [x for x in self.repo_data.keys() if prev_version in x] + pkg_version = 1 + for number in pkg_numbers: + number = int(number.split("-")[1]) + if number > pkg_version: + pkg_version = number + major, minor = prev_version.split(".") + return major, minor, prev_version, pkg_version + + def __attrs_post_init__(self): + self.major, self.minor, self.prev_version, self.pkg_version = self.get_version() + file_ext_re = r"tar\.gz" + if platform.is_darwin(): + file_ext_re = r"tar\.gz|pkg" + if platform.is_windows(): + file_ext_re = "zip|exe" + for f_path in ARTIFACTS_DIR.glob("**/*.*"): + f_path = str(f_path) + if re.search(f"salt-(.*).({file_ext_re})$", f_path): + # Compressed can be zip, tar.gz, exe, or pkg. All others are + # deb and rpm + self.compressed = True + file_ext = os.path.splitext(f_path)[1].strip(".") + if file_ext == "gz": + if f_path.endswith("tar.gz"): + file_ext = "tar.gz" + self.pkgs.append(f_path) + if platform.is_windows(): + self.root = pathlib.Path(os.getenv("LocalAppData")).resolve() + if file_ext == "zip": + with ZipFile(f_path, "r") as zip: + first = zip.infolist()[0] + if first.filename == "salt/ssm.exe": + self.onedir = True + self.bin_dir = self.root / "salt" / "salt" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.root / "salt" / "ssm.exe" + elif first.filename == "salt.exe": + self.singlebin = True + self.run_root = self.root / "salt.exe" + self.ssm_bin = self.root / "ssm.exe" + else: + log.error( + "Unexpected archive layout. First: %s", + first.filename, + ) + elif file_ext == "exe": + self.onedir = True + self.installer_pkg = True + self.bin_dir = self.install_dir / "bin" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.bin_dir / "ssm.exe" + else: + log.error("Unexpected file extension: %s", file_ext) + else: + if platform.is_darwin(): + self.root = pathlib.Path(os.sep, "opt") + else: + self.root = pathlib.Path(os.sep, "usr", "local", "bin") + + if file_ext == "pkg": + self.onedir = True + self.installer_pkg = True + self.bin_dir = self.root / "salt" / "bin" + elif file_ext == "tar.gz": + with tarfile.open(f_path) as tar: + # The first item will be called salt + first = next(iter(tar.getmembers())) + if first.name == "salt" and first.isdir(): + self.onedir = True + self.bin_dir = self.root / "salt" / "run" + self.run_root = self.bin_dir / "run" + elif first.name == "salt" and first.isfile(): + self.singlebin = True + self.run_root = self.root / "salt" + else: + log.error( + "Unexpected archive layout. First: %s (isdir: %s, isfile: %s)", + first.name, + first.isdir(), + first.isfile(), + ) + else: + log.error("Unexpected file extension: %s", file_ext) + + if re.search( + r"salt(.*)(x86_64|all|amd64|aarch64|arm64)\.(rpm|deb)$", f_path + ): + self.installer_pkg = True + self.pkgs.append(f_path) + + if not self.pkgs: + pytest.fail("Could not find Salt Artifacts") + + if not self.compressed: + self.binary_paths = { + "salt": ["salt"], + "api": ["salt-api"], + "call": ["salt-call"], + "cloud": ["salt-cloud"], + "cp": ["salt-cp"], + "key": ["salt-key"], + "master": ["salt-master"], + "minion": ["salt-minion"], + "proxy": ["salt-proxy"], + "run": ["salt-run"], + "ssh": ["salt-ssh"], + "syndic": ["salt-syndic"], + "spm": ["spm"], + "pip": ["salt-pip"], + "python": [self.install_dir / "bin" / "python3"], + } + else: + if self.salt_pkg_install.run_root and os.path.exists( + self.salt_pkg_install.run_root + ): + self.binary_paths = { + "salt": [str(self.run_root)], + "api": [str(self.run_root), "api"], + "call": [str(self.run_root), "call"], + "cloud": [str(self.run_root), "cloud"], + "cp": [str(self.run_root), "cp"], + "key": [str(self.run_root), "key"], + "master": [str(self.run_root), "master"], + "minion": [str(self.run_root), "minion"], + "proxy": [str(self.run_root), "proxy"], + "run": [str(self.run_root), "run"], + "ssh": [str(self.run_root), "ssh"], + "syndic": [str(self.run_root), "syndic"], + "spm": [str(self.run_root), "spm"], + "pip": [str(self.run_root), "pip"], + } + else: + self.binary_paths = { + "salt": [self.install_dir / "salt"], + "api": [self.install_dir / "salt-api"], + "call": [self.install_dir / "salt-call"], + "cloud": [self.install_dir / "salt-cloud"], + "cp": [self.install_dir / "salt-cp"], + "key": [self.install_dir / "salt-key"], + "master": [self.install_dir / "salt-master"], + "minion": [self.install_dir / "salt-minion"], + "proxy": [self.install_dir / "salt-proxy"], + "run": [self.install_dir / "salt-run"], + "ssh": [self.install_dir / "salt-ssh"], + "syndic": [self.install_dir / "salt-syndic"], + "spm": [self.install_dir / "spm"], + "pip": [self.install_dir / "salt-pip"], + "python": [self.install_dir / "bin" / "python3"], + } + + @staticmethod + def salt_factories_root_dir(system_service: bool = False) -> pathlib.Path: + if system_service is False: + return None + if platform.is_windows(): + return pathlib.Path("C:/salt") + if platform.is_darwin(): + return pathlib.Path("/opt/salt") + return pathlib.Path("/") + + def _check_retcode(self, ret): + """ + helper function ot check subprocess.run + returncode equals 0, if not raise assertionerror + """ + if ret.returncode != 0: + log.error(ret) + assert ret.returncode == 0 + return True + + @property + def salt_hashes(self): + for _hash in self.hashes.keys(): + for fpath in ARTIFACTS_DIR.glob(f"**/*{_hash}*"): + fpath = str(fpath) + if re.search(f"{_hash}", fpath): + self.hashes[_hash]["file"] = fpath + + return self.hashes + + def _install_ssm_service(self): + # Register the services + # run_root and ssm_bin are configured in helper.py to point to the + # correct binary location + log.debug("Installing master service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-master", + str(self.run_root), + "master", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + log.debug("Installing minion service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-minion", + str(self.run_root), + "minion", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + log.debug("Installing api service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-api", + str(self.run_root), + "api", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + + def _install_compressed(self, upgrade=False): + pkg = self.pkgs[0] + log.info("Installing %s", pkg) + if platform.is_windows(): + if pkg.endswith("zip"): + # Extract the files + log.debug("Extracting zip file") + with ZipFile(pkg, "r") as zip: + zip.extractall(path=self.root) + elif pkg.endswith("exe"): + # Install the package + log.debug("Installing: %s", str(pkg)) + if upgrade: + ret = self.proc.run(str(pkg), "/S") + else: + ret = self.proc.run(str(pkg), "/start-minion=0", "/S") + self._check_retcode(ret) + # Remove the service installed by the installer + log.debug("Removing installed salt-minion service") + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-minion", + "confirm", + ) + else: + log.error("Unknown package type: %s", pkg) + if self.system_service: + self._install_ssm_service() + elif platform.is_darwin(): + if pkg.endswith("pkg"): + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + service_name = "com.saltstack.salt.minion" + plist_file = daemons_dir / f"{service_name}.plist" + log.debug("Installing: %s", str(pkg)) + ret = self.proc.run("installer", "-pkg", str(pkg), "-target", "/") + self._check_retcode(ret) + # Stop the service installed by the installer + self.proc.run( + "launchctl", + "disable", + f"system/{service_name}", + ) + self.proc.run("launchctl", "bootout", "system", str(plist_file)) + else: + log.debug("Extracting tarball into %s", self.root) + with tarfile.open(pkg) as tar: # , "r:gz") + tar.extractall(path=str(self.root)) + else: + log.debug("Extracting tarball into %s", self.root) + with tarfile.open(pkg) as tar: # , "r:gz") + tar.extractall(path=str(self.root)) + + def _install_pkgs(self, upgrade=False): + if upgrade: + log.info("Installing packages:\n%s", pprint.pformat(self.pkgs)) + if self.distro_id in ("ubuntu", "debian"): + # --allow-downgrades and yum's downgrade is a workaround since + # dpkg/yum is seeing 3005 version as a greater version than our nightly builds. + # Also this helps work around the situation when the Salt + # branch has not been updated with code so the versions might + # be the same and you can still install and test the new + # package. + ret = self.proc.run( + self.pkg_mngr, "upgrade", "-y", "--allow-downgrades", *self.pkgs + ) + else: + ret = self.proc.run(self.pkg_mngr, "upgrade", "-y", *self.pkgs) + if ( + ret.returncode != 0 + or "does not update installed package" in ret.stdout + or "cannot update it" in ret.stderr + ): + log.info( + "The new packages version is not returning as new. Attempting to downgrade" + ) + ret = self.proc.run(self.pkg_mngr, "downgrade", "-y", *self.pkgs) + if ret.returncode != 0: + log.error("Could not install the packages") + return False + else: + log.info("Installing packages:\n%s", pprint.pformat(self.pkgs)) + ret = self.proc.run(self.pkg_mngr, "install", "-y", *self.pkgs) + log.info(ret) + self._check_retcode(ret) + + def install(self, upgrade=False): + if self.compressed: + self._install_compressed(upgrade=upgrade) + else: + self._install_pkgs(upgrade=upgrade) + if self.distro_id in ("ubuntu", "debian"): + self.stop_services() + + def stop_services(self): + """ + Debian distros automatically start the services + We want to ensure our tests start with the config + settings we have set. This will also verify the expected + services are up and running. + """ + for service in ["salt-syndic", "salt-master", "salt-minion"]: + check_run = self.proc.run("systemctl", "status", service) + if check_run.returncode != 0: + # The system was not started automatically and we + # are expecting it to be on install + log.debug("The service %s was not started on install.", service) + return False + stop_service = self.proc.run("systemctl", "stop", service) + self._check_retcode(stop_service) + return True + + def install_previous(self): + """ + Install previous version. This is used for + upgrade tests. + """ + if platform.is_darwin(): + major_ver = f"{self.major}-{self.pkg_version}" + else: + major_ver = self.major + min_ver = f"{major_ver}" + os_name, version, code_name = distro.linux_distribution() + if os_name: + os_name = os_name.split()[0].lower() + if os_name == "centos" or os_name == "fedora": + os_name = "redhat" + root_url = "salt/py3/" + if self.classic: + root_url = "py3/" + + if os_name.lower() in ["redhat", "centos", "amazon", "fedora"]: + for fp in pathlib.Path("/etc", "yum.repos.d").glob("epel*"): + fp.unlink() + gpg_key = "SALTSTACK-GPG-KEY.pub" + if version == "9": + gpg_key = "SALTSTACK-GPG-KEY2.pub" + ret = self.proc.run( + "rpm", + "--import", + f"https://repo.saltproject.io/{root_url}{os_name}/{version}/x86_64/{major_ver}/{gpg_key}", + ) + self._check_retcode(ret) + ret = self.proc.run( + "curl", + "-fsSL", + f"https://repo.saltproject.io/{root_url}{os_name}/{version}/x86_64/{major_ver}.repo", + "-o", + f"/etc/yum.repos.d/salt-{os_name}.repo", + ) + self._check_retcode(ret) + ret = self.proc.run(self.pkg_mngr, "clean", "expire-cache") + self._check_retcode(ret) + ret = self.proc.run( + self.pkg_mngr, + "install", + *self.salt_pkgs, + "-y", + ) + self._check_retcode(ret) + + elif os_name.lower() in ["debian", "ubuntu"]: + ret = self.proc.run(self.pkg_mngr, "install", "curl", "-y") + self._check_retcode(ret) + ret = self.proc.run(self.pkg_mngr, "install", "apt-transport-https", "-y") + self._check_retcode(ret) + ret = self.proc.run( + "curl", + "-fsSL", + "-o", + "/usr/share/keyrings/salt-archive-keyring.gpg", + f"https://repo.saltproject.io/{root_url}{os_name}/{version}/amd64/{major_ver}/salt-archive-keyring.gpg", + ) + self._check_retcode(ret) + with open( + pathlib.Path("/etc", "apt", "sources.list.d", "salt.list"), "w" + ) as fp: + fp.write( + "deb [signed-by=/usr/share/keyrings/salt-archive-keyring.gpg arch=amd64] " + f"https://repo.saltproject.io/{root_url}{os_name}/{version}/amd64/{major_ver} {code_name} main" + ) + ret = self.proc.run(self.pkg_mngr, "update") + self._check_retcode(ret) + ret = self.proc.run( + self.pkg_mngr, + "install", + *self.salt_pkgs, + "-y", + ) + self._check_retcode(ret) + + elif platform.is_windows(): + win_pkg = f"salt-{min_ver}-1-windows-amd64.exe" + win_pkg_url = ( + f"https://repo.saltproject.io/salt/py3/windows/{major_ver}/{win_pkg}" + ) + + if self.classic: + win_pkg = f"Salt-Minion-{min_ver}-1-Py3-AMD64-Setup.exe" + win_pkg_url = f"https://repo.saltproject.io/windows/{win_pkg}" + pkg_path = pathlib.Path(r"C:\TEMP", win_pkg) + pkg_path.parent.mkdir(exist_ok=True) + ret = requests.get(win_pkg_url) + with open(pkg_path, "wb") as fp: + fp.write(ret.content) + ret = self.proc.run(pkg_path, "/start-minion=0", "/S") + self._check_retcode(ret) + log.debug("Removing installed salt-minion service") + self.proc.run( + "cmd", "/c", str(self.ssm_bin), "remove", "salt-minion", "confirm" + ) + + if self.system_service: + self._install_system_service() + + self.onedir = True + self.installer_pkg = True + self.bin_dir = self.install_dir / "bin" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.bin_dir / "ssm.exe" + + elif platform.is_darwin(): + mac_pkg = f"salt-{min_ver}-macos-x86_64.pkg" + mac_pkg_url = ( + f"https://repo.saltproject.io/salt/py3/macos/{major_ver}/{mac_pkg}" + ) + if self.classic: + mac_pkg = f"salt-{min_ver}-1-py3-x86_64.pkg" + mac_pkg_url = f"https://repo.saltproject.io/osx/{mac_pkg}" + mac_pkg_path = f"/tmp/{mac_pkg}" + ret = self.proc.run( + "curl", + "-fsSL", + "-o", + f"/tmp/{mac_pkg}", + f"{mac_pkg_url}", + ) + self._check_retcode(ret) + + ret = self.proc.run("installer", "-pkg", mac_pkg_path, "-target", "/") + self._check_retcode(ret) + + def _uninstall_compressed(self): + if platform.is_windows(): + if self.system_service: + # Uninstall the services + log.debug("Uninstalling master service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-master", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-master", + "confirm", + ) + log.debug("Uninstalling minion service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-minion", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-minion", + "confirm", + ) + log.debug("Uninstalling api service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-api", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-api", + "confirm", + ) + log.debug("Removing the Salt Service Manager") + if self.ssm_bin: + try: + self.ssm_bin.unlink() + except PermissionError: + atexit.register(self.ssm_bin.unlink) + if platform.is_darwin(): + # From here: https://stackoverflow.com/a/46118276/4581998 + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + for service in ("minion", "master", "api", "syndic"): + service_name = f"com.saltstack.salt.{service}" + plist_file = daemons_dir / f"{service_name}.plist" + # Stop the services + self.proc.run("launchctl", "disable", f"system/{service_name}") + self.proc.run("launchctl", "bootout", "system", str(plist_file)) + + # Remove Symlink to salt-config + if os.path.exists("/usr/local/sbin/salt-config"): + os.unlink("/usr/local/sbin/salt-config") + + # Remove supporting files + self.proc.run( + "pkgutil", + "--only-files", + "--files", + "com.saltstack.salt", + "|", + "grep", + "-v", + "opt", + "|", + "tr", + "'\n'", + "' '", + "|", + "xargs", + "-0", + "rm", + "-f", + ) + + # Remove directories + if os.path.exists("/etc/salt"): + shutil.rmtree("/etc/salt") + + # Remove path + if os.path.exists("/etc/paths.d/salt"): + os.remove("/etc/paths.d/salt") + + # Remove receipt + self.proc.run("pkgutil", "--forget", "com.saltstack.salt") + + if self.singlebin: + log.debug("Deleting the salt binary: %s", self.run_root) + if self.run_root: + try: + self.run_root.unlink() + except PermissionError: + atexit.register(self.run_root.unlink) + else: + log.debug("Deleting the onedir directory: %s", self.root / "salt") + shutil.rmtree(str(self.root / "salt")) + + def _uninstall_pkgs(self): + log.debug("Un-Installing packages:\n%s", pprint.pformat(self.salt_pkgs)) + ret = self.proc.run(self.pkg_mngr, self.rm_pkg, "-y", *self.salt_pkgs) + self._check_retcode(ret) + + def uninstall(self): + if self.compressed: + self._uninstall_compressed() + else: + self._uninstall_pkgs() + + def assert_uninstalled(self): + """ + Assert that the paths in /opt/saltstack/ were correctly + removed or not removed + """ + return + if platform.is_windows(): + # I'm not sure where the /opt/saltstack path is coming from + # This is the path we're using to test windows + opt_path = pathlib.Path(os.getenv("LocalAppData"), "salt", "pypath") + else: + opt_path = pathlib.Path(os.sep, "opt", "saltstack", "salt", "pypath") + if not opt_path.exists(): + if platform.is_windows(): + assert not opt_path.parent.exists() + else: + assert not opt_path.parent.parent.exists() + else: + opt_path_contents = list(opt_path.rglob("*")) + if not opt_path_contents: + pytest.fail( + f"The path '{opt_path}' exists but there are no files in it." + ) + else: + for path in list(opt_path_contents): + if path.name in (".installs.json", "__pycache__"): + opt_path_contents.remove(path) + if opt_path_contents: + pytest.fail( + "The test left some files behind: {}".format( + ", ".join([str(p) for p in opt_path_contents]) + ) + ) + + def write_launchd_conf(self, service): + service_name = f"com.saltstack.salt.{service}" + ret = self.proc.run("launchctl", "list", service_name) + # 113 means it couldn't find a service with that name + if ret.returncode == 113: + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + plist_file = daemons_dir / f"{service_name}.plist" + # Make sure we're using this plist file + if plist_file.exists(): + log.warning("Removing existing plist file for service: %s", service) + plist_file.unlink() + + log.debug("Creating plist file for service: %s", service) + contents = textwrap.dedent( + f"""\ + + + + + Label + {service_name} + RunAtLoad + + KeepAlive + + ProgramArguments + + {self.run_root} + {service} + -c + {self.conf_dir} + + SoftResourceLimits + + NumberOfFiles + 100000 + + HardResourceLimits + + NumberOfFiles + 100000 + + + + """ + ) + plist_file.write_text(contents, encoding="utf-8") + contents = plist_file.read_text() + log.debug("Created '%s'. Contents:\n%s", plist_file, contents) + + # Delete the plist file upon completion + atexit.register(plist_file.unlink) + + def write_systemd_conf(self, service, binary): + ret = self.proc.run("systemctl", "daemon-reload") + self._check_retcode(ret) + ret = self.proc.run("systemctl", "status", service) + if ret.returncode in (3, 4): + log.warning( + "No systemd unit file was found for service %s. Creating one.", service + ) + contents = textwrap.dedent( + """\ + [Unit] + Description={service} + + [Service] + KillMode=process + Type=notify + NotifyAccess=all + LimitNOFILE=8192 + ExecStart={tgt} -c {conf_dir} + + [Install] + WantedBy=multi-user.target + """ + ) + if isinstance(binary, list) and len(binary) == 1: + binary = shutil.which(binary[0]) or binary[0] + elif isinstance(binary, list): + binary = " ".join(binary) + unit_path = pathlib.Path( + os.sep, "etc", "systemd", "system", f"{service}.service" + ) + contents = contents.format( + service=service, tgt=binary, conf_dir=self.conf_dir + ) + log.info("Created '%s'. Contents:\n%s", unit_path, contents) + unit_path.write_text(contents, encoding="utf-8") + ret = self.proc.run("systemctl", "daemon-reload") + atexit.register(unit_path.unlink) + self._check_retcode(ret) + + def __enter__(self): + if not self.no_install: + if self.upgrade: + self.install_previous() + else: + self.install() + return self + + def __exit__(self, *_): + if not self.no_uninstall: + self.uninstall() + self.assert_uninstalled() + + +class PkgSystemdSaltDaemonImpl(SystemdSaltDaemonImpl): + def get_service_name(self): + if self._service_name is None: + self._service_name = self.factory.script_name + return self._service_name + + +@attr.s(kw_only=True) +class PkgLaunchdSaltDaemonImpl(PkgSystemdSaltDaemonImpl): + + plist_file = attr.ib() + + @plist_file.default + def _default_plist_file(self): + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + return daemons_dir / f"{self.get_service_name()}.plist" + + def get_service_name(self): + if self._service_name is None: + service_name = super().get_service_name() + if "-" in service_name: + service_name = service_name.split("-")[-1] + self._service_name = f"com.saltstack.salt.{service_name}" + return self._service_name + + def cmdline(self, *args): # pylint: disable=arguments-differ + """ + Construct a list of arguments to use when starting the subprocess. + + :param str args: + Additional arguments to use when starting the subprocess + + """ + if args: # pragma: no cover + log.debug( + "%s.run() is ignoring the passed in arguments: %r", + self.__class__.__name__, + args, + ) + self._internal_run( + "launchctl", + "enable", + f"system/{self.get_service_name()}", + ) + return ( + "launchctl", + "bootstrap", + "system", + str(self.plist_file), + ) + + def is_running(self): + """ + Returns true if the sub-process is alive. + """ + if self._process is None: + ret = self._internal_run("launchctl", "list", self.get_service_name()) + if ret.stdout == "": + return False + + if "PID" not in ret.stdout: + return False + + pid = None + # PID in a line that looks like this + # "PID" = 445; + for line in ret.stdout.splitlines(): + if "PID" in line: + pid = line.rstrip(";").split(" = ")[1] + + if pid is None: + return False + + self._process = psutil.Process(int(pid)) + + return self._process.is_running() + + def _terminate(self): + """ + This method actually terminates the started daemon. + """ + # We completely override the parent class method because we're not using + # the self._terminal property, it's a launchd service + if self._process is None: # pragma: no cover + if TYPE_CHECKING: + # Make mypy happy + assert self._terminal_result + return ( + self._terminal_result + ) # pylint: disable=access-member-before-definition + + atexit.unregister(self.terminate) + log.info("Stopping %s", self.factory) + pid = self.pid + # Collect any child processes information before terminating the process + with contextlib.suppress(psutil.NoSuchProcess): + for child in psutil.Process(pid).children(recursive=True): + if ( + child not in self._children + ): # pylint: disable=access-member-before-definition + self._children.append( + child + ) # pylint: disable=access-member-before-definition + + if self._process.is_running(): # pragma: no cover + cmdline = _get_cmdline(self._process) + else: + cmdline = [] + + # Disable the service + self._internal_run( + "launchctl", + "disable", + f"system/{self.get_service_name()}", + ) + # Unload the service + self._internal_run("launchctl", "bootout", "system", str(self.plist_file)) + + if self._process.is_running(): # pragma: no cover + try: + self._process.wait() + except psutil.TimeoutExpired: + self._process.terminate() + try: + self._process.wait() + except psutil.TimeoutExpired: + pass + + exitcode = self._process.wait() or 0 + + # Dereference the internal _process attribute + self._process = None + # Lets log and kill any child processes left behind, including the main subprocess + # if it failed to properly stop + terminate_process( + pid=pid, + kill_children=True, + children=self._children, # pylint: disable=access-member-before-definition + slow_stop=self.factory.slow_stop, + ) + + if self._terminal_stdout is not None: + self._terminal_stdout.close() # pylint: disable=access-member-before-definition + if self._terminal_stderr is not None: + self._terminal_stderr.close() # pylint: disable=access-member-before-definition + stdout = stderr = "" + try: + self._terminal_result = ProcessResult( + returncode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmdline + ) + log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) + return self._terminal_result + finally: + self._terminal = None + self._terminal_stdout = None + self._terminal_stderr = None + self._terminal_timeout = None + self._children = [] + + +@attr.s(kw_only=True) +class PkgSsmSaltDaemonImpl(PkgSystemdSaltDaemonImpl): + def cmdline(self, *args): # pylint: disable=arguments-differ + """ + Construct a list of arguments to use when starting the subprocess. + + :param str args: + Additional arguments to use when starting the subprocess + + """ + if args: # pragma: no cover + log.debug( + "%s.run() is ignoring the passed in arguments: %r", + self.__class__.__name__, + args, + ) + return ( + str(self.factory.salt_pkg_install.ssm_bin), + "start", + self.get_service_name(), + ) + + def is_running(self): + """ + Returns true if the sub-process is alive. + """ + if self._process is None: + n = 1 + while True: + if self._process is not None: + break + time.sleep(1) + ret = self._internal_run( + str(self.factory.salt_pkg_install.ssm_bin), + "processes", + self.get_service_name(), + ) + log.warning(ret) + if not ret.stdout or (ret.stdout and not ret.stdout.strip()): + if n >= 120: + return False + n += 1 + continue + for line in ret.stdout.splitlines(): + log.warning("Line: %s", line) + if not line.strip(): + continue + mainpid = line.strip().split()[0] + self._process = psutil.Process(int(mainpid)) + break + return self._process.is_running() + + def _terminate(self): + """ + This method actually terminates the started daemon. + """ + # We completely override the parent class method because we're not using the + # self._terminal property, it's a systemd service + if self._process is None: # pragma: no cover + if TYPE_CHECKING: + # Make mypy happy + assert self._terminal_result + return ( + self._terminal_result + ) # pylint: disable=access-member-before-definition + + atexit.unregister(self.terminate) + log.info("Stopping %s", self.factory) + pid = self.pid + # Collect any child processes information before terminating the process + with contextlib.suppress(psutil.NoSuchProcess): + for child in psutil.Process(pid).children(recursive=True): + if ( + child not in self._children + ): # pylint: disable=access-member-before-definition + self._children.append( + child + ) # pylint: disable=access-member-before-definition + + if self._process.is_running(): # pragma: no cover + cmdline = _get_cmdline(self._process) + else: + cmdline = [] + + # Tell ssm to stop the service + try: + self._internal_run( + str(self.factory.salt_pkg_install.ssm_bin), + "stop", + self.get_service_name(), + ) + except FileNotFoundError: + pass + + if self._process.is_running(): # pragma: no cover + try: + self._process.wait() + except psutil.TimeoutExpired: + self._process.terminate() + try: + self._process.wait() + except psutil.TimeoutExpired: + pass + + exitcode = self._process.wait() or 0 + + # Dereference the internal _process attribute + self._process = None + # Lets log and kill any child processes left behind, including the main subprocess + # if it failed to properly stop + terminate_process( + pid=pid, + kill_children=True, + children=self._children, # pylint: disable=access-member-before-definition + slow_stop=self.factory.slow_stop, + ) + + if self._terminal_stdout is not None: + self._terminal_stdout.close() # pylint: disable=access-member-before-definition + if self._terminal_stderr is not None: + self._terminal_stderr.close() # pylint: disable=access-member-before-definition + stdout = stderr = "" + try: + self._terminal_result = ProcessResult( + returncode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmdline + ) + log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) + return self._terminal_result + finally: + self._terminal = None + self._terminal_stdout = None + self._terminal_stderr = None + self._terminal_timeout = None + self._children = [] + + +@attr.s(kw_only=True) +class PkgMixin: + salt_pkg_install: SaltPkgInstall = attr.ib() + + def get_script_path(self): + if self.salt_pkg_install.compressed: + if self.salt_pkg_install.run_root and os.path.exists( + self.salt_pkg_install.run_root + ): + return str(self.salt_pkg_install.run_root) + else: + return str(self.salt_pkg_install.install_dir / self.script_name) + return super().get_script_path() + + def get_base_script_args(self): + base_script_args = [] + if self.salt_pkg_install.run_root and os.path.exists( + self.salt_pkg_install.run_root + ): + if self.salt_pkg_install.compressed: + if self.script_name == "spm": + base_script_args.append(self.script_name) + elif self.script_name != "salt": + base_script_args.append(self.script_name.split("salt-")[-1]) + base_script_args.extend(super().get_base_script_args()) + return base_script_args + + def cmdline(self, *args, **kwargs): + _cmdline = super().cmdline(*args, **kwargs) + if self.salt_pkg_install.compressed is False: + return _cmdline + if _cmdline[0] == self.python_executable: + _cmdline.pop(0) + return _cmdline + + +@attr.s(kw_only=True) +class DaemonPkgMixin(PkgMixin): + def __attrs_post_init__(self): + if not platform.is_windows() and self.salt_pkg_install.system_service: + if platform.is_darwin(): + self.write_launchd_conf() + else: + self.write_systemd_conf() + + def get_service_name(self): + return self.script_name + + def write_launchd_conf(self): + raise NotImplementedError + + def write_systemd_conf(self): + raise NotImplementedError + + +@attr.s(kw_only=True) +class SaltMaster(DaemonPkgMixin, master.SaltMaster): + """ + Subclassed just to tweak the binary paths if needed and factory classes. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-master" + master.SaltMaster.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("master") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-master", self.salt_pkg_install.binary_paths["master"] + ) + + def salt_minion_daemon(self, minion_id, **kwargs): + return super().salt_minion_daemon( + minion_id, + factory_class=SaltMinion, + salt_pkg_install=self.salt_pkg_install, + **kwargs, + ) + + def salt_api_daemon(self, **kwargs): + return super().salt_api_daemon( + factory_class=SaltApi, salt_pkg_install=self.salt_pkg_install, **kwargs + ) + + def salt_key_cli(self, **factory_class_kwargs): + return super().salt_key_cli( + factory_class=SaltKey, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + def salt_cli(self, **factory_class_kwargs): + return super().salt_cli( + factory_class=SaltCli, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + +@attr.s(kw_only=True, slots=True) +class SaltMinion(DaemonPkgMixin, minion.SaltMinion): + """ + Subclassed just to tweak the binary paths if needed and factory classes. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-minion" + minion.SaltMinion.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("minion") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-minion", self.salt_pkg_install.binary_paths["minion"] + ) + + def salt_call_cli(self, **factory_class_kwargs): + return super().salt_call_cli( + factory_class=SaltCall, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + +@attr.s(kw_only=True, slots=True) +class SaltApi(DaemonPkgMixin, api.SaltApi): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-api" + api.SaltApi.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("api") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-api", + self.salt_pkg_install.binary_paths["api"], + ) + + +@attr.s(kw_only=True, slots=True) +class SaltCall(PkgMixin, call.SaltCall): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + call.SaltCall.__attrs_post_init__(self) + self.script_name = "salt-call" + + +@attr.s(kw_only=True, slots=True) +class SaltCli(PkgMixin, salt.SaltCli): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt" + salt.SaltCli.__attrs_post_init__(self) + + +@attr.s(kw_only=True, slots=True) +class SaltKey(PkgMixin, key.SaltKey): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-key" + key.SaltKey.__attrs_post_init__(self) + + +@attr.s(kw_only=True, slots=True) +class TestUser: + """ + Add a test user + """ + + salt_call_cli = attr.ib() + + username = attr.ib(default="saltdev") + # Must follow Windows Password Complexity requirements + password = attr.ib(default="P@ssW0rd") + _pw_record = attr.ib(init=False, repr=False, default=None) + + def salt_call_local(self, *args): + ret = self.salt_call_cli.run("--local", *args) + if ret.returncode != 0: + log.error(ret) + assert ret.returncode == 0 + return ret.data + + def add_user(self): + log.debug("Adding system account %r", self.username) + if platform.is_windows(): + self.salt_call_local("user.add", self.username, self.password) + else: + self.salt_call_local("user.add", self.username) + hash_passwd = crypt.crypt(self.password, crypt.mksalt(crypt.METHOD_SHA512)) + self.salt_call_local("shadow.set_password", self.username, hash_passwd) + assert self.username in self.salt_call_local("user.list_users") + + def remove_user(self): + log.debug("Removing system account %r", self.username) + if platform.is_windows(): + self.salt_call_local( + "user.delete", self.username, "purge=True", "force=True" + ) + else: + self.salt_call_local("user.delete", self.username, "remove=True") + + @property + def pw_record(self): + if self._pw_record is None and HAS_PWD: + self._pw_record = pwd.getpwnam(self.username) + return self._pw_record + + @property + def uid(self): + if HAS_PWD: + return self.pw_record.pw_uid + return None + + @property + def gid(self): + if HAS_PWD: + return self.pw_record.pw_gid + return None + + @property + def env(self): + environ = os.environ.copy() + environ["LOGNAME"] = environ["USER"] = self.username + environ["HOME"] = self.pw_record.pw_dir + return environ + + def __enter__(self): + self.add_user() + return self + + def __exit__(self, *_): + self.remove_user() + + +@attr.s(kw_only=True, slots=True) +class ApiRequest: + salt_api: SaltApi = attr.ib(repr=False) + test_account: TestUser = attr.ib(repr=False) + session: requests.Session = attr.ib(init=False, repr=False) + api_uri: str = attr.ib(init=False) + auth_data: Dict[str, str] = attr.ib(init=False) + + @session.default + def _default_session(self): + return requests.Session() + + @api_uri.default + def _default_api_uri(self): + return f"http://localhost:{self.salt_api.config['rest_cherrypy']['port']}" + + @auth_data.default + def _default_auth_data(self): + return { + "username": self.test_account.username, + "password": self.test_account.password, + "eauth": "auto", + "out": "json", + } + + def post(self, url, data): + post_data = dict(**self.auth_data, **data) + resp = self.session.post(f"{self.api_uri}/run", data=post_data).json() + minion = next(iter(resp["return"][0])) + return resp["return"][0][minion] + + def __enter__(self): + self.session.__enter__() + return self + + def __exit__(self, *args): + self.session.__exit__(*args) + + +@pytest.helpers.register +def remove_stale_minion_key(master, minion_id): + key_path = os.path.join(master.config["pki_dir"], "minions", minion_id) + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug("The minion(id=%r) key was not found at %s", minion_id, key_path) + + +@pytest.helpers.register +def remove_stale_master_key(master): + keys_path = os.path.join(master.config["pki_dir"], "master") + for key_name in ("master.pem", "master.pub"): + key_path = os.path.join(keys_path, key_name) + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug( + "The master(id=%r) %s key was not found at %s", + master.id, + key_name, + key_path, + ) + key_path = os.path.join(master.config["pki_dir"], "minion", "minion_master.pub") + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug( + "The master(id=%r) minion_master.pub key was not found at %s", + master.id, + key_path, + ) diff --git a/pkg/tests/upgrade/test_salt_upgrade.py b/pkg/tests/upgrade/test_salt_upgrade.py new file mode 100644 index 000000000000..c5c3771e71ed --- /dev/null +++ b/pkg/tests/upgrade/test_salt_upgrade.py @@ -0,0 +1,76 @@ +import pytest + + +@pytest.mark.skip_on_windows( + reason="Salt Master scripts not included in old windows packages" +) +def test_salt_upgrade(salt_call_cli, salt_minion, install_salt): + """ + Test upgrade of Salt + """ + if not install_salt.upgrade: + pytest.skip("Not testing an upgrade, do not run") + # verify previous install version is setup correctly and works + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + + # test pip install before an upgrade + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + # upgrade Salt from previous version and test + install_salt.install(upgrade=True) + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + + # install dep following upgrade + # TODO: Remove this once we figure out how to + # preserve things installed via PIP between upgrades. + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + + # test pip install after an upgrade + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + + +@pytest.mark.skip_unless_on_windows() +def test_salt_upgrade_windows_1(install_salt, salt_call_cli): + """ + Test upgrade of Salt on windows + """ + if not install_salt.upgrade: + pytest.skip("Not testing an upgrade, do not run") + # verify previous install version is setup correctly and works + ret = salt_call_cli.run("--local", "test.ping") + assert ret.data is True + assert ret.returncode == 0 + # test pip install before an upgrade + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + + +@pytest.mark.skip_unless_on_windows() +def test_salt_upgrade_windows_2(salt_call_cli, salt_minion, install_salt): + """ + Test upgrade of Salt on windows + """ + if install_salt.no_uninstall: + pytest.skip("Not testing an upgrade, do not run") + # upgrade Salt from previous version and test + install_salt.install(upgrade=True) + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + repo = "https://github.com/saltstack/salt.git" + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr diff --git a/requirements/static/ci/pkgtests.in b/requirements/static/ci/pkgtests.in new file mode 100644 index 000000000000..e40f7d075e23 --- /dev/null +++ b/requirements/static/ci/pkgtests.in @@ -0,0 +1,2 @@ +cherrypy +pytest-salt-factories==1.0.0rc17 diff --git a/requirements/static/ci/py3.10/pkgtests.txt b/requirements/static/ci/py3.10/pkgtests.txt new file mode 100644 index 000000000000..17d2fdd1f91b --- /dev/null +++ b/requirements/static/ci/py3.10/pkgtests.txt @@ -0,0 +1,151 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.10/pkgtests.txt requirements/base.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # pytest +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pytest-helpers-namespace==2021.12.29 + # via + # pytest-salt-factories + # pytest-shell-utilities +pytest-salt-factories==1.0.0rc17 + # via -r requirements/static/ci/pkgtests.in +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via pytest-salt-factories +pytest==7.2.1 + # via + # pytest-helpers-namespace + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics + # pytest-tempdir +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via -r requirements/base.txt +pyzmq==25.0.0 ; python_version >= "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via -r requirements/base.txt +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via requests +virtualenv==20.17.1 + # via pytest-salt-factories +zc.lockfile==2.0 + # via cherrypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.6/pkgtests.txt b/requirements/static/ci/py3.6/pkgtests.txt new file mode 100644 index 000000000000..a1cd3f7ad0b5 --- /dev/null +++ b/requirements/static/ci/py3.6/pkgtests.txt @@ -0,0 +1,162 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.6/pkgtests.txt requirements/base.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +certifi==2022.12.7 + # via requests +charset-normalizer==2.0.12 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +filelock==3.4.1 + # via virtualenv +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +importlib-metadata==4.8.3 + # via + # cheroot + # pluggy + # pytest + # virtualenv +importlib-resources==5.4.0 + # via + # jaraco.text + # virtualenv +iniconfig==1.1.1 + # via pytest +jaraco.classes==3.2.1 + # via jaraco.collections +jaraco.collections==3.4.0 + # via cherrypy +jaraco.context==4.1.1 + # via jaraco.text +jaraco.functools==3.4.0 + # via + # cheroot + # jaraco.text + # tempora +jaraco.text==3.7.0 + # via jaraco.collections +jinja2==3.0.3 + # via -r requirements/base.txt +jmespath==0.10.0 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.0.1 + # via + # -r requirements/base.txt + # jinja2 +more-itertools==8.14.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==21.3 + # via + # -r requirements/base.txt + # pytest +platformdirs==2.4.0 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.0.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +py==1.11.0 + # via pytest +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pyparsing==3.0.9 + # via packaging +pytest-helpers-namespace==2021.12.29 + # via + # pytest-salt-factories + # pytest-shell-utilities +pytest-salt-factories==1.0.0rc17 + # via -r requirements/static/ci/pkgtests.in +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.3.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via pytest-salt-factories +pytest==7.0.1 + # via + # pytest-helpers-namespace + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics + # pytest-tempdir +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via -r requirements/base.txt +pyzmq==25.0.0 ; python_version < "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.27.1 + # via -r requirements/base.txt +six==1.16.0 + # via cheroot +tempora==4.1.2 + # via portend +tomli==1.2.3 + # via pytest +typing-extensions==4.1.1 + # via + # immutables + # importlib-metadata + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via requests +virtualenv==20.17.1 + # via pytest-salt-factories +zc.lockfile==2.0 + # via cherrypy +zipp==3.6.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.7/pkgtests.txt b/requirements/static/ci/py3.7/pkgtests.txt new file mode 100644 index 000000000000..60daeb146ae9 --- /dev/null +++ b/requirements/static/ci/py3.7/pkgtests.txt @@ -0,0 +1,166 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.7/pkgtests.txt requirements/base.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +importlib-metadata==6.0.0 + # via + # cheroot + # pluggy + # pytest + # virtualenv +importlib-resources==5.10.2 + # via jaraco.text +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # pytest +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pytest-helpers-namespace==2021.12.29 + # via + # pytest-salt-factories + # pytest-shell-utilities +pytest-salt-factories==1.0.0rc17 + # via -r requirements/static/ci/pkgtests.in +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via pytest-salt-factories +pytest==7.2.1 + # via + # pytest-helpers-namespace + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics + # pytest-tempdir +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via -r requirements/base.txt +pyzmq==25.0.0 ; python_version < "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via -r requirements/base.txt +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # immutables + # importlib-metadata + # platformdirs + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via requests +virtualenv==20.17.1 + # via pytest-salt-factories +zc.lockfile==2.0 + # via cherrypy +zipp==3.11.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.8/pkgtests.txt b/requirements/static/ci/py3.8/pkgtests.txt new file mode 100644 index 000000000000..afe9e74796f9 --- /dev/null +++ b/requirements/static/ci/py3.8/pkgtests.txt @@ -0,0 +1,155 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.8/pkgtests.txt requirements/base.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +importlib-resources==5.10.2 + # via jaraco.text +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # pytest +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pytest-helpers-namespace==2021.12.29 + # via + # pytest-salt-factories + # pytest-shell-utilities +pytest-salt-factories==1.0.0rc17 + # via -r requirements/static/ci/pkgtests.in +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via pytest-salt-factories +pytest==7.2.1 + # via + # pytest-helpers-namespace + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics + # pytest-tempdir +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via -r requirements/base.txt +pyzmq==25.0.0 ; python_version < "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via -r requirements/base.txt +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via requests +virtualenv==20.17.1 + # via pytest-salt-factories +zc.lockfile==2.0 + # via cherrypy +zipp==3.11.0 + # via importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.9/pkgtests.txt b/requirements/static/ci/py3.9/pkgtests.txt new file mode 100644 index 000000000000..dee0f80444ac --- /dev/null +++ b/requirements/static/ci/py3.9/pkgtests.txt @@ -0,0 +1,151 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.9/pkgtests.txt requirements/base.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # pytest +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pytest-helpers-namespace==2021.12.29 + # via + # pytest-salt-factories + # pytest-shell-utilities +pytest-salt-factories==1.0.0rc17 + # via -r requirements/static/ci/pkgtests.in +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via pytest-salt-factories +pytest==7.2.1 + # via + # pytest-helpers-namespace + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics + # pytest-tempdir +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via -r requirements/base.txt +pyzmq==25.0.0 ; python_version >= "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via -r requirements/base.txt +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via requests +virtualenv==20.17.1 + # via pytest-salt-factories +zc.lockfile==2.0 + # via cherrypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/tools/ci.py b/tools/ci.py index b3c9e07e98ab..50d16696a4be 100644 --- a/tools/ci.py +++ b/tools/ci.py @@ -383,3 +383,26 @@ def transport_matrix(ctx: Context, distro_slug: str): _matrix.append({"transport": transport}) print(json.dumps(_matrix)) ctx.exit(0) + + +@ci.command( + name="pkg-matrix", + arguments={ + "distro_slug": { + "help": "The distribution slug to generate the matrix for", + }, + }, +) +def pkg_matrix(ctx: Context, distro_slug: str): + """ + Generate the test matrix. + """ + _matrix = [] + for sess in ( + "test-pkgs-3", + "'test-upgrade-pkgs-3(classic=False)'", + "'test-upgrade-pkgs-3(classic=True)'", + ): + _matrix.append({"nox-session": sess}) + print(json.dumps(_matrix)) + ctx.exit(0) diff --git a/tools/vm.py b/tools/vm.py index 8e1eed6cac50..b93e99748848 100644 --- a/tools/vm.py +++ b/tools/vm.py @@ -990,6 +990,8 @@ def upload_checkout(self, verbose=True): "artifacts/", "--include", "artifacts/salt", + "--include", + "pkg/artifacts/*", # But we also want to exclude all other entries under artifacts/ "--exclude", "artifacts/*",