diff --git a/.github/workflows/auto_release_prep.yml b/.github/workflows/auto_release_prep.yml new file mode 100644 index 0000000..57a12de --- /dev/null +++ b/.github/workflows/auto_release_prep.yml @@ -0,0 +1,12 @@ +name: Automated release prep + +on: + workflow_dispatch: + +jobs: + auto_release_prep: + uses: puppetlabs/release-engineering-repo-standards/.github/workflows/auto_release_prep.yml@v1 + secrets: inherit + with: + project-type: ruby + version-file-path: lib/vmpooler/version.rb diff --git a/.github/workflows/dependabot_merge.yml b/.github/workflows/dependabot_merge.yml new file mode 100644 index 0000000..75b9cea --- /dev/null +++ b/.github/workflows/dependabot_merge.yml @@ -0,0 +1,8 @@ +name: Dependabot auto-merge + +on: pull_request + +jobs: + dependabot_merge: + uses: puppetlabs/release-engineering-repo-standards/.github/workflows/dependabot_merge.yml@v1 + secrets: inherit diff --git a/.github/workflows/ensure_label.yml b/.github/workflows/ensure_label.yml new file mode 100644 index 0000000..50a5fa8 --- /dev/null +++ b/.github/workflows/ensure_label.yml @@ -0,0 +1,8 @@ +name: Ensure label + +on: pull_request + +jobs: + ensure_label: + uses: puppetlabs/release-engineering-repo-standards/.github/workflows/ensure_label.yml@v1 + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 058b2cd..d020d40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'puppetlabs/vmpooler' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get Current Version - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: cv with: script: | @@ -29,37 +29,6 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT echo "Found version $version from lib/vmpooler/version.rb" - - name: Generate Changelog - uses: docker://githubchangeloggenerator/github-changelog-generator:1.16.2 - with: - args: >- - --future-release ${{ steps.nv.outputs.version }} - env: - CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Validate Changelog - run : | - set -e - if [[ -n $(git status --porcelain) ]]; then - echo "Here is the current git status:" - git status - echo - echo "The following changes were detected:" - git --no-pager diff - echo "Uncommitted PRs found in the changelog. Please submit a release prep PR of changes after running `./update-changelog`" - exit 1 - fi - - - name: Generate Release Notes - uses: docker://githubchangeloggenerator/github-changelog-generator:1.16.2 - with: - args: >- - --since-tag ${{ steps.cv.outputs.result }} - --future-release ${{ steps.nv.outputs.version }} - --output release-notes.md - env: - CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Tag Release uses: ncipollo/release-action@v1 with: @@ -70,10 +39,10 @@ jobs: prerelease: false # This step should closely match what is used in `docker/Dockerfile` in vmpooler-deployment - - name: Install Ruby jruby-9.4.3.0 + - name: Install Ruby jruby-9.4.12.1 uses: ruby/setup-ruby@v1 with: - ruby-version: 'jruby-9.4.3.0' + ruby-version: 'jruby-9.4.12.1' - name: Build gem run: gem build *.gemspec diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 666c602..ba273f5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout repo content - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: setup ruby @@ -22,7 +22,7 @@ jobs: - name: check lock run: '[ -f "Gemfile.lock" ] && echo "package lock file exists, skipping" || bundle lock' # install java - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 105fc8e..d93859a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,9 +18,9 @@ jobs: strategy: matrix: ruby-version: - - 'jruby-9.4.3.0' + - 'jruby-9.4.12.1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -34,9 +34,9 @@ jobs: strategy: matrix: ruby-version: - - 'jruby-9.4.3.0' + - 'jruby-9.4.12.1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.github_changelog_generator b/.github_changelog_generator index f5bee9c..ebeb260 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -1,3 +1,5 @@ project=vmpooler user=puppetlabs -exclude_labels=maintenance \ No newline at end of file +exclude_labels=maintenance +github-api=https://api.github.com +release-branch=main \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f09aed2..af092e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,118 @@ # Changelog +## [3.8.1](https://github.com/puppetlabs/vmpooler/tree/3.8.1) (2026-01-14) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.7.0...3.8.1) + +**Implemented enhancements:** + +- \(P4DEVOPS-9434\) Add rate limiting and input validation security enhancements [\#690](https://github.com/puppetlabs/vmpooler/pull/690) ([mahima-singh](https://github.com/mahima-singh)) +- \(P4DEVOPS-8570\) Add Phase 2 optimizations: status API caching and improved Redis pipelining [\#689](https://github.com/puppetlabs/vmpooler/pull/689) ([mahima-singh](https://github.com/mahima-singh)) +- \(P4DEVOPS-8567\) Add DLQ, auto-purge, and health checks for Redis queues [\#688](https://github.com/puppetlabs/vmpooler/pull/688) ([mahima-singh](https://github.com/mahima-singh)) +- Add retry logic for immediate clone failures [\#687](https://github.com/puppetlabs/vmpooler/pull/687) ([mahima-singh](https://github.com/mahima-singh)) + +**Fixed bugs:** + +- \(P4DEVOPS-8567\) Prevent VM allocation for already-deleted request-ids [\#688](https://github.com/puppetlabs/vmpooler/pull/688) ([mahima-singh](https://github.com/mahima-singh)) +- Prevent re-queueing requests already marked as failed [\#687](https://github.com/puppetlabs/vmpooler/pull/687) ([mahima-singh](https://github.com/mahima-singh)) + +## [3.7.0](https://github.com/puppetlabs/vmpooler/tree/3.7.0) (2025-06-04) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.6.0...3.7.0) + +**Implemented enhancements:** + +- \(P4DEVOPS-6096\) Include VMs that have been requested but not moved to pending when getting queue metrics [\#681](https://github.com/puppetlabs/vmpooler/pull/681) ([isaac-hammes](https://github.com/isaac-hammes)) +- Bump redis from 5.1.0 to 5.2.0 [\#675](https://github.com/puppetlabs/vmpooler/pull/675) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rake from 13.1.0 to 13.2.1 [\#673](https://github.com/puppetlabs/vmpooler/pull/673) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump redis from 5.0.8 to 5.1.0 [\#665](https://github.com/puppetlabs/vmpooler/pull/665) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rspec from 3.12.0 to 3.13.0 [\#664](https://github.com/puppetlabs/vmpooler/pull/664) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump opentelemetry-sdk from 1.3.1 to 1.4.0 [\#663](https://github.com/puppetlabs/vmpooler/pull/663) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump mock\_redis from 0.43.0 to 0.44.0 [\#662](https://github.com/puppetlabs/vmpooler/pull/662) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump mock\_redis from 0.41.0 to 0.43.0 [\#658](https://github.com/puppetlabs/vmpooler/pull/658) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump net-ldap from 0.18.0 to 0.19.0 [\#653](https://github.com/puppetlabs/vmpooler/pull/653) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump sinatra from 3.1.0 to 3.2.0 [\#652](https://github.com/puppetlabs/vmpooler/pull/652) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump mock\_redis from 0.40.0 to 0.41.0 [\#650](https://github.com/puppetlabs/vmpooler/pull/650) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump mock\_redis from 0.37.0 to 0.40.0 [\#643](https://github.com/puppetlabs/vmpooler/pull/643) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rake from 13.0.6 to 13.1.0 [\#638](https://github.com/puppetlabs/vmpooler/pull/638) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump thor from 1.2.2 to 1.3.0 [\#635](https://github.com/puppetlabs/vmpooler/pull/635) ([dependabot[bot]](https://github.com/apps/dependabot)) + +**Fixed bugs:** + +- Bump opentelemetry-sdk from 1.4.0 to 1.4.1 [\#672](https://github.com/puppetlabs/vmpooler/pull/672) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rack from 2.2.8.1 to 2.2.9 [\#671](https://github.com/puppetlabs/vmpooler/pull/671) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump thor from 1.3.0 to 1.3.1 [\#668](https://github.com/puppetlabs/vmpooler/pull/668) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rack from 2.2.8 to 2.2.8.1 [\#666](https://github.com/puppetlabs/vmpooler/pull/666) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump concurrent-ruby from 1.2.2 to 1.2.3 [\#660](https://github.com/puppetlabs/vmpooler/pull/660) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump puma from 6.4.1 to 6.4.2 [\#655](https://github.com/puppetlabs/vmpooler/pull/655) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump puma from 6.4.0 to 6.4.1 [\#654](https://github.com/puppetlabs/vmpooler/pull/654) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Update opentelemetry-instrumentation-http\_client requirement from = 0.22.2 to = 0.22.3 [\#646](https://github.com/puppetlabs/vmpooler/pull/646) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Update opentelemetry-instrumentation-concurrent\_ruby requirement from = 0.21.1 to = 0.21.2 [\#645](https://github.com/puppetlabs/vmpooler/pull/645) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump opentelemetry-sdk from 1.3.0 to 1.3.1 [\#642](https://github.com/puppetlabs/vmpooler/pull/642) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump prometheus-client from 4.2.1 to 4.2.2 [\#641](https://github.com/puppetlabs/vmpooler/pull/641) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump redis from 5.0.7 to 5.0.8 [\#637](https://github.com/puppetlabs/vmpooler/pull/637) ([dependabot[bot]](https://github.com/apps/dependabot)) +- \(RE-15817\) Reword fail warning and get error from redis before generating message [\#633](https://github.com/puppetlabs/vmpooler/pull/633) ([isaac-hammes](https://github.com/isaac-hammes)) + +**Merged pull requests:** + +- \(P4DEVOPS-6096\) Fix gems to prevent warnings in logs [\#685](https://github.com/puppetlabs/vmpooler/pull/685) ([isaac-hammes](https://github.com/isaac-hammes)) +- \(maint\) Revert gems to last release [\#683](https://github.com/puppetlabs/vmpooler/pull/683) ([isaac-hammes](https://github.com/isaac-hammes)) +- Bump actions/setup-java from 3 to 4 [\#648](https://github.com/puppetlabs/vmpooler/pull/648) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump actions/github-script from 6 to 7 [\#644](https://github.com/puppetlabs/vmpooler/pull/644) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [3.6.0](https://github.com/puppetlabs/vmpooler/tree/3.6.0) (2023-10-05) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.5.1...3.6.0) + +**Fixed bugs:** + +- \(maint\) Fix message for timeout notification. [\#624](https://github.com/puppetlabs/vmpooler/pull/624) ([isaac-hammes](https://github.com/isaac-hammes)) + +**Merged pull requests:** + +- Bump rubocop from 1.56.3 to 1.56.4 [\#631](https://github.com/puppetlabs/vmpooler/pull/631) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump puma from 6.3.1 to 6.4.0 [\#630](https://github.com/puppetlabs/vmpooler/pull/630) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rubocop from 1.56.2 to 1.56.3 [\#628](https://github.com/puppetlabs/vmpooler/pull/628) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump actions/checkout from 3 to 4 [\#627](https://github.com/puppetlabs/vmpooler/pull/627) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Update opentelemetry-resource\_detectors requirement from = 0.24.1 to = 0.24.2 [\#626](https://github.com/puppetlabs/vmpooler/pull/626) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rubocop from 1.56.1 to 1.56.2 [\#625](https://github.com/puppetlabs/vmpooler/pull/625) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump rubocop from 1.56.0 to 1.56.1 [\#623](https://github.com/puppetlabs/vmpooler/pull/623) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [3.5.1](https://github.com/puppetlabs/vmpooler/tree/3.5.1) (2023-08-24) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.5.0...3.5.1) + +**Fixed bugs:** + +- \(maint\) Fix bugs from redis and timeout notification updates. [\#621](https://github.com/puppetlabs/vmpooler/pull/621) ([isaac-hammes](https://github.com/isaac-hammes)) + +## [3.5.0](https://github.com/puppetlabs/vmpooler/tree/3.5.0) (2023-08-23) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.4.0...3.5.0) + +**Implemented enhancements:** + +- Improve LDAP auth [\#616](https://github.com/puppetlabs/vmpooler/issues/616) +- \(maint\) Raise error when ip address is not given to vm after clone. [\#619](https://github.com/puppetlabs/vmpooler/pull/619) ([isaac-hammes](https://github.com/isaac-hammes)) +- \(POD-8\) Add timeout\_notification config to log warning before vm is destroyed. [\#618](https://github.com/puppetlabs/vmpooler/pull/618) ([isaac-hammes](https://github.com/isaac-hammes)) +- \(RE-15565\) Add ability to use bind\_as with a service account [\#617](https://github.com/puppetlabs/vmpooler/pull/617) ([yachub](https://github.com/yachub)) + +**Merged pull requests:** + +- Bump puma from 6.3.0 to 6.3.1 [\#615](https://github.com/puppetlabs/vmpooler/pull/615) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [3.4.0](https://github.com/puppetlabs/vmpooler/tree/3.4.0) (2023-08-18) + +[Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.3.0...3.4.0) + +**Implemented enhancements:** + +- \(POD-10\) Log reason for failed VM checks. [\#611](https://github.com/puppetlabs/vmpooler/pull/611) ([isaac-hammes](https://github.com/isaac-hammes)) + +**Closed issues:** + +- Log reason connection on port 22 of a failed VM [\#609](https://github.com/puppetlabs/vmpooler/issues/609) + ## [3.3.0](https://github.com/puppetlabs/vmpooler/tree/3.3.0) (2023-08-16) [Full Changelog](https://github.com/puppetlabs/vmpooler/compare/3.2.0...3.3.0) @@ -133,6 +246,7 @@ - \(maint\) Adding a provider method tag\_vm\_user [\#469](https://github.com/puppetlabs/vmpooler/pull/469) ([sbeaulie](https://github.com/sbeaulie)) - Update testing.yml [\#468](https://github.com/puppetlabs/vmpooler/pull/468) ([sbeaulie](https://github.com/sbeaulie)) - Move vsphere specific methods out of vmpooler [\#467](https://github.com/puppetlabs/vmpooler/pull/467) ([sbeaulie](https://github.com/sbeaulie)) +- Release prep for v2.0.0 [\#465](https://github.com/puppetlabs/vmpooler/pull/465) ([genebean](https://github.com/genebean)) ## [2.0.0](https://github.com/puppetlabs/vmpooler/tree/2.0.0) (2021-12-08) @@ -141,7 +255,6 @@ **Merged pull requests:** - Use credentials file for Rubygems auth [\#466](https://github.com/puppetlabs/vmpooler/pull/466) ([genebean](https://github.com/genebean)) -- Release prep for v2.0.0 [\#465](https://github.com/puppetlabs/vmpooler/pull/465) ([genebean](https://github.com/genebean)) - Add Gem release workflow [\#464](https://github.com/puppetlabs/vmpooler/pull/464) ([genebean](https://github.com/genebean)) - Update icon in the readme to reference this repo [\#463](https://github.com/puppetlabs/vmpooler/pull/463) ([genebean](https://github.com/genebean)) - \(DIO-2769\) Move vsphere provider to its own gem [\#462](https://github.com/puppetlabs/vmpooler/pull/462) ([genebean](https://github.com/genebean)) @@ -186,13 +299,17 @@ **Merged pull requests:** - \(POOLER-176\) Add Operation Label to User Metric [\#455](https://github.com/puppetlabs/vmpooler/pull/455) ([yachub](https://github.com/yachub)) -- Update OTel gems to 0.15.0 [\#450](https://github.com/puppetlabs/vmpooler/pull/450) ([genebean](https://github.com/genebean)) -- Migrate testing to GH Actions from Travis [\#446](https://github.com/puppetlabs/vmpooler/pull/446) ([genebean](https://github.com/genebean)) ## [1.1.0-rc.1](https://github.com/puppetlabs/vmpooler/tree/1.1.0-rc.1) (2021-08-11) [Full Changelog](https://github.com/puppetlabs/vmpooler/compare/1.0.0...1.1.0-rc.1) +**Merged pull requests:** + +- \(POOLER-176\) Add Operation Label to User Metric [\#454](https://github.com/puppetlabs/vmpooler/pull/454) ([yachub](https://github.com/yachub)) +- Update OTel gems to 0.15.0 [\#450](https://github.com/puppetlabs/vmpooler/pull/450) ([genebean](https://github.com/genebean)) +- Migrate testing to GH Actions from Travis [\#446](https://github.com/puppetlabs/vmpooler/pull/446) ([genebean](https://github.com/genebean)) + ## [1.0.0](https://github.com/puppetlabs/vmpooler/tree/1.0.0) (2021-02-02) [Full Changelog](https://github.com/puppetlabs/vmpooler/compare/0.18.2...1.0.0) @@ -701,13 +818,13 @@ - Do not have a hardcoded list of VM providers [\#230](https://github.com/puppetlabs/vmpooler/issues/230) - Use a dynamic check\_pool period [\#226](https://github.com/puppetlabs/vmpooler/issues/226) - vmpooler doesn't seem to recognize ready VMs [\#218](https://github.com/puppetlabs/vmpooler/issues/218) -- `find_vmdks` in `vsphere_helper` should not use `vmdk_datastore._connection` [\#213](https://github.com/puppetlabs/vmpooler/issues/213) -- `get_base_vm_container_from` in `vsphere_helper` ensures the wrong connection [\#212](https://github.com/puppetlabs/vmpooler/issues/212) +- `find\_vmdks` in `vsphere\_helper` should not use `vmdk\_datastore.\_connection` [\#213](https://github.com/puppetlabs/vmpooler/issues/213) +- `get\_base\_vm\_container\_from` in `vsphere\_helper` ensures the wrong connection [\#212](https://github.com/puppetlabs/vmpooler/issues/212) - `close` in vsphere\_helper throws an error if a connection was never made [\#211](https://github.com/puppetlabs/vmpooler/issues/211) -- `find_pool` in vsphere\_helper.rb has subtle errors [\#210](https://github.com/puppetlabs/vmpooler/issues/210) -- `find_pool` in vsphere\_helper tends to throw instead of returning nil for missing pools [\#209](https://github.com/puppetlabs/vmpooler/issues/209) +- `find\_pool` in vsphere\_helper.rb has subtle errors [\#210](https://github.com/puppetlabs/vmpooler/issues/210) +- `find\_pool` in vsphere\_helper tends to throw instead of returning nil for missing pools [\#209](https://github.com/puppetlabs/vmpooler/issues/209) - Vsphere connections are always insecure \(Ignore cert errors\) [\#207](https://github.com/puppetlabs/vmpooler/issues/207) -- `find_folder` in vsphere\_helper.rb has subtle errors [\#204](https://github.com/puppetlabs/vmpooler/issues/204) +- `find\_folder` in vsphere\_helper.rb has subtle errors [\#204](https://github.com/puppetlabs/vmpooler/issues/204) - Should not use `abort` in vsphere\_helper [\#203](https://github.com/puppetlabs/vmpooler/issues/203) - No reason why get\_snapshot\_list is defined in vsphere\_helper [\#202](https://github.com/puppetlabs/vmpooler/issues/202) - Setting max\_tries in configuration results in vSphereHelper going into infinite loop [\#199](https://github.com/puppetlabs/vmpooler/issues/199) @@ -769,7 +886,7 @@ - \(POOLER-93\) Extend API endpoint to provide just what is needed [\#245](https://github.com/puppetlabs/vmpooler/pull/245) ([sbeaulie](https://github.com/sbeaulie)) - \(POOLER-92\) Add the alias information in the API status page for each… [\#244](https://github.com/puppetlabs/vmpooler/pull/244) ([sbeaulie](https://github.com/sbeaulie)) - \(QENG-5305\) Improve vmpooler host selection [\#242](https://github.com/puppetlabs/vmpooler/pull/242) ([mattkirby](https://github.com/mattkirby)) -- Allow user to specify a configuration file in VMPOOLER\_CONFIG\_FILE variable [\#241](https://github.com/puppetlabs/vmpooler/pull/241) ([adamdav](https://github.com/adamdav)) +- Allow user to specify a configuration file in VMPOOLER\_CONFIG\_FILE variable [\#241](https://github.com/puppetlabs/vmpooler/pull/241) ([amcdson](https://github.com/amcdson)) - Fix no implicit conversion to rational from nil [\#239](https://github.com/puppetlabs/vmpooler/pull/239) ([sbeaulie](https://github.com/sbeaulie)) - Updated Vagrant box and associated docs [\#237](https://github.com/puppetlabs/vmpooler/pull/237) ([genebean](https://github.com/genebean)) - \(GH-226\) Respond quickly to VMs being consumed [\#236](https://github.com/puppetlabs/vmpooler/pull/236) ([glennsarti](https://github.com/glennsarti)) @@ -803,88 +920,7 @@ - \(maint\) Add rubocop and allow failures in Travis CI [\#183](https://github.com/puppetlabs/vmpooler/pull/183) ([glennsarti](https://github.com/glennsarti)) - \(POOLER-73\) Update unit tests prior to refactoring [\#182](https://github.com/puppetlabs/vmpooler/pull/182) ([glennsarti](https://github.com/glennsarti)) - \(POOLER-71\) Add dummy authentication provider [\#180](https://github.com/puppetlabs/vmpooler/pull/180) ([glennsarti](https://github.com/glennsarti)) -- \(maint\) Remove Ruby 1.9.3 testing from Travis [\#178](https://github.com/puppetlabs/vmpooler/pull/178) ([glennsarti](https://github.com/glennsarti)) - \(maint\) Enhance VM Pooler developer experience [\#177](https://github.com/puppetlabs/vmpooler/pull/177) ([glennsarti](https://github.com/glennsarti)) -- \(POOLER-47\) Send clone errors up [\#175](https://github.com/puppetlabs/vmpooler/pull/175) ([mattkirby](https://github.com/mattkirby)) -- \(POOLER-48\) Clear migrations at application start time [\#174](https://github.com/puppetlabs/vmpooler/pull/174) ([mattkirby](https://github.com/mattkirby)) -- Add retry logic with a delay for vsphere connections [\#173](https://github.com/puppetlabs/vmpooler/pull/173) ([mattkirby](https://github.com/mattkirby)) -- \(POOLER-44\) Fix vmpooler.migrate reference [\#172](https://github.com/puppetlabs/vmpooler/pull/172) ([mattkirby](https://github.com/mattkirby)) -- Add `puma` as required gem [\#171](https://github.com/puppetlabs/vmpooler/pull/171) ([sschneid](https://github.com/sschneid)) -- Fix JavaScript error on nil `weekly_data` [\#170](https://github.com/puppetlabs/vmpooler/pull/170) ([sschneid](https://github.com/sschneid)) -- Containerize vmpooler [\#169](https://github.com/puppetlabs/vmpooler/pull/169) ([sschneid](https://github.com/sschneid)) -- Add vagrant-vmpooler plugin to readme [\#168](https://github.com/puppetlabs/vmpooler/pull/168) ([briancain](https://github.com/briancain)) -- Improve vmpooler scheduling logic [\#167](https://github.com/puppetlabs/vmpooler/pull/167) ([mattkirby](https://github.com/mattkirby)) -- \[QENG-4181\] Add per-pool stats to `/status` API [\#162](https://github.com/puppetlabs/vmpooler/pull/162) ([rick](https://github.com/rick)) -- Merge CI.next into Master [\#161](https://github.com/puppetlabs/vmpooler/pull/161) ([shermdog](https://github.com/shermdog)) -- \(maint\) update README.md and LICENSE to reflect rebranding [\#157](https://github.com/puppetlabs/vmpooler/pull/157) ([erosa](https://github.com/erosa)) -- Add info about vmfloaty [\#156](https://github.com/puppetlabs/vmpooler/pull/156) ([briancain](https://github.com/briancain)) -- Added IP lookup functionality for /vm/hostname [\#154](https://github.com/puppetlabs/vmpooler/pull/154) ([frozenfoxx](https://github.com/frozenfoxx)) -- Improved tests for vmpooler [\#152](https://github.com/puppetlabs/vmpooler/pull/152) ([rick](https://github.com/rick)) -- Added prefix parameter to the vmpooler configuration [\#149](https://github.com/puppetlabs/vmpooler/pull/149) ([frozenfoxx](https://github.com/frozenfoxx)) -- Update license copyright [\#148](https://github.com/puppetlabs/vmpooler/pull/148) ([sschneid](https://github.com/sschneid)) -- Allow new disks to be added to running VMs via vmpooler API [\#147](https://github.com/puppetlabs/vmpooler/pull/147) ([sschneid](https://github.com/sschneid)) -- Updated YAML config variables in create\_template\_deltas.rb [\#145](https://github.com/puppetlabs/vmpooler/pull/145) ([frozenfoxx](https://github.com/frozenfoxx)) -- \(QA-2036\) Update README for Client Utility [\#143](https://github.com/puppetlabs/vmpooler/pull/143) ([cowofevil](https://github.com/cowofevil)) -- add guestinfo.hostname to VirtualMachineConfigSpecs [\#139](https://github.com/puppetlabs/vmpooler/pull/139) ([heathseals](https://github.com/heathseals)) -- \(QENG-2807\) Allow pool 'alias' names [\#138](https://github.com/puppetlabs/vmpooler/pull/138) ([sschneid](https://github.com/sschneid)) -- \(QENG-2995\) Display associated VMs in GET /token/:token endpoint [\#137](https://github.com/puppetlabs/vmpooler/pull/137) ([sschneid](https://github.com/sschneid)) -- Update API docs to include "domain" key for get vm requests [\#136](https://github.com/puppetlabs/vmpooler/pull/136) ([briancain](https://github.com/briancain)) -- \(MAINT\) Remove Ping Check on Running VMs [\#133](https://github.com/puppetlabs/vmpooler/pull/133) ([colinPL](https://github.com/colinPL)) -- \(maint\) Move VM Only When SSH Check Succeeds [\#131](https://github.com/puppetlabs/vmpooler/pull/131) ([colinPL](https://github.com/colinPL)) -- \(QENG-2952\) Check that SSH is available [\#130](https://github.com/puppetlabs/vmpooler/pull/130) ([sschneid](https://github.com/sschneid)) -- \(maint\) Update license copyright [\#128](https://github.com/puppetlabs/vmpooler/pull/128) ([sschneid](https://github.com/sschneid)) -- \(maint\) Remove duplicate \(nested\) "ok" responses [\#127](https://github.com/puppetlabs/vmpooler/pull/127) ([sschneid](https://github.com/sschneid)) -- \(maint\) Documentation updates [\#126](https://github.com/puppetlabs/vmpooler/pull/126) ([sschneid](https://github.com/sschneid)) -- Track token use times [\#125](https://github.com/puppetlabs/vmpooler/pull/125) ([sschneid](https://github.com/sschneid)) -- Docs update [\#124](https://github.com/puppetlabs/vmpooler/pull/124) ([sschneid](https://github.com/sschneid)) -- User token list [\#123](https://github.com/puppetlabs/vmpooler/pull/123) ([sschneid](https://github.com/sschneid)) -- \(maint\) Additional utility and reporting scripts [\#122](https://github.com/puppetlabs/vmpooler/pull/122) ([sschneid](https://github.com/sschneid)) -- \(maint\) Syntax fixup [\#121](https://github.com/puppetlabs/vmpooler/pull/121) ([sschneid](https://github.com/sschneid)) -- \(MAINT\) Reduce redis Calls in API [\#120](https://github.com/puppetlabs/vmpooler/pull/120) ([colinPL](https://github.com/colinPL)) -- \(maint\) Use expect\_json helper method for determining JSON response status [\#119](https://github.com/puppetlabs/vmpooler/pull/119) ([sschneid](https://github.com/sschneid)) -- \(QENG-1304\) vmpooler should require an auth key for VM destruction [\#118](https://github.com/puppetlabs/vmpooler/pull/118) ([sschneid](https://github.com/sschneid)) -- \(QENG-2636\) Host snapshots [\#117](https://github.com/puppetlabs/vmpooler/pull/117) ([sschneid](https://github.com/sschneid)) -- \(maint\) Use dep caching and containers [\#116](https://github.com/puppetlabs/vmpooler/pull/116) ([sschneid](https://github.com/sschneid)) -- \(maint\) Include travis-ci build status in README [\#115](https://github.com/puppetlabs/vmpooler/pull/115) ([sschneid](https://github.com/sschneid)) -- Show test contexts and names [\#114](https://github.com/puppetlabs/vmpooler/pull/114) ([sschneid](https://github.com/sschneid)) -- \(QENG-2246\) Add Default Rake Task [\#113](https://github.com/puppetlabs/vmpooler/pull/113) ([colinPL](https://github.com/colinPL)) -- Log empty pools [\#112](https://github.com/puppetlabs/vmpooler/pull/112) ([sschneid](https://github.com/sschneid)) -- \(QENG-2246\) Add Travis CI [\#111](https://github.com/puppetlabs/vmpooler/pull/111) ([colinPL](https://github.com/colinPL)) -- \(QENG-2388\) Tagging restrictions [\#110](https://github.com/puppetlabs/vmpooler/pull/110) ([sschneid](https://github.com/sschneid)) -- An updated dashboard [\#109](https://github.com/puppetlabs/vmpooler/pull/109) ([sschneid](https://github.com/sschneid)) -- API summary rework [\#108](https://github.com/puppetlabs/vmpooler/pull/108) ([sschneid](https://github.com/sschneid)) -- Only filter regex matches [\#106](https://github.com/puppetlabs/vmpooler/pull/106) ([sschneid](https://github.com/sschneid)) -- \(QENG-2518\) Tag-filtering [\#105](https://github.com/puppetlabs/vmpooler/pull/105) ([sschneid](https://github.com/sschneid)) -- \(QENG-2360\) check\_running\_vm Spec Tests [\#104](https://github.com/puppetlabs/vmpooler/pull/104) ([colinPL](https://github.com/colinPL)) -- \(QENG-2056\) Create daily tag indexes, report in /summary [\#102](https://github.com/puppetlabs/vmpooler/pull/102) ([sschneid](https://github.com/sschneid)) -- Store token metadata in vmpooler\_\_vm\_\_ Redis hash [\#101](https://github.com/puppetlabs/vmpooler/pull/101) ([sschneid](https://github.com/sschneid)) -- Display VM state in GET /vm/:hostname route [\#100](https://github.com/puppetlabs/vmpooler/pull/100) ([sschneid](https://github.com/sschneid)) -- Add basic auth token functionality [\#98](https://github.com/puppetlabs/vmpooler/pull/98) ([sschneid](https://github.com/sschneid)) -- Add basic HTTP authentication and /token routes [\#97](https://github.com/puppetlabs/vmpooler/pull/97) ([sschneid](https://github.com/sschneid)) -- \(QENG-2208\) Add more helper tests [\#95](https://github.com/puppetlabs/vmpooler/pull/95) ([colinPL](https://github.com/colinPL)) -- \(QENG-2208\) Move Sinatra Helpers to own file [\#94](https://github.com/puppetlabs/vmpooler/pull/94) ([colinPL](https://github.com/colinPL)) -- Fix rspec tests broken in f9de28236b726e37977123cea9b4f3a562bfdcdb [\#93](https://github.com/puppetlabs/vmpooler/pull/93) ([sschneid](https://github.com/sschneid)) -- Redirect / to /dashboard [\#92](https://github.com/puppetlabs/vmpooler/pull/92) ([sschneid](https://github.com/sschneid)) -- Ensure 'lifetime' val returned by GET /vm/:hostname is an int [\#91](https://github.com/puppetlabs/vmpooler/pull/91) ([sschneid](https://github.com/sschneid)) -- running-to-lifetime comparison should be 'greater than or equal to' [\#90](https://github.com/puppetlabs/vmpooler/pull/90) ([sschneid](https://github.com/sschneid)) -- Auto-expire Redis metadata key via Redis EXPIRE [\#89](https://github.com/puppetlabs/vmpooler/pull/89) ([sschneid](https://github.com/sschneid)) -- \(QENG-1906\) Add specs for Dashboard and root API class [\#88](https://github.com/puppetlabs/vmpooler/pull/88) ([colinPL](https://github.com/colinPL)) -- \(maint\) Fix bad redis reference [\#87](https://github.com/puppetlabs/vmpooler/pull/87) ([colinPL](https://github.com/colinPL)) -- \(QENG-1906\) Break apart check\_pending\_vm and add spec tests [\#86](https://github.com/puppetlabs/vmpooler/pull/86) ([colinPL](https://github.com/colinPL)) -- Remove defined? when checking configuration for graphite server. [\#85](https://github.com/puppetlabs/vmpooler/pull/85) ([colinPL](https://github.com/colinPL)) -- \(QENG-1906\) Add spec tests for Janitor [\#78](https://github.com/puppetlabs/vmpooler/pull/78) ([colinPL](https://github.com/colinPL)) -- \(QENG-1906\) Refactor initialize to allow config passing [\#77](https://github.com/puppetlabs/vmpooler/pull/77) ([colinPL](https://github.com/colinPL)) -- Use 'checkout' time to calculate 'running' time [\#75](https://github.com/puppetlabs/vmpooler/pull/75) ([sschneid](https://github.com/sschneid)) -- Catch improperly-formatted data payloads [\#73](https://github.com/puppetlabs/vmpooler/pull/73) ([sschneid](https://github.com/sschneid)) -- \(QENG-1905\) Adding VM-tagging support via PUT /vm/:hostname endpoint [\#72](https://github.com/puppetlabs/vmpooler/pull/72) ([sschneid](https://github.com/sschneid)) -- \(QENG-2057\) Historic Redis VM metadata [\#71](https://github.com/puppetlabs/vmpooler/pull/71) ([sschneid](https://github.com/sschneid)) -- \(QENG-1899\) Add documentation for /summary [\#67](https://github.com/puppetlabs/vmpooler/pull/67) ([colinPL](https://github.com/colinPL)) -- Use $redis.hgetall rather than hget in a loop [\#66](https://github.com/puppetlabs/vmpooler/pull/66) ([sschneid](https://github.com/sschneid)) -- /summary per-pool metrics [\#65](https://github.com/puppetlabs/vmpooler/pull/65) ([sschneid](https://github.com/sschneid)) -- Show boot metrics in /status and /summary endpoints [\#64](https://github.com/puppetlabs/vmpooler/pull/64) ([sschneid](https://github.com/sschneid)) -- \(maint\) Fixing spacing [\#63](https://github.com/puppetlabs/vmpooler/pull/63) ([sschneid](https://github.com/sschneid)) -- Metric calc via helpers [\#62](https://github.com/puppetlabs/vmpooler/pull/62) ([sschneid](https://github.com/sschneid)) -- More granular metrics [\#61](https://github.com/puppetlabs/vmpooler/pull/61) ([sschneid](https://github.com/sschneid)) diff --git a/Gemfile b/Gemfile index 122d6b5..0313b80 100644 --- a/Gemfile +++ b/Gemfile @@ -3,11 +3,11 @@ source ENV['GEM_SOURCE'] || 'https://rubygems.org' gemspec # Evaluate Gemfile.local if it exists -if File.exists? "#{__FILE__}.local" +if File.exist? "#{__FILE__}.local" instance_eval(File.read("#{__FILE__}.local")) end # Evaluate ~/.gemfile if it exists -if File.exists?(File.join(Dir.home, '.gemfile')) +if File.exist?(File.join(Dir.home, '.gemfile')) instance_eval(File.read(File.join(Dir.home, '.gemfile'))) end diff --git a/Gemfile.lock b/Gemfile.lock index 59fe3b6..a63b584 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - vmpooler (3.3.0) + vmpooler (3.8.1) concurrent-ruby (~> 1.1) connection_pool (~> 2.4) deep_merge (~> 1.2) @@ -9,10 +9,11 @@ PATH opentelemetry-exporter-jaeger (= 0.23.0) opentelemetry-instrumentation-concurrent_ruby (= 0.21.1) opentelemetry-instrumentation-http_client (= 0.22.2) + opentelemetry-instrumentation-rack (= 0.23.4) opentelemetry-instrumentation-redis (= 0.25.3) opentelemetry-instrumentation-sinatra (= 0.23.2) - opentelemetry-resource_detectors (= 0.24.1) - opentelemetry-sdk (~> 1.3, >= 1.3.0) + opentelemetry-resource_detectors (= 0.24.2) + opentelemetry-sdk (~> 1.8) pickup (~> 0.0.11) prometheus-client (>= 2, < 5) puma (>= 5.0.4, < 7) @@ -26,36 +27,41 @@ PATH GEM remote: https://rubygems.org/ specs: - ast (2.4.2) - base64 (0.1.1) - bindata (2.4.15) - builder (3.2.4) + ast (2.4.3) + base64 (0.1.2) + bindata (2.5.1) + builder (3.3.0) climate_control (1.2.0) coderay (1.1.3) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) deep_merge (1.2.2) - diff-lcs (1.5.0) - docile (1.4.0) - faraday (2.7.10) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.15.5-java) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - json (2.6.3) - json (2.6.3-java) - language_server-protocol (3.17.0.3) - method_source (1.0.0) + diff-lcs (1.6.2) + docile (1.4.1) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + ffi (1.17.2-java) + google-cloud-env (2.2.1) + faraday (>= 1.0, < 3.a) + json (2.12.2) + json (2.12.2-java) + language_server-protocol (3.17.0.5) + logger (1.7.0) + method_source (1.1.0) mock_redis (0.37.0) - mustermann (3.0.0) + mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - net-ldap (0.18.0) - nio4r (2.5.9) - nio4r (2.5.9-java) - opentelemetry-api (1.2.1) - opentelemetry-common (0.20.0) + net-http (0.6.0) + uri + net-ldap (0.19.0) + nio4r (2.7.4) + nio4r (2.7.4-java) + opentelemetry-api (1.5.0) + opentelemetry-common (0.20.1) opentelemetry-api (~> 1.0) opentelemetry-exporter-jaeger (0.23.0) opentelemetry-api (~> 1.1) @@ -63,7 +69,7 @@ GEM opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions thrift - opentelemetry-instrumentation-base (0.22.2) + opentelemetry-instrumentation-base (0.22.3) opentelemetry-api (~> 1.0) opentelemetry-registry (~> 0.1) opentelemetry-instrumentation-concurrent_ruby (0.21.1) @@ -86,64 +92,67 @@ GEM opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-registry (0.3.0) + opentelemetry-registry (0.4.0) opentelemetry-api (~> 1.1) - opentelemetry-resource_detectors (0.24.1) + opentelemetry-resource_detectors (0.24.2) google-cloud-env opentelemetry-sdk (~> 1.0) - opentelemetry-sdk (1.3.0) + opentelemetry-sdk (1.8.0) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-registry (~> 0.2) opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.10.0) + opentelemetry-semantic_conventions (1.11.0) opentelemetry-api (~> 1.0) - parallel (1.23.0) - parser (3.2.2.3) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc pickup (0.0.11) - prometheus-client (4.2.1) - pry (0.14.2) + prism (1.4.0) + prometheus-client (4.2.4) + base64 + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.14.2-java) + pry (0.15.2-java) coderay (~> 1.1) method_source (~> 1.0) spoon (~> 0.0) - puma (6.3.0) + puma (6.6.0) nio4r (~> 2.0) - puma (6.3.0-java) + puma (6.6.0-java) nio4r (~> 2.0) - racc (1.7.1) - racc (1.7.1-java) - rack (2.2.8) - rack-protection (3.1.0) + racc (1.8.1) + racc (1.8.1-java) + rack (2.2.17) + rack-protection (3.2.0) + base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) rainbow (3.1.1) - rake (13.0.6) - redis (5.0.7) - redis-client (>= 0.9.0) - redis-client (0.15.0) + rake (13.3.0) + redis (5.4.0) + redis-client (>= 0.22.0) + redis-client (0.24.0) connection_pool - regexp_parser (2.8.1) - rexml (3.2.6) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + regexp_parser (2.10.0) + rexml (3.4.1) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.4) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.1) - rubocop (1.56.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + rubocop (1.56.4) base64 (~> 0.1.1) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -155,41 +164,48 @@ GEM rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - sinatra (3.1.0) + sinatra (3.2.0) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.1.0) + rack-protection (= 3.2.0) tilt (~> 2.0) spicy-proton (2.1.15) bindata (~> 2.3) spoon (0.0.6) ffi statsd-ruby (1.5.0) - thor (1.2.2) - thrift (0.18.1) - tilt (2.2.0) - unicode-display_width (2.4.2) + thor (1.3.2) + thrift (0.22.0) + tilt (2.6.0) + unicode-display_width (2.6.0) + uri (1.0.3) yarjuf (2.0.0) builder rspec (~> 3) PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + arm64-darwin-25 universal-java-11 + universal-java-17 + x86_64-darwin-22 x86_64-linux DEPENDENCIES climate_control (>= 0.2.0) - mock_redis (>= 0.17.0) + mock_redis (= 0.37.0) pry rack-test (>= 0.6) rspec (>= 3.2) diff --git a/docs/configuration.md b/docs/configuration.md index e577025..560c328 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -246,6 +246,18 @@ This can be a string providing a single DN. For multiple DNs please specify the The LDAP object-type used to designate a user object. (optional) +### LDAP\_SERVICE_ACCOUNT\_HASH + +A hash containing the following parameters for a service account to perform the +initial bind. After the initial bind, then a search query is performed using the +'base' and 'user_object', then re-binds as the returned user. + +- :user_dn: The full distinguished name (DN) of the service account used to bind. + +- :password: The password for the service account used to bind. + +(optional) + ### SITE\_NAME The name of your deployment. diff --git a/examples/vmpooler.yaml.dummy-example.aliasedpools b/examples/vmpooler.yaml.dummy-example.aliasedpools index ebece50..55bf9ff 100644 --- a/examples/vmpooler.yaml.dummy-example.aliasedpools +++ b/examples/vmpooler.yaml.dummy-example.aliasedpools @@ -17,6 +17,7 @@ logfile: '/Users/samuel/workspace/vmpooler/vmpooler.log' task_limit: 10 timeout: 15 + timeout_notification: 5 vm_checktime: 1 vm_lifetime: 12 vm_lifetime_auth: 24 @@ -38,6 +39,7 @@ datastore: 'vmstorage' size: 5 timeout: 15 + timeout_notification: 5 ready_ttl: 1440 provider: dummy dns_plugin: 'example' @@ -48,6 +50,7 @@ datastore: 'vmstorage' size: 5 timeout: 15 + timeout_notification: 5 ready_ttl: 1440 provider: dummy dns_plugin: 'example' @@ -58,6 +61,7 @@ datastore: 'vmstorage' size: 5 timeout: 15 + timeout_notification: 5 ready_ttl: 1440 provider: dummy dns_plugin: 'example' @@ -67,6 +71,7 @@ datastore: 'vmstorage' size: 5 timeout: 15 + timeout_notification: 5 ready_ttl: 1440 provider: dummy dns_plugin: 'example' @@ -77,6 +82,7 @@ datastore: 'other-vmstorage' size: 5 timeout: 15 + timeout_notification: 5 ready_ttl: 1440 provider: dummy dns_plugin: 'example' diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 985a72b..2fcde30 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -82,6 +82,7 @@ module Vmpooler end parsed_config[:config]['clone_target'] = ENV['CLONE_TARGET'] if ENV['CLONE_TARGET'] parsed_config[:config]['timeout'] = string_to_int(ENV['TIMEOUT']) if ENV['TIMEOUT'] + parsed_config[:config]['timeout_notification'] = string_to_int(ENV['TIMEOUT_NOTIFICATION']) if ENV['TIMEOUT_NOTIFICATION'] parsed_config[:config]['vm_lifetime_auth'] = string_to_int(ENV['VM_LIFETIME_AUTH']) if ENV['VM_LIFETIME_AUTH'] parsed_config[:config]['max_tries'] = string_to_int(ENV['MAX_TRIES']) if ENV['MAX_TRIES'] parsed_config[:config]['retry_factor'] = string_to_int(ENV['RETRY_FACTOR']) if ENV['RETRY_FACTOR'] diff --git a/lib/vmpooler/api/helpers.rb b/lib/vmpooler/api/helpers.rb index c6351a9..747640d 100644 --- a/lib/vmpooler/api/helpers.rb +++ b/lib/vmpooler/api/helpers.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true +require 'vmpooler/api/input_validator' + module Vmpooler class API module Helpers + include InputValidator def tracer @tracer ||= OpenTelemetry.tracer_provider.tracer('api', Vmpooler::VERSION) @@ -68,7 +71,7 @@ module Vmpooler end end - def authenticate_ldap(port, host, encryption_hash, user_object, base, username_str, password_str) + def authenticate_ldap(port, host, encryption_hash, user_object, base, username_str, password_str, service_account_hash = nil) tracer.in_span( "Vmpooler::API::Helpers.#{__method__}", attributes: { @@ -79,6 +82,14 @@ module Vmpooler }, kind: :client ) do + if service_account_hash + username = service_account_hash[:user_dn] + password = service_account_hash[:password] + else + username = "#{user_object}=#{username_str},#{base}" + password = password_str + end + ldap = Net::LDAP.new( :host => host, :port => port, @@ -86,12 +97,22 @@ module Vmpooler :base => base, :auth => { :method => :simple, - :username => "#{user_object}=#{username_str},#{base}", - :password => password_str + :username => username, + :password => password } ) - return true if ldap.bind + if service_account_hash + return true if ldap.bind_as( + :base => base, + :filter => "(#{user_object}=#{username_str})", + :password => password_str + ) + elsif ldap.bind + return true + else + return false + end return false end @@ -116,6 +137,7 @@ module Vmpooler :method => :start_tls, :tls_options => { :ssl_version => 'TLSv1' } } + service_account_hash = auth[:ldap]['service_account_hash'] unless ldap_base.is_a? Array ldap_base = ldap_base.split @@ -134,7 +156,8 @@ module Vmpooler search_user_obj, search_base, username_str, - password_str + password_str, + service_account_hash ) return true if result end @@ -269,6 +292,7 @@ module Vmpooler def get_queue_metrics(pools, backend) tracer.in_span("Vmpooler::API::Helpers.#{__method__}") do queue = { + requested: 0, pending: 0, cloning: 0, booting: 0, @@ -278,15 +302,35 @@ module Vmpooler total: 0 } - queue[:pending] = get_total_across_pools_redis_scard(pools, 'vmpooler__pending__', backend) - queue[:ready] = get_total_across_pools_redis_scard(pools, 'vmpooler__ready__', backend) - queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend) - queue[:completed] = get_total_across_pools_redis_scard(pools, 'vmpooler__completed__', backend) + # Use a single pipeline to fetch all queue counts at once for better performance + results = backend.pipelined do |pipeline| + # Order matters - we'll use indices to extract values + pools.each do |pool| + pipeline.scard("vmpooler__provisioning__request#{pool['name']}") # 0..n-1 + pipeline.scard("vmpooler__provisioning__processing#{pool['name']}") # n..2n-1 + pipeline.scard("vmpooler__odcreate__task#{pool['name']}") # 2n..3n-1 + pipeline.scard("vmpooler__pending__#{pool['name']}") # 3n..4n-1 + pipeline.scard("vmpooler__ready__#{pool['name']}") # 4n..5n-1 + pipeline.scard("vmpooler__running__#{pool['name']}") # 5n..6n-1 + pipeline.scard("vmpooler__completed__#{pool['name']}") # 6n..7n-1 + end + pipeline.get('vmpooler__tasks__clone') # 7n + pipeline.get('vmpooler__tasks__ondemandclone') # 7n+1 + end - queue[:cloning] = backend.get('vmpooler__tasks__clone').to_i + backend.get('vmpooler__tasks__ondemandclone').to_i - queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i - queue[:booting] = 0 if queue[:booting] < 0 - queue[:total] = queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i + n = pools.length + # Safely extract results with default to empty array if slice returns nil + queue[:requested] = (results[0...n] || []).sum(&:to_i) + + (results[n...(2 * n)] || []).sum(&:to_i) + + (results[(2 * n)...(3 * n)] || []).sum(&:to_i) + queue[:pending] = (results[(3 * n)...(4 * n)] || []).sum(&:to_i) + queue[:ready] = (results[(4 * n)...(5 * n)] || []).sum(&:to_i) + queue[:running] = (results[(5 * n)...(6 * n)] || []).sum(&:to_i) + queue[:completed] = (results[(6 * n)...(7 * n)] || []).sum(&:to_i) + queue[:cloning] = (results[7 * n] || 0).to_i + (results[7 * n + 1] || 0).to_i + queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i + queue[:booting] = 0 if queue[:booting] < 0 + queue[:total] = queue[:requested] + queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i queue end @@ -551,18 +595,6 @@ module Vmpooler end end end - - def vm_ready?(vm_name, domain = nil) - tracer.in_span("Vmpooler::API::Helpers.#{__method__}") do - begin - open_socket(vm_name, domain) - rescue StandardError => _e - return false - end - - true - end - end end end end diff --git a/lib/vmpooler/api/input_validator.rb b/lib/vmpooler/api/input_validator.rb new file mode 100644 index 0000000..add4d6a --- /dev/null +++ b/lib/vmpooler/api/input_validator.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +module Vmpooler + class API + # Input validation helpers to enhance security + module InputValidator + # Maximum lengths to prevent abuse + MAX_HOSTNAME_LENGTH = 253 + MAX_TAG_KEY_LENGTH = 50 + MAX_TAG_VALUE_LENGTH = 255 + MAX_REASON_LENGTH = 500 + MAX_POOL_NAME_LENGTH = 100 + MAX_TOKEN_LENGTH = 64 + + # Valid patterns + HOSTNAME_PATTERN = /\A[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)* \z/ix.freeze + POOL_NAME_PATTERN = /\A[a-zA-Z0-9_-]+\z/.freeze + TAG_KEY_PATTERN = /\A[a-zA-Z0-9_\-.]+\z/.freeze + TOKEN_PATTERN = /\A[a-zA-Z0-9\-_]+\z/.freeze + INTEGER_PATTERN = /\A\d+\z/.freeze + + class ValidationError < StandardError; end + + # Validate hostname format and length + def validate_hostname(hostname) + return error_response('Hostname is required') if hostname.nil? || hostname.empty? + return error_response('Hostname too long') if hostname.length > MAX_HOSTNAME_LENGTH + return error_response('Invalid hostname format') unless hostname.match?(HOSTNAME_PATTERN) + + true + end + + # Validate pool/template name + def validate_pool_name(pool_name) + return error_response('Pool name is required') if pool_name.nil? || pool_name.empty? + return error_response('Pool name too long') if pool_name.length > MAX_POOL_NAME_LENGTH + return error_response('Invalid pool name format') unless pool_name.match?(POOL_NAME_PATTERN) + + true + end + + # Validate tag key and value + def validate_tag(key, value) + return error_response('Tag key is required') if key.nil? || key.empty? + return error_response('Tag key too long') if key.length > MAX_TAG_KEY_LENGTH + return error_response('Invalid tag key format') unless key.match?(TAG_KEY_PATTERN) + + if value + return error_response('Tag value too long') if value.length > MAX_TAG_VALUE_LENGTH + + # Sanitize value to prevent injection attacks + sanitized_value = value.gsub(/[^\w\s\-.@:\/]/, '') + return error_response('Tag value contains invalid characters') if sanitized_value != value + end + + true + end + + # Validate token format + def validate_token_format(token) + return error_response('Token is required') if token.nil? || token.empty? + return error_response('Token too long') if token.length > MAX_TOKEN_LENGTH + return error_response('Invalid token format') unless token.match?(TOKEN_PATTERN) + + true + end + + # Validate integer parameter + def validate_integer(value, name = 'value', min: nil, max: nil) + return error_response("#{name} is required") if value.nil? + + value_str = value.to_s + return error_response("#{name} must be a valid integer") unless value_str.match?(INTEGER_PATTERN) + + int_value = value.to_i + return error_response("#{name} must be at least #{min}") if min && int_value < min + return error_response("#{name} must be at most #{max}") if max && int_value > max + + int_value + end + + # Validate VM request count + def validate_vm_count(count) + validated = validate_integer(count, 'VM count', min: 1, max: 100) + return validated if validated.is_a?(Hash) # error response + + validated + end + + # Validate disk size + def validate_disk_size(size) + validated = validate_integer(size, 'Disk size', min: 1, max: 2048) + return validated if validated.is_a?(Hash) # error response + + validated + end + + # Validate lifetime (TTL) in hours + def validate_lifetime(lifetime) + validated = validate_integer(lifetime, 'Lifetime', min: 1, max: 168) # max 1 week + return validated if validated.is_a?(Hash) # error response + + validated + end + + # Validate reason text + def validate_reason(reason) + return true if reason.nil? || reason.empty? + return error_response('Reason too long') if reason.length > MAX_REASON_LENGTH + + # Sanitize to prevent XSS/injection + sanitized = reason.gsub(/[<>"']/, '') + return error_response('Reason contains invalid characters') if sanitized != reason + + true + end + + # Sanitize JSON body to prevent injection + def sanitize_json_body(body) + return {} if body.nil? || body.empty? + + begin + parsed = JSON.parse(body) + return error_response('Request body must be a JSON object') unless parsed.is_a?(Hash) + + # Limit depth and size to prevent DoS + return error_response('Request body too complex') if json_depth(parsed) > 5 + return error_response('Request body too large') if body.length > 10_240 # 10KB max + + parsed + rescue JSON::ParserError => e + error_response("Invalid JSON: #{e.message}") + end + end + + # Check if validation result is an error + def validation_error?(result) + result.is_a?(Hash) && result['ok'] == false + end + + private + + def error_response(message) + { 'ok' => false, 'error' => message } + end + + def json_depth(obj, depth = 0) + return depth unless obj.is_a?(Hash) || obj.is_a?(Array) + return depth + 1 if obj.empty? + + if obj.is_a?(Hash) + depth + 1 + obj.values.map { |v| json_depth(v, 0) }.max + else + depth + 1 + obj.map { |v| json_depth(v, 0) }.max + end + end + end + end +end diff --git a/lib/vmpooler/api/rate_limiter.rb b/lib/vmpooler/api/rate_limiter.rb new file mode 100644 index 0000000..8ecfb62 --- /dev/null +++ b/lib/vmpooler/api/rate_limiter.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Vmpooler + class API + # Rate limiter middleware to protect against abuse + # Uses Redis to track request counts per IP and token + class RateLimiter + DEFAULT_LIMITS = { + global_per_ip: { limit: 100, period: 60 }, # 100 requests per minute per IP + authenticated: { limit: 500, period: 60 }, # 500 requests per minute with token + vm_creation: { limit: 20, period: 60 }, # 20 VM creations per minute + vm_deletion: { limit: 50, period: 60 } # 50 VM deletions per minute + }.freeze + + def initialize(app, redis, config = {}) + @app = app + @redis = redis + @config = DEFAULT_LIMITS.merge(config[:rate_limits] || {}) + @enabled = config.fetch(:rate_limiting_enabled, true) + end + + def call(env) + return @app.call(env) unless @enabled + + request = Rack::Request.new(env) + client_id = identify_client(request) + endpoint_type = classify_endpoint(request) + + # Check rate limits + return rate_limit_response(client_id, endpoint_type) if rate_limit_exceeded?(client_id, endpoint_type, request) + + # Track the request + increment_request_count(client_id, endpoint_type) + + @app.call(env) + end + + private + + def identify_client(request) + # Prioritize token-based identification for authenticated requests + token = request.env['HTTP_X_AUTH_TOKEN'] + return "token:#{token}" if token && !token.empty? + + # Fall back to IP address + ip = request.ip || request.env['REMOTE_ADDR'] || 'unknown' + "ip:#{ip}" + end + + def classify_endpoint(request) + path = request.path + method = request.request_method + + return :vm_creation if method == 'POST' && path.include?('/vm') + return :vm_deletion if method == 'DELETE' && path.include?('/vm') + return :authenticated if request.env['HTTP_X_AUTH_TOKEN'] + + :global_per_ip + end + + def rate_limit_exceeded?(client_id, endpoint_type, _request) + limit_config = @config[endpoint_type] || @config[:global_per_ip] + key = "vmpooler__ratelimit__#{endpoint_type}__#{client_id}" + + current_count = @redis.get(key).to_i + current_count >= limit_config[:limit] + rescue StandardError => e + # If Redis fails, allow the request through (fail open) + warn "Rate limiter Redis error: #{e.message}" + false + end + + def increment_request_count(client_id, endpoint_type) + limit_config = @config[endpoint_type] || @config[:global_per_ip] + key = "vmpooler__ratelimit__#{endpoint_type}__#{client_id}" + + @redis.pipelined do |pipeline| + pipeline.incr(key) + pipeline.expire(key, limit_config[:period]) + end + rescue StandardError => e + # Log error but don't fail the request + warn "Rate limiter increment error: #{e.message}" + end + + def rate_limit_response(client_id, endpoint_type) + limit_config = @config[endpoint_type] || @config[:global_per_ip] + key = "vmpooler__ratelimit__#{endpoint_type}__#{client_id}" + + begin + ttl = @redis.ttl(key) + rescue StandardError + ttl = limit_config[:period] + end + + headers = { + 'Content-Type' => 'application/json', + 'X-RateLimit-Limit' => limit_config[:limit].to_s, + 'X-RateLimit-Remaining' => '0', + 'X-RateLimit-Reset' => (Time.now.to_i + ttl).to_s, + 'Retry-After' => ttl.to_s + } + + body = JSON.pretty_generate({ + 'ok' => false, + 'error' => 'Rate limit exceeded', + 'limit' => limit_config[:limit], + 'period' => limit_config[:period], + 'retry_after' => ttl + }) + + [429, headers, [body]] + end + end + end +end diff --git a/lib/vmpooler/api/v3.rb b/lib/vmpooler/api/v3.rb index 41c6480..21bc4e3 100644 --- a/lib/vmpooler/api/v3.rb +++ b/lib/vmpooler/api/v3.rb @@ -9,6 +9,20 @@ module Vmpooler api_version = '3' api_prefix = "/api/v#{api_version}" + # Simple in-memory cache for status endpoint + # rubocop:disable Style/ClassVars + @@status_cache = {} + @@status_cache_mutex = Mutex.new + # rubocop:enable Style/ClassVars + STATUS_CACHE_TTL = 30 # seconds + + # Clear cache (useful for testing) + def self.clear_status_cache + @@status_cache_mutex.synchronize do + @@status_cache.clear + end + end + helpers do include Vmpooler::API::Helpers end @@ -283,11 +297,9 @@ module Vmpooler def update_user_metrics(operation, vmname) tracer.in_span("Vmpooler::API::V3.#{__method__}") do |span| begin - backend.multi - backend.hget("vmpooler__vm__#{vmname}", 'tag:jenkins_build_url') - backend.hget("vmpooler__vm__#{vmname}", 'token:user') - backend.hget("vmpooler__vm__#{vmname}", 'template') - jenkins_build_url, user, poolname = backend.exec + jenkins_build_url = backend.hget("vmpooler__vm__#{vmname}", 'tag:jenkins_build_url') + user = backend.hget("vmpooler__vm__#{vmname}", 'token:user') + poolname = backend.hget("vmpooler__vm__#{vmname}", 'template') poolname = poolname.gsub('.', '_') if user @@ -466,6 +478,32 @@ module Vmpooler end end + # Cache helper methods for status endpoint + def get_cached_status(cache_key) + @@status_cache_mutex.synchronize do + cached = @@status_cache[cache_key] + if cached && (Time.now - cached[:timestamp]) < STATUS_CACHE_TTL + return cached[:data] + end + + nil + end + end + + def set_cached_status(cache_key, data) + @@status_cache_mutex.synchronize do + @@status_cache[cache_key] = { + data: data, + timestamp: Time.now + } + # Cleanup old cache entries (keep only last 10 unique view combinations) + if @@status_cache.size > 10 + oldest = @@status_cache.min_by { |_k, v| v[:timestamp] } + @@status_cache.delete(oldest[0]) + end + end + end + def sync_pool_templates tracer.in_span("Vmpooler::API::V3.#{__method__}") do pool_index = pool_index(pools) @@ -648,6 +686,13 @@ module Vmpooler get "#{api_prefix}/status/?" do content_type :json + # Create cache key based on view parameters + cache_key = params[:view] ? "status_#{params[:view]}" : "status_all" + + # Try to get cached response + cached_response = get_cached_status(cache_key) + return cached_response if cached_response + if params[:view] views = params[:view].split(",") end @@ -708,7 +753,12 @@ module Vmpooler result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime] - JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }]) + response = JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }]) + + # Cache the response + set_cached_status(cache_key, response) + + response end # request statistics for specific pools by passing parameter 'pool' @@ -1087,9 +1137,29 @@ module Vmpooler result = { 'ok' => false } metrics.increment('http_requests_vm_total.post.vm.checkout') - payload = JSON.parse(request.body.read) + # Validate and sanitize JSON body + payload = sanitize_json_body(request.body.read) + if validation_error?(payload) + status 400 + return JSON.pretty_generate(payload) + end - if payload + # Validate each template and count + payload.each do |template, count| + validation = validate_pool_name(template) + if validation_error?(validation) + status 400 + return JSON.pretty_generate(validation) + end + + validated_count = validate_vm_count(count) + if validation_error?(validated_count) + status 400 + return JSON.pretty_generate(validated_count) + end + end + + if payload && !payload.empty? invalid = invalid_templates(payload) if invalid.empty? result = atomically_allocate_vms(payload) @@ -1208,6 +1278,7 @@ module Vmpooler result = { 'ok' => false } metrics.increment('http_requests_vm_total.get.vm.template') + # Template can contain multiple pools separated by +, so validate after parsing payload = extract_templates_from_query_params(params[:template]) if payload @@ -1237,6 +1308,13 @@ module Vmpooler status 404 result['ok'] = false + # Validate hostname + validation = validate_hostname(params[:hostname]) + if validation_error?(validation) + status 400 + return JSON.pretty_generate(validation) + end + params[:hostname] = hostname_shorten(params[:hostname]) rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}") @@ -1375,6 +1453,13 @@ module Vmpooler status 404 result['ok'] = false + # Validate hostname + validation = validate_hostname(params[:hostname]) + if validation_error?(validation) + status 400 + return JSON.pretty_generate(validation) + end + params[:hostname] = hostname_shorten(params[:hostname]) rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}") @@ -1405,16 +1490,21 @@ module Vmpooler failure = [] + # Validate hostname + validation = validate_hostname(params[:hostname]) + if validation_error?(validation) + status 400 + return JSON.pretty_generate(validation) + end + params[:hostname] = hostname_shorten(params[:hostname]) if backend.exists?("vmpooler__vm__#{params[:hostname]}") - begin - jdata = JSON.parse(request.body.read) - rescue StandardError => e - span = OpenTelemetry::Trace.current_span - span.record_exception(e) - span.status = OpenTelemetry::Trace::Status.error(e.to_s) - halt 400, JSON.pretty_generate(result) + # Validate and sanitize JSON body + jdata = sanitize_json_body(request.body.read) + if validation_error?(jdata) + status 400 + return JSON.pretty_generate(jdata) end # Validate data payload @@ -1423,6 +1513,13 @@ module Vmpooler when 'lifetime' need_token! if Vmpooler::API.settings.config[:auth] + # Validate lifetime is a positive integer + lifetime_int = arg.to_i + if lifetime_int <= 0 + failure.push("Lifetime must be a positive integer (got #{arg})") + next + end + # in hours, defaults to one week max_lifetime_upper_limit = config['max_lifetime_upper_limit'] if max_lifetime_upper_limit @@ -1432,13 +1529,17 @@ module Vmpooler end end - # validate lifetime is within boundaries - unless arg.to_i > 0 - failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.") - end - when 'tags' failure.push("You provided tags (#{arg}) as something other than a hash.") unless arg.is_a?(Hash) + + # Validate each tag key and value + arg.each do |key, value| + tag_validation = validate_tag(key, value) + if validation_error?(tag_validation) + failure.push(tag_validation['error']) + end + end + failure.push("You provided unsuppored tags (#{arg}).") if config['allowed_tags'] && !(arg.keys - config['allowed_tags']).empty? else failure.push("Unknown argument #{arg}.") @@ -1480,9 +1581,23 @@ module Vmpooler status 404 result = { 'ok' => false } + # Validate hostname + validation = validate_hostname(params[:hostname]) + if validation_error?(validation) + status 400 + return JSON.pretty_generate(validation) + end + + # Validate disk size + validated_size = validate_disk_size(params[:size]) + if validation_error?(validated_size) + status 400 + return JSON.pretty_generate(validated_size) + end + params[:hostname] = hostname_shorten(params[:hostname]) - if ((params[:size].to_i > 0 )and (backend.exists?("vmpooler__vm__#{params[:hostname]}"))) + if backend.exists?("vmpooler__vm__#{params[:hostname]}") result[params[:hostname]] = {} result[params[:hostname]]['disk'] = "+#{params[:size]}gb" diff --git a/lib/vmpooler/metrics/promstats.rb b/lib/vmpooler/metrics/promstats.rb index f24f9b9..d0e1ab9 100644 --- a/lib/vmpooler/metrics/promstats.rb +++ b/lib/vmpooler/metrics/promstats.rb @@ -329,6 +329,30 @@ module Vmpooler buckets: REDIS_CONNECT_BUCKETS, docstring: 'vmpooler redis connection wait time', param_labels: %i[type provider] + }, + vmpooler_health: { + mtype: M_GAUGE, + torun: %i[manager], + docstring: 'vmpooler health check metrics', + param_labels: %i[metric_path] + }, + vmpooler_purge: { + mtype: M_GAUGE, + torun: %i[manager], + docstring: 'vmpooler purge metrics', + param_labels: %i[metric_path] + }, + vmpooler_destroy: { + mtype: M_GAUGE, + torun: %i[manager], + docstring: 'vmpooler destroy metrics', + param_labels: %i[poolname] + }, + vmpooler_clone: { + mtype: M_GAUGE, + torun: %i[manager], + docstring: 'vmpooler clone metrics', + param_labels: %i[poolname] } } end diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 3bc020b..9c6def6 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -82,31 +82,31 @@ module Vmpooler end # Check the state of a VM - def check_pending_vm(vm, pool, timeout, provider) + def check_pending_vm(vm, pool, timeout, timeout_notification, provider) Thread.new do begin - _check_pending_vm(vm, pool, timeout, provider) + _check_pending_vm(vm, pool, timeout, timeout_notification, provider) rescue StandardError => e $logger.log('s', "[!] [#{pool}] '#{vm}' #{timeout} #{provider} errored while checking a pending vm : #{e}") @redis.with_metrics do |redis| - fail_pending_vm(vm, pool, timeout, redis) + fail_pending_vm(vm, pool, timeout, timeout_notification, redis) end raise end end end - def _check_pending_vm(vm, pool, timeout, provider) + def _check_pending_vm(vm, pool, timeout, timeout_notification, provider) mutex = vm_mutex(vm) return if mutex.locked? mutex.synchronize do @redis.with_metrics do |redis| request_id = redis.hget("vmpooler__vm__#{vm}", 'request_id') - if provider.vm_ready?(pool, vm) + if provider.vm_ready?(pool, vm, redis) move_pending_vm_to_ready(vm, pool, redis, request_id) else - fail_pending_vm(vm, pool, timeout, redis) + fail_pending_vm(vm, pool, timeout, timeout_notification, redis) end end end @@ -122,34 +122,121 @@ module Vmpooler $logger.log('d', "[!] [#{pool}] '#{vm}' no longer exists. Removing from pending.") end - def fail_pending_vm(vm, pool, timeout, redis, exists: true) + def fail_pending_vm(vm, pool, timeout, timeout_notification, redis, exists: true) clone_stamp = redis.hget("vmpooler__vm__#{vm}", 'clone') - time_since_clone = (Time.now - Time.parse(clone_stamp)) / 60 - if time_since_clone > timeout - if exists - request_id = redis.hget("vmpooler__vm__#{vm}", 'request_id') - pool_alias = redis.hget("vmpooler__vm__#{vm}", 'pool_alias') if request_id - redis.smove("vmpooler__pending__#{pool}", "vmpooler__completed__#{pool}", vm) - if request_id - ondemandrequest_hash = redis.hgetall("vmpooler__odrequest__#{request_id}") - if ondemandrequest_hash && ondemandrequest_hash['status'] != 'failed' && ondemandrequest_hash['status'] != 'deleted' - # will retry a VM that did not come up as vm_ready? only if it has not been market failed or deleted - redis.zadd('vmpooler__odcreate__task', 1, "#{pool_alias}:#{pool}:1:#{request_id}") - end - end - $metrics.increment("errors.markedasfailed.#{pool}") - $logger.log('d', "[!] [#{pool}] '#{vm}' marked as 'failed' after #{timeout} minutes") - else + + already_timed_out = time_since_clone > timeout + timing_out_soon = time_since_clone > timeout_notification && !redis.hget("vmpooler__vm__#{vm}", 'timeout_notification') + + return true if !already_timed_out && !timing_out_soon + + if already_timed_out + unless exists remove_nonexistent_vm(vm, pool, redis) + return true end + open_socket_error = handle_timed_out_vm(vm, pool, redis) end + + redis.hset("vmpooler__vm__#{vm}", 'timeout_notification', 1) if timing_out_soon + + nonexist_warning = if already_timed_out + "[!] [#{pool}] '#{vm}' marked as 'failed' after #{timeout} minutes with error: #{open_socket_error}" + elsif timing_out_soon + time_remaining = timeout - timeout_notification + open_socket_error = redis.hget("vmpooler__vm__#{vm}", 'open_socket_error') + "[!] [#{pool}] '#{vm}' impending failure in #{time_remaining} minutes with error: #{open_socket_error}" + else + "[!] [#{pool}] '#{vm}' This error is wholly unexpected" + end + $logger.log('d', nonexist_warning) true rescue StandardError => e $logger.log('d', "Fail pending VM failed with an error: #{e}") false end + def handle_timed_out_vm(vm, pool, redis) + request_id = redis.hget("vmpooler__vm__#{vm}", 'request_id') + pool_alias = redis.hget("vmpooler__vm__#{vm}", 'pool_alias') if request_id + open_socket_error = redis.hget("vmpooler__vm__#{vm}", 'open_socket_error') + retry_count = redis.hget("vmpooler__odrequest__#{request_id}", 'retry_count').to_i if request_id + + # Move to DLQ before moving to completed queue + move_to_dlq(vm, pool, 'pending', 'Timeout', + open_socket_error || 'VM timed out during pending phase', + redis, request_id: request_id, pool_alias: pool_alias, retry_count: retry_count) + + clone_error = redis.hget("vmpooler__vm__#{vm}", 'clone_error') + clone_error_class = redis.hget("vmpooler__vm__#{vm}", 'clone_error_class') + redis.smove("vmpooler__pending__#{pool}", "vmpooler__completed__#{pool}", vm) + + if request_id + ondemandrequest_hash = redis.hgetall("vmpooler__odrequest__#{request_id}") + if ondemandrequest_hash && ondemandrequest_hash['status'] != 'failed' && ondemandrequest_hash['status'] != 'deleted' + # Check retry count and max retry limit before retrying + retry_count = (redis.hget("vmpooler__odrequest__#{request_id}", 'retry_count') || '0').to_i + max_retries = $config[:config]['max_vm_retries'] || 3 + + $logger.log('s', "[!] [#{pool}] '#{vm}' checking retry logic: error='#{clone_error}', error_class='#{clone_error_class}', retry_count=#{retry_count}, max_retries=#{max_retries}") + + # Determine if error is likely permanent (configuration issues) + permanent_error = permanent_error?(clone_error, clone_error_class) + $logger.log('s', "[!] [#{pool}] '#{vm}' permanent_error check result: #{permanent_error}") + + if retry_count < max_retries && !permanent_error + # Increment retry count and retry VM creation + redis.hset("vmpooler__odrequest__#{request_id}", 'retry_count', retry_count + 1) + redis.zadd('vmpooler__odcreate__task', 1, "#{pool_alias}:#{pool}:1:#{request_id}") + $logger.log('s', "[!] [#{pool}] '#{vm}' failed, retrying (attempt #{retry_count + 1}/#{max_retries})") + else + # Max retries exceeded or permanent error, mark request as permanently failed + failure_reason = if permanent_error + "Configuration error: #{clone_error}" + else + 'Max retry attempts exceeded' + end + redis.hset("vmpooler__odrequest__#{request_id}", 'status', 'failed') + redis.hset("vmpooler__odrequest__#{request_id}", 'failure_reason', failure_reason) + $logger.log('s', "[!] [#{pool}] '#{vm}' permanently failed: #{failure_reason}") + $metrics.increment("vmpooler_errors.permanently_failed.#{pool}") + end + end + end + $metrics.increment("vmpooler_errors.markedasfailed.#{pool}") + open_socket_error || clone_error + end + + # Determine if an error is likely permanent (configuration issue) vs transient + def permanent_error?(error_message, error_class) + return false if error_message.nil? || error_class.nil? + + permanent_error_patterns = [ + /template.*not found/i, + /template.*does not exist/i, + /invalid.*path/i, + /folder.*not found/i, + /datastore.*not found/i, + /resource pool.*not found/i, + /permission.*denied/i, + /authentication.*failed/i, + /invalid.*credentials/i, + /configuration.*error/i + ] + + permanent_error_classes = [ + 'ArgumentError', + 'NoMethodError', + 'NameError' + ] + + # Check error message patterns + permanent_error_patterns.any? { |pattern| error_message.match?(pattern) } || + # Check error class types + permanent_error_classes.include?(error_class) + end + def move_pending_vm_to_ready(vm, pool, redis, request_id = nil) clone_time = redis.hget("vmpooler__vm__#{vm}", 'clone') finish = format('%