├── .expeditor ├── config.yml ├── run_linux_tests.sh ├── update_version.sh └── verify.pipeline.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG_TEMPLATE.md │ ├── DESIGN_PROPOSAL.md │ ├── ENHANCEMENT_REQUEST_TEMPLATE.md │ └── SUPPORT_QUESTION.md ├── dependabot.yml └── workflows │ └── linters.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── kitchen-vcenter.gemspec ├── lib ├── kitchen-vcenter │ └── version.rb ├── kitchen │ └── driver │ │ └── vcenter.rb └── support │ ├── clone_vm.rb │ ├── guest_customization.rb │ └── guest_operations.rb └── spec ├── spec_helper.rb └── unit ├── kitchen └── driver │ └── vcenter_spec.rb └── support └── clone_vm_spec.rb /.expeditor/config.yml: -------------------------------------------------------------------------------- 1 | # Documentation available at https://expeditor.chef.io/docs/getting-started/ 2 | --- 3 | 4 | # Slack channel in Chef Software slack to send notifications about build failures, etc 5 | slack: 6 | notify_channel: chef-notify 7 | 8 | # This publish is triggered by the `built_in:publish_rubygems` artifact_action. 9 | rubygems: 10 | - kitchen-vcenter 11 | 12 | github: 13 | # This deletes the GitHub PR branch after successfully merged into the release branch 14 | delete_branch_on_merge: true 15 | # The tag format to use (e.g. v1.0.0) 16 | version_tag_format: "v{{version}}" 17 | # allow bumping the minor release via label 18 | minor_bump_labels: 19 | - "Expeditor: Bump Version Minor" 20 | # allow bumping the major release via label 21 | major_bump_labels: 22 | - "Expeditor: Bump Version Major" 23 | 24 | changelog: 25 | rollup_header: Changes not yet released to rubygems.org 26 | 27 | subscriptions: 28 | # These actions are taken, in order they are specified, anytime a Pull Request is merged. 29 | - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:* 30 | actions: 31 | - built_in:bump_version: 32 | ignore_labels: 33 | - "Expeditor: Skip Version Bump" 34 | - "Expeditor: Skip All" 35 | - bash:.expeditor/update_version.sh: 36 | only_if: built_in:bump_version 37 | - built_in:update_changelog: 38 | ignore_labels: 39 | - "Expeditor: Skip Changelog" 40 | - "Expeditor: Skip All" 41 | - built_in:build_gem: 42 | only_if: built_in:bump_version 43 | 44 | - workload: project_promoted:{{agent_id}}:* 45 | actions: 46 | - built_in:rollover_changelog 47 | - built_in:publish_rubygems 48 | 49 | pipelines: 50 | - verify: 51 | description: Pull Request validation tests 52 | public: true 53 | -------------------------------------------------------------------------------- /.expeditor/run_linux_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script runs a passed in command, but first setups up the bundler caching on the repo 4 | 5 | set -ue 6 | 7 | export USER="root" 8 | export LANG=C.UTF-8 LANGUAGE=C.UTF-8 9 | 10 | echo "--- bundle install" 11 | 12 | bundle config --local path vendor/bundle 13 | bundle install --jobs=7 --retry=3 14 | 15 | echo "+++ bundle exec task" 16 | bundle exec $@ 17 | -------------------------------------------------------------------------------- /.expeditor/update_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # After a PR merge, Chef Expeditor will bump the PATCH version in the VERSION file. 4 | # It then executes this file to update any other files/components with that new version. 5 | # 6 | 7 | set -evx 8 | 9 | sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"$(cat VERSION)\"/" lib/kitchen-vcenter/version.rb 10 | 11 | # Once Expeditor finshes executing this script, it will commit the changes and push 12 | # the commit as a new tag corresponding to the value in the VERSION file. 13 | -------------------------------------------------------------------------------- /.expeditor/verify.pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | expeditor: 3 | cached_folders: 4 | - vendor 5 | defaults: 6 | buildkite: 7 | retry: 8 | automatic: 9 | limit: 1 10 | timeout_in_minutes: 30 11 | 12 | steps: 13 | - label: run-lint-and-specs-ruby-3.1 14 | command: 15 | - .expeditor/run_linux_tests.sh rake 16 | expeditor: 17 | executor: 18 | docker: 19 | image: ruby:3.1 20 | 21 | - label: run-lint-and-specs-ruby-3.2 22 | command: 23 | - .expeditor/run_linux_tests.sh rake 24 | expeditor: 25 | executor: 26 | docker: 27 | image: ruby:3.2 28 | 29 | - label: run-lint-and-specs-ruby-3.3 30 | command: 31 | - .expeditor/run_linux_tests.sh rake 32 | expeditor: 33 | executor: 34 | docker: 35 | image: ruby:3.3 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Order is important. The last matching pattern has the most precedence. 2 | 3 | * @chef/chef-workstation-reviewers @chef/chef-workstation-approvers @chef/chef-workstation-owners 4 | .expeditor/ @chef/jex-team 5 | *.md @chef/docs-team -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: � Bug Report 3 | about: If something isn't working as expected �. 4 | labels: "Status: Untriaged, Type: Bug" 5 | --- 6 | 7 | ### Versions: 8 | 9 | 10 | * Version of kitchen-vcenter: 11 | * Version of test-kitchen: 12 | * Version of chef: 13 | 14 | ### Platform Details 15 | 16 | * Version of vCenter: 17 | * Version of ESXi: 18 | 19 | ### Scenario: 20 | 21 | 22 | ### Steps to Reproduce: 23 | 24 | 25 | ### Expected Result: 26 | 27 | 28 | ### Actual Result: 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Design Proposal 3 | about: I have a significant change I would like to propose and discuss before starting 4 | labels: "Status: Untriaged, Type: Design Proposal" 5 | --- 6 | 7 | ### When a Change Needs a Design Proposal 8 | 9 | A design proposal should be opened any time a change meets one of the following qualifications: 10 | 11 | - Significantly changes the user experience of a project in a way that impacts users. 12 | - Significantly changes the underlying architecture of the project in a way that impacts other developers. 13 | - Changes the development or testing process of the project such as a change of CI systems or test frameworks. 14 | 15 | ### Why We Use This Process 16 | 17 | - Allows all interested parties (including any community member) to discuss large impact changes to a project. 18 | - Serves as a durable paper trail for discussions regarding project architecture. 19 | - Forces design discussions to occur before PRs are created. 20 | - Reduces PR refactoring and rejected PRs. 21 | 22 | --- 23 | 24 | 25 | 26 | ## Motivation 27 | 28 | 33 | 34 | ## Specification 35 | 36 | 37 | 38 | ## Downstream Impact 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Enhancement Request 3 | about: I have a suggestion (and may want to implement it 🙂)! 4 | labels: "Status: Untriaged" 5 | --- 6 | 7 | ### Describe the Enhancement 8 | 9 | 10 | ### Describe the Need 11 | 12 | 13 | ### Current Alternative 14 | 15 | 16 | ### Can We Help You Implement This? 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🤗 Support Question 3 | about: If you have a question 💬, please check out our Slack! 4 | --- 5 | 6 | We use GitHub issues to track bugs and feature requests. If you need help please post to our Mailing List or join the Chef Community Slack. 7 | 8 | * Chef Community Slack at 9 | * Chef Mailing List 10 | 11 | Support issues opened here will be closed and redirected to Slack or Discourse. 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "06:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 10 10 | ignore: 11 | - dependency-name: chefstyle 12 | versions: 13 | - 1.7.4 14 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: linters 3 | 4 | 'on': 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | chefstyle: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby: ['3.1', '3.2', '3.3'] 16 | name: Chefstyle on Ruby ${{ matrix.ruby }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true 23 | - run: bundle exec rake style 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | .direnv/ 16 | .envrc 17 | .ruby-version 18 | .chef 19 | local/ 20 | .vscode/ 21 | vendor/ -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.6 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ## [v2.12.2](https://github.com/chef/kitchen-vcenter/tree/v2.12.2) (2025-02-26) 10 | 11 | #### Merged Pull Requests 12 | - updating to newer images for tests [#181](https://github.com/chef/kitchen-vcenter/pull/181) ([sean-simmons-progress](https://github.com/sean-simmons-progress)) 13 | - Fix scsinumber [#179](https://github.com/chef/kitchen-vcenter/pull/179) ([jarvin521](https://github.com/jarvin521)) 14 | 15 | 16 | ## [v2.12.0](https://github.com/chef/kitchen-vcenter/tree/v2.12.0) (2023-01-09) 17 | 18 | #### Merged Pull Requests 19 | - Use rbvmomi2 gem [#174](https://github.com/chef/kitchen-vcenter/pull/174) ([ashiqueps](https://github.com/ashiqueps)) 20 | 21 | ## [v2.11.13](https://github.com/chef/kitchen-vcenter/tree/v2.11.13) (2022-06-01) 22 | 23 | #### Merged Pull Requests 24 | - [INFCT-53] Support for managing multiple networks [#172](https://github.com/chef/kitchen-vcenter/pull/172) ([ashiqueps](https://github.com/ashiqueps)) 25 | 26 | ## [v2.11.12](https://github.com/chef/kitchen-vcenter/tree/v2.11.12) (2022-03-17) 27 | 28 | #### Merged Pull Requests 29 | - Dummy PR to test the gem upload to Artifactory [#171](https://github.com/chef/kitchen-vcenter/pull/171) ([ashiqueps](https://github.com/ashiqueps)) 30 | 31 | ## [v2.11.11](https://github.com/chef/kitchen-vcenter/tree/v2.11.11) (2022-03-11) 32 | 33 | #### Merged Pull Requests 34 | - Bundle win32-security for windows platforms dependency [#169](https://github.com/chef/kitchen-vcenter/pull/169) ([ashiqueps](https://github.com/ashiqueps)) 35 | - Add vm name to initial clone log [#170](https://github.com/chef/kitchen-vcenter/pull/170) ([jasonwbarnett](https://github.com/jasonwbarnett)) 36 | 37 | ## [v2.11.9](https://github.com/chef/kitchen-vcenter/tree/v2.11.9) (2022-03-03) 38 | 39 | #### Merged Pull Requests 40 | - Support for adding new NIC while cloning the vm [#167](https://github.com/chef/kitchen-vcenter/pull/167) ([ashiqueps](https://github.com/ashiqueps)) 41 | 42 | ## [v2.11.8](https://github.com/chef/kitchen-vcenter/tree/v2.11.8) (2022-02-28) 43 | 44 | #### Merged Pull Requests 45 | - Use chefstyle linting [#165](https://github.com/chef/kitchen-vcenter/pull/165) ([sanjain-progress](https://github.com/sanjain-progress)) 46 | - Move config docs to kitchen.ci [#164](https://github.com/chef/kitchen-vcenter/pull/164) ([tas50](https://github.com/tas50)) 47 | - Cleanup the readme [#163](https://github.com/chef/kitchen-vcenter/pull/163) ([tas50](https://github.com/tas50)) 48 | - Update chefstyle requirement from 2.1.3 to 2.2.2 [#166](https://github.com/chef/kitchen-vcenter/pull/166) ([dependabot[bot]](https://github.com/dependabot[bot])) 49 | 50 | ## [v2.11.4](https://github.com/chef/kitchen-vcenter/tree/v2.11.4) (2021-11-22) 51 | 52 | #### Merged Pull Requests 53 | - Update chefstyle requirement from 2.0.9 to 2.1.0 [#154](https://github.com/chef/kitchen-vcenter/pull/154) ([dependabot[bot]](https://github.com/dependabot[bot])) 54 | - Remove EOL Ruby 2.5 references [#158](https://github.com/chef/kitchen-vcenter/pull/158) ([vkarve-chef](https://github.com/vkarve-chef)) 55 | - Update chefstyle requirement from 2.1.0 to 2.1.3 [#157](https://github.com/chef/kitchen-vcenter/pull/157) ([dependabot[bot]](https://github.com/dependabot[bot])) 56 | - Fix cloning of machines without network interface [#160](https://github.com/chef/kitchen-vcenter/pull/160) ([tecracer-theinen](https://github.com/tecracer-theinen)) 57 | 58 | ## [v2.11.0](https://github.com/chef/kitchen-vcenter/tree/v2.11.0) (2021-09-28) 59 | 60 | #### Merged Pull Requests 61 | - Update chefstyle requirement from 2.0.8 to 2.0.9 [#152](https://github.com/chef/kitchen-vcenter/pull/152) ([dependabot[bot]](https://github.com/dependabot[bot])) 62 | - Enable setting guestinfo [#153](https://github.com/chef/kitchen-vcenter/pull/153) ([jasonwbarnett](https://github.com/jasonwbarnett)) 63 | 64 | ## [v2.10.2](https://github.com/chef/kitchen-vcenter/tree/v2.10.2) (2021-08-16) 65 | 66 | #### Merged Pull Requests 67 | - Fix error handling and maintenance mode [#150](https://github.com/chef/kitchen-vcenter/pull/150) ([tecracer-theinen](https://github.com/tecracer-theinen)) 68 | - Fix CI failures / nuke some dev deps [#151](https://github.com/chef/kitchen-vcenter/pull/151) ([tas50](https://github.com/tas50)) 69 | 70 | ## [v2.10.0](https://github.com/chef/kitchen-vcenter/tree/v2.10.0) (2021-07-02) 71 | 72 | #### Merged Pull Requests 73 | - Update chefstyle requirement from 1.6.2 to 1.7.1 [#137](https://github.com/chef/kitchen-vcenter/pull/137) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 74 | - Update chefstyle requirement from 1.7.1 to 1.7.2 [#138](https://github.com/chef/kitchen-vcenter/pull/138) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 75 | - Update chefstyle requirement from 1.7.2 to 1.7.5 [#140](https://github.com/chef/kitchen-vcenter/pull/140) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 76 | - Upgrade to GitHub-native Dependabot [#142](https://github.com/chef/kitchen-vcenter/pull/142) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 77 | - Support Test Kitchen 3.0 / Require Ruby 2.5+ [#145](https://github.com/chef/kitchen-vcenter/pull/145) ([tas50](https://github.com/tas50)) 78 | 79 | ## [v2.9.8](https://github.com/chef/kitchen-vcenter/tree/v2.9.8) (2021-02-04) 80 | 81 | #### Merged Pull Requests 82 | - Update chefstyle requirement from 1.4.5 to 1.5.0 [#125](https://github.com/chef/kitchen-vcenter/pull/125) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 83 | - Update chefstyle requirement from 1.5.0 to 1.5.1 [#126](https://github.com/chef/kitchen-vcenter/pull/126) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 84 | - Update chefstyle requirement from 1.5.1 to 1.5.2 [#127](https://github.com/chef/kitchen-vcenter/pull/127) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 85 | - Update chefstyle requirement from 1.5.7 to 1.5.8 [#130](https://github.com/chef/kitchen-vcenter/pull/130) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 86 | - Update chefstyle requirement from 1.5.8 to 1.5.9 [#131](https://github.com/chef/kitchen-vcenter/pull/131) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 87 | - Update chefstyle requirement from 1.5.9 to 1.6.1 [#133](https://github.com/chef/kitchen-vcenter/pull/133) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 88 | - Update chefstyle requirement from 1.6.1 to 1.6.2 [#134](https://github.com/chef/kitchen-vcenter/pull/134) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 89 | - Add windows password customization [#136](https://github.com/chef/kitchen-vcenter/pull/136) ([lomeroe](https://github.com/lomeroe)) 90 | 91 | ## [v2.9.0](https://github.com/chef/kitchen-vcenter/tree/v2.9.0) (2020-10-30) 92 | 93 | #### Merged Pull Requests 94 | - Add Windows guest customization [#124](https://github.com/chef/kitchen-vcenter/pull/124) ([tecracer-theinen](https://github.com/tecracer-theinen)) 95 | 96 | ## [v2.8.6](https://github.com/chef/kitchen-vcenter/tree/v2.8.6) (2020-10-22) 97 | 98 | #### Merged Pull Requests 99 | - Update chefstyle requirement from 1.4.0 to 1.4.2 [#118](https://github.com/chef/kitchen-vcenter/pull/118) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 100 | - Update chefstyle requirement from 1.4.3 to 1.4.4 [#120](https://github.com/chef/kitchen-vcenter/pull/120) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 101 | - Update chefstyle requirement from 1.4.4 to 1.4.5 [#122](https://github.com/chef/kitchen-vcenter/pull/122) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 102 | - Support datacenters in folders when falling back in dc lookup [#123](https://github.com/chef/kitchen-vcenter/pull/123) ([clintoncwolfe](https://github.com/clintoncwolfe)) 103 | 104 | ## [v2.8.2](https://github.com/chef/kitchen-vcenter/tree/v2.8.2) (2020-09-29) 105 | 106 | #### Merged Pull Requests 107 | - Minor perf optimizations from rubocop-performance [#116](https://github.com/chef/kitchen-vcenter/pull/116) ([tas50](https://github.com/tas50)) 108 | - Update chefstyle requirement from 1.3.2 to 1.4.0 [#117](https://github.com/chef/kitchen-vcenter/pull/117) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 109 | 110 | ## [v2.8.0](https://github.com/chef/kitchen-vcenter/tree/v2.8.0) (2020-09-09) 111 | 112 | #### Merged Pull Requests 113 | - Update chefstyle requirement from 1.2.1 to 1.3.2 [#114](https://github.com/chef/kitchen-vcenter/pull/114) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 114 | - Enable IP customization using DHCP [#112](https://github.com/chef/kitchen-vcenter/pull/112) ([cattywampus](https://github.com/cattywampus)) 115 | - Minor readme tweaks [#115](https://github.com/chef/kitchen-vcenter/pull/115) ([tas50](https://github.com/tas50)) 116 | 117 | ## [v2.7.12](https://github.com/chef/kitchen-vcenter/tree/v2.7.12) (2020-08-21) 118 | 119 | #### Merged Pull Requests 120 | - Update chefstyle requirement from 1.2.0 to 1.2.1 [#107](https://github.com/chef/kitchen-vcenter/pull/107) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 121 | - Allow folder to support nested paths [#108](https://github.com/chef/kitchen-vcenter/pull/108) ([cattywampus](https://github.com/cattywampus)) 122 | - Optimize our requires [#113](https://github.com/chef/kitchen-vcenter/pull/113) ([tas50](https://github.com/tas50)) 123 | 124 | ## [v2.7.9](https://github.com/chef/kitchen-vcenter/tree/v2.7.9) (2020-08-06) 125 | 126 | #### Merged Pull Requests 127 | - Resolve Style/RedundantAssignment warning [#106](https://github.com/chef/kitchen-vcenter/pull/106) ([tas50](https://github.com/tas50)) 128 | 129 | ## [v2.7.8](https://github.com/chef/kitchen-vcenter/tree/v2.7.8) (2020-07-31) 130 | 131 | #### Merged Pull Requests 132 | - Fix minor spelling mistakes [#104](https://github.com/chef/kitchen-vcenter/pull/104) ([tas50](https://github.com/tas50)) 133 | - Update rbvmomi requirement from >= 1.11, < 3.0 to >= 1.11, < 4.0 [#105](https://github.com/chef/kitchen-vcenter/pull/105) ([dependabot-preview[bot]](https://github.com/dependabot-preview[bot])) 134 | 135 | ## [v2.7.6](https://github.com/chef/kitchen-vcenter/tree/v2.7.6) (2020-07-23) 136 | 137 | #### Merged Pull Requests 138 | - Enable Guest OS customization [#97](https://github.com/chef/kitchen-vcenter/pull/97) ([mkennedy85](https://github.com/mkennedy85)) 139 | - Cleaning up a linting error [#103](https://github.com/chef/kitchen-vcenter/pull/103) ([tyler-ball](https://github.com/tyler-ball)) 140 | 141 | ## [v2.7.4](https://github.com/chef/kitchen-vcenter/tree/v2.7.4) (2020-07-16) 142 | 143 | #### Merged Pull Requests 144 | - Support datacenters stored in folders [#98](https://github.com/chef/kitchen-vcenter/pull/98) ([clintoncwolfe](https://github.com/clintoncwolfe)) 145 | - Add chefstyle validation on each PR [#100](https://github.com/chef/kitchen-vcenter/pull/100) ([tas50](https://github.com/tas50)) 146 | - Cleanup the github templates and codeowners [#102](https://github.com/chef/kitchen-vcenter/pull/102) ([tas50](https://github.com/tas50)) 147 | - Drop support for Ruby 2.3 [#101](https://github.com/chef/kitchen-vcenter/pull/101) ([tas50](https://github.com/tas50)) 148 | 149 | ## [v2.7.0](https://github.com/chef/kitchen-vcenter/tree/v2.7.0) (2020-04-14) 150 | 151 | #### Merged Pull Requests 152 | - Add ability to transform VM IP for 1:1 NAT environments [#92](https://github.com/chef/kitchen-vcenter/pull/92) ([tecracer-theinen](https://github.com/tecracer-theinen)) 153 | 154 | ## [v2.6.5](https://github.com/chef/kitchen-vcenter/tree/v2.6.5) (2020-03-16) 155 | 156 | #### Merged Pull Requests 157 | - Fallback to datacenter lookup that requires less privileges [#82](https://github.com/chef/kitchen-vcenter/pull/82) ([jasonwbarnett](https://github.com/jasonwbarnett)) 158 | 159 | ## [v2.6.4](https://github.com/chef/kitchen-vcenter/tree/v2.6.4) (2020-02-18) 160 | 161 | #### Merged Pull Requests 162 | - Fix reference to undefined variable in CloneError message [#90](https://github.com/chef/kitchen-vcenter/pull/90) ([cattywampus](https://github.com/cattywampus)) 163 | - Fix active discovery command [#89](https://github.com/chef/kitchen-vcenter/pull/89) ([leotaglia](https://github.com/leotaglia)) 164 | 165 | ## [v2.6.2](https://github.com/chef/kitchen-vcenter/tree/v2.6.2) (2020-02-12) 166 | 167 | #### Merged Pull Requests 168 | - declare network_device var in the correct scope [#84](https://github.com/chef/kitchen-vcenter/pull/84) ([teknofire](https://github.com/teknofire)) 169 | - Remove unused savon dep and use require_relative [#85](https://github.com/chef/kitchen-vcenter/pull/85) ([tas50](https://github.com/tas50)) 170 | 171 | ## [v2.6.0](https://github.com/chef/kitchen-vcenter/tree/v2.6.0) (2019-11-05) 172 | 173 | #### Merged Pull Requests 174 | - Update to allow for the new rbvmomi [#80](https://github.com/chef/kitchen-vcenter/pull/80) ([tas50](https://github.com/tas50)) 175 | 176 | ## [v2.5.2](https://github.com/chef/kitchen-vcenter/tree/v2.5.2) (2019-09-16) 177 | 178 | #### Merged Pull Requests 179 | - Improve loose target host and folder filters [#73](https://github.com/chef/kitchen-vcenter/pull/73) ([sandratiffin](https://github.com/sandratiffin)) 180 | - Fix error if no clusters are defined or targethost is not a member [#75](https://github.com/chef/kitchen-vcenter/pull/75) ([tecracer-theinen](https://github.com/tecracer-theinen)) 181 | - Implement functionality to add disks to the cloned VM [#76](https://github.com/chef/kitchen-vcenter/pull/76) ([tecracer-theinen](https://github.com/tecracer-theinen)) 182 | 183 | ## [v2.4.0](https://github.com/chef/kitchen-vcenter/tree/v2.4.0) (2019-06-19) 184 | 185 | #### Merged Pull Requests 186 | - Implement aggressive IP polling mode [#66](https://github.com/chef/kitchen-vcenter/pull/66) ([tecracer-theinen](https://github.com/tecracer-theinen)) 187 | - Improve and fix support for Instant Clones, add Benchmark option [#70](https://github.com/chef/kitchen-vcenter/pull/70) ([tecracer-theinen](https://github.com/tecracer-theinen)) 188 | 189 | ## [v2.2.2](https://github.com/chef/kitchen-vcenter/tree/v2.2.2) (2019-03-20) 190 | 191 | #### Merged Pull Requests 192 | - Loosen the Test Kitchen dep to allow 2.X [#65](https://github.com/chef/kitchen-vcenter/pull/65) ([tas50](https://github.com/tas50)) 193 | 194 | ## [v2.2.1](https://github.com/chef/kitchen-vcenter/tree/v2.2.1) (2019-03-04) 195 | 196 | #### Merged Pull Requests 197 | - Network Interface Selection Support [#63](https://github.com/chef/kitchen-vcenter/pull/63) ([tecracer-theinen](https://github.com/tecracer-theinen)) 198 | - Fix instant clones, which failed after implementing VM reconfiguration [#64](https://github.com/chef/kitchen-vcenter/pull/64) ([tecracer-theinen](https://github.com/tecracer-theinen)) 199 | 200 | ## [v2.1.0](https://github.com/chef/kitchen-vcenter/tree/v2.1.0) (2019-02-27) 201 | 202 | #### Merged Pull Requests 203 | - Implement reconfiguration of VM memory/CPUs and other parameters [#61](https://github.com/chef/kitchen-vcenter/pull/61) ([tecracer-theinen](https://github.com/tecracer-theinen)) 204 | - Implement using both cluster/resource_pool [#62](https://github.com/chef/kitchen-vcenter/pull/62) ([tecracer-theinen](https://github.com/tecracer-theinen)) 205 | 206 | ## [v2.0.2](https://github.com/chef/kitchen-vcenter/tree/v2.0.2) (2019-02-12) 207 | 208 | #### Merged Pull Requests 209 | - Implementation of configurable timeouts and automatic rollback (v2) [#58](https://github.com/chef/kitchen-vcenter/pull/58) ([tecracer-theinen](https://github.com/tecracer-theinen)) 210 | 211 | ## [v2.0.1](https://github.com/chef/kitchen-vcenter/tree/v2.0.1) (2019-02-12) 212 | 213 | #### Merged Pull Requests 214 | - Add support for network_name to accept vSphere Distributed Switches [#60](https://github.com/chef/kitchen-vcenter/pull/60) ([tecracer-theinen](https://github.com/tecracer-theinen)) 215 | 216 | ## [v2.0.0](https://github.com/chef/kitchen-vcenter/tree/v2.0.0) (2019-01-30) 217 | 218 | - Bump the major version since we're using the rubygems published API now [#56](https://github.com/chef/kitchen-vcenter/pull/56) ([tas50](https://github.com/tas50)) 219 | - Migration to new openapi based SDK [#55](https://github.com/chef/kitchen-vcenter/pull/55) ([tecracer-theinen](https://github.com/tecracer-theinen)) 220 | 221 | ## [v1.5.0](https://github.com/chef/kitchen-vcenter/tree/v1.5.0) (2019-01-12) 222 | 223 | #### Merged Pull Requests 224 | - Add ability to move the created VM onto another network via using the "network_name" parameter [#47](https://github.com/chef/kitchen-vcenter/pull/47) ([tecracer-theinen](https://github.com/tecracer-theinen)) 225 | - Fix destroy action [#50](https://github.com/chef/kitchen-vcenter/pull/50) ([tecracer-theinen](https://github.com/tecracer-theinen)) 226 | - Add feature to tag kitchen instances [#52](https://github.com/chef/kitchen-vcenter/pull/52) ([tecracer-theinen](https://github.com/tecracer-theinen)) 227 | - Allow template names to include a VM folder path [#48](https://github.com/chef/kitchen-vcenter/pull/48) ([tecracer-theinen](https://github.com/tecracer-theinen)) 228 | - Update the author of the gem to be Chef Software [#53](https://github.com/chef/kitchen-vcenter/pull/53) ([tas50](https://github.com/tas50)) 229 | 230 | ## [v1.4.2](https://github.com/chef/kitchen-vcenter/tree/v1.4.2) (2019-01-04) 231 | 232 | #### Merged Pull Requests 233 | - Require Ruby 2.3+ and slim the files we ship in the gem [#45](https://github.com/chef/kitchen-vcenter/pull/45) ([tas50](https://github.com/tas50)) 234 | - Add support for provisioning on a specific cluster [#42](https://github.com/chef/kitchen-vcenter/pull/42) ([tecracer-theinen](https://github.com/tecracer-theinen)) 235 | - Add support for instant clones and documentation [#43](https://github.com/chef/kitchen-vcenter/pull/43) - Thank you [Siemens Gamesa](https://www.siemensgamesa.com) for sponsoring this contribution. 236 | 237 | ## [v1.3.4](https://github.com/chef/kitchen-vcenter/tree/v1.3.4) (2019-01-04) 238 | 239 | #### Merged Pull Requests 240 | - README edits [#38](https://github.com/chef/kitchen-vcenter/pull/38) ([mjingle](https://github.com/mjingle)) 241 | - Feature/check parameters [#40](https://github.com/chef/kitchen-vcenter/pull/40) ([tecracer-theinen](https://github.com/tecracer-theinen)) 242 | - Fix for Issues #26 and #30 [#44](https://github.com/chef/kitchen-vcenter/pull/44) ([tecracer-theinen](https://github.com/tecracer-theinen)) 243 | 244 | ## [v1.3.1](https://github.com/chef/kitchen-vcenter/tree/v1.3.1) (2018-11-19) 245 | 246 | #### Merged Pull Requests 247 | - Fixed behaviour when not having any resource pools (Issue #28) [#31](https://github.com/chef/kitchen-vcenter/pull/31) ([tecracer-theinen](https://github.com/tecracer-theinen)) 248 | - Implement support for linked clones (feature #18) [#32](https://github.com/chef/kitchen-vcenter/pull/32) ([tecracer-theinen](https://github.com/tecracer-theinen)) 249 | - Chefstyle fixes [#37](https://github.com/chef/kitchen-vcenter/pull/37) ([tas50](https://github.com/tas50)) 250 | 251 | ## [1.2.1](https://github.com/chef/kitchen-vcenter/tree/1.2.1) (2017-09-14) 252 | [Full Changelog](https://github.com/chef/kitchen-vcenter/compare/v1.2.0...1.2.1) 253 | 254 | **Closed issues:** 255 | 256 | - Install attempt errors with message about requiring vsphere-automation-sdk [\#11](https://github.com/chef/kitchen-vcenter/issues/11) 257 | 258 | **Merged pull requests:** 259 | 260 | - Update dependency reqs. Update README for dep install. [\#12](https://github.com/chef/kitchen-vcenter/pull/12) ([akulbe](https://github.com/akulbe)) 261 | 262 | ## [v1.2.0](https://github.com/chef/kitchen-vcenter/tree/v1.2.0) (2017-09-12) 263 | [Full Changelog](https://github.com/chef/kitchen-vcenter/compare/v1.1.0...v1.2.0) 264 | 265 | **Merged pull requests:** 266 | 267 | - Resource pool and target host no longer have to be specified. [\#10](https://github.com/chef/kitchen-vcenter/pull/10) ([russellseymour](https://github.com/russellseymour)) 268 | 269 | ## [v1.1.0](https://github.com/chef/kitchen-vcenter/tree/v1.1.0) (2017-09-07) 270 | [Full Changelog](https://github.com/chef/kitchen-vcenter/compare/v1.0.0...v1.1.0) 271 | 272 | **Closed issues:** 273 | 274 | - Resource pool is not specified when creating a new machine [\#7](https://github.com/chef/kitchen-vcenter/issues/7) 275 | - Unhelpful messages from driver if specified item does not exist [\#6](https://github.com/chef/kitchen-vcenter/issues/6) 276 | 277 | **Merged pull requests:** 278 | 279 | - Updated to handle resource\_pools [\#8](https://github.com/chef/kitchen-vcenter/pull/8) ([russellseymour](https://github.com/russellseymour)) 280 | 281 | ## [v1.0.0](https://github.com/chef/kitchen-vcenter/tree/v1.0.0) (2017-08-28) 282 | **Merged pull requests:** 283 | 284 | - 1.0.0 release [\#4](https://github.com/chef/kitchen-vcenter/pull/4) ([jjasghar](https://github.com/jjasghar)) 285 | - Second walk through [\#3](https://github.com/chef/kitchen-vcenter/pull/3) ([jjasghar](https://github.com/jjasghar)) 286 | - Updated so that the vm\_name is set according to suite and platform. [\#2](https://github.com/chef/kitchen-vcenter/pull/2) ([russellseymour](https://github.com/russellseymour)) 287 | - Prep-for-public release [\#1](https://github.com/chef/kitchen-vcenter/pull/1) ([jjasghar](https://github.com/jjasghar)) 288 | 289 | 290 | 291 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please refer to the Chef Community Code of Conduct at https://www.chef.io/code-of-conduct/ 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/chef/chef/blob/master/CONTRIBUTING.md 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "rake" 7 | gem "rspec" 8 | gem "chefstyle", "2.2.2" 9 | end 10 | 11 | group :debug do 12 | gem "pry" 13 | gem "pry-byebug" 14 | gem "pry-stack_explorer" 15 | gem "rb-readline" 16 | end 17 | 18 | # This is a required dependency for windows platforms 19 | platforms :mswin, :mswin64, :mingw, :x64_mingw do 20 | gem "win32-security" 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kitchen-vcenter 2 | 3 | [![Gem Version](https://badge.fury.io/rb/kitchen-vcenter.svg)](https://rubygems.org/gems/kitchen-vcenter) 4 | [![Build status](https://badge.buildkite.com/4b0ca1bb5cd02dee51d9ce789f8346eb05730685c5be7fbba9.svg?branch=master)](https://buildkite.com/chef-oss/chef-kitchen-vcenter-master-verify) 5 | 6 | This is the official Test Kitchen plugin for VMware vCenter via the vCenter REST API. This plugin allows Test Kitchen the ability to create, bootstrap, and test VMs in VMware infrastructures. 7 | 8 | - Documentation: [https://github.com/chef/kitchen-vcenter/blob/master/README.md](https://github.com/chef/kitchen-vcenter/blob/master/README.md) 9 | - Source: [https://github.com/chef/kitchen-vcenter/tree/master](https://github.com/chef/kitchen-vcenter/tree/master) 10 | - Issues: [https://github.com/chef/kitchen-vcenter/issues](https://github.com/chef/knife-vcenter/issues) 11 | - Mailing list: [https://discourse.chef.io/](https://discourse.chef.io/) 12 | 13 | Please refer to the [CHANGELOG](CHANGELOG.md) for version history and known issues. 14 | 15 | ## Requirements 16 | 17 | - Ruby 2.6 or higher 18 | - VMware vCenter/vSphere 5.5 or higher 19 | - VMs or templates to clone, with open-vm-tools installed 20 | - DHCP server to assign IPs to kitchen instances 21 | 22 | ## Installation 23 | 24 | The kitchen-vcenter driver ships as part of Chef Workstation. The easiest way to use this driver is to [Download Chef Workstation](https://www.chef.io/downloads/tools/workstation). 25 | 26 | If you want to install the driver directly into a Ruby installation: 27 | 28 | ```sh 29 | gem install kitchen-vcenter 30 | ``` 31 | 32 | If you're using Bundler, simply add it to your Gemfile: 33 | 34 | ```ruby 35 | gem "kitchen-vcenter" 36 | ``` 37 | 38 | ... and then run `bundle install`. 39 | 40 | ## Configuration 41 | 42 | See the [kitchen.ci vCenter Driver Page](https://kitchen.ci/docs/drivers/vcenter/) for documentation on configuring this driver. 43 | 44 | ## Contributing 45 | 46 | For information on contributing to this project see 47 | 48 | ## Development 49 | 50 | * Report issues/questions/feature requests on [GitHub Issues][issues] 51 | 52 | Pull requests are very welcome! Make sure your patches are well tested. Ideally create a topic branch for every separate change you make. For example: 53 | 54 | 1. Fork the repo 55 | 2. Create your feature branch (`git checkout -b my-new-feature`) 56 | 3. Run the tests and chefstyle, `bundle exec rake spec` and `bundle exec rake style` 57 | 4. Commit your changes (`git commit -am 'Added some feature'`) 58 | 5. Push to the branch (`git push origin my-new-feature`) 59 | 6. Create new Pull Request 60 | 61 | ## License 62 | 63 | - Author:: Russell Seymour ([rseymour@chef.io](mailto:rseymour@chef.io)) 64 | - Author:: JJ Asghar ([jj@chef.io](mailto:jj@chef.io)) 65 | - Author:: Thomas Heinen ([theinen@tecracer.de](mailto:theinen@tecracer.de)) 66 | - Author:: Michael Kennedy ([michael_l_kennedy@me.com](mailto:michael_l_kennedy@me.com)) 67 | 68 | Copyright:: Copyright (c) 2017-2022 Chef Software, Inc. 69 | 70 | License:: Apache License, Version 2.0 71 | 72 | ```text 73 | Licensed under the Apache License, Version 2.0 (the "License"); 74 | you may not use this file except in compliance with the License. 75 | You may obtain a copy of the License at 76 | 77 | http://www.apache.org/licenses/LICENSE-2.0 78 | 79 | Unless required by applicable law or agreed to in writing, software 80 | distributed under the License is distributed on an "AS IS" BASIS, 81 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 82 | See the License for the specific language governing permissions and 83 | limitations under the License. 84 | ``` 85 | 86 | [issues]: https://github.com/chef/kitchen-vcenter/issues 87 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "bundler/gem_tasks" 3 | 4 | begin 5 | require "chefstyle" 6 | require "rubocop/rake_task" 7 | desc "Run Chefstyle tests" 8 | RuboCop::RakeTask.new(:style) do |task| 9 | task.options += ["--display-cop-names", "--no-color"] 10 | end 11 | rescue LoadError 12 | puts "chefstyle gem is not installed. bundle install first to make sure all dependencies are installed." 13 | end 14 | 15 | RuboCop::RakeTask.new(:style) 16 | 17 | begin 18 | require "rspec/core/rake_task" 19 | 20 | desc "Run all specs in spec directory" 21 | RSpec::Core::RakeTask.new(:spec) do |t| 22 | t.verbose = false 23 | t.rspec_opts = %w{--profile} 24 | t.pattern = FileList["spec/**/*_spec.rb"] 25 | end 26 | rescue LoadError 27 | STDERR.puts "\n*** RSpec not available. (sudo) gem install rspec to run unit tests. ***\n\n" 28 | end 29 | 30 | task default: %i{style spec} 31 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.12.2 -------------------------------------------------------------------------------- /kitchen-vcenter.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require "kitchen-vcenter/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "kitchen-vcenter" 8 | spec.version = KitchenVcenter::VERSION 9 | spec.authors = ["Chef Software"] 10 | spec.email = ["oss@chef.io"] 11 | spec.summary = "Test Kitchen driver for VMware vCenter" 12 | spec.description = "Test Kitchen driver for VMware vCenter using SDK" 13 | spec.homepage = "https://github.com/chef/kitchen-vcenter" 14 | spec.license = "Apache-2.0" 15 | 16 | spec.files = Dir["LICENSE", "lib/**/*"] 17 | spec.require_paths = ["lib"] 18 | 19 | spec.required_ruby_version = ">= 3.1" 20 | 21 | spec.add_dependency "net-ping", ">= 2.0.0", "< 3.0" 22 | spec.add_dependency "rbvmomi2", ">= 3.5.0", "< 4.0" 23 | spec.add_dependency "test-kitchen", ">= 1.16", "< 4" 24 | spec.add_dependency "vsphere-automation-sdk", "~> 0.4" 25 | end 26 | -------------------------------------------------------------------------------- /lib/kitchen-vcenter/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Author:: Chef Partner Engineering () 4 | # Copyright:: Copyright (c) 2017 Chef Software, Inc. 5 | # License:: Apache License, Version 2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | # The main kitchen-vcenter module 21 | module KitchenVcenter 22 | # The version of this version of test-kitchen we assume enterprises want. 23 | VERSION = "2.12.2" 24 | end 25 | -------------------------------------------------------------------------------- /lib/kitchen/driver/vcenter.rb: -------------------------------------------------------------------------------- 1 | # Author:: Chef Partner Engineering () 2 | # Copyright:: Copyright (c) 2017 Chef Software, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require "kitchen" 19 | require "rbvmomi" 20 | require "vsphere-automation-cis" 21 | require "vsphere-automation-vcenter" 22 | require_relative "../../kitchen-vcenter/version" 23 | require_relative "../../support/clone_vm" 24 | require "securerandom" unless defined?(SecureRandom) 25 | require "uri" unless defined?(URI) 26 | 27 | # The main kitchen module 28 | module Kitchen 29 | # The main driver module for kitchen-vcenter 30 | module Driver 31 | # Extends the Base class for vCenter 32 | class Vcenter < Kitchen::Driver::Base 33 | class UnauthenticatedError < RuntimeError; end 34 | class ResourceMissingError < RuntimeError; end 35 | class ResourceAmbiguousError < RuntimeError; end 36 | 37 | attr_accessor :connection_options, :ipaddress, :api_client 38 | 39 | UNAUTH_CLASSES = [ 40 | VSphereAutomation::CIS::VapiStdErrorsUnauthenticated, 41 | VSphereAutomation::VCenter::VapiStdErrorsUnauthenticated, 42 | ].freeze 43 | 44 | required_config :vcenter_username 45 | required_config :vcenter_password 46 | required_config :vcenter_host 47 | required_config :datacenter 48 | required_config :template 49 | 50 | default_config :vcenter_disable_ssl_verify, false 51 | default_config :targethost, nil 52 | default_config :folder, nil 53 | default_config :poweron, true 54 | default_config :vm_name, nil 55 | default_config :resource_pool, nil 56 | default_config :clone_type, :full 57 | default_config :cluster, nil 58 | default_config :network_name, nil 59 | default_config :networks, [] 60 | default_config :tags, nil 61 | default_config :vm_wait_timeout, 90 62 | default_config :vm_wait_interval, 2.0 63 | default_config :vm_rollback, false 64 | default_config :vm_customization, nil 65 | default_config :guest_customization, nil 66 | default_config :interface, nil 67 | default_config :active_discovery, false 68 | default_config :active_discovery_command, nil 69 | default_config :vm_os, nil 70 | default_config :vm_username, "vagrant" 71 | default_config :vm_password, "vagrant" 72 | default_config :vm_win_network, "Ethernet0" 73 | default_config :transform_ip, nil 74 | 75 | default_config :benchmark, false 76 | default_config :benchmark_file, "kitchen-vcenter.csv" 77 | 78 | deprecate_config_for :aggressive_mode, Util.outdent!(<<-MSG) 79 | The 'aggressive_mode' setting was renamed to 'active_discovery' and 80 | will be removed in future versions 81 | MSG 82 | deprecate_config_for :aggressive_os, Util.outdent!(<<-MSG) 83 | The 'aggressive_os' setting was renamed to 'vm_os' and will be 84 | removed in future versions. 85 | MSG 86 | deprecate_config_for :aggressive_username, Util.outdent!(<<-MSG) 87 | The 'aggressive_username' setting was renamed to 'vm_username' and will 88 | be removed in future versions. 89 | MSG 90 | deprecate_config_for :aggressive_password, Util.outdent!(<<-MSG) 91 | The 'aggressive_password' setting was renamed to 'vm_password' and will 92 | be removed in future versions. 93 | MSG 94 | deprecate_config_for :customize, Util.outdent!(<<-MSG) 95 | The `customize` setting was renamed to `vm_customization` and will 96 | be removed in future versions. 97 | MSG 98 | deprecate_config_for :network_name, Util.outdent!(<<-MSG) 99 | The `network_name` setting is deprecated and will be removed in the 100 | future version. Please use the new settings `networks` and refer 101 | documentation for the usage. 102 | MSG 103 | 104 | # The main create method 105 | # 106 | # @param [Object] state is the state of the vm 107 | def create(state) 108 | debug format("Starting kitchen-vcenter %s", ::KitchenVcenter::VERSION) 109 | 110 | save_and_validate_parameters 111 | connect 112 | 113 | # Use the root resource pool of a specified cluster, if any 114 | if config[:cluster].nil? 115 | # Find the first resource pool on any cluster 116 | config[:resource_pool] = get_resource_pool(config[:resource_pool]) 117 | else 118 | cluster = get_cluster(config[:cluster]) 119 | root_pool = cluster.resource_pool 120 | 121 | if config[:resource_pool].nil? 122 | config[:resource_pool] = root_pool 123 | else 124 | rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client) 125 | raise_if_unauthenticated rp_api, "checking for resource pools" 126 | 127 | found_pool = nil 128 | pools = rp_api.get(root_pool).value.resource_pools 129 | pools.each do |pool| 130 | name = rp_api.get(pool).value.name 131 | found_pool = pool if name == config[:resource_pool] 132 | end 133 | 134 | raise_if_missing found_pool, format("Resource pool `%s` not found on cluster `%s`", config[:resource_pool], config[:cluster]) 135 | 136 | config[:resource_pool] = found_pool 137 | end 138 | end 139 | 140 | # Check that the datacenter exists 141 | dc_folder = File.dirname(config[:datacenter]) 142 | dc_folder = nil if dc_folder == "." 143 | dc_name = File.basename(config[:datacenter]) 144 | datacenter_exists?(dc_folder, dc_name) 145 | 146 | # Get datacenter and cluster information 147 | datacenter = get_datacenter(dc_folder, dc_name) 148 | cluster_id = get_cluster_id(config[:cluster]) 149 | 150 | # Find the identifier for the targethost to pass to rbvmomi 151 | config[:targethost] = get_host(config[:targethost], datacenter, cluster_id) 152 | 153 | # Check if network exists, if to be changed 154 | config[:networks].each { |network| network_exists?(network[:name]) } 155 | 156 | # Same thing needs to happen with the folder name if it has been set 157 | unless config[:folder].nil? 158 | config[:folder] = { 159 | name: config[:folder], 160 | id: get_folder(config[:folder], "VIRTUAL_MACHINE", datacenter), 161 | } 162 | end 163 | 164 | # Check for valid tags before cloning 165 | vm_tags = map_tags(config[:tags]) 166 | 167 | # Allow different clone types 168 | config[:clone_type] = :linked if config[:clone_type] == "linked" 169 | config[:clone_type] = :instant if config[:clone_type] == "instant" 170 | 171 | # Create a hash of options that the clone requires 172 | options = { 173 | vm_name: config[:vm_name], 174 | targethost: config[:targethost], 175 | poweron: config[:poweron], 176 | template: config[:template], 177 | datacenter: config[:datacenter], 178 | folder: config[:folder], 179 | resource_pool: config[:resource_pool], 180 | clone_type: config[:clone_type].to_sym, 181 | networks: config[:networks], 182 | interface: config[:interface], 183 | wait_timeout: config[:vm_wait_timeout], 184 | wait_interval: config[:vm_wait_interval], 185 | vm_customization: config[:vm_customization], 186 | guest_customization: config[:guest_customization], 187 | active_discovery: config[:active_discovery], 188 | active_discovery_command: config[:active_discovery_command], 189 | vm_os: config[:vm_os], 190 | vm_username: config[:vm_username], 191 | vm_password: config[:vm_password], 192 | vm_win_network: config[:vm_win_network], 193 | transform_ip: config[:transform_ip], 194 | benchmark: config[:benchmark], 195 | benchmark_file: config[:benchmark_file], 196 | } 197 | 198 | begin 199 | # Create an object from which the clone operation can be called 200 | new_vm = Support::CloneVm.new(connection_options, options) 201 | new_vm.clone 202 | 203 | state[:hostname] = new_vm.ip 204 | state[:vm_name] = new_vm.vm_name 205 | 206 | rescue # Kitchen::ActionFailed => e 207 | if config[:vm_rollback] == true 208 | error format("Rolling back VM `%s` after critical error", config[:vm_name]) 209 | 210 | # Inject name of failed VM for destroy to work 211 | state[:vm_name] = config[:vm_name] 212 | 213 | destroy(state) 214 | end 215 | 216 | raise 217 | end 218 | 219 | if vm_tags 220 | debug format("Setting tags on machine: `%s`", vm_tags.keys.join("`, `")) 221 | 222 | tag_service = VSphereAutomation::CIS::TaggingTagAssociationApi.new(api_client) 223 | raise_if_unauthenticated tag_service, "connecting to tagging service" 224 | 225 | request_body = { 226 | object_id: { 227 | id: get_vm(config[:vm_name]).vm, 228 | type: "VirtualMachine", 229 | }, 230 | tag_ids: vm_tags.values, 231 | } 232 | tag_service.attach_multiple_tags_to_object(request_body) 233 | end 234 | end 235 | 236 | # The main destroy method 237 | # 238 | # @param [Object] state is the state of the vm 239 | def destroy(state) 240 | return if state[:vm_name].nil? 241 | 242 | # Reset resource pool, as it's not needed for the destroy action but might be a remnant of earlier calls to "connect" 243 | # Temporary fix until setting cluster + resource_pool at the same time is implemented (lines #64/#187) 244 | config[:resource_pool] = nil 245 | 246 | save_and_validate_parameters 247 | connect 248 | 249 | vm = get_vm(state[:vm_name]) 250 | unless vm.nil? 251 | vm_api = VSphereAutomation::VCenter::VMApi.new(api_client) 252 | raise_if_unauthenticated vm_api, "connecting to VM API" 253 | 254 | # shut the machine down if it is running 255 | if vm.power_state == "POWERED_ON" 256 | power = VSphereAutomation::VCenter::VmPowerApi.new(api_client) 257 | power.stop(vm.vm) 258 | end 259 | 260 | # delete the vm 261 | vm_api.delete(vm.vm) 262 | end 263 | end 264 | 265 | private 266 | 267 | # Helper method for storing and validating configuration parameters 268 | # 269 | def save_and_validate_parameters 270 | # Configure the hash for use when connecting for cloning a machine 271 | @connection_options = { 272 | user: config[:vcenter_username], 273 | password: config[:vcenter_password], 274 | insecure: config[:vcenter_disable_ssl_verify] ? true : false, 275 | host: config[:vcenter_host], 276 | rev: config[:clone_type] == "instant" ? "6.7" : nil, 277 | } 278 | 279 | # If the vm_name has not been set then set it now based on the suite, platform and a random number 280 | if config[:vm_name].nil? 281 | config[:vm_name] = format("%s-%s-%s", instance.suite.name, instance.platform.name, SecureRandom.hex(4)) 282 | end 283 | 284 | # See details in function get_resource_pool for more details 285 | # if config[:cluster].nil? && config[:resource_pool].nil? 286 | # warn("It is recommended to specify cluster and/or resource_pool to avoid unpredictable machine placement on large deployments") 287 | # end 288 | 289 | # Process deprecated parameters 290 | config[:active_discovery] = config[:aggressive_mode] unless config[:aggressive_mode].nil? 291 | config[:vm_os] = config[:aggressive_os] unless config[:aggressive_os].nil? 292 | config[:vm_username] = config[:aggressive_username] unless config[:aggressive_username].nil? 293 | config[:vm_password] = config[:aggressive_password] unless config[:aggressive_password].nil? 294 | config[:vm_customization] = config[:customize] unless config[:customize].nil? 295 | validate_network_parameters 296 | end 297 | 298 | def validate_network_parameters 299 | return if config[:network_name].nil? 300 | 301 | config[:networks] = [{ name: config[:network_name], operation: "edit" }] 302 | end 303 | 304 | # A helper method to validate the state 305 | # 306 | # @param [Object] state is the state of the vm 307 | def validate_state(state = {}); end 308 | 309 | def existing_state_value?(state, property) 310 | state.key?(property) && !state[property].nil? 311 | end 312 | 313 | # Handle the non-ruby way of the SDK to report errors. 314 | # 315 | # @param api_response [Object] a generic API response class, which might include an error type 316 | # @param message [String] description to output in case of error 317 | # @raise UnauthenticatedError 318 | def raise_if_unauthenticated(api_response, message) 319 | session_id = api_response.api_client.default_headers["vmware-api-session-id"] 320 | return unless UNAUTH_CLASSES.include? session_id.class 321 | 322 | message = format("Authentication or permissions error on %s", message) 323 | raise UnauthenticatedError.new(message) 324 | end 325 | 326 | # Handle missing resources in a query. 327 | # 328 | # @param collection [Enumerable] list which is supposed to have at least one entry 329 | # @param message [String] description to output in case of error 330 | # @raise ResourceMissingError 331 | def raise_if_missing(collection, message) 332 | return unless collection.nil? || collection.empty? 333 | 334 | raise ResourceMissingError.new(message) 335 | end 336 | 337 | # Handle ambiguous resources in a query. 338 | # 339 | # @param collection [Enumerable] list which is supposed to one entry at most 340 | # @param message [String] description to output in case of error 341 | # @raise ResourceAmbiguousError 342 | def raise_if_ambiguous(collection, message) 343 | return unless collection.length > 1 344 | 345 | raise ResourceAmbiguousError.new(message) 346 | end 347 | 348 | # Access to legacy SOAP based vMOMI API for some functionality 349 | # 350 | # @return [RbVmomi::VIM] VIM instance 351 | def vim 352 | @vim ||= RbVmomi::VIM.connect(connection_options) 353 | end 354 | 355 | # Search host data via vMOMI 356 | # 357 | # @param moref [String] identifier of a host system ("host-xxxx") 358 | # @return [RbVmomi::VIM::HostSystem] 359 | def host_by_moref(moref) 360 | vim.serviceInstance.content.hostSpecManager.RetrieveHostSpecification(host: moref, fromHost: false).host 361 | end 362 | 363 | # Sees in the datacenter exists or not 364 | # 365 | # @param [folder] folder is the name of the folder in which the Datacenter is stored in inventory, possibly nil 366 | # @param [name] name is the name of the datacenter 367 | def datacenter_exists?(folder, name) 368 | dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client) 369 | raise_if_unauthenticated dc_api, "checking for datacenter `#{name}`" 370 | 371 | opts = { filter_names: name } 372 | opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder 373 | dcs = dc_api.list(opts).value 374 | 375 | raise_if_missing dcs, format("Unable to find data center `%s`", name) 376 | end 377 | 378 | # Checks if a network exists or not 379 | # 380 | # @param [name] name is the name of the Network 381 | def network_exists?(name) 382 | net_api = VSphereAutomation::VCenter::NetworkApi.new(api_client) 383 | raise_if_unauthenticated net_api, "checking for VM network `#{name}`" 384 | 385 | nets = net_api.list({ filter_names: name }).value 386 | 387 | raise_if_missing nets, format("Unable to find target network: `%s`", name) 388 | end 389 | 390 | # Map VCenter tag names to URNs (VCenter needs tags to be predefined) 391 | # 392 | # @param tags [tags] tags is the list of tags to associate 393 | # @return [Hash] mapping of VCenter tag name to URN 394 | # @raise UnauthenticatedError 395 | # @raise ResourceMissingError 396 | def map_tags(tags) 397 | return nil if tags.nil? || tags.empty? 398 | 399 | tag_api = VSphereAutomation::CIS::TaggingTagApi.new(api_client) 400 | raise_if_unauthenticated tag_api, "checking for tags" 401 | 402 | vm_tags = tag_api.list.value 403 | raise_if_missing vm_tags, format("No configured tags found on VCenter, but `%s` specified", config[:tags].to_s) 404 | 405 | # Create list of all VCenter defined tags, associated with their internal ID 406 | valid_tags = {} 407 | vm_tags.each do |uid| 408 | tag = tag_api.get(uid) 409 | 410 | valid_tags[tag.value.name] = tag.value.id if tag.is_a? VSphereAutomation::CIS::CisTaggingTagResult 411 | end 412 | 413 | invalid = config[:tags] - valid_tags.keys 414 | unless invalid.empty? 415 | message = format("Specified tag(s) `%s` not preconfigured on VCenter", invalid.join("`, `")) 416 | raise ResourceMissingError.new(message) 417 | end 418 | 419 | valid_tags.select { |tag, _urn| config[:tags].include? tag } 420 | end 421 | 422 | # Validates the host name of the server you can connect to 423 | # 424 | # @param [name] name is the name of the host 425 | def get_host(name, datacenter, cluster = nil) 426 | # create a host object to work with 427 | host_api = VSphereAutomation::VCenter::HostApi.new(api_client) 428 | raise_if_unauthenticated host_api, "checking for target host `#{name || "(any)"}`" 429 | 430 | hosts = host_api.list({ filter_names: name, 431 | filter_datacenters: datacenter, 432 | filter_clusters: cluster, 433 | filter_connection_states: ["CONNECTED"] }).value 434 | 435 | raise_if_missing hosts, format("Unable to find target host `%s`", name || "(any)") 436 | 437 | filter_maintenance!(hosts) 438 | raise_if_missing hosts, "Unable to find active target host in datacenter (check maintenance mode?)" 439 | 440 | # Randomize returned hosts 441 | host = hosts.sample 442 | debug format("Selected host `%s` randomly for deployment", host.name) 443 | 444 | host 445 | end 446 | 447 | def filter_maintenance!(hosts) 448 | # Exclude hosts which are in maintenance mode (via SOAP API only) 449 | hosts.reject! do |hostinfo| 450 | host = host_by_moref(hostinfo.host) 451 | host.runtime.inMaintenanceMode 452 | end 453 | end 454 | 455 | # Gets the folder you want to create the VM 456 | # 457 | # @param [name] name is the name of the folder 458 | # @param [type] type is the type of the folder, one of VIRTUAL_MACHINE, DATACENTER, possibly other values 459 | # @param [datacenter] datacenter is the datacenter of the folder 460 | def get_folder(name, type = "VIRTUAL_MACHINE", datacenter = nil) 461 | folder_api = VSphereAutomation::VCenter::FolderApi.new(api_client) 462 | raise_if_unauthenticated folder_api, "checking for folder `#{name}`" 463 | 464 | parent_path, basename = File.split(name) 465 | filter = { filter_names: basename, filter_type: type } 466 | filter[:filter_datacenters] = datacenter if datacenter 467 | filter[:filter_parent_folders] = get_folder(parent_path, type, datacenter) unless parent_path == "." 468 | 469 | folders = folder_api.list(filter).value 470 | 471 | raise_if_missing folders, format("Unable to find VM/template folder: `%s`", basename) 472 | raise_if_ambiguous folders, format("`%s` returned too many VM/template folders", basename) 473 | 474 | folders.first.folder 475 | end 476 | 477 | # Gets the name of the VM you are creating 478 | # 479 | # @param [name] name is the name of the VM 480 | def get_vm(name) 481 | vm_api = VSphereAutomation::VCenter::VMApi.new(api_client) 482 | raise_if_unauthenticated vm_api, "checking for VM `#{name}`" 483 | 484 | vms = vm_api.list({ filter_names: name }).value 485 | 486 | raise_if_missing vms, format("Unable to find VM `%s`", name) 487 | raise_if_ambiguous vms, format("`%s` returned too many VMs", name) 488 | 489 | vms.first 490 | end 491 | 492 | # Gets the info of the datacenter 493 | # 494 | # @param [folder] folder is the name of the folder in which the Datacenter is stored in inventory, possibly nil 495 | # @param [name] name is the name of the Datacenter 496 | def get_datacenter(folder, name) 497 | dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client) 498 | raise_if_unauthenticated dc_api, "checking for datacenter `#{name}` in folder `#{folder}`" 499 | 500 | opts = { filter_names: name } 501 | opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder 502 | dcs = dc_api.list(opts).value 503 | 504 | raise_if_missing dcs, format("Unable to find data center: `%s`", name) 505 | raise_if_ambiguous dcs, format("`%s` returned too many data centers", name) 506 | 507 | dcs.first.datacenter 508 | end 509 | 510 | # Gets the ID of the cluster 511 | # 512 | # @param [name] name is the name of the Cluster 513 | def get_cluster_id(name) 514 | return if name.nil? 515 | 516 | cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client) 517 | raise_if_unauthenticated cluster_api, "checking for ID of cluster `#{name}`" 518 | 519 | clusters = cluster_api.list({ filter_names: name }).value 520 | 521 | raise_if_missing clusters, format("Unable to find Cluster: `%s`", name) 522 | raise_if_ambiguous clusters, format("`%s` returned too many clusters", name) 523 | 524 | clusters.first.cluster 525 | end 526 | 527 | # Gets the info of the cluster 528 | # 529 | # @param [name] name is the name of the Cluster 530 | def get_cluster(name) 531 | cluster_id = get_cluster_id(name) 532 | 533 | host_api = VSphereAutomation::VCenter::HostApi.new(api_client) 534 | raise_if_unauthenticated host_api, "checking for cluster `#{name}`" 535 | 536 | hosts = host_api.list({ filter_clusters: cluster_id, connection_states: "CONNECTED" }).value 537 | filter_maintenance!(hosts) 538 | raise_if_missing hosts, format("Unable to find active hosts in cluster `%s`", name) 539 | 540 | cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client) 541 | cluster_api.get(cluster_id).value 542 | end 543 | 544 | # Gets the name of the resource pool 545 | # 546 | # @todo Will not yet work with nested pools ("Pool1/Subpool1") 547 | # @param [name] name is the name of the ResourcePool 548 | def get_resource_pool(name) 549 | # Create a resource pool object 550 | rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client) 551 | raise_if_unauthenticated rp_api, "checking for resource pool `#{name || "(default)"}`" 552 | 553 | # If no name has been set, use the first resource pool that can be found, 554 | # otherwise try to find by given name 555 | if name.nil? 556 | # Unpredictable results can occur, if neither cluster nor resource_pool are specified, 557 | # as this relies on the order in which VMware saves the objects. This does not have large 558 | # impact on small environments, but on large deployments with lots of clusters and pools, 559 | # provisioned machines are likely to "jump around" available hosts. 560 | # 561 | # This behavior is carried on from versions 1.2.1 and lower, but likely to be removed in 562 | # a new major version due to these insufficiencies and the confusing code for it 563 | 564 | # Remove default pool for first pass (<= 1.2.1 behavior to pick first user-defined pool found) 565 | resource_pools = rp_api.list.value.delete_if { |pool| pool.name == "Resources" } 566 | debug("Search of all resource pools found: " + resource_pools.map(&:name).to_s) 567 | 568 | # Revert to default pool, if no user-defined pool found (> 1.2.1 behavior) 569 | # (This one might not be found under some circumstances by the statement above) 570 | return get_resource_pool("Resources") if resource_pools.empty? 571 | else 572 | resource_pools = rp_api.list({ filter_names: name }).value 573 | debug("Search for resource pools found: " + resource_pools.map(&:name).to_s) 574 | end 575 | 576 | raise_if_missing resource_pools, format("Unable to find resource pool `%s`", name || "(default)") 577 | 578 | resource_pools.first.resource_pool 579 | end 580 | 581 | # The main connect method 582 | # 583 | def connect 584 | configuration = VSphereAutomation::Configuration.new.tap do |c| 585 | c.host = config[:vcenter_host] 586 | c.username = config[:vcenter_username] 587 | c.password = config[:vcenter_password] 588 | c.scheme = "https" 589 | c.verify_ssl = config[:vcenter_disable_ssl_verify] ? false : true 590 | c.verify_ssl_host = config[:vcenter_disable_ssl_verify] ? false : true 591 | end 592 | 593 | @api_client = VSphereAutomation::ApiClient.new(configuration) 594 | api_client.default_headers["Authorization"] = configuration.basic_auth_token 595 | 596 | session_api = VSphereAutomation::CIS::SessionApi.new(api_client) 597 | session_id = session_api.create("").value 598 | 599 | api_client.default_headers["vmware-api-session-id"] = session_id 600 | end 601 | end 602 | end 603 | end 604 | -------------------------------------------------------------------------------- /lib/support/clone_vm.rb: -------------------------------------------------------------------------------- 1 | require "kitchen" 2 | require "rbvmomi" 3 | 4 | require_relative "guest_customization" 5 | require_relative "guest_operations" 6 | 7 | class Support 8 | class CloneError < RuntimeError; end 9 | 10 | class CloneVm 11 | attr_reader :vim, :vem, :options, :ssl_verify, :src_vm, :vm, :vm_name, :ip, :guest_auth, :username 12 | 13 | include GuestCustomization 14 | 15 | def initialize(conn_opts, options) 16 | @options = options 17 | @vm_name = options[:vm_name] 18 | @ssl_verify = !conn_opts[:insecure] 19 | 20 | # Connect to vSphere 21 | @vim ||= RbVmomi::VIM.connect conn_opts 22 | @vem ||= vim.serviceContent.eventManager 23 | 24 | @username = options[:vm_username] 25 | password = options[:vm_password] 26 | @guest_auth = RbVmomi::VIM::NamePasswordAuthentication(interactiveSession: false, username: username, password: password) 27 | 28 | @benchmark_data = {} 29 | end 30 | 31 | def active_discovery? 32 | options[:active_discovery] == true 33 | end 34 | 35 | def ip_from_tools 36 | return if vm.guest.net.empty? 37 | 38 | # Don't simply use vm.guest.ipAddress to allow specifying a different interface 39 | nics = vm.guest.net 40 | if options[:interface] 41 | nics.select! { |nic| nic.network == options[:interface] } 42 | 43 | raise Support::CloneError.new(format("No interfaces found on VM which are attached to network '%s'", options[:interface])) if nics.empty? 44 | end 45 | 46 | vm_ip = nil 47 | nics.each do |net| 48 | vm_ip = net.ipConfig.ipAddress.detect { |addr| addr.origin != "linklayer" } 49 | break unless vm_ip.nil? 50 | end 51 | 52 | vm_ip&.ipAddress 53 | end 54 | 55 | def wait_for_tools(timeout = 30.0, interval = 2.0) 56 | start = Time.new 57 | 58 | loop do 59 | if vm.guest.toolsRunningStatus == "guestToolsRunning" 60 | benchmark_checkpoint("tools_detected") if benchmark? 61 | 62 | Kitchen.logger.debug format("Tools detected after %.1f seconds", Time.new - start) 63 | return 64 | end 65 | break if (Time.new - start) >= timeout 66 | 67 | sleep interval 68 | end 69 | 70 | raise Support::CloneError.new("Timeout waiting for VMware Tools") 71 | end 72 | 73 | def wait_for_ip(timeout = 60.0, interval = 2.0) 74 | start = Time.new 75 | 76 | ip = nil 77 | loop do 78 | ip = ip_from_tools 79 | if ip || (Time.new - start) >= timeout 80 | Kitchen.logger.debug format("IP retrieved after %.1f seconds", Time.new - start) if ip 81 | break 82 | end 83 | sleep interval 84 | end 85 | 86 | raise Support::CloneError.new("Timeout waiting for IP address") if ip.nil? 87 | raise Support::CloneError.new(format("Error getting accessible IP address, got %s. Check DHCP server and scope exhaustion", ip)) if ip =~ /^169\.254\./ 88 | 89 | # Allow IP rewriting (e.g. for 1:1 NAT) 90 | if options[:transform_ip] 91 | Kitchen.logger.info format("Received IP: %s", ip) 92 | 93 | # rubocop:disable Security/Eval 94 | ip = lambda { eval options[:transform_ip] }.call 95 | # rubocop:enable Security/Eval 96 | 97 | Kitchen.logger.info format("Transformed to IP: %s", ip) 98 | end 99 | 100 | @ip = ip 101 | end 102 | 103 | def benchmark? 104 | options[:benchmark] == true 105 | end 106 | 107 | def benchmark_file 108 | options[:benchmark_file] 109 | end 110 | 111 | def benchmark_start 112 | Kitchen.logger.debug("Starting benchmark data collection.") 113 | 114 | @benchmark_data = { 115 | template: options[:template], 116 | clonetype: options[:clone_type], 117 | checkpoints: [ 118 | { title: "timestamp", value: Time.new.to_f }, 119 | ], 120 | } 121 | end 122 | 123 | def benchmark_checkpoint(title) 124 | timestamp = Time.new 125 | checkpoints = @benchmark_data[:checkpoints] 126 | 127 | total = timestamp - checkpoints.first.fetch(:value) 128 | Kitchen.logger.debug format( 129 | 'Benchmark: Step "%s" at %d (%.1f since start)', 130 | title, timestamp, total.to_f 131 | ) 132 | 133 | @benchmark_data[:checkpoints] << { 134 | title: title.to_sym, 135 | value: total, 136 | } 137 | end 138 | 139 | def benchmark_persist 140 | # Add total time spent as well 141 | checkpoints = @benchmark_data[:checkpoints] 142 | checkpoints << { 143 | title: :total, 144 | value: Time.new - checkpoints.first.fetch(:value), 145 | } 146 | 147 | # Include CSV headers 148 | unless File.exist?(benchmark_file) 149 | header = "template, clonetype, active_discovery, " 150 | header += checkpoints.map { |entry| entry[:title] }.join(", ") + "\n" 151 | File.write(benchmark_file, header) 152 | end 153 | 154 | active_discovery = options[:active_discovery] || instant_clone? 155 | data = [@benchmark_data[:template], @benchmark_data[:clonetype], active_discovery.to_s] 156 | data << checkpoints.map { |entry| format("%.1f", entry[:value]) } 157 | 158 | file = File.new(benchmark_file, "a") 159 | file.puts(data.join(", ") + "\n") 160 | 161 | Kitchen.logger.debug format("Benchmark: Appended data to file %s", benchmark_file) 162 | end 163 | 164 | def detect_os(vm_or_template) 165 | vm_or_template.config&.guestId&.match(/^win/) ? :windows : :linux 166 | end 167 | 168 | def windows? 169 | options[:vm_os].downcase.to_sym == :windows 170 | end 171 | 172 | def linux? 173 | options[:vm_os].downcase.to_sym == :linux 174 | end 175 | 176 | # Network configured to update the existing one in the template 177 | # 178 | def networks_to_update 179 | options[:networks].select { |n| n[:operation] == "edit" } 180 | end 181 | 182 | # New networks that needs to be attached to newly created vm 183 | # 184 | def networks_to_add 185 | options[:networks].select { |n| [nil, "add"].include?(n[:operation]) } 186 | end 187 | 188 | # A network should update if there is a network_device available in the template 189 | # and the user configured a new network with edit operation. 190 | # 191 | def update_network?(network_device) 192 | networks_to_update.any? && network_device 193 | end 194 | 195 | # Checks whether any networks configured for addition 196 | def add_network? 197 | networks_to_add.any? 198 | end 199 | 200 | # TODO: Remove this method and its invocations after the deprecation of `network_name` config 201 | # For backward compatibility 202 | # If the template doesn't have any NIC and the user use the old 203 | # configuration(network_name), then that network should be attached to the vm. 204 | # 205 | def attach_new_network?(network_device) 206 | network_device.nil? && networks_to_update.any? 207 | end 208 | 209 | def network_device(vm) 210 | all_network_devices = vm.config.hardware.device.select do |device| 211 | device.is_a?(RbVmomi::VIM::VirtualEthernetCard) 212 | end 213 | 214 | # Only support for first NIC so far 215 | all_network_devices.first 216 | end 217 | 218 | def reconnect_network_device(vm) 219 | network_device = network_device(vm) 220 | return unless network_device 221 | 222 | network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo( 223 | allowGuestControl: true, 224 | startConnected: true, 225 | connected: true 226 | ) 227 | 228 | config_spec = RbVmomi::VIM.VirtualMachineConfigSpec( 229 | deviceChange: [ 230 | RbVmomi::VIM.VirtualDeviceConfigSpec( 231 | operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"), 232 | device: network_device 233 | ), 234 | ] 235 | ) 236 | 237 | task = vm.ReconfigVM_Task(spec: config_spec) 238 | task.wait_for_completion 239 | 240 | benchmark_checkpoint("nic_reconfigured") if benchmark? 241 | end 242 | 243 | def standard_ip_discovery 244 | Kitchen.logger.info format("Waiting for IP (timeout: %d seconds)...", options[:wait_timeout]) 245 | wait_for_ip(options[:wait_timeout], options[:wait_interval]) 246 | end 247 | 248 | def command_separator 249 | case options[:vm_os].downcase.to_sym 250 | when :linux 251 | " && " 252 | when :windows 253 | " & " 254 | end 255 | end 256 | 257 | # Rescan network adapters for MAC/IP changes 258 | def rescan_commands 259 | Kitchen.logger.info "Refreshing network interfaces in OS" 260 | 261 | case options[:vm_os].downcase.to_sym 262 | when :linux 263 | # @todo: allow override if no dhclient 264 | [ 265 | "/sbin/modprobe -r vmxnet3", 266 | "/sbin/modprobe vmxnet3", 267 | "/sbin/dhclient", 268 | ] 269 | when :windows 270 | [ 271 | "netsh interface set Interface #{options[:vm_win_network]} disable", 272 | "netsh interface set Interface #{options[:vm_win_network]} enable", 273 | "ipconfig /renew", 274 | ] 275 | end 276 | end 277 | 278 | # Available from VMware Tools 10.1.0 this pushes the IP instead of the standard 30 second poll 279 | # This will be used to provide a quick fallback, if active discovery fails. 280 | def trigger_tools 281 | case options[:vm_os].downcase.to_sym 282 | when :linux 283 | [ 284 | "/usr/bin/vmware-toolbox-cmd info update network", 285 | ] 286 | when :windows 287 | [ 288 | '"C:\Program Files\VMware\VMware Tools\VMwareToolboxCmd.exe" info update network', 289 | ] 290 | end 291 | end 292 | 293 | # Retrieve IP via OS commands 294 | def discovery_commands 295 | if options[:active_discovery_command].nil? 296 | case options[:vm_os].downcase.to_sym 297 | when :linux 298 | "ip address show scope global | grep global | cut -b10- | cut -d/ -f1" 299 | when :windows 300 | ["sleep 5", "ipconfig"] 301 | # "ipconfig /renew" 302 | # "wmic nicconfig get IPAddress", 303 | # "netsh interface ip show ipaddress #{options[:vm_win_network]}" 304 | end 305 | else 306 | options[:active_discovery_command] 307 | end 308 | end 309 | 310 | def active_ip_discovery(prefix_commands = []) 311 | # Instant clone needs this to have synchronous reply on the new IP 312 | return unless active_discovery? || instant_clone? 313 | 314 | Kitchen.logger.info "Attempting active IP discovery" 315 | begin 316 | tools = Support::GuestOperations.new(vim, vm, guest_auth, ssl_verify) 317 | 318 | commands = [] 319 | commands << rescan_commands if instant_clone? 320 | # commands << trigger_tools # deactivated for now, as benefit is doubtful 321 | commands << discovery_commands 322 | script = commands.flatten.join(command_separator) 323 | 324 | stdout = tools.run_shell_capture_output(script, :auto, 20) 325 | 326 | # Windows returns wrongly encoded UTF-8 for some reason 327 | stdout = stdout.bytes.map { |b| (32..126).cover?(b.ord) ? b.chr : nil }.join unless stdout.ascii_only? 328 | @ip = stdout.match(/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/m)&.captures&.first 329 | 330 | Kitchen.logger.debug format("Script output: %s", stdout) 331 | raise Support::CloneError.new(format("Could not find IP in script output, fallback to standard discovery")) if ip.nil? 332 | raise Support::CloneError.new(format("Error getting accessible IP address, got %s. Check DHCP server, scope exhaustion or timing issues", ip)) if ip =~ /^169\.254\./ 333 | rescue RbVmomi::Fault => e 334 | if e.fault.class.wsdl_name == "InvalidGuestLogin" 335 | message = format('Error authenticating to guest OS as "%s", check configuration of "vm_username"/"vm_password"', username) 336 | else 337 | message = e.message 338 | end 339 | 340 | raise Support::CloneError.new(message) 341 | rescue ::StandardError => e 342 | Kitchen.logger.info format("Active discovery failed: %s", e.message) 343 | return false 344 | end 345 | 346 | true 347 | end 348 | 349 | def check_add_disk_config(disk_config) 350 | valid_types = %w{thin flat flat_lazy flat_eager} 351 | 352 | unless valid_types.include? disk_config[:type].to_s 353 | message = format("Unknown disk type in add_disks: %s. Allowed: %s", 354 | disk_config[:type].to_s, 355 | valid_types.join(", ")) 356 | 357 | raise Support::CloneError.new(message) 358 | end 359 | end 360 | 361 | def vm_customization 362 | Kitchen.logger.info "Waiting for VM customization..." 363 | 364 | # Pass some contents right through 365 | # https://pubs.vmware.com/vsphere-6-5/index.jsp?topic=%2Fcom.vmware.wssdk.smssdk.doc%2Fvim.vm.ConfigSpec.html 366 | config = options[:vm_customization].select { |key, _| %i{annotation memoryMB numCPUs}.include? key } 367 | 368 | add_disks = options[:vm_customization]&.fetch(:add_disks, nil) 369 | unless add_disks.nil? 370 | config[:deviceChange] = [] 371 | 372 | # Will create a stem like "default-ubuntu-12345678/default-ubuntu-12345678" 373 | filename_base = vm.disks.first.backing.fileName.gsub(/(-[0-9]+)?.vmdk/, "") 374 | 375 | # Storage Controller and ID mapping 376 | controller = vm.config.hardware.device.select { |device| device.is_a? RbVmomi::VIM::VirtualSCSIController }.first 377 | 378 | # Move these variables outside the loop so they aren't overwritten 379 | highest_id = vm.disks.map(&:unitNumber).max 380 | next_id = highest_id 381 | 382 | add_disks.each_with_index do |disk_config, idx| 383 | # Default to Thin Provisioning and 10GB disk size 384 | disk_config[:type] ||= :thin 385 | disk_config[:size_mb] ||= 10240 386 | 387 | check_add_disk_config(disk_config) 388 | 389 | disk_spec = RbVmomi::VIM.VirtualDeviceConfigSpec( 390 | fileOperation: "create", 391 | operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("add"), 392 | device: RbVmomi::VIM.VirtualDisk( 393 | backing: RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo( 394 | thinProvisioned: true, 395 | diskMode: "persistent", 396 | fileName: format("%s_disk%03d.vmdk", filename_base, idx + 1), 397 | datastore: vm.disks.first.backing.datastore 398 | ), 399 | deviceInfo: RbVmomi::VIM::Description( 400 | label: format("Additional disk %d", idx + 1), 401 | summary: format("%d MB", disk_config[:size_mb]) 402 | ) 403 | ) 404 | ) 405 | 406 | # capacityInKB is marked a deprecated in 6.7 but still a required parameter 407 | disk_spec.device.capacityInBytes = disk_config[:size_mb] * 1024**2 408 | disk_spec.device.capacityInKB = disk_config[:size_mb] * 1024 409 | 410 | disk_spec.device.controllerKey = controller.key 411 | 412 | next_id += 1 413 | 414 | # Avoid the SCSI controller ID 415 | next_id += 1 if next_id == controller.scsiCtlrUnitNumber 416 | 417 | # Theoretically could add another SCSI controller, but there are limits to what kitchen should support 418 | if next_id > 14 419 | raise Support::CloneError.new(format("Ran out of SCSI IDs while trying to assign new disk %d", idx + 1)) 420 | end 421 | 422 | disk_spec.device.unitNumber = next_id 423 | 424 | device_keys = vm.config.hardware.device.map(&:key).sort 425 | disk_spec.device.key = device_keys.last + (idx + 1) * 1000 426 | 427 | disk_spec.device.backing.eagerlyScrub = true if disk_config[:type].to_s == "flat_eager" 428 | disk_spec.device.backing.thinProvisioned = false if disk_config[:type].to_s =~ /^flat/ 429 | 430 | config[:deviceChange] << disk_spec 431 | end 432 | end 433 | 434 | guestinfo = options[:vm_customization].select { |key, _| key =~ /^guestinfo\..*/ } 435 | unless guestinfo.empty? 436 | gi = guestinfo.map { |k, v| { key: k, value: v } } 437 | config[:extraConfig] = gi 438 | end 439 | 440 | config_spec = RbVmomi::VIM.VirtualMachineConfigSpec(config) 441 | 442 | task = vm.ReconfigVM_Task(spec: config_spec) 443 | task.wait_for_completion 444 | 445 | benchmark_checkpoint("reconfigured") if benchmark? 446 | end 447 | 448 | def instant_clone? 449 | options[:clone_type] == :instant 450 | end 451 | 452 | def linked_clone? 453 | options[:clone_type] == :linked 454 | end 455 | 456 | def full_clone? 457 | options[:clone_type] == :full 458 | end 459 | 460 | def root_folder 461 | @root_folder ||= vim.serviceInstance.content.rootFolder 462 | end 463 | 464 | # 465 | # @return [String] 466 | # 467 | def datacenter 468 | options[:datacenter] 469 | end 470 | 471 | # 472 | # @return [RbVmomi::VIM::Datacenter] 473 | # 474 | def find_datacenter 475 | vim.serviceInstance.find_datacenter(datacenter) 476 | rescue RbVmomi::Fault 477 | dc = root_folder.findByInventoryPath(datacenter) 478 | return dc if dc.is_a?(RbVmomi::VIM::Datacenter) 479 | 480 | raise Support::CloneError.new("Unable to locate datacenter at '#{datacenter}'") 481 | end 482 | 483 | def ip?(string) 484 | IPAddr.new(string) 485 | true 486 | rescue IPAddr::InvalidAddressError 487 | false 488 | end 489 | 490 | def vm_events(event_types = []) 491 | raise Support::CloneError.new("`vm_events` called before VM clone") unless vm 492 | 493 | vem.QueryEvents(filter: RbVmomi::VIM::EventFilterSpec( 494 | entity: RbVmomi::VIM::EventFilterSpecByEntity( 495 | entity: vm, 496 | recursion: RbVmomi::VIM::EventFilterSpecRecursionOption(:self) 497 | ), 498 | eventTypeId: event_types 499 | )) 500 | end 501 | 502 | # This method will fetch the network which is configured in the kitchen.yml file with 503 | # network_name configuration. 504 | # If there are multiple networks with the same name, first one will be used. 505 | # 506 | # @return Network object 507 | # 508 | def fetch_network(datacenter, network_name) 509 | networks = datacenter.network.select { |n| n.name == network_name } 510 | raise Support::CloneError, format("Could not find network named %s", network_name) if networks.empty? 511 | 512 | if networks.count > 1 513 | Kitchen.logger.warn( 514 | format("Found %d networks named %s, picking first one", networks.count, network_name) 515 | ) 516 | end 517 | networks.first 518 | end 519 | 520 | # This is a helper method that can be used to create the deviceChange spec which can be used 521 | # to add a new network device or update the existing network device 522 | # 523 | # The network_obj will be used as a backing for the network_device. 524 | def network_change_spec(network_device, network_obj, network_name, operation: :edit) 525 | if network_obj.is_a? RbVmomi::VIM::DistributedVirtualPortgroup 526 | Kitchen.logger.info format("Assigning network %s...", network_obj.pretty_path) 527 | 528 | vds_obj = network_obj.config.distributedVirtualSwitch 529 | Kitchen.logger.info format("Using vDS '%s' for network connectivity...", vds_obj.name) 530 | 531 | network_device.backing = RbVmomi::VIM.VirtualEthernetCardDistributedVirtualPortBackingInfo( 532 | port: RbVmomi::VIM.DistributedVirtualSwitchPortConnection( 533 | portgroupKey: network_obj.key, 534 | switchUuid: vds_obj.uuid 535 | ) 536 | ) 537 | elsif network_obj.is_a? RbVmomi::VIM::Network 538 | Kitchen.logger.info format("Assigning network %s...", network_name) 539 | 540 | network_device.backing = RbVmomi::VIM.VirtualEthernetCardNetworkBackingInfo( 541 | deviceName: network_name 542 | ) 543 | else 544 | raise Support::CloneError, format("Unknown network type %s for network name %s", network_obj.class.to_s, network_name) 545 | end 546 | 547 | RbVmomi::VIM.VirtualDeviceConfigSpec( 548 | operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation(operation), 549 | device: network_device 550 | ) 551 | end 552 | 553 | # This method can be used to add new network device to the target vm 554 | # This fill find the network which defined in kitchen.yml in network_name configuration 555 | # and attach that to the target vm. 556 | def add_new_network_device(datacenter, networks) 557 | device_change = [] 558 | networks.each do |network| 559 | network_obj = fetch_network(datacenter, network[:name]) 560 | network_device = RbVmomi::VIM.VirtualVmxnet3( 561 | key: 0, 562 | deviceInfo: { 563 | label: network[:name], 564 | summary: network[:name], 565 | } 566 | ) 567 | 568 | device_change << network_change_spec(network_device, network_obj, network[:name], operation: :add) 569 | end 570 | 571 | config_spec = RbVmomi::VIM.VirtualMachineConfigSpec( 572 | { 573 | deviceChange: device_change, 574 | } 575 | ) 576 | 577 | task = vm.ReconfigVM_Task(spec: config_spec) 578 | task.wait_for_completion 579 | end 580 | 581 | def clone 582 | benchmark_start if benchmark? 583 | 584 | # set the datacenter name 585 | dc = find_datacenter 586 | 587 | # reference template using full inventory path 588 | inventory_path = format("/%s/vm/%s", datacenter, options[:template]) 589 | @src_vm = root_folder.findByInventoryPath(inventory_path) 590 | raise Support::CloneError.new(format("Unable to find template: %s", options[:template])) if src_vm.nil? 591 | 592 | if src_vm.config.template && !full_clone? 593 | Kitchen.logger.warn "Source is a template, thus falling back to full clone. Reference a VM for linked/instant clones." 594 | options[:clone_type] = :full 595 | end 596 | 597 | if src_vm.snapshot.nil? && !full_clone? 598 | Kitchen.logger.warn "Source VM has no snapshot available, thus falling back to full clone. Create a snapshot for linked/instant clones." 599 | options[:clone_type] = :full 600 | end 601 | 602 | # Autodetect OS, if none given 603 | if options[:vm_os].nil? 604 | os = detect_os(src_vm) 605 | Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize) 606 | options[:vm_os] = os 607 | end 608 | 609 | # Specify where the machine is going to be created 610 | relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec 611 | 612 | # Setting the host is not allowed for instant clone due to VM memory sharing 613 | relocate_spec.host = options[:targethost].host unless instant_clone? 614 | 615 | # Change to delta disks for linked clones 616 | relocate_spec.diskMoveType = :moveChildMostDiskBacking if linked_clone? 617 | 618 | # Set the resource pool 619 | relocate_spec.pool = options[:resource_pool] 620 | 621 | # Change network, if wanted 622 | network_device = network_device(src_vm) 623 | Kitchen.logger.warn format("Source VM/template does not have any network device (use VMware IPPools and vsphere-gom transport or govc to access)") unless network_device 624 | 625 | if update_network?(network_device) 626 | network_spec = [] 627 | networks_to_update.each do |network| 628 | network_obj = fetch_network(dc, network[:name]) 629 | network_spec << network_change_spec(network_device, network_obj, network[:name]) 630 | end 631 | relocate_spec.deviceChange = network_spec 632 | end 633 | 634 | # Set the folder to use 635 | dest_folder = options[:folder].nil? ? dc.vmFolder : options[:folder][:id] 636 | 637 | Kitchen.logger.info format("Cloning '%s' to create the %s VM...", options[:template], vm_name) 638 | if instant_clone? 639 | vcenter_data = vim.serviceInstance.content.about 640 | raise Support::CloneError.new("Instant clones only supported with vCenter 6.7 or higher") unless vcenter_data.version.to_f >= 6.7 641 | 642 | Kitchen.logger.debug format("Detected %s", vcenter_data.fullName) 643 | 644 | resources = dc.hostFolder.children 645 | hosts = resources.select { |resource| resource.class.to_s =~ /ComputeResource$/ }.map(&:host).flatten 646 | targethost = hosts.select { |host| host.summary.config.name == options[:targethost].name }.first 647 | raise Support::CloneError.new("No matching ComputeResource found in host folder") if targethost.nil? 648 | 649 | esx_data = targethost.summary.config.product 650 | raise Support::CloneError.new("Instant clones only supported with ESX 6.7 or higher") unless esx_data.version.to_f >= 6.7 651 | 652 | Kitchen.logger.debug format("Detected %s", esx_data.fullName) 653 | 654 | # Other tools check for VMWare Tools status, but that will be toolsNotRunning on frozen VMs 655 | raise Support::CloneError.new("Need a running VM for instant clones") unless src_vm.runtime.powerState == "poweredOn" 656 | 657 | # In first iterations, only support the Frozen Source VM workflow. This is more efficient 658 | # but needs preparations (freezing the source VM). Running Source VM support is to be 659 | # added later 660 | raise Support::CloneError.new("Need a frozen VM for instant clones, running source VM not supported yet") unless src_vm.runtime.instantCloneFrozen 661 | 662 | # Swapping NICs not needed anymore (blog posts mention this), instant clones get a new 663 | # MAC at least with 6.7.0 build 9433931 664 | 665 | # Disconnect network device, so wo don't get IP collisions on start 666 | if network_device 667 | network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo( 668 | allowGuestControl: true, 669 | startConnected: true, 670 | connected: false, 671 | migrateConnect: "disconnect" 672 | ) 673 | relocate_spec.deviceChange = [ 674 | RbVmomi::VIM.VirtualDeviceConfigSpec( 675 | operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"), 676 | device: network_device 677 | ), 678 | ] 679 | end 680 | 681 | clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec, 682 | name: vm_name) 683 | 684 | benchmark_checkpoint("initialized") if benchmark? 685 | task = src_vm.InstantClone_Task(spec: clone_spec) 686 | else 687 | clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec( 688 | location: relocate_spec, 689 | powerOn: options[:poweron] && options[:vm_customization].nil?, 690 | template: false 691 | ) 692 | 693 | clone_spec.customization = guest_customization_spec if options[:guest_customization] 694 | 695 | benchmark_checkpoint("initialized") if benchmark? 696 | task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: vm_name) 697 | end 698 | task.wait_for_completion 699 | 700 | benchmark_checkpoint("cloned") if benchmark? 701 | 702 | # get the IP address of the machine for bootstrapping 703 | # machine name is based on the path, e.g. that includes the folder 704 | path = options[:folder].nil? ? vm_name : format("%s/%s", options[:folder][:name], vm_name) 705 | @vm = dc.find_vm(path) 706 | raise Support::CloneError.new(format("Unable to find machine: %s", path)) if vm.nil? 707 | 708 | # Reconnect network device after Instant Clone is ready 709 | if instant_clone? 710 | Kitchen.logger.info "Reconnecting network adapter" 711 | reconnect_network_device(vm) 712 | end 713 | 714 | vm_customization if options[:vm_customization] 715 | 716 | # TODO: Remove this line after the deprecation of `network_name` config 717 | add_new_network_device(dc, networks_to_update) if attach_new_network?(network_device) 718 | 719 | add_new_network_device(dc, networks_to_add) if add_network? 720 | 721 | # Start only if specified or customizations wanted; no need for instant clones as they start in running state 722 | if options[:poweron] && !options[:vm_customization].nil? && !instant_clone? 723 | task = vm.PowerOnVM_Task 724 | task.wait_for_completion 725 | end 726 | benchmark_checkpoint("powered_on") if benchmark? 727 | 728 | # Windows customization takes a while, so check for its completion 729 | guest_customization_wait if options[:guest_customization] 730 | 731 | Kitchen.logger.info format("Waiting for VMware tools to become available (timeout: %d seconds)...", options[:wait_timeout]) 732 | wait_for_tools(options[:wait_timeout], options[:wait_interval]) 733 | 734 | active_ip_discovery || standard_ip_discovery 735 | benchmark_checkpoint("ip_detected") if benchmark? 736 | 737 | benchmark_persist if benchmark? 738 | Kitchen.logger.info format("Created machine %s with IP %s", vm_name, ip) 739 | end 740 | end 741 | end 742 | -------------------------------------------------------------------------------- /lib/support/guest_customization.rb: -------------------------------------------------------------------------------- 1 | require "net/ping" 2 | require "rbvmomi" 3 | 4 | class Support 5 | class GuestCustomizationError < RuntimeError; end 6 | class GuestCustomizationOptionsError < RuntimeError; end 7 | 8 | module GuestCustomization 9 | DEFAULT_LINUX_TIMEZONE = "Etc/UTC".freeze 10 | DEFAULT_WINDOWS_ORG = "TestKitchen".freeze 11 | DEFAULT_WINDOWS_TIMEZONE = 0x80000050 # Etc/UTC 12 | DEFAULT_TIMEOUT_TASK = 600 13 | DEFAULT_TIMEOUT_IP = 60 14 | 15 | # Generic Volume License Keys for temporary Windows Server setup. 16 | # 17 | # @see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys 18 | WINDOWS_KMS_KEYS = { 19 | "Microsoft Windows Server 2019 (64-bit)" => "N69G4-B89J2-4G8F4-WWYCC-J464C", 20 | "Microsoft Windows Server 2016 (64-bit)" => "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY", 21 | "Microsoft Windows Server 2012R2 (64-bit)" => "D2N9P-3P6X9-2R39C-7RTCD-MDVJX", 22 | "Microsoft Windows Server 2012 (64-bit)" => "BN3D2-R7TKB-3YPBD-8DRP2-27GG4", 23 | }.freeze 24 | 25 | # Configuration values for Guest Customization 26 | # 27 | # @returns [Hash] Configuration values from file 28 | def guest_customization 29 | options[:guest_customization] 30 | end 31 | 32 | # Build CustomizationSpec for Guest OS Customization 33 | # 34 | # @returns [RbVmomi::VIM::CustomizationSpec] Customization Spec for guest adjustments 35 | def guest_customization_spec 36 | return unless guest_customization 37 | 38 | guest_customization_validate_options 39 | 40 | if guest_customization[:ip_address] 41 | customized_ip = RbVmomi::VIM::CustomizationIPSettings.new( 42 | ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: guest_customization[:ip_address]), 43 | gateway: guest_customization[:gateway], 44 | subnetMask: guest_customization[:subnet_mask], 45 | dnsDomain: guest_customization[:dns_domain] 46 | ) 47 | else 48 | customized_ip = RbVmomi::VIM::CustomizationIPSettings.new( 49 | ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new, 50 | dnsDomain: guest_customization[:dns_domain] 51 | ) 52 | end 53 | 54 | RbVmomi::VIM::CustomizationSpec.new( 55 | identity: guest_customization_identity, 56 | globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new( 57 | dnsServerList: guest_customization[:dns_server_list], 58 | dnsSuffixList: guest_customization[:dns_suffix_list] 59 | ), 60 | nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new( 61 | adapter: customized_ip 62 | )] 63 | ) 64 | end 65 | 66 | # Check options for existance and format 67 | # 68 | # @raise [Support::GuestCustomizationOptionsError] For any violation 69 | def guest_customization_validate_options 70 | if guest_customization_ip_change? 71 | unless ip?(guest_customization[:ip_address]) 72 | raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` is required to be formatted as an IPv4 address") 73 | end 74 | 75 | unless guest_customization[:subnet_mask] 76 | raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required if assigning a fixed IPv4 address") 77 | end 78 | 79 | unless ip?(guest_customization[:subnet_mask]) 80 | raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required to be formatted as an IPv4 address") 81 | end 82 | 83 | if up?(guest_customization[:ip_address]) 84 | raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` points to a host reachable via ICMP") unless guest_customization[:continue_on_ip_conflict] 85 | 86 | Kitchen.logger.warn("Continuing customization despite `ip_address` conflicting with a reachable host per user request") 87 | end 88 | end 89 | 90 | if guest_customization[:gateway] 91 | unless guest_customization[:gateway].is_a?(Array) 92 | raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` must be an array") 93 | end 94 | 95 | guest_customization[:gateway].each do |v| 96 | unless ip?(v) 97 | raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` is required to be formatted as an IPv4 address") 98 | end 99 | end 100 | end 101 | 102 | required = %i{dns_domain dns_server_list dns_suffix_list} 103 | missing = required - guest_customization.keys 104 | unless missing.empty? 105 | raise Support::GuestCustomizationOptionsError.new("Parameters `#{missing.join("`, `")}` are required to support guest customization") 106 | end 107 | 108 | guest_customization[:dns_server_list].each do |v| 109 | unless ip?(v) 110 | raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` is required to be formatted as an IPv4 address") 111 | end 112 | end 113 | 114 | if !guest_customization[:dns_server_list].is_a?(Array) 115 | raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` must be an array") 116 | elsif !guest_customization[:dns_suffix_list].is_a?(Array) 117 | raise Support::GuestCustomizationOptionsError.new("Parameter `dns_suffix_list` must be an array") 118 | end 119 | end 120 | 121 | # Check if an IP change is requested 122 | # 123 | # @returns [Boolean] If `ip_address` is to be changed 124 | def guest_customization_ip_change? 125 | guest_customization[:ip_address] 126 | end 127 | 128 | # Return OS-specific CustomizationIdentity object 129 | def guest_customization_identity 130 | if linux? 131 | guest_customization_identity_linux 132 | elsif windows? 133 | guest_customization_identity_windows 134 | else 135 | raise Support::GuestCustomizationError.new("Unknown OS, no valid customization found") 136 | end 137 | end 138 | 139 | # Construct Linux-specific customization information 140 | def guest_customization_identity_linux 141 | timezone = guest_customization[:timezone] 142 | if timezone && !valid_linux_timezone?(timezone) 143 | raise Support::GuestCustomizationError.new <<~ERROR 144 | Linux customization requires `timezone` in `Area/Location` format. 145 | See https://kb.vmware.com/s/article/2145518 146 | ERROR 147 | end 148 | 149 | Kitchen.logger.warn("Linux guest customization: No timezone passed, assuming UTC") unless timezone 150 | 151 | RbVmomi::VIM::CustomizationLinuxPrep.new( 152 | domain: guest_customization[:dns_domain], 153 | hostName: guest_hostname, 154 | hwClockUTC: true, 155 | timeZone: timezone || DEFAULT_LINUX_TIMEZONE 156 | ) 157 | end 158 | 159 | # Construct Windows-specific customization information 160 | def guest_customization_identity_windows 161 | timezone = guest_customization[:timezone] 162 | if timezone && !valid_windows_timezone?(timezone) 163 | raise Support::GuestCustomizationOptionsError.new <<~ERROR 164 | Windows customization requires `timezone` as decimal number or hex number (0x55). 165 | See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values 166 | ERROR 167 | end 168 | 169 | Kitchen.logger.warn("Windows guest customization: No timezone passed, assuming UTC") unless timezone 170 | 171 | product_id = guest_customization[:product_id] 172 | 173 | # Try to look up and use a known, documented 120-day trial key 174 | unless product_id 175 | guest_os = src_vm.guest&.guestFullName 176 | product_id = windows_kms_for_guest(guest_os) 177 | 178 | Kitchen.logger.warn format("Windows guest customization:: Using KMS Key `%s` for %s", key: product_id, os: guest_os) if product_id 179 | end 180 | 181 | unless valid_windows_key? product_id 182 | raise Support::GuestCustomizationOptionsError.new <<~ERROR 183 | Windows customization requires `product_id` to work. Add a valid product key or 184 | see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys for KMS trial keys 185 | ERROR 186 | end 187 | 188 | customization_pass = nil 189 | if guest_customization[:administrator_password] 190 | customization_pass = RbVmomi::VIM::CustomizationPassword.new( 191 | plainText: true, 192 | value: guest_customization[:administrator_password] 193 | ) 194 | end 195 | 196 | RbVmomi::VIM::CustomizationSysprep.new( 197 | guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new( 198 | timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE, 199 | autoLogon: false, 200 | autoLogonCount: 1, 201 | password: customization_pass 202 | ), 203 | identification: RbVmomi::VIM::CustomizationIdentification.new, 204 | userData: RbVmomi::VIM::CustomizationUserData.new( 205 | computerName: guest_hostname, 206 | fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG, 207 | orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG, 208 | productId: product_id 209 | ) 210 | ) 211 | end 212 | 213 | # Check if a host is reachable 214 | def up?(host) 215 | check = Net::Ping::External.new(host) 216 | check.ping? 217 | end 218 | 219 | # Retrieve a GVLK (evaluation key) for the named OS 220 | # 221 | # @param [String] name Name of the OS as reported by VMware 222 | # @returns [String] GVLK key, if any 223 | def windows_kms_for_guest(name) 224 | WINDOWS_KMS_KEYS.fetch(name, false) 225 | end 226 | 227 | # Check format of Linux-specific timezone, according to VMware support 228 | # 229 | # @param [Integer] input Value to check for validity 230 | # @returns [Boolean] if value is valid 231 | def valid_linux_timezone?(input) 232 | # Specific to VMware: https://kb.vmware.com/s/article/2145518 233 | linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$} 234 | 235 | input.to_s.match? linux_timezone_pattern 236 | end 237 | 238 | # Check format of Windows-specific timezone 239 | # 240 | # @param [Integer] input Value to check for validity 241 | # @returns [Boolean] if value is valid 242 | def valid_windows_timezone?(input) 243 | # Accept decimals and hex 244 | # See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values 245 | windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/ 246 | 247 | input.to_s.match? windows_timezone_pattern 248 | end 249 | 250 | # Check for format of Windows Product IDs 251 | # 252 | # @param [String] input String to check 253 | # @returns [Boolean] if value is in Windows Key format 254 | def valid_windows_key?(input) 255 | windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/ 256 | 257 | input.to_s.match? windows_key_pattern 258 | end 259 | 260 | # Return Guest hostname to be configured and check for validity. 261 | # 262 | # @returns [String] New hostname to assign 263 | def guest_hostname 264 | hostname = guest_customization[:hostname] || options[:vm_name] 265 | 266 | hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/ 267 | unless hostname.match?(hostname_pattern) 268 | raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed") 269 | end 270 | 271 | RbVmomi::VIM::CustomizationFixedName.new(name: hostname) 272 | end 273 | 274 | # Wait for vSphere task completion and subsequent IP address update (if any). 275 | def guest_customization_wait 276 | guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK) 277 | guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP) 278 | end 279 | 280 | # Wait for Guest customization to finish successfully. 281 | # 282 | # @param [Integer] timeout Timeout in seconds 283 | # @param [Integer] sleep_time Time to wait between tries 284 | def guest_customization_wait_task(timeout = 600, sleep_time = 10) 285 | waited_seconds = 0 286 | 287 | Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..." 288 | 289 | while waited_seconds < timeout 290 | events = guest_customization_events 291 | 292 | if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded } 293 | return 294 | elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed }) 295 | # Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools 296 | raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}") 297 | end 298 | 299 | sleep(sleep_time) 300 | waited_seconds += sleep_time 301 | end 302 | 303 | raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.") 304 | end 305 | 306 | # Wait for new IP to be reported, if any. 307 | # 308 | # @param [Integer] timeout Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds 309 | # @param [Integer] sleep_time Time to wait between tries 310 | def guest_customization_wait_ip(timeout = 30, sleep_time = 1) 311 | return unless guest_customization_ip_change? 312 | 313 | waited_seconds = 0 314 | 315 | Kitchen.logger.info "Waiting for guest customization IP update..." 316 | 317 | while waited_seconds < timeout 318 | found_ip = wait_for_ip(timeout, 1.0) 319 | 320 | return if found_ip == guest_customization[:ip_address] 321 | 322 | sleep(sleep_time) 323 | waited_seconds += sleep_time 324 | end 325 | 326 | raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.") 327 | end 328 | 329 | # Filter Customization events for the current VM 330 | # 331 | # @returns [Array] All matching events 332 | def guest_customization_events 333 | vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent} 334 | end 335 | end 336 | end 337 | -------------------------------------------------------------------------------- /lib/support/guest_operations.rb: -------------------------------------------------------------------------------- 1 | require "rbvmomi" 2 | require "net/http" unless defined?(Net::HTTP) 3 | 4 | class Support 5 | # Encapsulate VMware Tools GOM interaction, inspired by github:dnuffer/raidopt 6 | class GuestOperations 7 | attr_reader :gom, :vm, :guest_auth, :ssl_verify 8 | 9 | def initialize(vim, vm, guest_auth, ssl_verify = true) 10 | @gom = vim.serviceContent.guestOperationsManager 11 | @vm = vm 12 | @guest_auth = guest_auth 13 | @ssl_verify = ssl_verify 14 | end 15 | 16 | def os_family 17 | return vm.guest.guestFamily == "windowsGuest" ? :windows : :linux if vm.guest.guestFamily 18 | 19 | # VMware tools are not initialized or missing, infer from Guest Id 20 | vm.config&.guestId&.match(/^win/) ? :windows : :linux 21 | end 22 | 23 | def linux? 24 | os_family == :linux 25 | end 26 | 27 | def windows? 28 | os_family == :windows 29 | end 30 | 31 | def delete_dir(dir) 32 | gom.fileManager.DeleteDirectoryInGuest(vm: vm, auth: guest_auth, directoryPath: dir, recursive: true) 33 | end 34 | 35 | def process_is_running(pid) 36 | procs = gom.processManager.ListProcessesInGuest(vm: vm, auth: guest_auth, pids: [pid]) 37 | procs.empty? || procs.any? { |gpi| gpi.exitCode.nil? } 38 | end 39 | 40 | def process_exit_code(pid) 41 | gom.processManager.ListProcessesInGuest(vm: vm, auth: guest_auth, pids: [pid])&.first&.exitCode 42 | end 43 | 44 | def wait_for_process_exit(pid, timeout = 60.0, interval = 1.0) 45 | start = Time.new 46 | 47 | loop do 48 | return unless process_is_running(pid) 49 | break if (Time.new - start) >= timeout 50 | 51 | sleep interval 52 | end 53 | 54 | raise format("Timeout waiting for process %d to exit after %d seconds", pid, timeout) if (Time.new - start) >= timeout 55 | end 56 | 57 | def run_program(path, args = "", timeout = 60.0) 58 | Kitchen.logger.debug format("Running %s %s", path, args) 59 | 60 | pid = gom.processManager.StartProgramInGuest(vm: vm, auth: guest_auth, spec: RbVmomi::VIM::GuestProgramSpec.new(programPath: path, arguments: args)) 61 | wait_for_process_exit(pid, timeout) 62 | 63 | exit_code = process_exit_code(pid) 64 | raise format("Failed to run '%s %s'. Exit code: %d", path, args, exit_code) if exit_code != 0 65 | 66 | exit_code 67 | end 68 | 69 | def run_shell_capture_output(command, shell = :auto, timeout = 60.0) 70 | if shell == :auto 71 | shell = :linux if linux? 72 | shell = :cmd if windows? 73 | end 74 | 75 | if shell == :linux 76 | tmp_out_fname = format("/tmp/vm_utils_run_out_%s", Random.rand) 77 | tmp_err_fname = format("/tmp/vm_utils_run_err_%s", Random.rand) 78 | shell = "/bin/sh" 79 | args = format("-c '(%s) > %s 2> %s'", command.gsub("'", %q{\\\'}), tmp_out_fname, tmp_err_fname) 80 | elsif shell == :cmd 81 | tmp_out_fname = format('C:\Windows\TEMP\vm_utils_run_out_%s', Random.rand) 82 | tmp_err_fname = format('C:\Windows\TEMP\vm_utils_run_err_%s', Random.rand) 83 | shell = "cmd.exe" 84 | args = format('/c "%s > %s 2> %s"', command.gsub("\"", %q{\\\"}), tmp_out_fname, tmp_err_fname) 85 | elsif shell == :powershell 86 | tmp_out_fname = format('C:\Windows\TEMP\vm_utils_run_out_%s', Random.rand) 87 | tmp_err_fname = format('C:\Windows\TEMP\vm_utils_run_err_%s', Random.rand) 88 | shell = 'C:\Windows\System32\WindowsPowershell\v1.0\powershell.exe' 89 | args = format('-Command "%s > %s 2> %s"', command.gsub("\"", %q{\\\"}), tmp_out_fname, tmp_err_fname) 90 | end 91 | 92 | begin 93 | exit_code = run_program(shell, args, timeout) 94 | rescue StandardError 95 | proc_err = "" # read_file(tmp_err_fname) 96 | raise format("Error executing command %s. Exit code: %d. StdErr %s", command, exit_code, proc_err) 97 | end 98 | 99 | read_file(tmp_out_fname) 100 | end 101 | 102 | def write_file(remote_file, contents) 103 | # Required privilege: VirtualMachine.GuestOperations.Modify 104 | put_url = gom.fileManager.InitiateFileTransferToGuest( 105 | vm: vm, 106 | auth: guest_auth, 107 | guestFilePath: remote_file, 108 | fileAttributes: RbVmomi::VIM::GuestFileAttributes(), 109 | fileSize: contents.size, 110 | overwrite: true 111 | ) 112 | put_url = put_url.gsub(%r{^https://\*:}, format("https://%s:%s", vm._connection.host, put_url)) 113 | uri = URI.parse(put_url) 114 | 115 | request = Net::HTTP::Put.new(uri.request_uri) 116 | request["Transfer-Encoding"] = "chunked" 117 | request["Content-Length"] = contents.size 118 | request.body = contents 119 | 120 | http = Net::HTTP.new(uri.host, uri.port) 121 | http.use_ssl = (uri.scheme == "https") 122 | http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 123 | http.request(request) 124 | end 125 | 126 | def read_file(remote_file) 127 | download_file(remote_file, nil) 128 | end 129 | 130 | def upload_file(local_file, remote_file) 131 | Kitchen.logger.debug format("Copy %s to %s", local_file, remote_file) 132 | write_file(remote_file, File.open(local_file, "rb").read) 133 | end 134 | 135 | def download_file(remote_file, local_file) 136 | info = gom.fileManager.InitiateFileTransferFromGuest(vm: vm, auth: guest_auth, guestFilePath: remote_file) 137 | uri = URI.parse(info.url) 138 | 139 | request = Net::HTTP::Get.new(uri.request_uri) 140 | http = Net::HTTP.new(uri.host, uri.port) 141 | http.use_ssl = (uri.scheme == "https") 142 | http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 143 | response = http.request(request) 144 | 145 | if response.body.size != info.size 146 | raise format("Downloaded file has different size than reported: %s (%d bytes instead of %d bytes)", remote_file, response.body.size, info.size) 147 | end 148 | 149 | local_file.nil? ? response.body : File.open(local_file, "w") { |file| file.write(response.body) } 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | RSpec.configure do |config| 15 | # Run specs in random order to surface order dependencies. If you find an 16 | # order dependency and want to debug it, you can fix the order by providing 17 | # the seed, which is printed after each run. 18 | # --seed 1234 19 | config.order = :random 20 | 21 | # Seed global randomization in this process using the `--seed` CLI option. 22 | # Setting this allows you to use `--seed` to deterministically reproduce 23 | # test failures related to randomization by passing the same `--seed` value 24 | # as the one that triggered the failure. 25 | Kernel.srand config.seed 26 | 27 | config.expose_dsl_globally = true 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/kitchen/driver/vcenter_spec.rb: -------------------------------------------------------------------------------- 1 | require "kitchen/driver/vcenter" 2 | 3 | describe Kitchen::Driver::Vcenter do 4 | end 5 | -------------------------------------------------------------------------------- /spec/unit/support/clone_vm_spec.rb: -------------------------------------------------------------------------------- 1 | require "support/clone_vm" 2 | require "securerandom" 3 | 4 | describe Support::CloneVm do 5 | subject { described_class.new(connection_options, options) } 6 | 7 | let(:vcenter_host) { "my-vcenter-dream.company.com" } 8 | let(:vcenter_username) { "barney" } 9 | let(:vcenter_password) { "Sssh...I love Chef <3" } 10 | let(:vcenter_disable_ssl_verify) { false } 11 | let(:clone_type) { :full } 12 | let(:connection_options) do 13 | { 14 | user: vcenter_username, 15 | password: vcenter_password, 16 | insecure: false, 17 | host: vcenter_host, 18 | rev: "6.7", 19 | cookie: nil, 20 | ssl: true, 21 | port: 443, 22 | path: "/sdk", 23 | ns: "urn:vim25", 24 | debug: false, 25 | } 26 | end 27 | 28 | let(:folder_name) { "my-folder" } 29 | let(:folder_id) { "group-v123" } 30 | 31 | let(:options) { 32 | { 33 | vm_name: vm_name, 34 | targethost: target_host, 35 | poweron: true, 36 | template: "other-folder/my-template", 37 | datacenter: datacenter_name, 38 | folder: { 39 | name: folder_name, 40 | id: folder_id, 41 | }, 42 | resource_pool: "resgroup-123", 43 | clone_type: :full, 44 | networks: [network_hash], 45 | interface: nil, 46 | wait_timeout: 540, 47 | wait_interval: 2.0, 48 | vm_customization: nil, 49 | guest_customization: nil, 50 | active_discovery: false, 51 | active_discovery_command: nil, 52 | vm_os: nil, 53 | vm_username: vm_username, 54 | vm_password: vm_password, 55 | vm_win_network: "Ethernet0", 56 | transform_ip: nil, 57 | benchmark: false, 58 | benchmark_file: "kitchen-vcenter.csv", 59 | } 60 | } 61 | let(:vm_name) { "my-vm-name" } 62 | let(:network_name) { "my-network" } 63 | let(:network_hash) { { name: "my-network", operation: "edit" } } 64 | let(:target_host) { instance_double("VSphereAutomation::VCenter::VcenterHostSummary", host: "host-123", name: "host-123.company.com", connection_state: "CONNECTED", power_state: "POWERED_ON") } 65 | let(:datacenter_name) { "my-datacenter" } 66 | let(:datacenter) { instance_double("RbVmomi::VIM::Datacenter", network: [network]) } 67 | let(:vm_username) { "vm-username" } 68 | let(:vm_password) { "vm-password" } 69 | 70 | let(:network) { instance_double("RbVmomi::VIM::DistributedVirtualPortgroup", name: network_name, pretty_path: "#{datacenter_name}/network/#{network_name}", config: network_config, key: "dvportgroup-123") } 71 | let(:network_config) { instance_double("RbVmomi::VIM::DVPortgroupConfigInfo", distributedVirtualSwitch: distributed_virtual_switch) } 72 | let(:distributed_virtual_switch) { instance_double("RbVmomi::VIM::VmwareDistributedVirtualSwitch", name: "dvs-123", uuid: SecureRandom.hex) } 73 | 74 | let(:rbvmomi_vim) { instance_double("RbVmomi::VIM", serviceContent: service_content, serviceInstance: service_instance) } 75 | let(:service_content) { instance_double("RbVmomi::ServiceContent", eventManager: event_manager ) } 76 | let(:event_manager) { instance_double("RbVmomi::EventManager") } 77 | let(:service_instance) { instance_double("RbVmomi::ServiceInstance", find_datacenter: datacenter, content: content) } 78 | let(:content) { instance_double("RbVmomi::ServiceInstance::Content", rootFolder: root_folder) } 79 | let(:root_folder) { instance_double("RbVmomi::ServiceInstance::Inv", findByInventoryPath: source_vm) } 80 | 81 | # Source VM 82 | let(:source_vm) { instance_double("RbVmomi::ServiceInstance::VM", config: source_vm_config, snapshot: nil) } 83 | let(:source_vm_config) { instance_double("RbVmomi::ServiceInstance::VM::Config", template: source_vm_template, guestId: "redhat", hardware: source_vm_hardware) } 84 | let(:source_vm_template) { instance_double("RbVmomi::ServiceInstance::VM::Template") } 85 | let(:source_vm_hardware) { instance_double("RbVmomi::VIM::VirtualHardware", device: [source_vm_network_device]) } 86 | let(:source_vm_network_device) { instance_double("RbVmomi::VIM::VirtualVmxnet3") } 87 | 88 | # Created VM 89 | let(:created_vm) { instance_double("RbVmomi::VIM::VirtualMachine", config: created_vm_config, snapshot: nil, guest: created_vm_guest, PowerOnVM_Task: created_vm_power_on_task) } 90 | let(:created_vm_config) { instance_double("RbVmomi::VIM::VirtualMachine", config: cloned_vm_config, snapshot: nil) } 91 | let(:created_vm_config) { instance_double("RbVmomi::ServiceInstance::VM::Config", guestId: "redhat", hardware: created_vm_hardware) } 92 | let(:created_vm_hardware) { instance_double("RbVmomi::VIM::VirtualHardware", device: [created_vm_network_device]) } 93 | let(:created_vm_network_device) { instance_double("RbVmomi::VIM::VirtualVmxnet3") } 94 | let(:created_vm_guest) { instance_double("VMGuest", toolsRunningStatus: "guestToolsRunning", net: [created_vm_guest_network]) } 95 | let(:created_vm_guest_network) { instance_double("RbVmomi::VIM::GuestNicInfo", ipConfig: created_vm_guest_ipconfig) } 96 | let(:created_vm_guest_ipconfig) { instance_double("RbVmomi::VIM::GuestIpConfig", ipAddress: [created_vm_guest_ipaddress]) } 97 | let(:created_vm_guest_ipaddress) { instance_double("RbVmomi::VIM::NetIpConfigInfoIpAddress", origin: "not_linklayer", ipAddress: "192.168.5.2") } 98 | let(:created_vm_power_on_task) { instance_double("VMPowerOnTask", wait_for_completion: nil) } 99 | let(:created_vm_reconfigure_task) { instance_double("VMReconfigureTask", wait_for_completion: nil) } 100 | let(:clone_vm_task) { instance_double("CloneVMTask") } 101 | 102 | before do 103 | allow(RbVmomi::VIM).to receive(:connect).with(connection_options).and_return(rbvmomi_vim) 104 | allow(RbVmomi::VIM).to receive(:NamePasswordAuthentication).with(interactiveSession: false, username: vm_username, password: vm_password) 105 | allow(service_instance).to receive(:find_datacenter).with(datacenter_name).and_return(datacenter) 106 | allow(network).to receive(:is_a?).with(RbVmomi::VIM::DistributedVirtualPortgroup).and_return(true) 107 | allow(source_vm_network_device).to receive(:is_a?).with(RbVmomi::VIM::VirtualEthernetCard).and_return(true) 108 | allow(source_vm_network_device).to receive(:backing=).with( 109 | RbVmomi::VIM.VirtualEthernetCardDistributedVirtualPortBackingInfo( 110 | port: RbVmomi::VIM.DistributedVirtualSwitchPortConnection( 111 | portgroupKey: network.key, 112 | switchUuid: distributed_virtual_switch.uuid 113 | ) 114 | ) 115 | ) 116 | allow(source_vm).to receive(:CloneVM_Task).with(spec: anything, folder: folder_id, name: vm_name).and_return(clone_vm_task) 117 | allow(clone_vm_task).to receive(:wait_for_completion) 118 | allow(datacenter).to receive(:find_vm).with("#{folder_name}/#{vm_name}").and_return(created_vm) 119 | allow(Kitchen.logger).to receive(:info) # Do not show logs during unit tests 120 | end 121 | 122 | describe "#clone" do 123 | it "does not raise an exception" do 124 | expect { subject.clone }.not_to raise_error 125 | end 126 | 127 | context "when customization is not provided" do 128 | before do 129 | options[:vm_customization] = nil 130 | end 131 | 132 | it "does not reconfigure vm with custom config" do 133 | expect(created_vm).not_to receive(:ReconfigVM_Task) 134 | subject.clone 135 | end 136 | end 137 | 138 | context "when customization is provided" do 139 | let(:reconfigure_task) { instance_double("ReconfigureTask", wait_for_completion: nil) } 140 | 141 | before do 142 | options[:vm_customization] = { 143 | "someconfig" => "yeehaw", 144 | } 145 | allow(created_vm).to receive(:ReconfigVM_Task).with(spec: anything).and_return(created_vm_reconfigure_task) 146 | end 147 | 148 | it "reconfigures vm with custom config" do 149 | expect(RbVmomi::VIM).to receive(:VirtualMachineConfigSpec) 150 | expect(created_vm).to receive(:ReconfigVM_Task).and_return(created_vm_reconfigure_task) 151 | subject.clone 152 | end 153 | 154 | %i{annotation memoryMB numCPUs}.each do |param| 155 | context "when #{param} is provided" do 156 | before do 157 | options[:vm_customization] = { 158 | param => "value", 159 | } 160 | end 161 | 162 | it "includes #{param} in customization" do 163 | conf = {} 164 | conf[param.to_sym] = "value" 165 | expect(RbVmomi::VIM).to receive(:VirtualMachineConfigSpec).with(conf) 166 | 167 | subject.clone 168 | end 169 | end 170 | end 171 | 172 | context "when guestinfo.* is provided" do 173 | before do 174 | options[:vm_customization] = { 175 | "guestinfo.one" => "value", 176 | } 177 | end 178 | 179 | it "includes :customConfig in customization" do 180 | expect(RbVmomi::VIM).to receive(:VirtualMachineConfigSpec).with({ extraConfig: [{ key: "guestinfo.one", value: "value" }] }) 181 | 182 | subject.clone 183 | end 184 | end 185 | 186 | context "when guestinfo.* is not provided" do 187 | before do 188 | options[:vm_customization] = {} 189 | end 190 | 191 | it "does not include :customConfig in customization" do 192 | expect(RbVmomi::VIM).to receive(:VirtualMachineConfigSpec).with({}) 193 | 194 | subject.clone 195 | end 196 | end 197 | end 198 | 199 | context "attaching a new vm" do 200 | before do 201 | options[:network_name] = "my-network" 202 | 203 | allow(subject).to receive(:network_device).and_return(nil) 204 | end 205 | 206 | context "#attach_new_network?" do 207 | it "should return true" do 208 | expect(subject.attach_new_network?(nil)).to be_truthy 209 | end 210 | end 211 | 212 | it "should call the reconfig vm task to add vm" do 213 | expect(RbVmomi::VIM).to receive(:VirtualMachineConfigSpec) 214 | expect(created_vm).to receive(:ReconfigVM_Task).and_return(created_vm_reconfigure_task) 215 | expect(subject).to receive(:network_change_spec) 216 | 217 | subject.clone 218 | end 219 | end 220 | 221 | context "clone with multiple networks" do 222 | 223 | let(:network2) { 224 | instance_double( 225 | "RbVmomi::VIM::DistributedVirtualPortgroup", 226 | name: "my-network-2", 227 | pretty_path: "#{datacenter_name}/network/my-network-2", 228 | config: network_config, key: "dvportgroup-123" 229 | ) 230 | } 231 | let(:network2_config) { 232 | instance_double( 233 | "RbVmomi::VIM::DVPortgroupConfigInfo", 234 | distributedVirtualSwitch: distributed_virtual_switch 235 | ) 236 | } 237 | 238 | let(:datacenter) { 239 | instance_double("RbVmomi::VIM::Datacenter", network: [network, network2]) 240 | } 241 | 242 | before(:each) do 243 | allow(network) 244 | .to receive(:is_a?) 245 | .with(RbVmomi::VIM::DistributedVirtualPortgroup) 246 | .and_return(true) 247 | 248 | allow(network2) 249 | .to receive(:is_a?) 250 | .with(RbVmomi::VIM::DistributedVirtualPortgroup) 251 | .and_return(true) 252 | 253 | options[:networks] = [network_hash, { name: "my-network-2", operation: "add" }] 254 | allow(created_vm).to receive(:ReconfigVM_Task).and_return(created_vm_reconfigure_task) 255 | end 256 | 257 | it "task should be successful" do 258 | expect { subject.clone }.not_to raise_error 259 | end 260 | 261 | it "network_change_spec should invoke twice" do 262 | expect(subject).to receive(:network_change_spec).twice 263 | 264 | subject.clone 265 | end 266 | 267 | it "helper method should return correct values" do 268 | expect(subject.add_network?).to be_truthy 269 | expect(subject.update_network?(network)).to be_truthy 270 | expect(subject.networks_to_update.first[:name]).to eq "my-network" 271 | expect(subject.networks_to_add.first[:name]).to eq "my-network-2" 272 | end 273 | end 274 | end 275 | end 276 | --------------------------------------------------------------------------------