├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── lint.yml │ └── publish.yml ├── .gitignore ├── .markdownlint.yaml ├── .mdlrc ├── .release-please-manifest.json ├── .rubocop.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── commit-msg ├── kitchen-azurerm.gemspec ├── lib └── kitchen │ └── driver │ ├── azure_credentials.rb │ ├── azurerm.rb │ └── azurerm_version.rb ├── release-please-config.json ├── renovate.json ├── spec ├── fixtures │ └── azure_credentials ├── spec_helper.rb └── unit │ └── kitchen │ └── driver │ ├── azure_credentials_spec.rb │ └── azurerm_spec.rb └── templates ├── empty.erb ├── internal.erb └── public.erb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @test-kitchen/maintainers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 5 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Lint, Unit & Integration Tests" 3 | 4 | "on": 5 | pull_request: 6 | 7 | jobs: 8 | lint-unit: 9 | uses: test-kitchen/.github/.github/workflows/lint-unit.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release-please 3 | 4 | "on": 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: googleapis/release-please-action@v4 13 | id: release 14 | with: 15 | token: ${{ secrets.PORTER_GITHUB_TOKEN }} 16 | 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | if: ${{ steps.release.outputs.release_created }} 20 | 21 | - name: Build and publish to GitHub Package 22 | uses: actionshub/publish-gem-to-github@main 23 | if: ${{ steps.release.outputs.release_created }} 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | owner: ${{ secrets.OWNER }} 27 | 28 | - name: Build and publish to RubyGems 29 | uses: actionshub/publish-gem-to-rubygems@main 30 | if: ${{ steps.release.outputs.release_created }} 31 | with: 32 | token: ${{ secrets.RUBYGEMS_API_KEY }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[opn] 2 | Gemfile.lock 3 | *.gem 4 | /docs/examples 5 | coverage 6 | *.log 7 | *kitchen.local.yml 8 | .kitchen/ 9 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default: true 3 | MD004: false 4 | MD012: false 5 | MD013: false 6 | MD024: false 7 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD036", "~MD013", "~MD024", "~MD029" 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.13.2" 3 | } 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: 3 | - cookstyle/chefstyle 4 | 5 | AllCops: 6 | TargetRubyVersion: 3.1 7 | Include: 8 | - "**/*.rb" 9 | Exclude: 10 | - "vendor/**/*" 11 | - "spec/**/*" 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rubocop.path": "chefstyle.bat" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # kitchen-azurerm Changelog 2 | 3 | ## Changelog 4 | 5 | This CHANGELOG is maintained by the [release-please-action](google-github-actions/release-please-action) 6 | 7 | ## [1.13.2](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.13.1...v1.13.2) (2025-03-03) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * Require Ruby 3.1 + lint with cookstyle ([#278](https://github.com/test-kitchen/kitchen-azurerm/issues/278)) ([b9bba1b](https://github.com/test-kitchen/kitchen-azurerm/commit/b9bba1b85c115272456f1d28926bba263aa6ef48)) 13 | 14 | ## [1.13.1](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.13.0...v1.13.1) (2024-06-21) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * release please configs ([#273](https://github.com/test-kitchen/kitchen-azurerm/issues/273)) ([8633756](https://github.com/test-kitchen/kitchen-azurerm/commit/8633756a593dc68087e1a6bb6905e88330bf1e54)) 20 | 21 | ## [1.13.0](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.12.0...v1.13.0) (2023-11-27) 22 | 23 | 24 | ### Features 25 | 26 | * add configurable vm prefix ([#264](https://github.com/test-kitchen/kitchen-azurerm/issues/264)) ([4b09973](https://github.com/test-kitchen/kitchen-azurerm/commit/4b099731f1132739aaaf203cc417d254feb6862e)) 27 | * Update workflows and run Chefstyle over the code base ([#267](https://github.com/test-kitchen/kitchen-azurerm/issues/267)) ([869ee8c](https://github.com/test-kitchen/kitchen-azurerm/commit/869ee8c5af9cf9c77786090c6b3dc1733b50b90d)) 28 | 29 | ### [1.12.0](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.11.0...v1.11.1) (2023-05-08) 30 | 31 | ### Features 32 | 33 | * Azure sdk namespace updates ([#258](https://github.com/test-kitchen/kitchen-azurerm/issues/258)) ([d3041f1](https://github.com/test-kitchen/kitchen-azurerm/commit/d3041f19dd68e4c3ea00631e9fa5d3a63ea92a76)) 34 | 35 | ### [1.11.0](http3://g11hub.com/test-kitchen/kitchen-azurerm/compare/v1.10.6...v1.11.0) (2023-04-11) 36 | 37 | ### Features 38 | 39 | * Replaced the deprecated azure SDK gems with separately maintained version twos. ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([c6da371](https://github.com/test-kitchen/kitchen-azurerm/commit/c6da371443912b9d689e445f3f714d5cae6dd3a0)) 40 | 41 | 42 | ### [1.10.7](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.6...v1.10.7) (2022-04-20) 43 | 44 | 45 | ### Features 46 | 47 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339)) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * Add PR template, release, publsh and unit workflows ([#242](https://github.com/test-kitchen/kitchen-azurerm/issues/242)) ([56b31cc](https://github.com/test-kitchen/kitchen-azurerm/commit/56b31ccc38bc53f997e35323ce5ef13e5ef61803)) 53 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f)) 54 | * publish workflow ([#247](https://github.com/test-kitchen/kitchen-azurerm/issues/247)) ([a68d380](https://github.com/test-kitchen/kitchen-azurerm/commit/a68d380bd44e7e4de7abb177034ba4109880dcef)) 55 | * switch to reusable GitHub workflows ([#244](https://github.com/test-kitchen/kitchen-azurerm/issues/244)) ([0abd514](https://github.com/test-kitchen/kitchen-azurerm/commit/0abd514aeb8c588409422be0c71d10cff82a8ebe)) 56 | 57 | ### [1.10.5](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.4...v1.10.5) (2022-04-13) 58 | 59 | ### Features 60 | 61 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339)) 62 | 63 | ### Bug Fixes 64 | 65 | * Add PR template, release, publsh and unit workflows ([#242](https://github.com/test-kitchen/kitchen-azurerm/issues/242)) ([56b31cc](https://github.com/test-kitchen/kitchen-azurerm/commit/56b31ccc38bc53f997e35323ce5ef13e5ef61803)) 66 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f)) 67 | 68 | ### [1.10.4](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.3...v1.10.4) (2022-04-04) 69 | 70 | ### Bug Fixes 71 | 72 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f)) 73 | 74 | ### [1.10.3](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.2...v1.10.3) (2022-04-04) 75 | 76 | ### Features 77 | 78 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339)) 79 | 80 | ## [1.10.2] - 2022-04-04 81 | 82 | * move warning about missing credentials into debug by @jasonwbarnett in 83 | * Deprecation/positional arguments by @damacus in 84 | * Publish gem to GitHub by @damacus in 85 | 86 | ## [1.10.1] - 2022-03-10 87 | 88 | * Rollback #228 by [@jasonwbarnett](https://github.com/jasonwbarnett) in #234 89 | 90 | ## [1.10.0] - 2022-02.28 91 | 92 | * Add a new `store_deployment_credentials_in_state` configuration option to skip storing sensitive data in the state [@jasonwbarnett](https://github.com/jasonwbarnett) 93 | 94 | ## [1.9.0] - 2022-02.04 95 | 96 | * Support setting the VM availability zone with a new `zone` config. [@pkazi](https://github.com/pkazi) 97 | * Drop support for EOL Ruby 2.5 98 | 99 | ## [1.8.0] - 2021-08.27 100 | 101 | * Increase max OS volume size from 1023 to 2048 [@jasonwbarnett](https://github.com/jasonwbarnett) 102 | 103 | ## [1.7.0] - 2021-07.02 104 | 105 | * Support Test Kitchen 3.0 106 | 107 | ## [1.6.0] - 2021-03.19 108 | 109 | * The default VM name has been changed from `vm` to `tk-RANDOMVALUE` to avoid name conflicts and make it easier to find systems in the portal [@jasonwbarnett](https://github.com/jasonwbarnett) 110 | 111 | ## [1.5.3] - 2021-02-24 112 | 113 | * Additional fixes for `public_ip_sku` to update the default behavior to match pre-1.5.0 behavior [@collinmcneese](https://github.com/collinmcneese) 114 | 115 | ## [1.5.2] - 2021-02-18 116 | 117 | * Fix using `storage_account_type` config option to set data disk storage types - [@reasland](https://github.com/reasland) 118 | 119 | ## [1.5.1] - 2021-02-18 120 | 121 | * Populate publicIPSKU in template only if provided by kitchen config [@collinmcneese](https://github.com/collinmcneese) 122 | 123 | ## [1.5.0] - 2021-02-11 124 | 125 | * Add support for setting the public IP SkU with a new `public_ip_sku` configuration option within the `subnet` config. Thanks [@simonjefford](https://github.com/simonjefford) 126 | 127 | ## [1.4.0] - 2020-09-29 128 | 129 | * Resolved an issue where VM state was persisted before VM is provisioned 130 | * Added linting and unit testing for each pull request via GitHub Actions 131 | * Resolved issues where resource groups where not being destroyed 132 | * Set 'az login' the default authentication mechanism 133 | * Added new `use_fqdn_hostname` config option to set Test Kitchen to communicate using the instance's FQDN 134 | * Resolved an issue where username was not being added to Test Kitchen's state 135 | 136 | ## [1.3.0] - 2020-09-09 137 | 138 | * Improve performance by loading dependencies only when we need them (@mwrock) 139 | 140 | ## [1.2.0] - 2020-08-20 141 | 142 | * Add support for deletion or preservation of resource group tags with a new `destroy_explicit_resource_group_tags` config that defaults to `true` (@StylusEaterChef) 143 | * Optimize our requires to make load the gem a tiny bit faster (@tas50) 144 | 145 | ## [1.1.0] - 2020-08-19 146 | 147 | * Update error messages to mention `kitchen.yml` not `.kitchen.yml` (@tas50) 148 | * Update the default password we generate to be 25 characters to avoid failures on newer Windows releases (@StylusEaterChef) 149 | * Remove `simplecov` development dependency (@tas50) 150 | * Updated Readme to be more explicit about credentials settings (@Vasu1105) 151 | * Remove tags in readme that could possibly confuse users (@jasonwbarnett) 152 | * Fix Azure SP documentation link and give an example on how to setup (@StylusEaterChef) 153 | * Update installation instructions not to mention ChefDK (@tas50) 154 | 155 | ## [1.0.0] - 2020-05-06 156 | 157 | * Add more specs and refactor Credentials [PR #135](https://github.com/test-kitchen/kitchen-azurerm/pull/135) (@jasonwbarnett) 158 | * Fix using `user_assigned_identities` config [PR #136](https://github.com/test-kitchen/kitchen-azurerm/pull/136) (@zanecodes) 159 | 160 | ## [0.17.0] - 2020-04-23 161 | 162 | * Add MSI Support [PR #134](https://github.com/test-kitchen/kitchen-azurerm/pull/134) (@jasonwbarnett) 163 | 164 | ## [0.16.0] - 2020-04-22 165 | 166 | * Add support for marketplace plan information [PR #132](https://github.com/test-kitchen/kitchen-azurerm/pull/132) (@jasonwbarnett) 167 | 168 | ## [0.15.2] - 2020-03-23 169 | 170 | * Fix require_relative for azure_credentials [PR #129](https://github.com/test-kitchen/kitchen-azurerm/pull/129) (@jasonwbarnett) 171 | * Refactor Kitchen::Driver::Credentials class [PR #128](https://github.com/test-kitchen/kitchen-azurerm/pull/128) (@jasonwbarnett) 172 | * Default password is now generated rather than hard-coded [#124](https://github.com/test-kitchen/kitchen-azurerm/pull/124) (@stuartpreston) 173 | * Add retry logic when checking deployment state [#125](https://github.com/test-kitchen/kitchen-azurerm/pull/125x) (@albertvaka) 174 | * Only add password to deployment template if ssh_key is not set [#126](https://github.com/test-kitchen/kitchen-azurerm/pull/126) (@KSerrania) 175 | 176 | ## [0.15.1] - 2020-01-14 177 | 178 | * Use require_relative instead of require [PR #123](https://github.com/test-kitchen/kitchen-azurerm/pull/123) (@tas50) 179 | 180 | ## [0.15.0] - 2019-11-29 181 | 182 | * Enable WinRM HTTP listener by default [PR #121](https://github.com/test-kitchen/kitchen-azurerm/pull/121) (@sean-nixon) 183 | * Default subscription_id to AZURE_SUBSCRIPTION_ID environment variable if not supplied[df79c787fa299cb6eff4a2fd7807fe28ce2bc725](https://github.com/test-kitchen/kitchen-azurerm/commit/df79c787fa299cb6eff4a2fd7807fe28ce2bc725) (@stuartpreston) 184 | * Allow nic name to be passed in as a parameter [PR #112](https://github.com/test-kitchen/kitchen-azurerm/pull/112) (@libertymutual) 185 | * Support for creating VM with Azure KeyVault certificate [PR #120](https://github.com/test-kitchen/kitchen-azurerm/pull/120) (@javgallegos) 186 | 187 | ## [0.14.9] - 2019-07-30 188 | 189 | * Support [Ephemeral OS Disk](https://azure.microsoft.com/en-us/updates/azure-ephemeral-os-disk-now-generally-available/), (@stuartpreston) 190 | 191 | ## [0.14.8] - 2018-12-30 192 | 193 | * Support [Azure Managed Identities](https://github.com/test-kitchen/kitchen-azurerm#kitchenyml-example-10---enabling-managed-service-identities), [PR #106](https://github.com/test-kitchen/kitchen-azurerm/pull/105) (@zanecodes) 194 | * Apply vm_tags to all resources in resource group [PR #105](https://github.com/test-kitchen/kitchen-azurerm/pull/105) (@josh-hetland) 195 | 196 | ## [0.14.7] - 2018-12-18 197 | 198 | * Updating Azure SDK dependencies, [PR #104](https://github.com/test-kitchen/kitchen-azurerm/pull/104) (@stuartpreston) 199 | 200 | ## [0.14.6] - 2018-12-11 201 | 202 | * Support tags at Resource Group level, [PR #102](https://github.com/test-kitchen/kitchen-azurerm/pull/102) (@pgryzan-chefio) 203 | * Pin azure_mgmt_resources to 0.18.0 to avoid issue retrieving IP address of node during kitchen create [#99](https://github.com/test-kitchen/kitchen-azurerm/issues/99) (@stuartpreston) 204 | 205 | ## [0.14.5] - 2018-09-30 206 | 207 | * Support Shared Image Gallery (preview Azure feature) (@zanecodes) 208 | 209 | ## [0.14.4] - 2018-08-10 210 | 211 | * Adding capability to execute ARM template after VM deployment, ```post_deployment_template``` and ```post_deployment_parameters``` added (@sebastiankasprzak) 212 | 213 | ## [0.14.3] - 2018-07-16 214 | 215 | * Add `destroy_resource_group_contents` (default: false) property to allow contents of Azure Resource Group to be deleted rather than entire Resource Group, fixes [#90](https://github.com/test-kitchen/kitchen-azurerm/issues/85) 216 | 217 | ## [0.14.2] - 2018-07-09 218 | 219 | * Add `destroy_explicit_resource_group` (default: false) property to allow reuse of specific Azure RG, fixes [#85](https://github.com/test-kitchen/kitchen-azurerm/issues/85) 220 | 221 | ## [0.14.1] - 2018-05-10 222 | 223 | * Support for soverign clouds with latest Azure SDK for Ruby, fixes [#79](https://github.com/test-kitchen/kitchen-azurerm/issues/79) 224 | * Raise error when subscription_id is not available, fixes [#74](https://github.com/test-kitchen/kitchen-azurerm/issues/74) 225 | 226 | ## [0.14.0] - 2018-04-10 227 | 228 | * Update Azure SDK to latest version, upgrade to latest build tools 229 | 230 | ## [0.13.0] - 2017-12-26 231 | 232 | * Switch to new Microsoft telemetry system [#73](https://github.com/test-kitchen/kitchen-azurerm/issues/73) 233 | 234 | ## [0.12.4] - 2017-11-17 235 | 236 | * Adding `explicit_resource_group_name` property to driver configuration 237 | 238 | ## [0.12.3] - 2017-10-18 239 | 240 | * Pinning to version 0.14.0 of Microsoft Azure SDK for Ruby, avoid namespace changes 241 | 242 | ## [0.12.2] - 2017-09-20 243 | 244 | * Fix issue with location of data_disks in internal.erb [#67](https://github.com/test-kitchen/kitchen-azurerm/pull/67https://github.com/test-kitchen/kitchen-azurerm/pull/67) (@ehanlon) 245 | 246 | ## [0.12.1] - 2017-09-10 247 | 248 | * Fix for undefined local variable when using pre_deployment_template [#65](https://github.com/test-kitchen/kitchen-azurerm/issue/65) 249 | 250 | ## [0.12.0] - 2017-09-01 251 | 252 | * Additional managed disks can be specified in configuration and left unformatted or formatted on Windows(@stuartpreston) 253 | * Added `azure_resource_group_prefix` and `azure_resource_group_suffix` parameter (@stuartpreston) 254 | 255 | ## [0.11.0] - 2017-07-20 256 | 257 | * Pin to latest ARM SDK and constants [#59](https://github.com/test-kitchen/kitchen-azurerm/pull/59) (@smurawski) 258 | 259 | ## [0.10.0] - 2017-07-03 260 | 261 | * Support for custom images (@elconas) 262 | * Support for custom-data (Linux only) (@elconas) 263 | * Support for custom OS sizes (@elconas) 264 | 265 | ## [0.9.1] - 2017-05-25 266 | 267 | * Support for Managed Disks enabled by default (@stuartpreston) 268 | * Add ```use_managed_disks``` driver_config parameter (@stuartpreston) 269 | 270 | ## [0.9.0] - 2017-04-28 271 | 272 | * Support for AzureUSGovernment, AzureChina and AzureGermanCloud environments 273 | * Add ```azure_environment``` driver_config parameter (@stuartpreston) 274 | 275 | ## [0.8.1] - 2017-02-28 276 | 277 | * Adding provider identifier tag to all created resources (@stuartpreston) 278 | 279 | ## [0.8.0] - 2017-01-16 280 | 281 | * [Unattend.xml used instead of Custom Script Extension to inject WinRM configuration/AKA support proxy server configurations](https://github.com/pendrica/kitchen-azurerm/pull/44) (@hbuckle) 282 | * [Public IP addresses can now be used to connect even if the VM is connected to an existing subnet](https://github.com/pendrica/kitchen-azurerm/pull/42) (@vlesierse) 283 | * [Resource Tags can now be applied to the created VMsPR](https://github.com/pendrica/kitchen-azurerm/pull/38) (@liamkirwan) 284 | 285 | ## [0.7.2] - 2016-11-03 286 | 287 | * Bug: When repeating a completed deployment, deployment would fail with a nil error on resource_name (@stuartpreston) 288 | 289 | ## [0.7.1] - 2016-09-17 290 | 291 | * Bug: WinRM is not enabled where the platform name does not contain 'nano' (@stuartpreston) 292 | 293 | ## [0.7.0] - 2016-09-15 294 | 295 | * Support creation of Windows Nano Server (ignoring automatic WinRM setting application) (@stuartpreston) 296 | 297 | ## [0.6.0] - 2016-08-22 298 | 299 | * Supports latest autogenerated resources from Azure SDK for Ruby (0.5.0) (@stuartpreston) 300 | * Removes unnecessary direct depdendencies on older ms_rest libraries (@stuartpreston) 301 | * ssh_key will be used in preference to password if both are supplied (@stuartpreston) 302 | 303 | ## [0.5.0] - 2016-08-07 304 | 305 | * Adding support for internal (e.g. ExpressRoute/VPN) access to created VM (@stuartpreston) 306 | 307 | ## [0.4.1] - 2016-07-01 308 | 309 | * Adding explicit depdendency on concurrent-ruby gem (@stuartpreston) 310 | 311 | ## [0.4.0] - 2016-06-26 312 | 313 | * Adding capability to execute ARM template prior to VM deployment, ```pre_deployment_template``` and ```pre_deployment_parameters``` added (@stuartpreston) 314 | 315 | ## [0.3.6] - 2016-05-10 316 | 317 | * Remove version pin on inifile gem dependency, compatible with newer ChefDK (@stuartpreston) 318 | 319 | ## [0.3.5] - 2016-03-21 320 | 321 | * Remove transport name restriction on SSH key upload (allow rsync support) (@stuartpreston) 322 | * Support SSH public keys with newlines as generated by ssh-keygen (@stuartpreston) 323 | 324 | ## [0.3.4] - 2016-03-19 325 | 326 | * Additional diagnostics when Azure Resource Group fails to create successfully (@stuartpreston) 327 | 328 | ## [0.3.3] - 2016-03-07 329 | 330 | * Pinning ms_rest_azure dependencies to avoid errors when using latest ms_rest_azure library (@stuartpreston) 331 | 332 | ## [0.3.2] - 2016-03-07 333 | 334 | * Breaking: Linux machines are now created using a temporary sshkey (~/.ssh/id_kitchen-azurerm) instead of password (@stuartpreston) 335 | * Real error message shown if credentials are incorrect (@stuartpreston) 336 | 337 | ## [0.2.4] - 2016-01-26 338 | 339 | * Support Premium Storage and Boot Diagnostics (@stuartpreston) 340 | * If deployment fails, show the message from the failing operation (@stuartpreston) 341 | * Updated Windows 2008 R2 example (@stuartpreston) 342 | 343 | ## [0.2.3] - 2015-12-17 344 | 345 | * ```kitchen create``` can now be executed multiple times, updating an existing deployment if an error occurs (@smurawski) 346 | 347 | ## [0.2.2] - 2015-12-10 348 | 349 | * Add an option for users to specify a custom script for WinRM (support Windows 2008 R2) (@andrewelizondo) 350 | * Add azure_management_url parameter for Azure Stack support (@andrewelizondo) 351 | 352 | ## [0.2.1] - 2015-10-06 353 | 354 | * Pointing to updated Azure SDK for Ruby, supports Linux 355 | 356 | ## [0.2.0] - 2015-09-29 357 | 358 | * Logs should be sent to info, not stdout (@stuartpreston) 359 | * Added WinRM support, enables WinRM and WinRM/s and configures server for Basic/Negotiate authentication (@stuartpreston) 360 | * Store server_id earlier so it can be retrieved if resources fail to create in Azure (@stuartpreston) 361 | 362 | ## [0.1.3] - 2015-09-23 363 | 364 | * Support *nix by changing the driver name to lowercase 'azurerm', remove Chef references (@gadgetmg) 365 | 366 | ## [0.1.2] - 2015-09-23 367 | 368 | * Initial release, supports provision of all public image types in Azure (@stuartpreston) 369 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @test-kitchen/maintainers 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please refer to the Chef Community Code of Conduct at 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem "rake", ">= 11.0" 7 | gem "rspec", "~> 3.5" 8 | gem "rspec-its", "~> 2.0.0" 9 | end 10 | 11 | group :debug do 12 | gem "pry" 13 | end 14 | 15 | group :linting do 16 | gem "cookstyle", "7.32.8" 17 | end 18 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kitchen-azurerm 2 | 3 | [![Gem Version](https://badge.fury.io/rb/kitchen-azurerm.svg)](https://badge.fury.io/rb/kitchen-azurerm) 4 | ![CI](https://github.com/test-kitchen/kitchen-azurerm/workflows/CI/badge.svg?branch=master) 5 | 6 | **kitchen-azurerm** is a driver for the popular test harness [Test Kitchen](http://kitchen.ci) that allows Microsoft Azure resources to be provisioned before testing. This driver uses the new Microsoft Azure Resource Management REST API via the [azure-sdk-for-ruby](https://github.com/azure/azure-sdk-for-ruby). 7 | 8 | This version has been tested on Windows, macOS, and Ubuntu. If you encounter a problem on your platform, please raise an issue. 9 | 10 | ## Quick-start 11 | 12 | ### Installation 13 | 14 | This plugin ships in Chef Workstation out of the box so there is no need to install it when using [Chef Workstation](https://downloads.chef.io/products/workstation). 15 | 16 | If you're not using Chef Workstation and need to install the plugin as a gem run: 17 | 18 | ```shell 19 | gem install kitchen-azurerm 20 | ``` 21 | 22 | ### Configuration 23 | 24 | For the driver to interact with the Microsoft Azure Resource Management REST API, you need to configure a Service Principal with Contributor rights for a specific subscription. Using an Organizational (AAD) account and related password is no longer supported. To create a Service Principal and apply the correct permissions, see the [create an Azure service principal with the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest#create-a-service-principal) and the [Azure CLI](https://azure.microsoft.com/en-us/documentation/articles/xplat-cli-install/) documentation. Make sure you stay within the section titled 'Password-based authentication'. 25 | 26 | If the above is TLDR then try this after `az login` using your target subscription ID and the desired SP name: 27 | 28 | ```bash 29 | # Create a Service Principal using the desired subscription id from the command above 30 | az ad sp create-for-rbac --name="kitchen-azurerm" --role="Contributor" --scopes="/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 31 | 32 | #Output 33 | # 34 | #{ 35 | # "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", <- Also known as the Client ID 36 | # "displayName": "azure-cli-2018-12-12-14-15-39", 37 | # "name": "http://azure-cli-2018-12-12-14-15-39", 38 | # "password": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 39 | # "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 40 | #} 41 | ``` 42 | 43 | NOTE: Don't forget to save the values from the output -- most importantly the `password`. 44 | 45 | You will also need to ensure you have an active Azure subscription (you can get started [for free](https://azure.microsoft.com/en-us/free/) or use your [MSDN Subscription](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits/)). 46 | 47 | You are now ready to configure kitchen-azurerm to use the credentials from the service principal you created above. You will use four elements from the output: 48 | 49 | 1. **Subscription ID**: available from the Azure portal 50 | 2. **Client ID**: the appId value from the output. 51 | 3. **Client Secret/Password**: the password from the output. 52 | 4. **Tenant ID**: the tenant from the output. 53 | 54 | Using a text editor, open or create the file ```~/.azure/credentials``` and add the following section, noting there is one section per Subscription ID. **Make sure you save the file with UTF-8 encoding** 55 | 56 | ```ruby 57 | [ADD-YOUR-AZURE-SUBSCRIPTION-ID-HERE-IN-SQUARE-BRACKET] 58 | client_id = "your-azure-client-id-here" 59 | client_secret = "your-client-secret-here" 60 | tenant_id = "your-azure-tenant-id-here" 61 | ``` 62 | 63 | If preferred, you may also set the following environment variables, however this would be incompatible with supporting multiple Azure subscriptions. 64 | 65 | ```ruby 66 | AZURE_CLIENT_ID="your-azure-client-id-here" 67 | AZURE_CLIENT_SECRET="your-client-secret-here" 68 | AZURE_TENANT_ID="your-azure-tenant-id-here" 69 | ``` 70 | 71 | Note that the environment variables, if set, take preference over the values in a configuration file. 72 | 73 | After adjusting your ```~/.azure/credentials``` file you will need to adjust your ```kitchen.yml``` file to leverage the azurerm driver. Use the following examples to achieve this, then check your configuration with standard kitchen commands. For example, 74 | 75 | ```bash 76 | % kitchen list 77 | Instance Driver Provisioner Verifier Transport Last Action Last Error 78 | wsus-windows-2019 Azurerm ChefZero Inspec Winrm 79 | wsus-windows-2016 Azurerm ChefZero Inspec Winrm 80 | ``` 81 | 82 | ### Driver Properties 83 | 84 | See the [kitchen.ci kitchen-azurem docs](https://kitchen.ci/docs/drivers/azurerm/) for a complete list of configuration options. 85 | 86 | ### kitchen.yml example 1 - Linux/Ubuntu 87 | 88 | Here's an example ```kitchen.yml``` file that provisions an Ubuntu Server, using Chef Zero as the provisioner and SSH as the transport. Note that if the key does not exist at the specified location, it will be created. Also note that if ```ssh_key``` is supplied, Test Kitchen will use this in preference to any default/configured passwords that are supplied. 89 | 90 | ```yaml 91 | --- 92 | driver: 93 | name: azurerm 94 | subscription_id: 'your-azure-subscription-id-here' 95 | location: 'West Europe' 96 | machine_size: 'Standard_D1' 97 | 98 | transport: 99 | ssh_key: ~/.ssh/id_kitchen-azurerm 100 | 101 | provisioner: 102 | name: chef_zero 103 | 104 | platforms: 105 | - name: ubuntu-14.04 106 | driver: 107 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 108 | vm_name: trusty-vm 109 | 110 | suites: 111 | - name: default 112 | attributes: 113 | ``` 114 | 115 | ### Concurrent execution 116 | 117 | Concurrent execution of create/converge/destroy is supported via the --concurrency parameter. Each machine is created in its own Azure Resource Group so it has no shared lifecycle with the other machines in the test run. To take advantage of parallel execution use the following command: 118 | 119 | ```kitchen test --concurrency ``` 120 | 121 | Where n is the number of threads to create. Note that any failure (e.g. an AzureOperationError) will cause the whole test to fail, though resources already in creation will continue to be created. 122 | 123 | ### kitchen.yml example 2 - Windows 124 | 125 | Here's a further example ```kitchen.yml``` file that will provision a Windows Server 2019 [smalldisk] instance, using WinRM as the transport. An [ephemeral os disk](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/ephemeral-os-disks) is used. The resource created in Azure will enable itself for remote access at deployment time (it does this by customizing the machine at provisioning time) and tags the Azure Resource Group with metadata using the ```resource_group_tags``` property. Notice that the ```vm_tags``` and ```resource_group_tags``` properties use a simple ```key : value``` structure per line: 126 | 127 | ```yaml 128 | --- 129 | driver: 130 | name: azurerm 131 | subscription_id: 'your-subscription-id-here' 132 | location: 'West Europe' 133 | machine_size: 'Standard_DS2_v2' 134 | 135 | provisioner: 136 | name: chef_zero 137 | 138 | platforms: 139 | - name: windows2019 140 | driver: 141 | image_urn: MicrosoftWindowsServer:WindowsServer:2019-Datacenter-smalldisk:latest 142 | use_ephemeral_osdisk: true 143 | resource_group_tags: 144 | project: 'My Cool Project' 145 | contact: 'me@somewhere.com' 146 | vm_tags: 147 | my_tag: its value 148 | another_tag: its awesome value 149 | transport: 150 | name: winrm 151 | suites: 152 | - name: default 153 | attributes: 154 | ``` 155 | 156 | ### kitchen.yml example 3 - "pre-deployment" ARM template 157 | 158 | The following example introduces the ```pre_deployment_template``` and ```pre_deployment_parameters``` properties in the configuration file. 159 | You can use this capability to execute an ARM template containing Azure resources to provision before the system under test is created. 160 | 161 | In the example the ARM template in the file ```predeploy.json``` would be executed with the parameters that are specified under ```pre_deployment_parameters```. 162 | These resources will be created in the same Azure Resource Group as the VM under test, and therefore will be destroyed when you type ```kitchen destroy```. 163 | 164 | ```yaml 165 | --- 166 | driver: 167 | name: azurerm 168 | subscription_id: 'your-azure-subscription-id-here' 169 | location: 'West Europe' 170 | machine_size: 'Standard_D1' 171 | pre_deployment_template: predeploy.json 172 | pre_deployment_parameters: 173 | test_parameter: 'This is a test.' 174 | 175 | transport: 176 | ssh_key: ~/.ssh/id_kitchen-azurerm 177 | 178 | provisioner: 179 | name: chef_zero 180 | 181 | platforms: 182 | - name: ubuntu-1404 183 | driver: 184 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 185 | 186 | suites: 187 | - name: default 188 | run_list: 189 | - recipe[kitchen-azurerm-demo::default] 190 | attributes: 191 | ``` 192 | 193 | Example predeploy.json: 194 | 195 | ```json 196 | { 197 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 198 | "contentVersion": "1.0.0.0", 199 | "parameters": { 200 | "test_parameter": { 201 | "type": "string", 202 | "defaultValue": "" 203 | } 204 | }, 205 | "variables": { 206 | 207 | }, 208 | "resources": [ 209 | { 210 | "name": "uniqueinstancenamehere01", 211 | "type": "Microsoft.Sql/servers", 212 | "location": "[resourceGroup().location]", 213 | "apiVersion": "2014-04-01-preview", 214 | "properties": { 215 | "version": "12.0", 216 | "administratorLogin": "azure", 217 | "administratorLoginPassword": "P2ssw0rd" 218 | } 219 | } 220 | ], 221 | "outputs": { 222 | "parameter testing": { 223 | "type": "string", 224 | "value": "[parameters('test_parameter')]" 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | ### kitchen.yml example 4 - deploy VM to existing virtual network/subnet (use for ExpressRoute/VPN scenarios) 231 | 232 | The following example introduces the ```vnet_id``` and ```subnet_id``` properties under "driver" in the configuration file. This can be applied at the top level, or per platform. 233 | You can use this capability to create the VM on an existing virtual network and subnet created in a different resource group. 234 | 235 | In this case, the public IP address is not used unless ```public_ip``` is set to ```true``` 236 | 237 | ```yaml 238 | --- 239 | driver: 240 | name: azurerm 241 | subscription_id: 'your-azure-subscription-id-here' 242 | location: 'West Europe' 243 | machine_size: 'Standard_D1' 244 | 245 | transport: 246 | ssh_key: ~/.ssh/id_kitchen-azurerm 247 | 248 | provisioner: 249 | name: chef_zero 250 | 251 | platforms: 252 | - name: ubuntu-1404 253 | driver: 254 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 255 | vnet_id: /subscriptions/b6e7eee9-YOUR-GUID-HERE-03ab624df016/resourceGroups/pendrica-infrastructure/providers/Microsoft.Network/virtualNetworks/pendrica-arm-vnet 256 | subnet_id: subnet-10.1.0 257 | 258 | suites: 259 | - name: default 260 | attributes: 261 | ``` 262 | 263 | ### kitchen.yml example 5 - deploy VM to existing virtual network/subnet with a Standard SKU public IP (use for ExpressRoute/VPN scenarios) 264 | 265 | The following example introduces the ```vnet_id``` and ```subnet_id``` properties under "driver" in the configuration file. This can be applied at the top level, or per platform. 266 | You can use this capability to create the VM on an existing virtual network and subnet created in a different resource group. 267 | 268 | This enables scenarios that require a Standard SKU public IP resource, for example when a NAT gateway is present on the target subnet. 269 | 270 | ```yaml 271 | --- 272 | driver: 273 | name: azurerm 274 | subscription_id: 'your-azure-subscription-id-here' 275 | location: 'West Europe' 276 | machine_size: 'Standard_D1' 277 | 278 | transport: 279 | ssh_key: ~/.ssh/id_kitchen-azurerm 280 | 281 | provisioner: 282 | name: chef_zero 283 | 284 | platforms: 285 | - name: ubuntu-1404 286 | driver: 287 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 288 | vnet_id: /subscriptions/b6e7eee9-YOUR-GUID-HERE-03ab624df016/resourceGroups/pendrica-infrastructure/providers/Microsoft.Network/virtualNetworks/pendrica-arm-vnet 289 | subnet_id: subnet-10.1.0 290 | public_ip: true 291 | public_ip_sku: Standard 292 | 293 | suites: 294 | - name: default 295 | attributes: 296 | ``` 297 | 298 | ### kitchen.yml example 6 - deploy VM to existing virtual network/subnet (use for ExpressRoute/VPN scenarios) with Private Managed Image 299 | 300 | This example is the same as above, but uses a private managed image to provision the vm. 301 | 302 | Note: The image must be available first. On deletion the disk and everything is removed. 303 | 304 | ```yaml 305 | --- 306 | driver: 307 | name: azurerm 308 | subscription_id: 'your-azure-subscription-id-here' 309 | location: 'West Europe' 310 | machine_size: 'Standard_D1' 311 | 312 | transport: 313 | ssh_key: ~/.ssh/id_kitchen-azurerm 314 | 315 | provisioner: 316 | name: chef_zero 317 | 318 | platforms: 319 | - name: ubuntu-1404 320 | driver: 321 | image_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESGROUP/providers/Microsoft.Compute/images/IMAGENAME 322 | vnet_id: /subscriptions/b6e7eee9-YOUR-GUID-HERE-03ab624df016/resourceGroups/pendrica-infrastructure/providers/Microsoft.Network/virtualNetworks/pendrica-arm-vnet 323 | subnet_id: subnet-10.1.0 324 | use_managed_disk: true 325 | 326 | suites: 327 | - name: default 328 | attributes: 329 | ``` 330 | 331 | ### kitchen.yml example 7 - deploy VM to existing virtual network/subnet (use for ExpressRoute/VPN scenarios) with Private Classic OS Image 332 | 333 | This example a classic Custom VM Image (aka a VHD file) is used. As the Image VHD must be in the same storage account then the disk of the instance, the os disk is created in an existing image account. 334 | 335 | Note: When the resource group ís deleted, the os disk is left in the existing storage account blob. You must clean up manually. 336 | 337 | This example will: 338 | 339 | * use the customized image (can be built with packer) 340 | * set the disk url of the vm to 341 | * set the os type to linux 342 | 343 | ```yaml 344 | --- 345 | driver: 346 | name: azurerm 347 | subscription_id: 'your-azure-subscription-id-here' 348 | location: 'West Europe' 349 | machine_size: 'Standard_D1' 350 | 351 | transport: 352 | ssh_key: ~/.ssh/id_kitchen-azurerm 353 | 354 | provisioner: 355 | name: chef_zero 356 | 357 | platforms: 358 | - name: ubuntu-1404 359 | driver: 360 | image_url: https://yourstorageaccount.blob.core.windows.net/system/Microsoft.Compute/Images/images/Cent7_P4-osDisk.170dd1b7-7dc3-4496-b248-f47c49f63965.vhd 361 | existing_storage_account_blob_url: https://yourstorageaccount.blob.core.windows.net 362 | os_type: linux 363 | use_managed_disk: false 364 | vnet_id: /subscriptions/b6e7eee9-YOUR-GUID-HERE-03ab624df016/resourceGroups/pendrica-infrastructure/providers/Microsoft.Network/virtualNetworks/pendrica-arm-vnet 365 | subnet_id: subnet-10.1.0 366 | 367 | suites: 368 | - name: default 369 | attributes: 370 | ``` 371 | 372 | ### kitchen.yml example 8 - deploy VM to existing virtual network/subnet (use for ExpressRoute/VPN scenarios) with Private Classic OS Image and providing custom data and extra large os disk 373 | 374 | This is the same as above, but uses custom data to customize the instance. 375 | 376 | Note: Custom data can be custom data or a file to custom data. Please also note that if you use winrm communication to non-nano windows servers custom data is not supported, as winrm is enabled via custom data. 377 | 378 | ```yaml 379 | --- 380 | driver: 381 | name: azurerm 382 | subscription_id: 'your-azure-subscription-id-here' 383 | location: 'West Europe' 384 | machine_size: 'Standard_D1' 385 | 386 | transport: 387 | ssh_key: ~/.ssh/id_kitchen-azurerm 388 | 389 | provisioner: 390 | name: chef_zero 391 | 392 | platforms: 393 | - name: ubuntu-1404 394 | driver: 395 | image_url: https://yourstorageaccount.blob.core.windows.net/system/Microsoft.Compute/Images/images/Cent7_P4-osDisk.170dd1b7-7dc3-4496-b248-f47c49f63965.vhd 396 | existing_storage_account_blob_url: https://yourstorageaccount.blob.core.windows.net 397 | os_type: linux 398 | use_managed_disk: false 399 | vnet_id: /subscriptions/b6e7eee9-YOUR-GUID-HERE-03ab624df016/resourceGroups/pendrica-infrastructure/providers/Microsoft.Network/virtualNetworks/pendrica-arm-vnet 400 | subnet_id: subnet-10.1.0 401 | os_disk_size_gb: 100 402 | #custom_data: /tmp/customdata.txt 403 | custom_data: | 404 | #cloud-config 405 | fqdn: myhostname 406 | preserve_hostname: false 407 | runcmd: 408 | - yum install -y telnet 409 | 410 | suites: 411 | - name: default 412 | attributes: 413 | ``` 414 | 415 | ### kitchen.yml example 9 - Windows 2016 VM with additional data disks 416 | 417 | This example demonstrates how to add 3 additional Managed data disks to a Windows Server 2016 VM. Not supported with legacy (pre-managed disk) storage accounts. 418 | 419 | Note the availability of a `format_data_disks` option (default: `false`). When set to true, a PowerShell script will execute at first boot to initialize and format the disks with an NTFS filesystem. This option does not affect Linux machines. 420 | 421 | ```yaml 422 | --- 423 | driver: 424 | name: azurerm 425 | subscription_id: 'your-azure-subscription-id-here' 426 | location: 'West Europe' 427 | machine_size: 'Standard_F2s' 428 | 429 | provisioner: 430 | name: chef_zero 431 | 432 | platforms: 433 | - name: windows2016-noformat 434 | driver: 435 | image_urn: MicrosoftWindowsServer:WindowsServer:2016-Datacenter:latest 436 | data_disks: 437 | - lun: 0 438 | disk_size_gb: 128 439 | - lun: 1 440 | disk_size_gb: 128 441 | - lun: 2 442 | disk_size_gb: 128 443 | # format_data_disks: false 444 | 445 | suites: 446 | - name: default 447 | attributes: 448 | ``` 449 | 450 | ### kitchen.yml example 10 - "post-deployment" ARM template with MSI authentication 451 | 452 | The following example introduces the ```post_deployment_template``` and ```post_deployment_parameters``` properties in the configuration file. 453 | You can use this capability to execute an ARM template containing Azure resources to provision after the system under test is created. 454 | 455 | In the example the ARM template in the file ```postdeploy.json``` would be executed with the parameters that are specified under ```post_deployment_parameters```. 456 | These resources will be created in the same Azure Resource Group as the VM under test, and therefore will be destroyed when you type ```kitchen destroy```. 457 | 458 | ```yaml 459 | --- 460 | driver: 461 | name: azurerm 462 | subscription_id: 'your-azure-subscription-id-here' 463 | location: 'West Europe' 464 | machine_size: 'Standard_D1' 465 | post_deployment_template: postdeploy.json 466 | post_deployment_parameters: 467 | test_parameter: 'This is a test.' 468 | 469 | transport: 470 | ssh_key: ~/.ssh/id_kitchen-azurerm 471 | 472 | provisioner: 473 | name: chef_zero 474 | 475 | platforms: 476 | - name: ubuntu-1404 477 | driver: 478 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 479 | 480 | suites: 481 | - name: default 482 | attributes: 483 | ``` 484 | 485 | Example postdeploy.json to enable MSI extention on VM: 486 | 487 | ```json 488 | { 489 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 490 | "contentVersion": "1.0.0.0", 491 | "parameters": { 492 | "vmName": { 493 | "type": "String" 494 | }, 495 | "location": { 496 | "type": "String" 497 | }, 498 | "msiExtensionName": { 499 | "type": "String" 500 | } 501 | }, 502 | "resources": [ 503 | { 504 | "type": "Microsoft.Compute/virtualMachines", 505 | "name": "[parameters('vmName')]", 506 | "apiVersion": "2017-12-01", 507 | "location": "[parameters('location')]", 508 | "identity": { 509 | "type": "systemAssigned" 510 | } 511 | }, 512 | { 513 | "type": "Microsoft.Compute/virtualMachines/extensions", 514 | "name": "[concat( parameters('vmName'), '/' , parameters('msiExtensionName') )]", 515 | "apiVersion": "2017-12-01", 516 | "location": "[parameters('location')]", 517 | "properties": { 518 | "publisher": "Microsoft.ManagedIdentity", 519 | "type": "[parameters('msiExtensionName')]", 520 | "typeHandlerVersion": "1.0", 521 | "autoUpgradeMinorVersion": true, 522 | "settings": { 523 | "port": 50342 524 | } 525 | }, 526 | "dependsOn": [ 527 | "[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]" 528 | ] 529 | } 530 | ] 531 | } 532 | ``` 533 | 534 | ### kitchen.yml example 11 - Enabling Managed Service Identities 535 | 536 | This example demonstrates how to enable a System Assigned Identity and User Assigned Identities on a Kitchen VM. 537 | Any combination of System and User assigned identities may be enabled, and multiple User Assigned Identities can be supplied. 538 | 539 | See the [Managed identities for Azure resources](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) documentation for more information on using Managed Service Identities. 540 | 541 | ```yaml 542 | --- 543 | driver: 544 | name: azurerm 545 | subscription_id: 'your-azure-subscription-id-here' 546 | location: 'West Europe' 547 | machine_size: 'Standard_D1' 548 | 549 | transport: 550 | ssh_key: ~/.ssh/id_kitchen-azurerm 551 | 552 | provisioner: 553 | name: chef_zero 554 | 555 | platforms: 556 | - name: ubuntu-1404 557 | driver: 558 | image_urn: Canonical:UbuntuServer:14.04.4-LTS:latest 559 | system_assigned_identity: true 560 | user_assigned_identities: 561 | - /subscriptions/4801fa9d-YOUR-GUID-HERE-b265ff49ce21/resourcegroups/test-kitchen-user/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-kitchen-user 562 | 563 | suites: 564 | - name: default 565 | attributes: 566 | ``` 567 | 568 | ### kitchen.yml example 12 - deploy VM with key vault certificate 569 | 570 | This following example introduces ```secret_url```, ```vault_name```, and ```vault_resource_group``` properties under "driver" in the configuration file. You can use this capability to create a VM with a specified key vault certificate. 571 | 572 | ```yaml 573 | --- 574 | driver: 575 | name: azurerm 576 | subscription_id: 'your-azure-subscription-id-here' 577 | location: 'CentralUS' 578 | machine_size: 'Standard_D2s_v3' 579 | secret_url: 'https://YOUR-SECRET-PATH' 580 | vault_name: 'YOUR-VAULT-NAME' 581 | vault_group_name: 'YOUR-VAULT-GROUP-NAME' 582 | transport: 583 | name: winrm 584 | elevated: true 585 | provisioner: 586 | name: chef_zero 587 | platforms: 588 | - name: win2012R2-sql2016 589 | driver: 590 | image_urn: MicrosoftSQLServer:SQL2016SP2-WS2012R2:SQLDEV:latest 591 | 592 | suites: 593 | - name: default 594 | attributes: 595 | ``` 596 | 597 | ## Support for Government and Sovereign Clouds (China and Germany) 598 | 599 | Starting with v0.9.0 this driver has support for Azure Government and Sovereign Clouds via the use of the ```azure_environment``` setting. Valid Azure environments are ```Azure```, ```AzureUSGovernment```, ```AzureChina``` and ```AzureGermanCloud``` 600 | 601 | Note that the ```use_managed_disks``` option should be set to false until supported by AzureUSGovernment. 602 | 603 | ### Example kitchen.yml for Azure US Government cloud 604 | 605 | ```yaml 606 | --- 607 | driver: 608 | name: azurerm 609 | subscription_id: 'your-azure-subscription-id-here' 610 | azure_environment: 'AzureUSGovernment' 611 | location: 'US Gov Iowa' 612 | machine_size: 'Standard_D2_v2_Promo' 613 | use_managed_disks: false 614 | 615 | provisioner: 616 | name: chef_zero 617 | 618 | verifier: 619 | name: inspec 620 | 621 | platforms: 622 | - name: ubuntu1604 623 | driver: 624 | image_urn: Canonical:UbuntuServer:16.04-LTS:latest 625 | transport: 626 | ssh_key: ~/.ssh/id_kitchen-azurerm 627 | 628 | suites: 629 | - name: default 630 | ``` 631 | 632 | ### How to retrieve the image_urn 633 | 634 | You can use the azure (azure-cli) command line tools to interrogate for the Urn. All 4 parts of the Urn must be specified, though the last part can be changed to "latest" to indicate you always wish to provision the latest operating system and patches. 635 | 636 | ```$ azure vm image list "West Europe" Canonical UbuntuServer``` 637 | 638 | This will return a list like the following, from which you can derive the Urn. 639 | *this list has been shortened for readability* 640 | 641 | ```bash 642 | data: Publisher Offer Sku Version Location Urn 643 | data: --------- ------------ ----------------- --------------- ---------- -------------------------------------------------------- 644 | data: Canonical UbuntuServer 12.04.5-LTS 12.04.201507301 westeurope Canonical:UbuntuServer:12.04.5-LTS:12.04.201507301 645 | data: Canonical UbuntuServer 12.04.5-LTS 12.04.201507311 westeurope Canonical:UbuntuServer:12.04.5-LTS:12.04.201507311 646 | data: Canonical UbuntuServer 12.04.5-LTS 12.04.201508190 westeurope Canonical:UbuntuServer:12.04.5-LTS:12.04.201508190 647 | data: Canonical UbuntuServer 12.04.5-LTS 12.04.201509060 westeurope Canonical:UbuntuServer:12.04.5-LTS:12.04.201509060 648 | data: Canonical UbuntuServer 12.04.5-LTS 12.04.201509090 westeurope Canonical:UbuntuServer:12.04.5-LTS:12.04.201509090 649 | data: Canonical UbuntuServer 12.10 12.10.201212180 westeurope Canonical:UbuntuServer:12.10:12.10.201212180 650 | data: Canonical UbuntuServer 14.04.3-DAILY-LTS 14.04.201509110 westeurope Canonical:UbuntuServer:14.04.3-DAILY-LTS:14.04.201509110 651 | data: Canonical UbuntuServer 14.04.3-DAILY-LTS 14.04.201509160 westeurope Canonical:UbuntuServer:14.04.3-DAILY-LTS:14.04.201509160 652 | data: Canonical UbuntuServer 14.04.3-DAILY-LTS 14.04.201509220 westeurope Canonical:UbuntuServer:14.04.3-DAILY-LTS:14.04.201509220 653 | data: Canonical UbuntuServer 14.04.3-LTS 14.04.201508050 westeurope Canonical:UbuntuServer:14.04.3-LTS:14.04.201508050 654 | data: Canonical UbuntuServer 14.04.3-LTS 14.04.201509080 westeurope Canonical:UbuntuServer:14.04.3-LTS:14.04.201509080 655 | data: Canonical UbuntuServer 15.04 15.04.201506161 westeurope Canonical:UbuntuServer:15.04:15.04.201506161 656 | data: Canonical UbuntuServer 15.04 15.04.201507070 westeurope Canonical:UbuntuServer:15.04:15.04.201507070 657 | data: Canonical UbuntuServer 15.04 15.04.201507220 westeurope Canonical:UbuntuServer:15.04:15.04.201507220 658 | data: Canonical UbuntuServer 15.04 15.04.201507280 westeurope Canonical:UbuntuServer:15.04:15.04.201507280 659 | data: Canonical UbuntuServer 15.10-DAILY 15.10.201509170 westeurope Canonical:UbuntuServer:15.10-DAILY:15.10.201509170 660 | data: Canonical UbuntuServer 15.10-DAILY 15.10.201509180 westeurope Canonical:UbuntuServer:15.10-DAILY:15.10.201509180 661 | data: Canonical UbuntuServer 15.10-DAILY 15.10.201509190 westeurope Canonical:UbuntuServer:15.10-DAILY:15.10.201509190 662 | data: Canonical UbuntuServer 15.10-DAILY 15.10.201509210 westeurope Canonical:UbuntuServer:15.10-DAILY:15.10.201509210 663 | data: Canonical UbuntuServer 15.10-DAILY 15.10.201509220 westeurope Canonical:UbuntuServer:15.10-DAILY:15.10.201509220 664 | info: vm image list command OK 665 | ``` 666 | 667 | ## Contributing 668 | 669 | Contributions to the project are welcome via submitting Pull Requests. 670 | 671 | 1. Fork it ( ) 672 | 2. Create your feature branch (`git checkout -b my-new-feature`) 673 | 3. Commit your changes (`git commit -am 'Add some feature'`) 674 | 4. Push to the branch (`git push origin my-new-feature`) 675 | 5. Create a new Pull Request 676 | 677 | ## Author 678 | 679 | Stuart Preston 680 | 681 | ## License and Copyright 682 | 683 | Copyright 2015-2021, Chef Software, Inc. 684 | 685 | ```text 686 | Licensed under the Apache License, Version 2.0 (the "License"); 687 | you may not use this file except in compliance with the License. 688 | You may obtain a copy of the License at 689 | 690 | http://www.apache.org/licenses/LICENSE-2.0 691 | 692 | Unless required by applicable law or agreed to in writing, software 693 | distributed under the License is distributed on an "AS IS" BASIS, 694 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 695 | See the License for the specific language governing permissions and 696 | limitations under the License. 697 | ``` 698 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | RSpec::Core::RakeTask.new(:test) 4 | 5 | begin 6 | require "cookstyle/chefstyle" 7 | require "rubocop/rake_task" 8 | RuboCop::RakeTask.new(:style) do |task| 9 | task.options += ["--display-cop-names", "--no-color"] 10 | end 11 | rescue LoadError 12 | puts "cookstyle/chefstyle is not available. (sudo) gem install cookstyle to do style checking." 13 | end 14 | 15 | task default: %i{test style} 16 | -------------------------------------------------------------------------------- /commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } -------------------------------------------------------------------------------- /kitchen-azurerm.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require "kitchen/driver/azurerm_version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "kitchen-azurerm" 8 | spec.version = Kitchen::Driver::AZURERM_VERSION 9 | spec.authors = ["Stuart Preston"] 10 | spec.email = ["stuart@chef.io"] 11 | spec.summary = "Test Kitchen driver for Azure Resource Manager." 12 | spec.description = "Test Kitchen driver for the Microsoft Azure Resource Manager (ARM) API" 13 | spec.homepage = "https://github.com/test-kitchen/kitchen-azurerm" 14 | spec.license = "Apache-2.0" 15 | 16 | spec.files = Dir["LICENSE", "lib/**/*", "templates/**/*"] 17 | spec.require_paths = ["lib"] 18 | 19 | spec.required_ruby_version = ">= 3.1" 20 | 21 | spec.add_dependency "azure_mgmt_network2", "~> 1.0.1", ">= 1.0.1" 22 | spec.add_dependency "azure_mgmt_resources2", "~> 1.0.1", ">= 1.0.1" 23 | spec.add_dependency "inifile", "~> 3.0", ">= 3.0.0" 24 | spec.add_dependency "sshkey", ">= 1.0.0", "< 4" 25 | spec.add_dependency "test-kitchen", ">= 1.20", "< 4.0" 26 | end 27 | -------------------------------------------------------------------------------- /lib/kitchen/driver/azure_credentials.rb: -------------------------------------------------------------------------------- 1 | require "inifile" 2 | 3 | require "kitchen/logging" 4 | autoload :MsRest2, "ms_rest2" 5 | autoload :MsRestAzure2, "ms_rest_azure2" 6 | 7 | module Kitchen 8 | module Driver 9 | # 10 | # AzureCredentials 11 | # 12 | class AzureCredentials 13 | include Kitchen::Logging 14 | 15 | CONFIG_PATH = "#{ENV["HOME"]}/.azure/credentials".freeze 16 | 17 | # 18 | # @return [String] 19 | # 20 | attr_reader :subscription_id 21 | 22 | # 23 | # @return [String] 24 | # 25 | attr_reader :environment 26 | 27 | # 28 | # Creates and initializes a new instance of the Credentials class. 29 | # 30 | def initialize(subscription_id:, environment: "Azure") 31 | @subscription_id = subscription_id 32 | @environment = environment 33 | end 34 | 35 | # 36 | # Retrieves an object containing options and credentials 37 | # 38 | # @return [Object] Object that can be supplied along with all Azure client requests. 39 | # 40 | def azure_options 41 | options = { tenant_id: tenant_id!, 42 | subscription_id:, 43 | credentials: ::MsRest2::TokenCredentials.new(token_provider), 44 | active_directory_settings: ad_settings, 45 | base_url: endpoint_settings.resource_manager_endpoint_url } 46 | options[:client_id] = client_id if client_id 47 | options[:client_secret] = client_secret if client_secret 48 | options 49 | end 50 | 51 | private 52 | 53 | def logger 54 | Kitchen.logger 55 | end 56 | 57 | def config_path 58 | @config_path ||= File.expand_path(ENV["AZURE_CONFIG_FILE"] || CONFIG_PATH) 59 | end 60 | 61 | def credentials 62 | @credentials ||= if File.file?(config_path) 63 | IniFile.load(config_path) 64 | else 65 | debug "#{config_path} was not found or not accessible." 66 | {} 67 | end 68 | end 69 | 70 | def credentials_property(property) 71 | credentials[subscription_id]&.[](property) 72 | end 73 | 74 | def tenant_id! 75 | tenant_id || warn("(#{config_path}) does not contain tenant_id neither is the AZURE_TENANT_ID environment variable set.") 76 | end 77 | 78 | def tenant_id 79 | ENV["AZURE_TENANT_ID"] || credentials_property("tenant_id") 80 | end 81 | 82 | def client_id 83 | ENV["AZURE_CLIENT_ID"] || credentials_property("client_id") 84 | end 85 | 86 | def client_secret 87 | ENV["AZURE_CLIENT_SECRET"] || credentials_property("client_secret") 88 | end 89 | 90 | # Retrieve a token based upon the preferred authentication method. 91 | # 92 | # @return [::MsRest2::TokenProvider] A new token provider object. 93 | def token_provider 94 | # Login with a credentials file or setting the environment variables 95 | # 96 | # Typically used with a service principal. 97 | # 98 | # SPN with client_id, client_secret and tenant_id 99 | if client_id && client_secret && tenant_id 100 | ::MsRestAzure2::ApplicationTokenProvider.new(tenant_id, client_id, client_secret, ad_settings) 101 | # Login with a Managed Service Identity. 102 | # 103 | # Typically used with a Managed Service Identity when you have a particular object registered in a tenant. 104 | # 105 | # MSI with client_id and tenant_id (aka User Assigned Identity). 106 | elsif client_id && tenant_id 107 | ::MsRestAzure2::MSITokenProvider.new(50342, ad_settings, { client_id: }) 108 | # Default approach to inheriting existing object permissions (application or device this code is running on). 109 | # 110 | # Typically used when you want to inherit the permissions of the system you're running on that are in a tenant. 111 | # 112 | # MSI with just tenant_id (aka System Assigned Identity). 113 | elsif tenant_id 114 | ::MsRestAzure2::MSITokenProvider.new(50342, ad_settings) 115 | # Login using the Azure CLI 116 | # 117 | # Typically used when you want to rely upon `az login` as your preferred authentication method. 118 | else 119 | warn("Using tenant id set through `az login`.") 120 | ::MsRestAzure2::AzureCliTokenProvider.new(ad_settings) 121 | end 122 | end 123 | 124 | # 125 | # Retrieves a [MsRestAzure2::ActiveDirectoryServiceSettings] object representing the AD settings for the given cloud. 126 | # 127 | # @return [MsRestAzure2::ActiveDirectoryServiceSettings] Settings to be used for subsequent requests 128 | # 129 | def ad_settings 130 | case environment.downcase 131 | when "azureusgovernment" 132 | ::MsRestAzure2::ActiveDirectoryServiceSettings.get_azure_us_government_settings 133 | when "azurechina" 134 | ::MsRestAzure2::ActiveDirectoryServiceSettings.get_azure_china_settings 135 | when "azuregermancloud" 136 | ::MsRestAzure2::ActiveDirectoryServiceSettings.get_azure_german_settings 137 | when "azure" 138 | ::MsRestAzure2::ActiveDirectoryServiceSettings.get_azure_settings 139 | end 140 | end 141 | 142 | # 143 | # Retrieves a [MsRestAzure2::AzureEnvironment] object representing endpoint settings for the given cloud. 144 | # 145 | # @return [MsRestAzure2::AzureEnvironment] Settings to be used for subsequent requests 146 | # 147 | def endpoint_settings 148 | case environment.downcase 149 | when "azureusgovernment" 150 | ::MsRestAzure2::AzureEnvironments::AzureUSGovernment 151 | when "azurechina" 152 | ::MsRestAzure2::AzureEnvironments::AzureChinaCloud 153 | when "azuregermancloud" 154 | ::MsRestAzure2::AzureEnvironments::AzureGermanCloud 155 | when "azure" 156 | ::MsRestAzure2::AzureEnvironments::AzureCloud 157 | end 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/kitchen/driver/azurerm.rb: -------------------------------------------------------------------------------- 1 | require "kitchen" 2 | 3 | autoload :MsRestAzure2, "ms_rest_azure2" 4 | require_relative "azure_credentials" 5 | require "securerandom" unless defined?(SecureRandom) 6 | module Azure 7 | autoload :Resources2, "azure_mgmt_resources2" 8 | autoload :Network2, "azure_mgmt_network2" 9 | end 10 | require "base64" unless defined?(Base64) 11 | autoload :SSHKey, "sshkey" 12 | require "fileutils" unless defined?(FileUtils) 13 | require "erb" unless defined?(Erb) 14 | require "ostruct" unless defined?(OpenStruct) 15 | require "json" unless defined?(JSON) 16 | autoload :Faraday, "faraday" 17 | 18 | module Kitchen 19 | module Driver 20 | # 21 | # Azurerm 22 | # Create a new resource group object and set the location and tags attributes then return it. 23 | # 24 | # @return [::Azure::Resources2::Profiles::Latest::Mgmt::Models::ResourceGroup] A new resource group object. 25 | class Azurerm < Kitchen::Driver::Base 26 | attr_accessor :resource_management_client 27 | attr_accessor :network_management_client 28 | 29 | kitchen_driver_api_version 2 30 | 31 | default_config(:azure_resource_group_prefix) do |_config| 32 | "kitchen-" 33 | end 34 | 35 | default_config(:azure_resource_group_suffix) do |_config| 36 | "" 37 | end 38 | 39 | default_config(:azure_resource_group_name) do |config| 40 | config.instance.name.to_s 41 | end 42 | 43 | default_config(:explicit_resource_group_name) do |_config| 44 | nil 45 | end 46 | 47 | default_config(:resource_group_tags) do |_config| 48 | {} 49 | end 50 | 51 | default_config(:image_urn) do |_config| 52 | "Canonical:UbuntuServer:14.04.3-LTS:latest" 53 | end 54 | 55 | default_config(:image_url) do |_config| 56 | "" 57 | end 58 | 59 | default_config(:image_id) do |_config| 60 | "" 61 | end 62 | 63 | default_config(:use_ephemeral_osdisk) do |_config| 64 | false 65 | end 66 | 67 | default_config(:os_disk_size_gb) do |_config| 68 | "" 69 | end 70 | 71 | default_config(:os_type) do |_config| 72 | "linux" 73 | end 74 | 75 | default_config(:custom_data) do |_config| 76 | "" 77 | end 78 | 79 | default_config(:username) do |_config| 80 | "azure" 81 | end 82 | 83 | default_config(:password) do |_config| 84 | SecureRandom.base64(25) 85 | end 86 | 87 | # This prefix MUST be no longer than 3 characters 88 | default_config(:vm_prefix) do |_config| 89 | "tk-" 90 | end 91 | 92 | default_config :vm_name, nil 93 | 94 | default_config :store_deployment_credentials_in_state, true 95 | 96 | default_config(:nic_name) do |_config| 97 | "" 98 | end 99 | 100 | default_config(:vnet_id) do |_config| 101 | "" 102 | end 103 | 104 | default_config(:subnet_id) do |_config| 105 | "" 106 | end 107 | 108 | default_config(:storage_account_type) do |_config| 109 | "Standard_LRS" 110 | end 111 | 112 | default_config(:existing_storage_account_blob_url) do |_config| 113 | "" 114 | end 115 | 116 | default_config(:existing_storage_account_container) do |_config| 117 | "vhds" 118 | end 119 | 120 | default_config(:boot_diagnostics_enabled) do |_config| 121 | "true" 122 | end 123 | 124 | default_config(:winrm_powershell_script) do |_config| 125 | false 126 | end 127 | 128 | default_config(:azure_environment) do |_config| 129 | "Azure" 130 | end 131 | 132 | default_config(:pre_deployment_template) do |_config| 133 | "" 134 | end 135 | 136 | default_config(:pre_deployment_parameters) do |_config| 137 | {} 138 | end 139 | 140 | default_config(:post_deployment_template) do |_config| 141 | "" 142 | end 143 | 144 | default_config(:post_deployment_parameters) do |_config| 145 | {} 146 | end 147 | 148 | default_config(:plan) do |_config| 149 | {} 150 | end 151 | 152 | default_config(:vm_tags) do |_config| 153 | {} 154 | end 155 | 156 | default_config(:public_ip) do |_config| 157 | false 158 | end 159 | 160 | default_config(:use_managed_disks) do |_config| 161 | true 162 | end 163 | 164 | default_config(:data_disks) do |_config| 165 | nil 166 | end 167 | 168 | default_config(:format_data_disks) do |_config| 169 | false 170 | end 171 | 172 | default_config(:format_data_disks_powershell_script) do |_config| 173 | false 174 | end 175 | 176 | default_config(:system_assigned_identity) do |_config| 177 | false 178 | end 179 | 180 | default_config(:user_assigned_identities) do |_config| 181 | [] 182 | end 183 | 184 | default_config(:destroy_explicit_resource_group) do |_config| 185 | true 186 | end 187 | 188 | default_config(:destroy_explicit_resource_group_tags) do |_config| 189 | true 190 | end 191 | 192 | default_config(:destroy_resource_group_contents) do |_config| 193 | false 194 | end 195 | 196 | default_config(:deployment_sleep) do |_config| 197 | 10 198 | end 199 | 200 | default_config(:secret_url) do |_config| 201 | "" 202 | end 203 | 204 | default_config(:vault_name) do |_config| 205 | "" 206 | end 207 | 208 | default_config(:vault_resource_group) do |_config| 209 | "" 210 | end 211 | 212 | default_config(:subscription_id) do |_config| 213 | ENV["AZURE_SUBSCRIPTION_ID"] 214 | end 215 | 216 | default_config(:public_ip_sku) do |_config| 217 | "Basic" 218 | end 219 | 220 | default_config(:azure_api_retries) do |_config| 221 | 5 222 | end 223 | 224 | default_config(:use_fqdn_hostname) do |_config| 225 | false 226 | end 227 | 228 | def create(state) 229 | state = validate_state(state) 230 | deployment_parameters = { 231 | location: config[:location], 232 | vmSize: config[:machine_size], 233 | storageAccountType: config[:storage_account_type], 234 | bootDiagnosticsEnabled: config[:boot_diagnostics_enabled], 235 | newStorageAccountName: "storage#{state[:uuid]}", 236 | adminUsername: config[:username], 237 | dnsNameForPublicIP: "kitchen-#{state[:uuid]}", 238 | vmName: state[:vm_name], 239 | systemAssignedIdentity: config[:system_assigned_identity], 240 | userAssignedIdentities: config[:user_assigned_identities].map { |identity| [identity, {}] }.to_h, 241 | secretUrl: config[:secret_url], 242 | vaultName: config[:vault_name], 243 | vaultResourceGroup: config[:vault_resource_group], 244 | } 245 | 246 | if instance.transport[:ssh_key].nil? 247 | deployment_parameters[:adminPassword] = config[:password] 248 | end 249 | 250 | deployment_parameters[:publicIPSKU] = config[:public_ip_sku] 251 | 252 | if config[:public_ip_sku] == "Standard" 253 | deployment_parameters[:publicIPAddressType] = "Static" 254 | end 255 | 256 | if config[:subscription_id].to_s == "" 257 | raise "A subscription_id config value was not detected and kitchen-azurerm cannot continue. Please check your kitchen.yml configuration. Exiting." 258 | end 259 | 260 | if config[:nic_name].to_s == "" 261 | vmnic = "nic-#{state[:vm_name]}" 262 | else 263 | vmnic = config[:nic_name] 264 | end 265 | deployment_parameters["nicName"] = vmnic.to_s 266 | 267 | if config[:custom_data].to_s != "" 268 | deployment_parameters["customData"] = prepared_custom_data 269 | end 270 | # When deploying in a shared storage account, we needs to add 271 | # a unique suffix to support multiple kitchen instances 272 | if config[:existing_storage_account_blob_url].to_s != "" 273 | deployment_parameters["osDiskNameSuffix"] = "-#{state[:azure_resource_group_name]}" 274 | end 275 | if config[:existing_storage_account_blob_url].to_s != "" 276 | deployment_parameters["existingStorageAccountBlobURL"] = config[:existing_storage_account_blob_url] 277 | end 278 | if config[:existing_storage_account_container].to_s != "" 279 | deployment_parameters["existingStorageAccountBlobContainer"] = config[:existing_storage_account_container] 280 | end 281 | if config[:os_disk_size_gb].to_s != "" 282 | deployment_parameters["osDiskSizeGb"] = config[:os_disk_size_gb] 283 | end 284 | 285 | # The three deployment modes 286 | # a) Private Image: Managed VM Image (by id) 287 | # b) Private Image: Using a VHD URL (note: we must use existing_storage_account_blob_url due to azure limitations) 288 | # c) Public Image: Using a marketplace image (urn) 289 | if config[:image_id].to_s != "" 290 | deployment_parameters["imageId"] = config[:image_id] 291 | elsif config[:image_url].to_s != "" 292 | deployment_parameters["imageUrl"] = config[:image_url] 293 | deployment_parameters["osType"] = config[:os_type] 294 | else 295 | image_publisher, image_offer, image_sku, image_version = config[:image_urn].split(":", 4) 296 | deployment_parameters["imagePublisher"] = image_publisher 297 | deployment_parameters["imageOffer"] = image_offer 298 | deployment_parameters["imageSku"] = image_sku 299 | deployment_parameters["imageVersion"] = image_version 300 | end 301 | 302 | options = Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id], 303 | environment: config[:azure_environment]).azure_options 304 | 305 | debug "Azure environment: #{config[:azure_environment]}" 306 | @resource_management_client = ::Azure::Resources2::Profiles::Latest::Mgmt::Client.new(options) 307 | 308 | # Create Resource Group 309 | begin 310 | info "Creating Resource Group: #{state[:azure_resource_group_name]}" 311 | create_resource_group(state[:azure_resource_group_name], get_resource_group) 312 | rescue ::MsRestAzure2::AzureOperationError => operation_error 313 | error operation_error.body 314 | raise operation_error 315 | end 316 | 317 | # Execute deployment steps 318 | begin 319 | if File.file?(config[:pre_deployment_template]) 320 | pre_deployment_name = "pre-deploy-#{state[:uuid]}" 321 | info "Creating deployment: #{pre_deployment_name}" 322 | create_deployment_async(state[:azure_resource_group_name], pre_deployment_name, pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])).value! 323 | follow_deployment_until_end_state(state[:azure_resource_group_name], pre_deployment_name) 324 | end 325 | deployment_name = "deploy-#{state[:uuid]}" 326 | info "Creating deployment: #{deployment_name}" 327 | create_deployment_async(state[:azure_resource_group_name], deployment_name, deployment(deployment_parameters)).value! 328 | follow_deployment_until_end_state(state[:azure_resource_group_name], deployment_name) 329 | 330 | if config[:store_deployment_credentials_in_state] == true 331 | state[:username] = deployment_parameters[:adminUsername] unless existing_state_value?(state, :username) 332 | state[:password] = deployment_parameters[:adminPassword] unless existing_state_value?(state, :password) && instance.transport[:ssh_key].nil? 333 | end 334 | 335 | if File.file?(config[:post_deployment_template]) 336 | post_deployment_name = "post-deploy-#{state[:uuid]}" 337 | info "Creating deployment: #{post_deployment_name}" 338 | create_deployment_async(state[:azure_resource_group_name], post_deployment_name, post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])).value! 339 | follow_deployment_until_end_state(state[:azure_resource_group_name], post_deployment_name) 340 | end 341 | rescue ::MsRestAzure2::AzureOperationError => operation_error 342 | rest_error = operation_error.body["error"] 343 | deployment_active = rest_error["code"] == "DeploymentActive" 344 | if deployment_active 345 | info "Deployment for resource group #{state[:azure_resource_group_name]} is ongoing." 346 | info "If you need to change the deployment template you'll need to rerun `kitchen create` for this instance." 347 | else 348 | info rest_error 349 | raise operation_error 350 | end 351 | end 352 | 353 | @network_management_client = ::Azure::Network2::Profiles::Latest::Mgmt::Client.new(options) 354 | 355 | if config[:vnet_id] == "" || config[:public_ip] 356 | # Retrieve the public IP from the resource group: 357 | result = get_public_ip(state[:azure_resource_group_name], "publicip") 358 | info "IP Address is: #{result.ip_address} [#{result.dns_settings.fqdn}]" 359 | state[:hostname] = result.ip_address 360 | if config[:use_fqdn_hostname] 361 | info "Using FQDN to communicate instead of IP" 362 | state[:hostname] = result.dns_settings.fqdn 363 | end 364 | else 365 | # Retrieve the internal IP from the resource group: 366 | result = get_network_interface(state[:azure_resource_group_name], vmnic.to_s) 367 | info "IP Address is: #{result.ip_configurations[0].private_ipaddress}" 368 | state[:hostname] = result.ip_configurations[0].private_ipaddress 369 | end 370 | end 371 | 372 | # Return a True of False if the state is already stored for a particular property. 373 | # 374 | # @param [Hash] Hash of existing state values. 375 | # @param [String] A property to check 376 | # @return [Boolean] 377 | def existing_state_value?(state, property) 378 | state.key?(property) && !state[property].nil? 379 | end 380 | 381 | # Leverage existing state values or bring state into existence from a configuration file. 382 | # 383 | # @param [Hash] Existing Hash of state values. 384 | # @return [Hash] Updated Hash of state values. 385 | def validate_state(state = {}) 386 | state[:uuid] = SecureRandom.hex(8) unless existing_state_value?(state, :uuid) 387 | state[:vm_name] = config[:vm_name] || "#{config[:vm_prefix]}#{state[:uuid][0..11]}" unless existing_state_value?(state, :vm_name) 388 | state[:server_id] = "vm#{state[:uuid]}" unless existing_state_value?(state, :server_id) 389 | state[:azure_resource_group_name] = azure_resource_group_name unless existing_state_value?(state, :azure_resource_group_name) 390 | %i{subscription_id azure_environment use_managed_disks}.each do |config_element| 391 | state[config_element] = config[config_element] unless existing_state_value?(state, config_element) 392 | end 393 | state.delete(:password) unless instance.transport[:ssh_key].nil? 394 | state 395 | end 396 | 397 | def azure_resource_group_name 398 | formatted_time = Time.now.utc.strftime "%Y%m%dT%H%M%S" 399 | return "#{config[:azure_resource_group_prefix]}#{config[:azure_resource_group_name]}-#{formatted_time}#{config[:azure_resource_group_suffix]}" unless config[:explicit_resource_group_name] 400 | 401 | config[:explicit_resource_group_name] 402 | end 403 | 404 | def data_disks_for_vm_json 405 | return nil if config[:data_disks].nil? 406 | 407 | disks = [] 408 | 409 | if config[:use_managed_disks] 410 | config[:data_disks].each do |data_disk| 411 | disks << { name: "datadisk#{data_disk[:lun]}", lun: data_disk[:lun], diskSizeGB: data_disk[:disk_size_gb], createOption: "Empty" } 412 | end 413 | debug "Additional disks being added to configuration: #{disks.inspect}" 414 | else 415 | warn 'Data disks are only supported when used with the "use_managed_disks" option. No additional disks were added to the configuration.' 416 | end 417 | disks.to_json 418 | end 419 | 420 | def template_for_transport_name 421 | template = JSON.parse(virtual_machine_deployment_template) 422 | if instance.transport.name.casecmp("winrm") == 0 423 | if instance.platform.name.index("nano").nil? 424 | info "Adding WinRM configuration to provisioning profile." 425 | encoded_command = Base64.strict_encode64(custom_data_script_windows) 426 | template["resources"].select { |h| h["type"] == "Microsoft.Compute/virtualMachines" }.each do |resource| 427 | resource["properties"]["osProfile"]["customData"] = encoded_command 428 | resource["properties"]["osProfile"]["windowsConfiguration"] = windows_unattend_content 429 | end 430 | end 431 | end 432 | 433 | unless instance.transport[:ssh_key].nil? 434 | info "Adding public key from #{File.expand_path(instance.transport[:ssh_key])}.pub to the deployment." 435 | public_key = public_key_for_deployment(File.expand_path(instance.transport[:ssh_key])) 436 | template["resources"].select { |h| h["type"] == "Microsoft.Compute/virtualMachines" }.each do |resource| 437 | resource["properties"]["osProfile"]["linuxConfiguration"] = JSON.parse(custom_linux_configuration(public_key)) 438 | end 439 | end 440 | template.to_json 441 | end 442 | 443 | def public_key_for_deployment(private_key_filename) 444 | if File.file?(private_key_filename) == false 445 | k = SSHKey.generate 446 | 447 | ::FileUtils.mkdir_p(File.dirname(private_key_filename)) 448 | 449 | private_key_file = File.new(private_key_filename, "w") 450 | private_key_file.syswrite(k.private_key) 451 | private_key_file.chmod(0600) 452 | private_key_file.close 453 | 454 | public_key_file = File.new("#{private_key_filename}.pub", "w") 455 | public_key_file.syswrite(k.ssh_public_key) 456 | public_key_file.chmod(0600) 457 | public_key_file.close 458 | 459 | output = k.ssh_public_key 460 | else 461 | output = if instance.transport[:ssh_public_key].nil? 462 | File.read("#{private_key_filename}.pub") 463 | else 464 | File.read(instance.transport[:ssh_public_key]) 465 | end 466 | end 467 | output.strip 468 | end 469 | 470 | def pre_deployment(pre_deployment_template_filename, pre_deployment_parameters) 471 | pre_deployment_template = ::File.read(pre_deployment_template_filename) 472 | pre_deployment = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::Deployment.new 473 | pre_deployment.properties = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentProperties.new 474 | pre_deployment.properties.mode = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentMode::Incremental 475 | pre_deployment.properties.template = JSON.parse(pre_deployment_template) 476 | pre_deployment.properties.parameters = parameters_in_values_format(pre_deployment_parameters) 477 | debug(pre_deployment.properties.template) 478 | pre_deployment 479 | end 480 | 481 | def deployment(parameters) 482 | template = template_for_transport_name 483 | deployment = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::Deployment.new 484 | deployment.properties = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentProperties.new 485 | deployment.properties.mode = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentMode::Incremental 486 | deployment.properties.template = JSON.parse(template) 487 | deployment.properties.parameters = parameters_in_values_format(parameters) 488 | debug(JSON.pretty_generate(deployment.properties.template)) 489 | deployment 490 | end 491 | 492 | def post_deployment(post_deployment_template_filename, post_deployment_parameters) 493 | post_deployment_template = ::File.read(post_deployment_template_filename) 494 | post_deployment = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::Deployment.new 495 | post_deployment.properties = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentProperties.new 496 | post_deployment.properties.mode = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentMode::Incremental 497 | post_deployment.properties.template = JSON.parse(post_deployment_template) 498 | post_deployment.properties.parameters = parameters_in_values_format(post_deployment_parameters) 499 | debug(post_deployment.properties.template) 500 | post_deployment 501 | end 502 | 503 | def empty_deployment 504 | template = virtual_machine_deployment_template_file("empty.erb", nil) 505 | empty_deployment = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::Deployment.new 506 | empty_deployment.properties = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentProperties.new 507 | empty_deployment.properties.mode = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::DeploymentMode::Complete 508 | empty_deployment.properties.template = JSON.parse(template) 509 | debug(JSON.pretty_generate(empty_deployment.properties.template)) 510 | empty_deployment 511 | end 512 | 513 | def vm_tag_string(vm_tags_in) 514 | tag_string = "" 515 | unless vm_tags_in.empty? 516 | tag_array = vm_tags_in.map do |key, value| 517 | "\"#{key}\": \"#{value}\",\n" 518 | end 519 | # Strip punctuation from last item 520 | tag_array[-1] = tag_array[-1][0..-3] 521 | tag_string = tag_array.join 522 | end 523 | tag_string 524 | end 525 | 526 | def parameters_in_values_format(parameters_in) 527 | parameters = parameters_in.map do |key, value| 528 | { key.to_sym => { "value" => value } } 529 | end 530 | parameters.reduce(:merge!) 531 | end 532 | 533 | def follow_deployment_until_end_state(resource_group, deployment_name) 534 | end_provisioning_states = "Canceled,Failed,Deleted,Succeeded" 535 | end_provisioning_state_reached = false 536 | until end_provisioning_state_reached 537 | list_outstanding_deployment_operations(resource_group, deployment_name) 538 | sleep config[:deployment_sleep] 539 | deployment_provisioning_state = get_deployment_state(resource_group, deployment_name) 540 | end_provisioning_state_reached = end_provisioning_states.split(",").include?(deployment_provisioning_state) 541 | end 542 | info "Resource Template deployment reached end state of '#{deployment_provisioning_state}'." 543 | show_failed_operations(resource_group, deployment_name) if deployment_provisioning_state == "Failed" 544 | end 545 | 546 | def show_failed_operations(resource_group, deployment_name) 547 | failed_operations = list_deployment_operations(resource_group, deployment_name) 548 | failed_operations.each do |val| 549 | resource_code = val.properties.status_code 550 | raise val.properties.status_message.inspect.to_s if resource_code != "OK" 551 | end 552 | end 553 | 554 | def list_outstanding_deployment_operations(resource_group, deployment_name) 555 | end_operation_states = "Failed,Succeeded" 556 | deployment_operations = list_deployment_operations(resource_group, deployment_name) 557 | deployment_operations.each do |val| 558 | resource_provisioning_state = val.properties.provisioning_state 559 | unless val.properties.target_resource.nil? 560 | resource_name = val.properties.target_resource.resource_name 561 | resource_type = val.properties.target_resource.resource_type 562 | end 563 | end_operation_state_reached = end_operation_states.split(",").include?(resource_provisioning_state) 564 | unless end_operation_state_reached 565 | info "Resource #{resource_type} '#{resource_name}' provisioning status is #{resource_provisioning_state}" 566 | end 567 | end 568 | end 569 | 570 | def destroy(state) 571 | # TODO: We have some not so fun state issues we need to clean up 572 | state[:azure_environment] = config[:azure_environment] unless state[:azure_environment] 573 | state[:subscription_id] = config[:subscription_id] unless state[:subscription_id] 574 | 575 | # Setup our authentication components for the SDK 576 | options = Kitchen::Driver::AzureCredentials.new(subscription_id: state[:subscription_id], 577 | environment: state[:azure_environment]).azure_options 578 | @resource_management_client = ::Azure::Resources2::Profiles::Latest::Mgmt::Client.new(options) 579 | 580 | # If we don't have any instances, let's check to see if the user wants to delete a resource group and if so let's delete! 581 | if state[:server_id].nil? && state[:azure_resource_group_name].nil? && !config[:explicit_resource_group_name].nil? && config[:destroy_explicit_resource_group] 582 | if resource_group_exists?(config[:explicit_resource_group_name]) 583 | info "This instance doesn't exist but you asked to delete the resource group." 584 | begin 585 | info "Destroying Resource Group: #{config[:explicit_resource_group_name]}" 586 | delete_resource_group_async(config[:explicit_resource_group_name]) 587 | info "Destroy operation accepted and will continue in the background." 588 | return 589 | rescue ::MsRestAzure2::AzureOperationError => operation_error 590 | error operation_error.body 591 | raise operation_error 592 | end 593 | end 594 | end 595 | 596 | # Our working environment 597 | info "Azure environment: #{state[:azure_environment]}" 598 | 599 | # Skip if we don't have any instances 600 | return if state[:server_id].nil? 601 | 602 | # Destroy resource group contents 603 | if config[:destroy_resource_group_contents] == true 604 | info "Destroying individual resources within the Resource Group." 605 | empty_deployment_name = "empty-deploy-#{state[:uuid]}" 606 | begin 607 | info "Creating deployment: #{empty_deployment_name}" 608 | create_deployment_async(state[:azure_resource_group_name], empty_deployment_name, empty_deployment).value! 609 | follow_deployment_until_end_state(state[:azure_resource_group_name], empty_deployment_name) 610 | 611 | # NOTE: We are using the internal wrapper function create_resource_group() which wraps the API 612 | # method of create_or_update() 613 | begin 614 | # Maintain tags on the resource group 615 | create_resource_group(state[:azure_resource_group_name], get_resource_group) unless config[:destroy_explicit_resource_group_tags] == true 616 | warn 'The "destroy_explicit_resource_group_tags" setting value is set to "false". The tags on the resource group will NOT be removed.' unless config[:destroy_explicit_resource_group_tags] == true 617 | # Corner case where we want to use kitchen to remove the tags 618 | resource_group = get_resource_group 619 | resource_group.tags = {} 620 | create_resource_group(state[:azure_resource_group_name], resource_group) unless config[:destroy_explicit_resource_group_tags] == false 621 | warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.' unless config[:destroy_explicit_resource_group_tags] == false 622 | rescue ::MsRestAzure2::AzureOperationError => operation_error 623 | error operation_error.body 624 | raise operation_error 625 | end 626 | 627 | rescue ::MsRestAzure2::AzureOperationError => operation_error 628 | error operation_error.body 629 | raise operation_error 630 | end 631 | end 632 | 633 | # Do not remove the explicitly named resource group 634 | if config[:destroy_explicit_resource_group] == false && !config[:explicit_resource_group_name].nil? 635 | warn 'The "destroy_explicit_resource_group" setting value is set to "false". The resource group will not be deleted.' 636 | warn 'Remember to manually destroy resources, or set "destroy_resource_group_contents: true" to save costs!' unless config[:destroy_resource_group_contents] == true 637 | return state 638 | end 639 | 640 | # Destroy the world 641 | begin 642 | info "Destroying Resource Group: #{state[:azure_resource_group_name]}" 643 | delete_resource_group_async(state[:azure_resource_group_name]) 644 | info "Destroy operation accepted and will continue in the background." 645 | # Remove resource group name from driver state 646 | state.delete(:azure_resource_group_name) 647 | rescue ::MsRestAzure2::AzureOperationError => operation_error 648 | error operation_error.body 649 | raise operation_error 650 | end 651 | 652 | # Clear state of components 653 | state.delete(:server_id) 654 | state.delete(:hostname) 655 | state.delete(:username) 656 | state.delete(:password) 657 | end 658 | 659 | def enable_winrm_powershell_script 660 | config[:winrm_powershell_script] || 661 | <<-PS1 662 | $cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\\LocalMachine\\My 663 | $config = '@{CertificateThumbprint="' + $cert.Thumbprint + '"}' 664 | winrm create winrm/config/listener?Address=*+Transport=HTTPS $config 665 | winrm create winrm/config/Listener?Address=*+Transport=HTTP 666 | winrm set winrm/config/service/auth '@{Basic="true";Kerberos="false";Negotiate="true";Certificate="false";CredSSP="true"}' 667 | New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP 668 | winrm set winrm/config/service '@{AllowUnencrypted="true"}' 669 | New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP 670 | PS1 671 | end 672 | 673 | def format_data_disks_powershell_script 674 | return unless config[:format_data_disks] 675 | 676 | info "Data disks will be initialized and formatted NTFS automatically." unless config[:data_disks].nil? 677 | config[:format_data_disks_powershell_script] || 678 | <<-PS1 679 | Write-Host "Initializing and formatting raw disks" 680 | $disks = Get-Disk | where partitionstyle -eq 'raw' 681 | $letters = New-Object System.Collections.ArrayList 682 | $letters.AddRange( ('F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z') ) 683 | Function AvailableVolumes() { 684 | $currentDrives = get-volume 685 | ForEach ($v in $currentDrives) { 686 | if ($letters -contains $v.DriveLetter.ToString()) { 687 | Write-Host "Drive letter $($v.DriveLetter) is taken, moving to next letter" 688 | $letters.Remove($v.DriveLetter.ToString()) 689 | } 690 | } 691 | } 692 | ForEach ($d in $disks) { 693 | AvailableVolumes 694 | $driveLetter = $letters[0] 695 | Write-Host "Creating volume $($driveLetter)" 696 | $d | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -DriveLetter $driveLetter -UseMaximumSize 697 | # Prevent error ' Cannot perform the requested operation while the drive is read only' 698 | Start-Sleep 1 699 | Format-Volume -FileSystem NTFS -NewFileSystemLabel "datadisk" -DriveLetter $driveLetter -Confirm:$false 700 | } 701 | PS1 702 | end 703 | 704 | def custom_data_script_windows 705 | <<-EOH 706 | #{enable_winrm_powershell_script} 707 | #{format_data_disks_powershell_script} 708 | logoff 709 | EOH 710 | end 711 | 712 | def custom_linux_configuration(public_key) 713 | <<-EOH 714 | { 715 | "disablePasswordAuthentication": "true", 716 | "ssh": { 717 | "publicKeys": [ 718 | { 719 | "path": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", 720 | "keyData": "#{public_key}" 721 | } 722 | ] 723 | } 724 | } 725 | EOH 726 | end 727 | 728 | def windows_unattend_content 729 | { 730 | additionalUnattendContent: [ 731 | { 732 | passName: "oobeSystem", 733 | componentName: "Microsoft-Windows-Shell-Setup", 734 | settingName: "FirstLogonCommands", 735 | content: 'cmd /c "copy C:\\AzureData\\CustomData.bin C:\\Config.ps1"copy1%windir%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -file C:\\Config.ps1script2', 736 | }, 737 | { 738 | passName: "oobeSystem", 739 | componentName: "Microsoft-Windows-Shell-Setup", 740 | settingName: "AutoLogon", 741 | content: "[concat('', parameters('adminPassword'), 'true1', parameters('adminUserName'), '')]", 742 | }, 743 | ], 744 | } 745 | end 746 | 747 | def virtual_machine_deployment_template 748 | if config[:vnet_id] == "" 749 | virtual_machine_deployment_template_file("public.erb", vm_tags: vm_tag_string(config[:vm_tags]), use_managed_disks: config[:use_managed_disks], image_url: config[:image_url], storage_account_type: config[:storage_account_type], existing_storage_account_blob_url: config[:existing_storage_account_blob_url], image_id: config[:image_id], existing_storage_account_container: config[:existing_storage_account_container], custom_data: config[:custom_data], os_disk_size_gb: config[:os_disk_size_gb], data_disks_for_vm_json:, use_ephemeral_osdisk: config[:use_ephemeral_osdisk], ssh_key: instance.transport[:ssh_key], plan_json:) 750 | else 751 | info "Using custom vnet: #{config[:vnet_id]}" 752 | virtual_machine_deployment_template_file("internal.erb", vnet_id: config[:vnet_id], subnet_id: config[:subnet_id], public_ip: config[:public_ip], vm_tags: vm_tag_string(config[:vm_tags]), use_managed_disks: config[:use_managed_disks], image_url: config[:image_url], storage_account_type: config[:storage_account_type], existing_storage_account_blob_url: config[:existing_storage_account_blob_url], image_id: config[:image_id], existing_storage_account_container: config[:existing_storage_account_container], custom_data: config[:custom_data], os_disk_size_gb: config[:os_disk_size_gb], data_disks_for_vm_json:, use_ephemeral_osdisk: config[:use_ephemeral_osdisk], ssh_key: instance.transport[:ssh_key], public_ip_sku: config[:public_ip_sku], plan_json:) 753 | end 754 | end 755 | 756 | def plan_json 757 | return nil if config[:plan].empty? 758 | 759 | plan = {} 760 | plan["name"] = config[:plan][:name] if config[:plan][:name] 761 | plan["product"] = config[:plan][:product] if config[:plan][:product] 762 | plan["promotionCode"] = config[:plan][:promotion_code] if config[:plan][:promotion_code] 763 | plan["publisher"] = config[:plan][:publisher] if config[:plan][:publisher] 764 | 765 | plan.to_json 766 | end 767 | 768 | def virtual_machine_deployment_template_file(template_file, data = {}) 769 | template = File.read(File.expand_path(File.join(__dir__, "../../../templates", template_file))) 770 | render_binding = OpenStruct.new(data) 771 | ERB.new(template, trim_mode: "-").result(render_binding.instance_eval { binding }) 772 | end 773 | 774 | def resource_manager_endpoint_url(azure_environment) 775 | case azure_environment.downcase 776 | when "azureusgovernment" 777 | MsRestAzure2::AzureEnvironments::AzureUSGovernment.resource_manager_endpoint_url 778 | when "azurechina" 779 | MsRestAzure2::AzureEnvironments::AzureChinaCloud.resource_manager_endpoint_url 780 | when "azuregermancloud" 781 | MsRestAzure2::AzureEnvironments::AzureGermanCloud.resource_manager_endpoint_url 782 | when "azure" 783 | MsRestAzure2::AzureEnvironments::AzureCloud.resource_manager_endpoint_url 784 | end 785 | end 786 | 787 | def prepared_custom_data 788 | # If user_data is a file reference, lets read it as such 789 | return nil if config[:custom_data].nil? 790 | 791 | @custom_data ||= if File.file?(config[:custom_data]) 792 | Base64.strict_encode64(File.read(config[:custom_data])) 793 | else 794 | Base64.strict_encode64(config[:custom_data]) 795 | end 796 | end 797 | 798 | private 799 | 800 | # 801 | # Wrapper methods for the Azure API calls to retry the calls when getting timeouts. 802 | # 803 | 804 | # Create a new resource group object and set the location and tags attributes then return it. 805 | # 806 | # @return [::Azure::Resources2::Profiles::Latest::Mgmt::Models::ResourceGroup] A new resource group object. 807 | def get_resource_group 808 | resource_group = ::Azure::Resources2::Profiles::Latest::Mgmt::Models::ResourceGroup.new 809 | resource_group.location = config[:location] 810 | resource_group.tags = config[:resource_group_tags] 811 | resource_group 812 | end 813 | 814 | # Checks whether a resource group exists. 815 | # 816 | # @param resource_group_name [String] The name of the resource group to check. 817 | # The name is case insensitive. 818 | # 819 | # @return [Boolean] operation results. 820 | # 821 | def resource_group_exists?(resource_group_name) 822 | retries = config[:azure_api_retries] 823 | begin 824 | resource_management_client.resource_groups.check_existence(resource_group_name) 825 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 826 | send_exception_message(exception, "while checking if resource group '#{resource_group_name}' exists. #{retries} retries left.") 827 | raise if retries == 0 828 | 829 | retries -= 1 830 | retry 831 | end 832 | end 833 | 834 | def create_resource_group(resource_group_name, resource_group) 835 | retries = config[:azure_api_retries] 836 | begin 837 | resource_management_client.resource_groups.create_or_update(resource_group_name, resource_group) 838 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 839 | send_exception_message(exception, "while creating resource group '#{resource_group_name}'. #{retries} retries left.") 840 | raise if retries == 0 841 | 842 | retries -= 1 843 | retry 844 | end 845 | end 846 | 847 | def create_deployment_async(resource_group, deployment_name, deployment) 848 | retries = config[:azure_api_retries] 849 | begin 850 | resource_management_client.deployments.begin_create_or_update_async(resource_group, deployment_name, deployment) 851 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 852 | send_exception_message(exception, "while sending deployment creation request for deployment '#{deployment_name}'. #{retries} retries left.") 853 | raise if retries == 0 854 | 855 | retries -= 1 856 | retry 857 | end 858 | end 859 | 860 | def get_public_ip(resource_group_name, public_ip_name) 861 | retries = config[:azure_api_retries] 862 | begin 863 | network_management_client.public_ipaddresses.get(resource_group_name, public_ip_name) 864 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 865 | send_exception_message(exception, "while fetching public ip '#{public_ip_name}' for resource group '#{resource_group_name}'. #{retries} retries left.") 866 | raise if retries == 0 867 | 868 | retries -= 1 869 | retry 870 | end 871 | end 872 | 873 | def get_network_interface(resource_group_name, network_interface_name) 874 | retries = config[:azure_api_retries] 875 | begin 876 | network_interfaces = ::Azure::Network2::Profiles::Latest::Mgmt::NetworkInterfaces.new(network_management_client) 877 | network_interfaces.get(resource_group_name, network_interface_name) 878 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 879 | send_exception_message(exception, "while fetching network interface '#{network_interface_name}' for resource group '#{resource_group_name}'. #{retries} retries left.") 880 | raise if retries == 0 881 | 882 | retries -= 1 883 | retry 884 | end 885 | end 886 | 887 | def list_deployment_operations(resource_group, deployment_name) 888 | retries = config[:azure_api_retries] 889 | begin 890 | resource_management_client.deployment_operations.list(resource_group, deployment_name) 891 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 892 | send_exception_message(exception, "while listing deployment operations for deployment '#{deployment_name}'. #{retries} retries left.") 893 | raise if retries == 0 894 | 895 | retries -= 1 896 | retry 897 | end 898 | end 899 | 900 | def get_deployment_state(resource_group, deployment_name) 901 | retries = config[:azure_api_retries] 902 | begin 903 | deployments = resource_management_client.deployments.get(resource_group, deployment_name) 904 | deployments.properties.provisioning_state 905 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 906 | send_exception_message(exception, "while retrieving state for deployment '#{deployment_name}'. #{retries} retries left.") 907 | raise if retries == 0 908 | 909 | retries -= 1 910 | retry 911 | end 912 | end 913 | 914 | def delete_resource_group_async(resource_group_name) 915 | retries = config[:azure_api_retries] 916 | begin 917 | resource_management_client.resource_groups.begin_delete(resource_group_name) 918 | rescue Faraday::TimeoutError, Faraday::ClientError => exception 919 | send_exception_message(exception, "while sending resource group deletion request for '#{resource_group_name}'. #{retries} retries left.") 920 | raise if retries == 0 921 | 922 | retries -= 1 923 | retry 924 | end 925 | end 926 | 927 | def send_exception_message(exception, message) 928 | if exception.is_a?(Faraday::TimeoutError) 929 | header = "Timed out" 930 | elsif exception.is_a?(Faraday::ClientError) 931 | header = "Connection reset by peer" 932 | else 933 | # Unhandled exception, return early 934 | info "Unrecognized exception type." 935 | return 936 | end 937 | info "#{header} #{message}" 938 | end 939 | end 940 | end 941 | end 942 | -------------------------------------------------------------------------------- /lib/kitchen/driver/azurerm_version.rb: -------------------------------------------------------------------------------- 1 | module Kitchen 2 | module Driver 3 | AZURERM_VERSION = "1.13.2".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "package-name": "kitchen-azurerm", 5 | "changelog-path": "CHANGELOG.md", 6 | "release-type": "ruby", 7 | "include-component-in-tag": false, 8 | "version-file": "lib/kitchen/driver/azurerm_version.rb" 9 | } 10 | }, 11 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 12 | } 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard", 6 | "schedule:automergeEarlyMondays" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/azure_credentials: -------------------------------------------------------------------------------- 1 | # For client_id && client_secret tests 2 | [f02932df-7e1d-410f-b094-c626d447f4dc] 3 | client_id = "b5f3d6df-00bf-4451-a4f2-db3bc7731b58" 4 | client_secret = ":Qnt[7?:7RXzdMXrXE0ygBROA1hY1iV[" 5 | tenant_id = "19d3ea3e-ea8f-48f3-9f7a-00ae2810991f" 6 | 7 | # For MSI test, with client_id 8 | [5d801ddc-acf4-406b-9830-587ca2c6fd80] 9 | client_id = "2801f9e6-c4c2-4667-a6e1-479f8827b0af" 10 | tenant_id = "1ba5986d-52e1-49eb-a77e-155b7440695f" 11 | 12 | # For MSI test, no client_id 13 | [7c664d3f-6dca-4e6d-9637-13dadbbe59d3] 14 | tenant_id = "76d8fa56-d867-4819-9894-f6dd4e1d2079" 15 | -------------------------------------------------------------------------------- /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 | require "rspec/its" 15 | require "ms_rest2" 16 | require "ms_rest_azure2" 17 | require "azure_mgmt_resources2" 18 | require_relative "../lib/kitchen/driver/azurerm" 19 | -------------------------------------------------------------------------------- /spec/unit/kitchen/driver/azure_credentials_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "ms_rest_azure2" 3 | 4 | describe Kitchen::Driver::AzureCredentials do 5 | CLIENT_ID_AND_SECRET_SUB = 0 6 | CLIENT_ID_SUB = 1 7 | NO_CLIENT_SUB = 2 8 | 9 | let(:instance) do 10 | opts = {} 11 | opts[:subscription_id] = subscription_id 12 | opts[:environment] = environment if environment 13 | described_class.new(**opts) 14 | end 15 | 16 | let(:environment) { "Azure" } 17 | let(:fixtures_path) { File.expand_path("../../../fixtures", __dir__) } 18 | let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] } 19 | let(:client_id) { ini_credentials[subscription_id]["client_id"] } 20 | let(:client_secret) { ini_credentials[subscription_id]["client_secret"] } 21 | let(:tenant_id) { ini_credentials[subscription_id]["tenant_id"] } 22 | let(:default_config_path) { File.expand_path(described_class::CONFIG_PATH) } 23 | let(:ini_credentials) { IniFile.load("#{fixtures_path}/azure_credentials") } 24 | 25 | before do 26 | allow(ENV).to receive(:[]).and_call_original 27 | allow(ENV).to receive(:[]).with("AZURE_CONFIG_FILE").and_return(nil) 28 | allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(nil) 29 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(nil) 30 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(nil) 31 | 32 | allow(File).to receive(:file?).and_call_original 33 | allow(File).to receive(:file?).with(default_config_path).and_return(true) 34 | 35 | allow(IniFile).to receive(:load).with(default_config_path).and_return(ini_credentials) 36 | end 37 | 38 | subject { instance } 39 | 40 | it { is_expected.to respond_to(:subscription_id) } 41 | it { is_expected.to respond_to(:environment) } 42 | it { is_expected.to respond_to(:azure_options) } 43 | 44 | describe "::new" do 45 | it "sets subscription_id" do 46 | expect(subject.subscription_id).to eq(subscription_id) 47 | end 48 | 49 | context "when an environment is provided" do 50 | let(:environment) { "AzureChina" } 51 | 52 | it "sets environment, when one is provided" do 53 | expect(subject.environment).to eq(environment) 54 | end 55 | end 56 | 57 | context "no environment is provided" do 58 | let(:environment) { nil } 59 | 60 | it "sets Azure as the environment" do 61 | expect(subject.environment).to eq("Azure") 62 | end 63 | end 64 | end 65 | 66 | describe "#azure_options" do 67 | subject { azure_options } 68 | 69 | let(:azure_options) { instance.azure_options } 70 | let(:credentials) { azure_options[:credentials] } 71 | let(:token_provider) { credentials.instance_variable_get(:@token_provider) } 72 | let(:active_directory_settings) { azure_options[:active_directory_settings] } 73 | 74 | context "when AZURE_CONFIG_FILE is set" do 75 | let(:overridden_config_path) { "/tmp/my-config" } 76 | 77 | before do 78 | allow(ENV).to receive(:[]).with("AZURE_CONFIG_FILE").and_return(overridden_config_path) 79 | end 80 | 81 | it "loads credentials from the path specified in environment variable" do 82 | allow(File).to receive(:file?).with(overridden_config_path).and_return(true) 83 | expect(IniFile).to receive(:load).with(overridden_config_path).and_return(ini_credentials) 84 | expect(IniFile).not_to receive(:load).with(default_config_path) 85 | azure_options 86 | end 87 | end 88 | 89 | context "when configuration file does not exist and at least one of the environment variables is not set" do 90 | before do 91 | allow(File).to receive(:file?).with(default_config_path).and_return(false) 92 | allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(tenant_id) 93 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(client_id) 94 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(nil) 95 | end 96 | 97 | it "logs a warning" do 98 | expect(Kitchen.logger).to receive(:debug).with("#{default_config_path} was not found or not accessible.") 99 | azure_options 100 | end 101 | end 102 | 103 | context "when AZURE_TENANT_ID is set" do 104 | let(:tenant_id) { "2d38055e-66a1-435c-be53-TENANT_ID" } 105 | 106 | before do 107 | allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(tenant_id) 108 | end 109 | 110 | its([:tenant_id]) { is_expected.to eq(tenant_id) } 111 | end 112 | 113 | context "when AZURE_CLIENT_ID is set" do 114 | let(:client_id) { "2e201a46-44a8-4508-84aa-CLIENT_ID" } 115 | 116 | before do 117 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(client_id) 118 | end 119 | 120 | its([:client_id]) { is_expected.to eq(client_id) } 121 | end 122 | 123 | context "when AZURE_CLIENT_SECRET is set" do 124 | let(:client_secret) { "2e201a46-44a8-4508-84aa-CLIENT_SECRET" } 125 | 126 | before do 127 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(client_secret) 128 | end 129 | 130 | its([:client_secret]) { is_expected.to eq(client_secret) } 131 | end 132 | 133 | context "when environment is Azure" do 134 | let(:environment) { "Azure" } 135 | 136 | its([:base_url]) { is_expected.to eq("https://management.azure.com/") } 137 | 138 | context "active_directory_settings" do 139 | it "sets the authentication_endpoint correctly" do 140 | expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.com/") 141 | end 142 | 143 | it "sets the token_audience correctly" do 144 | expect(active_directory_settings.token_audience).to eq("https://management.core.windows.net/") 145 | end 146 | end 147 | end 148 | 149 | context "when environment is AzureUSGovernment" do 150 | let(:environment) { "AzureUSGovernment" } 151 | 152 | its([:base_url]) { is_expected.to eq("https://management.usgovcloudapi.net") } 153 | 154 | context "active_directory_settings" do 155 | it "sets the authentication_endpoint correctly" do 156 | expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.us/") 157 | end 158 | 159 | it "sets the token_audience correctly" do 160 | expect(active_directory_settings.token_audience).to eq("https://management.core.usgovcloudapi.net/") 161 | end 162 | end 163 | end 164 | 165 | context "when environment is AzureChina" do 166 | let(:environment) { "AzureChina" } 167 | 168 | its([:base_url]) { is_expected.to eq("https://management.chinacloudapi.cn") } 169 | 170 | context "active_directory_settings" do 171 | it "sets the authentication_endpoint correctly" do 172 | expect(active_directory_settings.authentication_endpoint).to eq("https://login.chinacloudapi.cn/") 173 | end 174 | 175 | it "sets the token_audience correctly" do 176 | expect(active_directory_settings.token_audience).to eq("https://management.core.chinacloudapi.cn/") 177 | end 178 | end 179 | end 180 | 181 | context "when environment is AzureGermanCloud" do 182 | let(:environment) { "AzureGermanCloud" } 183 | 184 | its([:base_url]) { is_expected.to eq("https://management.microsoftazure.de") } 185 | 186 | context "active_directory_settings" do 187 | it "sets the authentication_endpoint correctly" do 188 | expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.de/") 189 | end 190 | 191 | it "sets the token_audience correctly" do 192 | expect(active_directory_settings.token_audience).to eq("https://management.core.cloudapi.de/") 193 | end 194 | end 195 | end 196 | 197 | shared_examples "common option specs" do 198 | it { is_expected.to be_instance_of(Hash) } 199 | its([:tenant_id]) { is_expected.to eq(tenant_id) } 200 | its([:subscription_id]) { is_expected.to eq(subscription_id) } 201 | its([:credentials]) { is_expected.to be_instance_of(MsRest2::TokenCredentials) } 202 | its([:client_id]) { is_expected.to eq(client_id) } 203 | its([:client_secret]) { is_expected.to eq(client_secret) } 204 | its([:base_url]) { is_expected.to eq("https://management.azure.com/") } 205 | end 206 | 207 | context "when using client_id and client_secret" do 208 | let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] } 209 | 210 | include_examples "common option specs" 211 | 212 | it "uses token provider: MsRestAzure2::ApplicationTokenProvider" do 213 | expect(token_provider).to be_instance_of(MsRestAzure2::ApplicationTokenProvider) 214 | end 215 | 216 | it "sets the client_id" do 217 | expect(token_provider.instance_variables).to include(:@client_id) 218 | expect(token_provider.send(:client_id)).to eq(client_id) 219 | end 220 | 221 | it "sets the client_secret" do 222 | expect(token_provider.instance_variables).to include(:@client_secret) 223 | expect(token_provider.send(:client_secret)).to eq(client_secret) 224 | end 225 | end 226 | 227 | context "when using client_id, without client_secret" do 228 | let(:subscription_id) { ini_credentials.sections[CLIENT_ID_SUB] } 229 | 230 | include_examples "common option specs" 231 | 232 | it "uses token provider: MsRestAzure2::MSITokenProvider" do 233 | expect(token_provider).to be_instance_of(MsRestAzure2::MSITokenProvider) 234 | end 235 | 236 | it "sets the client_id" do 237 | expect(token_provider.instance_variables).to include(:@client_id) 238 | expect(token_provider.send(:client_id)).to eq(client_id) 239 | end 240 | 241 | it "does not set client_secret" do 242 | expect(token_provider.instance_variables).not_to include(:@client_secret) 243 | end 244 | end 245 | 246 | context "when not using client_id or client_secret" do 247 | let(:subscription_id) { ini_credentials.sections[NO_CLIENT_SUB] } 248 | 249 | include_examples "common option specs" 250 | 251 | it "uses token provider: MsRestAzure2::MSITokenProvider" do 252 | expect(token_provider).to be_instance_of(MsRestAzure2::MSITokenProvider) 253 | end 254 | 255 | it "does not set the client_id" do 256 | expect(token_provider.instance_variables).not_to include(:@client_id) 257 | end 258 | 259 | it "does not set client_secret" do 260 | expect(token_provider.instance_variables).not_to include(:@client_secret) 261 | end 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /spec/unit/kitchen/driver/azurerm_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "kitchen/transport/dummy" 3 | 4 | describe Kitchen::Driver::Azurerm do 5 | let(:logged_output) { StringIO.new } 6 | let(:logger) { Logger.new(logged_output) } 7 | let(:platform) { Kitchen::Platform.new(name: "fake_platform") } 8 | let(:transport) { Kitchen::Transport::Dummy.new } 9 | let(:instance_name) { "my-instance-name" } 10 | let(:driver) { described_class.new(config) } 11 | 12 | let(:subscription_id) { "115b12cb-b0d3-4ed9-94db-f73733be6f3c" } 13 | let(:location) { "eastus2" } 14 | let(:machine_size) { "Standard_D4_v3" } 15 | let(:vm_tags) do 16 | { 17 | os_type: "linux", 18 | distro: "redhat", 19 | } 20 | end 21 | 22 | let(:azure_environment) { "AzureChina" } 23 | 24 | let(:image_urn) { "RedHat:rhel-byos:rhel-raw76:7.6.20190620" } 25 | let(:vm_name) { "my-awesome-vm" } 26 | 27 | let(:config) do 28 | { 29 | subscription_id: subscription_id, 30 | location: location, 31 | machine_size: machine_size, 32 | vm_tags: vm_tags, 33 | image_urn: image_urn, 34 | vm_name: vm_name, 35 | azure_environment: azure_environment, 36 | } 37 | end 38 | 39 | let(:credentials) do 40 | Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id], 41 | environment: config[:azure_environment]) 42 | end 43 | 44 | let(:options) do 45 | credentials.azure_options 46 | end 47 | 48 | let(:client) do 49 | Azure::Resources2::Profiles::Latest::Mgmt::Client.new(options) 50 | end 51 | 52 | let(:instance) do 53 | instance_double(Kitchen::Instance, 54 | name: instance_name, 55 | logger: logger, 56 | transport: transport, 57 | platform: platform, 58 | to_str: "instance_str") 59 | end 60 | 61 | let(:resource_group) do 62 | Azure::Resources2::Profiles::Latest::Mgmt::Models::ResourceGroup.new 63 | end 64 | 65 | let(:resource_groups) do 66 | client.resource_groups 67 | end 68 | 69 | before do 70 | allow(driver).to receive(:instance).and_return(instance) 71 | end 72 | 73 | it "driver API version is 2" do 74 | expect(driver.diagnose_plugin[:api_version]).to eq(2) 75 | end 76 | 77 | describe "#name" do 78 | it "has an overridden name" do 79 | expect(driver.name).to eq("Azurerm") 80 | end 81 | end 82 | 83 | describe "#default_config" do 84 | let(:default_config) { driver.instance_variable_get(:@config) } 85 | 86 | it "Should have the username option available" do 87 | expect(default_config).to have_key(:username) 88 | end 89 | 90 | it "Should use 'azure' as the default username" do 91 | expect(default_config[:username]).to eq("azure") 92 | end 93 | 94 | it "Should have the password option available" do 95 | expect(default_config).to have_key(:password) 96 | end 97 | 98 | it "Should have the use_fqdn_hostname option available" do 99 | expect(default_config).to have_key(:use_fqdn_hostname) 100 | end 101 | 102 | it "Should use the IP to communicate with VM by default" do 103 | expect(default_config[:use_fqdn_hostname]).to eq(false) 104 | end 105 | 106 | it "Should use basic public IP resources" do 107 | expect(default_config[:public_ip_sku]).to eq("Basic") 108 | end 109 | 110 | it "should set store_deployment_credentials_in_state to true" do 111 | expect(default_config[:store_deployment_credentials_in_state]).to eq(true) 112 | end 113 | 114 | it "Should use tk- vm prefix" do 115 | expect(default_config[:vm_prefix]).to eq("tk-") 116 | end 117 | end 118 | 119 | describe "#validate_state" do 120 | let(:state) { {} } 121 | let(:uuid) { SecureRandom.hex(8) } 122 | 123 | it "generates uuid, when one does not exist" do 124 | driver.validate_state(state) 125 | expect(state[:uuid].length).to eq(16) 126 | expect(state[:uuid]).to be_an_instance_of(String) 127 | expect(state[:uuid]).not_to eq(uuid) 128 | end 129 | 130 | it "does not set uuid, when one exists" do 131 | state[:uuid] = uuid 132 | driver.validate_state(state) 133 | expect(state[:uuid]).to eq(uuid) 134 | end 135 | 136 | context "when vm_name is set in config" do 137 | before do 138 | config[:vm_name] = vm_name 139 | end 140 | 141 | it "sets state[:vm_name] to config vm_name" do 142 | driver.validate_state(state) 143 | expect(state[:vm_name]).to eq(vm_name) 144 | end 145 | end 146 | 147 | context "when vm_name is not set in config" do 148 | before do 149 | config.delete(:vm_name) 150 | end 151 | 152 | it "generates vm_name, when one does not exist in state" do 153 | driver.validate_state(state) 154 | expect(state[:vm_name].length).to eq(15) 155 | expect(state[:vm_name]).to be_an_instance_of(String) 156 | expect(state[:vm_name]).not_to eq(vm_name) 157 | expect(state[:vm_name]).to start_with("tk-") 158 | end 159 | 160 | it "does not generate vm_name, when one exists in state" do 161 | vm_name_in_state = "blah-doh" 162 | state[:vm_name] = vm_name_in_state 163 | driver.validate_state(state) 164 | expect(state[:vm_name]).to eq(vm_name_in_state) 165 | end 166 | 167 | context "when vm_prefix is set in config" do 168 | before do 169 | config[:vm_prefix] = "ab-" 170 | end 171 | 172 | it "generates vm_name with prefix, when one does not exist in state" do 173 | driver.validate_state(state) 174 | expect(state[:vm_name].length).to eq(15) 175 | expect(state[:vm_name]).to be_an_instance_of(String) 176 | expect(state[:vm_name]).not_to eq(vm_name) 177 | expect(state[:vm_name]).to start_with("ab-") 178 | end 179 | end 180 | end 181 | end 182 | 183 | describe "#create" do 184 | let(:tenant_id) { "2d38055e-66a1-435c-be53-TENANT_ID" } 185 | let(:client_id) { "2e201a46-44a8-4508-84aa-CLIENT_ID" } 186 | let(:client_secret) { "2e201a46-44a8-4508-84aa-CLIENT_SECRET" } 187 | let(:environment) { "AzureChina" } 188 | let(:resource_group_name) { "testingrocks" } 189 | let(:base_url) { "https://management.chinacloudapi.cn" } 190 | 191 | let(:deployment_double) { double("DeploymentDouble", value!: nil) } 192 | let(:network_interfaces_double) { double("NetworkInterfacesDouble", ip_configurations: [ip_configuration_double]) } 193 | let(:ip_configuration_double) { double("IPConfigurationDouble", private_ipaddress: "192.168.1.5") } 194 | let(:public_ip_double) { double("PublicIPDouble", ip_address: "100.100.2.5", dns_settings: dns_settings_double) } 195 | let(:dns_settings_double) { double("DNSSettingsDouble", fqdn: "dns-settings-fqdn") } 196 | 197 | before do 198 | allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(tenant_id) 199 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(client_id) 200 | allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(client_secret) 201 | allow(ENV).to receive(:[]).with("AZURE_SUBSCRIPTION_ID").and_return(subscription_id) 202 | allow(ENV).to receive(:[]).with("https_proxy").and_return("") 203 | allow(ENV).to receive(:[]).with("AZURE_HTTP_LOGGING").and_return("") 204 | allow(ENV).to receive(:[]).with("GEM_SKIP").and_return("") 205 | allow(ENV).to receive(:[]).with("http_proxy").and_return("") 206 | allow(ENV).to receive(:[]).with("GEM_REQUIREMENT_AZURE_MGMT_RESOURCES").and_return("azure_mgmt_resources") 207 | allow(ENV).to receive(:[]).with("SSL_CERT_FILE").and_call_original 208 | end 209 | 210 | it "has credentials available" do 211 | expect(credentials).to be_an_instance_of(Kitchen::Driver::AzureCredentials) 212 | end 213 | 214 | it "has options" do 215 | expect(options[:tenant_id]).to eq(tenant_id) 216 | expect(options[:client_id]).to eq(client_id) 217 | expect(options[:client_secret]).to eq(client_secret) 218 | end 219 | 220 | # it "fails to create or update a resource group because we are not authenticated" do 221 | # rgn = resource_group_name 222 | # rg = resource_group 223 | # rg.location = location 224 | # rg.tags = vm_tags 225 | 226 | # # https://github.com/Azure/azure-sdk-for-ruby/blob/master/runtime/ms_rest_azure2/spec/azure_operation_error_spec.rb 227 | # expect { resource_groups.create_or_update(rgn, rg) }.to raise_error( an_instance_of(MsRestAzure2::AzureOperationError) ) 228 | # end 229 | 230 | # it "saves deployment credentials to state, when store_deployment_credentials_in_state is true" do 231 | # # This MUST come first 232 | # config[:store_deployment_credentials_in_state] = true 233 | # config[:username] = "azure" 234 | # config[:password] = "admin-password" 235 | 236 | # allow(driver).to receive(:create_resource_group) 237 | # allow(driver).to receive(:deployment) 238 | # allow(driver).to receive(:create_deployment_async).and_return(deployment_double) 239 | # allow(driver).to receive(:follow_deployment_until_end_state) 240 | # allow(driver).to receive(:get_network_interface).and_return(network_interfaces_double) 241 | # allow(driver).to receive(:get_public_ip).and_return(public_ip_double) 242 | 243 | # state = {} 244 | # driver.create(state) 245 | # expect(state[:username]).to eq("azure") 246 | # expect(state[:password]).to eq("admin-password") 247 | # end 248 | 249 | # it "does not save deployment credentials to state, when store_deployment_credentials_in_state is false" do 250 | # # This MUST come first 251 | # config[:store_deployment_credentials_in_state] = false 252 | # config[:username] = "azure" 253 | # config[:password] = "admin-password" 254 | 255 | # allow(driver).to receive(:create_resource_group) 256 | # allow(driver).to receive(:deployment) 257 | # allow(driver).to receive(:create_deployment_async).and_return(deployment_double) 258 | # allow(driver).to receive(:follow_deployment_until_end_state) 259 | # allow(driver).to receive(:get_network_interface).and_return(network_interfaces_double) 260 | # allow(driver).to receive(:get_public_ip).and_return(public_ip_double) 261 | 262 | # state = {} 263 | # driver.create(state) 264 | # expect(state[:username]).to eq(nil) 265 | # expect(state[:password]).to eq(nil) 266 | # end 267 | end 268 | 269 | describe "#virtual_machine_deployment_template" do 270 | subject { driver.send(:virtual_machine_deployment_template) } 271 | 272 | let(:parsed_json) { JSON.parse(subject) } 273 | let(:vm_resource) { parsed_json["resources"].find { |x| x["type"] == "Microsoft.Compute/virtualMachines" } } 274 | 275 | context "when plan config is provided" do 276 | let(:plan_name) { "plan-abc" } 277 | let(:plan_product) { "my-product" } 278 | let(:plan_publisher) { "captain-america" } 279 | let(:plan_promotion_code) { "50-percent-off" } 280 | 281 | let(:plan) do 282 | { 283 | name: plan_name, 284 | product: plan_product, 285 | publisher: plan_publisher, 286 | promotion_code: plan_promotion_code, 287 | } 288 | end 289 | 290 | let(:config) do 291 | { 292 | subscription_id: subscription_id, 293 | location: location, 294 | machine_size: machine_size, 295 | vm_tags: vm_tags, 296 | plan: plan, 297 | image_urn: image_urn, 298 | vm_name: vm_name, 299 | } 300 | end 301 | 302 | it "includes plan information in deployment template" do 303 | expect(vm_resource).to have_key("plan") 304 | expect(vm_resource["plan"]["name"]).to eq(plan_name) 305 | expect(vm_resource["plan"]["product"]).to eq(plan_product) 306 | expect(vm_resource["plan"]["publisher"]).to eq(plan_publisher) 307 | expect(vm_resource["plan"]["promotionCode"]).to eq(plan_promotion_code) 308 | end 309 | end 310 | 311 | context "when plan config is not provided" do 312 | let(:config) do 313 | { 314 | subscription_id: subscription_id, 315 | location: location, 316 | machine_size: machine_size, 317 | vm_tags: vm_tags, 318 | image_urn: image_urn, 319 | vm_name: vm_name, 320 | } 321 | end 322 | 323 | it "does not include plan information in deployment template" do 324 | expect(vm_resource).not_to have_key("plan") 325 | end 326 | end 327 | end 328 | end 329 | -------------------------------------------------------------------------------- /templates/empty.erb: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": {}, 5 | "variables": {}, 6 | "resources": [] 7 | } 8 | -------------------------------------------------------------------------------- /templates/internal.erb: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The location where the resources will be created." 9 | } 10 | }, 11 | "vmSize": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "The size of the VM to be created" 15 | } 16 | }, 17 | "newStorageAccountName": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." 21 | } 22 | }, 23 | "adminUsername": { 24 | "type": "string", 25 | "metadata": { 26 | "description": "User name for the Virtual Machine." 27 | } 28 | }, 29 | <%- if ssh_key.nil? -%> 30 | "adminPassword": { 31 | "type": "securestring", 32 | "metadata": { 33 | "description": "Password for the Virtual Machine." 34 | } 35 | }, 36 | <%- end -%> 37 | "dnsNameForPublicIP": { 38 | "type": "string", 39 | "metadata": { 40 | "description": "Unique DNS Name for the Public IP used to access the Virtual Machine." 41 | } 42 | }, 43 | "publicIPSKU": { 44 | "type": "string", 45 | "defaultValue": "Basic", 46 | "metadata": { 47 | "description": "SKU name for the Public IP used to access the Virtual Machine." 48 | } 49 | }, 50 | "publicIPAddressType": { 51 | "type": "string", 52 | "defaultValue": "Dynamic", 53 | "metadata": { 54 | "description": "SKU name for the Public IP used to access the Virtual Machine." 55 | } 56 | }, 57 | <%- unless os_disk_size_gb.to_s.empty? -%> 58 | "osDiskSizeGb": { 59 | "type": "int", 60 | "minValue": 1, 61 | "maxValue": 2048, 62 | "metadata": { 63 | "description": "Size of the OS disks in GB." 64 | } 65 | }, 66 | <%- end -%> 67 | "secretUrl": { 68 | "type": "string", 69 | "metadata": { 70 | "description": "Secret vault certificate URL" 71 | } 72 | }, 73 | "vaultName" : { 74 | "type": "string", 75 | "metadata": { 76 | "description": "Name of key vault where certificate is located." 77 | } 78 | }, 79 | "vaultResourceGroup": { 80 | "type": "string", 81 | "metadata": { 82 | "description": "Resource group name where key vault is located." 83 | } 84 | }, 85 | <%- unless custom_data.empty? -%> 86 | "customData": { 87 | "type": "string", 88 | "metadata": { 89 | "description": "Custom Data for the instance (e.g. cloud-init or script) - not compatible with winrm." 90 | } 91 | }, 92 | <%- end -%> 93 | <%- if !existing_storage_account_blob_url.empty? -%> 94 | "existingStorageAccountBlobURL": { 95 | "type": "string", 96 | "metadata": { 97 | "description": "The URL of the existing storage account (blob) (without container)" 98 | } 99 | }, 100 | <%- end -%> 101 | <%- if !existing_storage_account_container.empty? -%> 102 | "existingStorageAccountBlobContainer": { 103 | "type": "string", 104 | "metadata": { 105 | "description": "The Container Name for OS Images (blob)" 106 | } 107 | }, 108 | <%- end -%> 109 | <%- if !image_url.empty? -%> 110 | "imageUrl": { 111 | "type": "string", 112 | "metadata": { 113 | "description": "An URL for a private Image (vhd)" 114 | } 115 | }, 116 | "osType": { 117 | "type": "string", 118 | "metadata": { 119 | "description": "An OS Type (linux, windows)" 120 | } 121 | }, 122 | <%- elsif !image_id.empty? -%> 123 | "imageId": { 124 | "type": "string", 125 | "metadata": { 126 | "description": "The id of a managed image" 127 | } 128 | }, 129 | <%- else -%> 130 | "imagePublisher": { 131 | "type": "string", 132 | "defaultValue": "Canonical", 133 | "metadata": { 134 | "description": "Publisher for the VM, e.g. Canonical, MicrosoftWindowsServer" 135 | } 136 | }, 137 | "imageOffer": { 138 | "type": "string", 139 | "defaultValue": "UbuntuServer", 140 | "metadata": { 141 | "description": "Offer for the VM, e.g. UbuntuServer, WindowsServer." 142 | } 143 | }, 144 | "imageSku": { 145 | "type": "string", 146 | "defaultValue": "14.04.3-LTS", 147 | "metadata": { 148 | "description": "Sku for the VM, e.g. 14.04.3-LTS" 149 | } 150 | }, 151 | "imageVersion": { 152 | "type": "string", 153 | "defaultValue": "latest", 154 | "metadata": { 155 | "description": "Either a date or latest." 156 | } 157 | }, 158 | <%- end -%> 159 | "osDiskNameSuffix": { 160 | "type": "string", 161 | "defaultValue": "", 162 | "metadata": { 163 | "description": "A disk Name Suffix to make the disk name unique in existing storage accounts." 164 | } 165 | }, 166 | "vmName": { 167 | "type": "string", 168 | "defaultValue": "vm", 169 | "metadata": { 170 | "description": "The vm name created inside of the resource group." 171 | } 172 | }, 173 | "nicName": { 174 | "type": "string", 175 | "defaultValue": "nic", 176 | "metadata": { 177 | "description": "The nic name created inside of the resource group." 178 | } 179 | }, 180 | "storageAccountType": { 181 | "type": "string", 182 | "defaultValue": "<%= storage_account_type %>", 183 | "metadata": { 184 | "description": "The type of storage to use (e.g. Standard_LRS or Premium_LRS)." 185 | } 186 | }, 187 | "systemAssignedIdentity": { 188 | "type": "bool", 189 | "defaultValue": false, 190 | "metadata": { 191 | "description": "Whether to enable system assigned identity for the vm." 192 | } 193 | }, 194 | "userAssignedIdentities": { 195 | "type": "object", 196 | "defaultValue": {}, 197 | "metadata": { 198 | "description": "An object whose keys are resource IDs for user identities to associate with the Virtual Machine and whose values are empty objects, or empty to disable user assigned identities." 199 | } 200 | }, 201 | "bootDiagnosticsEnabled": { 202 | "type": "string", 203 | "defaultValue": "true", 204 | "metadata": { 205 | "description": "Whether to enable (true) or disable (false) boot diagnostics. Default: true (requires Standard storage)." 206 | } 207 | } 208 | }, 209 | "variables": { 210 | "location": "[parameters('location')]", 211 | "OSDiskName": "osdisk", 212 | "nicName": "[parameters('nicName')]", 213 | "addressPrefix": "10.0.0.0/16", 214 | "subnetName": "<%= subnet_id %>", 215 | "subnetPrefix": "10.0.0.0/24", 216 | "storageAccountType": "[parameters('storageAccountType')]", 217 | "publicIPAddressName": "publicip", 218 | "vmStorageAccountContainerName": "vhds", 219 | "vmName": "[parameters('vmName')]", 220 | "vmSize": "[parameters('vmSize')]", 221 | "vmIdentityType": "[if(parameters('systemAssignedIdentity'), if(empty(parameters('userAssignedIdentities')), 'SystemAssigned', 'SystemAssigned, UserAssigned'), if(empty(parameters('userAssignedIdentities')), 'None', 'UserAssigned'))]", 222 | "virtualNetworkName": "vnet", 223 | "vnetID": "<%= vnet_id %>", 224 | "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" 225 | }, 226 | "resources": [ 227 | { 228 | "apiVersion": "2017-05-10", 229 | "name": "pid-18d63047-6cdf-4f34-beed-62f01fc73fc2", 230 | "type": "Microsoft.Resources/deployments", 231 | "properties": { 232 | "mode": "Incremental", 233 | "template": { 234 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 235 | "contentVersion": "1.0.0.0", 236 | "resources": [] 237 | } 238 | } 239 | }, 240 | <%- unless use_managed_disks -%> 241 | <%- if existing_storage_account_blob_url.empty? -%> 242 | { 243 | "type": "Microsoft.Storage/storageAccounts", 244 | "name": "[parameters('newStorageAccountName')]", 245 | "apiVersion": "2015-05-01-preview", 246 | "location": "[variables('location')]", 247 | "properties": { 248 | "accountType": "[variables('storageAccountType')]" 249 | }, 250 | "tags": { 251 | <%= vm_tags unless vm_tags.empty? %> 252 | } 253 | }, 254 | <%- end -%> 255 | <%- end -%> 256 | <%- if public_ip -%> 257 | { 258 | "apiVersion": "2017-08-01", 259 | "type": "Microsoft.Network/publicIPAddresses", 260 | "name": "[variables('publicIPAddressName')]", 261 | "location": "[variables('location')]", 262 | "sku": { 263 | "name": "[parameters('publicIPSKU')]" 264 | }, 265 | "properties": { 266 | "publicIPAllocationMethod": "[parameters('publicIPAddressType')]", 267 | "dnsSettings": { 268 | "domainNameLabel": "[parameters('dnsNameForPublicIP')]" 269 | } 270 | }, 271 | "tags": { 272 | <%= vm_tags unless vm_tags.empty? %> 273 | } 274 | }, 275 | <%- end -%> 276 | { 277 | "apiVersion": "2015-05-01-preview", 278 | "type": "Microsoft.Network/networkInterfaces", 279 | "name": "[variables('nicName')]", 280 | "location": "[variables('location')]", 281 | <%- if public_ip -%> 282 | "dependsOn": [ 283 | "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" 284 | ], 285 | <%- end -%> 286 | "properties": { 287 | "ipConfigurations": [ 288 | { 289 | "name": "ipconfig1", 290 | "properties": { 291 | "privateIPAllocationMethod": "Dynamic", 292 | <%- if public_ip -%> 293 | "publicIPAddress": { 294 | "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" 295 | }, 296 | <%- end -%> 297 | "subnet": { 298 | "id": "[variables('subnetRef')]" 299 | } 300 | } 301 | } 302 | ] 303 | }, 304 | "tags": { 305 | <%= vm_tags unless vm_tags.empty? %> 306 | } 307 | }, 308 | { 309 | "apiVersion": "2018-06-01", 310 | "type": "Microsoft.Compute/virtualMachines", 311 | "name": "[variables('vmName')]", 312 | "location": "[variables('location')]", 313 | "dependsOn": [ 314 | <%- unless use_managed_disks -%> 315 | <%- if existing_storage_account_blob_url.empty? -%> 316 | "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]", 317 | <%- end -%> 318 | <%- end -%> 319 | "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" 320 | ], 321 | "properties": { 322 | "hardwareProfile": { 323 | "vmSize": "[variables('vmSize')]" 324 | }, 325 | "osProfile": { 326 | "computername": "[variables('vmName')]", 327 | <%- unless custom_data.empty? -%> 328 | "customData": "[parameters('customData')]", 329 | <%- end -%> 330 | <%- unless secretUrl.to_s.empty? && vaultName.to_s.empty? && vaultResourceGroup.to_s.empty? -%> 331 | "secret": [ 332 | "sourceVault": { 333 | "id": "[resourceId(parameters('vaultResourceGroup'), 'Microsoft,KeyVault/vaults', parameters('vaultName'))]" 334 | }, 335 | "vaultCertificates": [ 336 | { 337 | "certificateUrl": "[parameters('secretUrl')]", 338 | "certificateStore": "My" 339 | } 340 | ] 341 | ], 342 | <%- end -%> 343 | <%- if ssh_key.nil? -%> 344 | "adminPassword": "[parameters('adminPassword')]", 345 | <%- end -%> 346 | "adminUsername": "[parameters('adminUsername')]" 347 | }, 348 | "storageProfile": { 349 | <%- if image_url.empty? and image_id.empty? -%> 350 | "imageReference": { 351 | "publisher": "[parameters('imagePublisher')]", 352 | "offer": "[parameters('imageOffer')]", 353 | "sku": "[parameters('imageSku')]", 354 | "version": "[parameters('imageVersion')]" 355 | }, 356 | <%- elsif !image_id.empty? -%> 357 | "imageReference": { 358 | "id": "[parameters('imageId')]" 359 | }, 360 | <%- end -%> 361 | <%- if use_ephemeral_osdisk -%> 362 | "osDisk": { 363 | "diffDiskSettings": { 364 | "option": "Local" 365 | }, 366 | "caching": "ReadOnly", 367 | "createOption": "FromImage" 368 | } 369 | <%- elsif use_managed_disks -%> 370 | "osDisk": { 371 | "name": "[concat('disk-', parameters('vmName'))]", 372 | <%- unless os_disk_size_gb.to_s.empty? -%> 373 | "diskSizeGB": "[parameters('osDiskSizeGB')]", 374 | <%- end -%> 375 | "managedDisk": { 376 | "storageAccountType": "[parameters('storageAccountType')]" 377 | }, 378 | "createOption": "FromImage" 379 | } 380 | <%- else -%> 381 | "osDisk": { 382 | "name": "[concat('disk-', parameters('vmName'))]", 383 | <%- unless os_disk_size_gb.to_s.empty? -%> 384 | "diskSizeGB": "[parameters('osDiskSizeGB')]", 385 | <%- end -%> 386 | <%- if !image_url.empty? -%> 387 | "image": { 388 | "uri": "[parameters('imageUrl')]" 389 | }, 390 | "osType": "[parameters('osType')]", 391 | <%- end -%> 392 | "vhd": { 393 | <%- if existing_storage_account_blob_url.empty? -%> 394 | "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName')), '2015-06-15').primaryEndpoints.blob, variables('vmStorageAccountContainerName'), '/',variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 395 | <%- else -%> 396 | <%- if existing_storage_account_container.empty? -%> 397 | "uri": "[concat(parameters('existingStorageAccountBlobURL'), '/', variables('vmStorageAccountContainerName'), '/', variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 398 | <%- else -%> 399 | "uri": "[concat(parameters('existingStorageAccountBlobURL'), '/', parameters('existingStorageAccountBlobContainer'), '/', variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 400 | <%- end -%> 401 | <%- end -%> 402 | }, 403 | "caching": "ReadWrite", 404 | "createOption": "FromImage" 405 | } 406 | <%- end -%> 407 | <%- unless data_disks_for_vm_json.nil? -%> 408 | ,"dataDisks": 409 | <%= data_disks_for_vm_json %> 410 | <%- end -%> 411 | }, 412 | "networkProfile": { 413 | "networkInterfaces": [ 414 | { 415 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 416 | } 417 | ] 418 | }, 419 | "diagnosticsProfile": { 420 | <%- unless use_managed_disks -%> 421 | "bootDiagnostics": { 422 | "enabled": "[parameters('bootDiagnosticsEnabled')]", 423 | <%- if existing_storage_account_blob_url.empty? -%> 424 | "storageUri": "[reference(concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName')), '2015-06-15').primaryEndpoints.blob]" 425 | <%- else -%> 426 | "storageUri": "[parameters('existingStorageAccountBlobURL')]" 427 | <%- end -%> 428 | } 429 | <%- end -%> 430 | } 431 | }, 432 | <%- unless plan_json.nil? -%> 433 | "plan": <%= plan_json %>, 434 | <%- end -%> 435 | "identity": { 436 | "type": "[variables('vmIdentityType')]", 437 | "userAssignedIdentities": "[if(empty(parameters('userAssignedIdentities')), json('null'), parameters('userAssignedIdentities'))]" 438 | }, 439 | "tags": { 440 | <%= vm_tags unless vm_tags.empty? %> 441 | } 442 | } 443 | ] 444 | } 445 | -------------------------------------------------------------------------------- /templates/public.erb: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The location where the resources will be created." 9 | } 10 | }, 11 | "vmSize": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "The size of the VM to be created" 15 | } 16 | }, 17 | "newStorageAccountName": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." 21 | } 22 | }, 23 | "adminUsername": { 24 | "type": "string", 25 | "metadata": { 26 | "description": "User name for the Virtual Machine." 27 | } 28 | }, 29 | <%- if ssh_key.nil? -%> 30 | "adminPassword": { 31 | "type": "securestring", 32 | "metadata": { 33 | "description": "Password for the Virtual Machine." 34 | } 35 | }, 36 | <%- end -%> 37 | "dnsNameForPublicIP": { 38 | "type": "string", 39 | "metadata": { 40 | "description": "Unique DNS Name for the Public IP used to access the Virtual Machine." 41 | } 42 | }, 43 | <%- unless os_disk_size_gb.to_s.empty? -%> 44 | "osDiskSizeGb": { 45 | "type": "int", 46 | "minValue": 1, 47 | "maxValue": 2048, 48 | "metadata": { 49 | "description": "Size of the OS disks in GB." 50 | } 51 | }, 52 | <%- end -%> 53 | "secretUrl": { 54 | "type": "string", 55 | "metadata": { 56 | "description": "Secret vault certificate URL" 57 | } 58 | }, 59 | "vaultName" : { 60 | "type": "string", 61 | "metadata": { 62 | "description": "Name of key vault where certificate is located." 63 | } 64 | }, 65 | "vaultResourceGroup": { 66 | "type": "string", 67 | "metadata": { 68 | "description": "Resource group name where key vault is located." 69 | } 70 | }, 71 | <%- unless custom_data.empty? -%> 72 | "customData": { 73 | "type": "string", 74 | "metadata": { 75 | "description": "Custom Data for the instance (e.g. cloud-init or script) - not compatible with winrm." 76 | } 77 | }, 78 | <%- end -%> 79 | <%- if !existing_storage_account_blob_url.empty? -%> 80 | "existingStorageAccountBlobURL": { 81 | "type": "string", 82 | "metadata": { 83 | "description": "The URL of the existing storage account (blob) (without container)" 84 | } 85 | }, 86 | <%- end -%> 87 | <%- if !existing_storage_account_container.empty? -%> 88 | "existingStorageAccountBlobContainer": { 89 | "type": "string", 90 | "metadata": { 91 | "description": "The Container Name for OS Images (blob)" 92 | } 93 | }, 94 | <%- end -%> 95 | <%- if !image_url.empty? -%> 96 | "imageUrl": { 97 | "type": "string", 98 | "metadata": { 99 | "description": "An URL for a private Image (vhd)" 100 | } 101 | }, 102 | "osType": { 103 | "type": "string", 104 | "metadata": { 105 | "description": "An OS Type (linux, windows)" 106 | } 107 | }, 108 | <%- elsif !image_id.empty? -%> 109 | "imageId": { 110 | "type": "string", 111 | "metadata": { 112 | "description": "The id of a managed image" 113 | } 114 | }, 115 | <%- else -%> 116 | "imagePublisher": { 117 | "type": "string", 118 | "defaultValue": "Canonical", 119 | "metadata": { 120 | "description": "Publisher for the VM, e.g. Canonical, MicrosoftWindowsServer" 121 | } 122 | }, 123 | "imageOffer": { 124 | "type": "string", 125 | "defaultValue": "UbuntuServer", 126 | "metadata": { 127 | "description": "Offer for the VM, e.g. UbuntuServer, WindowsServer." 128 | } 129 | }, 130 | "imageSku": { 131 | "type": "string", 132 | "defaultValue": "14.04.3-LTS", 133 | "metadata": { 134 | "description": "Sku for the VM, e.g. 14.04.3-LTS" 135 | } 136 | }, 137 | "imageVersion": { 138 | "type": "string", 139 | "defaultValue": "latest", 140 | "metadata": { 141 | "description": "Either a date or latest." 142 | } 143 | }, 144 | <%- end -%> 145 | "osDiskNameSuffix": { 146 | "type": "string", 147 | "defaultValue": "", 148 | "metadata": { 149 | "description": "A disk Name Suffix to make the disk name unique in existing storage accounts." 150 | } 151 | }, 152 | "vmName": { 153 | "type": "string", 154 | "defaultValue": "vm", 155 | "metadata": { 156 | "description": "The vm name created inside of the resource group." 157 | } 158 | }, 159 | "nicName": { 160 | "type": "string", 161 | "defaultValue": "nic", 162 | "metadata": { 163 | "description": "The nic name created inside of the resource group." 164 | } 165 | }, 166 | "publicIPSKU": { 167 | "type": "string", 168 | "defaultValue": "Basic", 169 | "metadata": { 170 | "description": "SKU name for the Public IP used to access the Virtual Machine." 171 | } 172 | }, 173 | "publicIPAddressType": { 174 | "type": "string", 175 | "defaultValue": "Dynamic", 176 | "metadata": { 177 | "description": "SKU name for the Public IP used to access the Virtual Machine." 178 | } 179 | }, 180 | "storageAccountType": { 181 | "type": "string", 182 | "defaultValue": "<%= storage_account_type %>", 183 | "metadata": { 184 | "description": "The type of storage to use (e.g. Standard_LRS or Premium_LRS)." 185 | } 186 | }, 187 | "systemAssignedIdentity": { 188 | "type": "bool", 189 | "defaultValue": false, 190 | "metadata": { 191 | "description": "Whether to enable system assigned identity for the vm." 192 | } 193 | }, 194 | "userAssignedIdentities": { 195 | "type": "object", 196 | "defaultValue": {}, 197 | "metadata": { 198 | "description": "An object whose keys are resource IDs for user identities to associate with the Virtual Machine and whose values are empty objects, or empty to disable user assigned identities." 199 | } 200 | }, 201 | "bootDiagnosticsEnabled": { 202 | "type": "string", 203 | "defaultValue": "true", 204 | "metadata": { 205 | "description": "Whether to enable (true) or disable (false) boot diagnostics. Default: false." 206 | } 207 | } 208 | }, 209 | "variables": { 210 | "location": "[parameters('location')]", 211 | "OSDiskName": "osdisk", 212 | "nicName": "[parameters('nicName')]", 213 | "addressPrefix": "10.0.0.0/16", 214 | "subnetName": "Subnet", 215 | "subnetPrefix": "10.0.0.0/24", 216 | "storageAccountType": "[parameters('storageAccountType')]", 217 | "publicIPAddressName": "publicip", 218 | "vmStorageAccountContainerName": "vhds", 219 | "vmName": "[parameters('vmName')]", 220 | "vmSize": "[parameters('vmSize')]", 221 | "vmIdentityType": "[if(parameters('systemAssignedIdentity'), if(empty(parameters('userAssignedIdentities')), 'SystemAssigned', 'SystemAssigned, UserAssigned'), if(empty(parameters('userAssignedIdentities')), 'None', 'UserAssigned'))]", 222 | "virtualNetworkName": "vnet", 223 | "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", 224 | "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" 225 | }, 226 | "resources": [ 227 | { 228 | "apiVersion": "2017-05-10", 229 | "name": "pid-18d63047-6cdf-4f34-beed-62f01fc73fc2", 230 | "type": "Microsoft.Resources/deployments", 231 | "properties": { 232 | "mode": "Incremental", 233 | "template": { 234 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 235 | "contentVersion": "1.0.0.0", 236 | "resources": [] 237 | } 238 | } 239 | }, 240 | <%- unless use_managed_disks -%> 241 | <%- if existing_storage_account_blob_url.empty? -%> 242 | { 243 | "type": "Microsoft.Storage/storageAccounts", 244 | "name": "[parameters('newStorageAccountName')]", 245 | "apiVersion": "2015-05-01-preview", 246 | "location": "[variables('location')]", 247 | "properties": { 248 | "accountType": "[variables('storageAccountType')]" 249 | }, 250 | "tags": { 251 | <%= vm_tags unless vm_tags.empty? %> 252 | } 253 | }, 254 | <%- end -%> 255 | <%- end -%> 256 | { 257 | "apiVersion": "2017-08-01", 258 | "type": "Microsoft.Network/publicIPAddresses", 259 | "name": "[variables('publicIPAddressName')]", 260 | "location": "[variables('location')]", 261 | "properties": { 262 | "publicIPAllocationMethod": "[parameters('publicIPAddressType')]", 263 | "dnsSettings": { 264 | "domainNameLabel": "[parameters('dnsNameForPublicIP')]" 265 | } 266 | }, 267 | "sku": { 268 | "name": "[parameters('publicIPSKU')]" 269 | }, 270 | "tags": { 271 | <%= vm_tags unless vm_tags.empty? %> 272 | } 273 | }, 274 | { 275 | "apiVersion": "2015-05-01-preview", 276 | "type": "Microsoft.Network/virtualNetworks", 277 | "name": "[variables('virtualNetworkName')]", 278 | "location": "[variables('location')]", 279 | "properties": { 280 | "addressSpace": { 281 | "addressPrefixes": [ 282 | "[variables('addressPrefix')]" 283 | ] 284 | }, 285 | "subnets": [ 286 | { 287 | "name": "[variables('subnetName')]", 288 | "properties": { 289 | "addressPrefix": "[variables('subnetPrefix')]" 290 | } 291 | } 292 | ] 293 | }, 294 | "tags": { 295 | <%= vm_tags unless vm_tags.empty? %> 296 | } 297 | }, 298 | { 299 | "apiVersion": "2015-05-01-preview", 300 | "type": "Microsoft.Network/networkInterfaces", 301 | "name": "[variables('nicName')]", 302 | "location": "[variables('location')]", 303 | "dependsOn": [ 304 | "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", 305 | "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" 306 | ], 307 | "properties": { 308 | "ipConfigurations": [ 309 | { 310 | "name": "ipconfig1", 311 | "properties": { 312 | "privateIPAllocationMethod": "Dynamic", 313 | "publicIPAddress": { 314 | "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" 315 | }, 316 | "subnet": { 317 | "id": "[variables('subnetRef')]" 318 | } 319 | } 320 | } 321 | ] 322 | }, 323 | "tags": { 324 | <%= vm_tags unless vm_tags.empty? %> 325 | } 326 | }, 327 | { 328 | "apiVersion": "2018-06-01", 329 | "type": "Microsoft.Compute/virtualMachines", 330 | "name": "[variables('vmName')]", 331 | "location": "[variables('location')]", 332 | "dependsOn": [ 333 | <%- unless use_managed_disks -%> 334 | <%- if existing_storage_account_blob_url.empty? -%> 335 | "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]", 336 | <%- end -%> 337 | <%- end -%> 338 | "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" 339 | ], 340 | "properties": { 341 | "hardwareProfile": { 342 | "vmSize": "[variables('vmSize')]" 343 | }, 344 | "osProfile": { 345 | "computername": "[variables('vmName')]", 346 | <%- unless custom_data.empty? -%> 347 | "customData": "[parameters('customData')]", 348 | <%- end -%> 349 | <%- unless secretUrl.to_s.empty? && vaultName.to_s.empty? && vaultResourceGroup.to_s.empty? -%> 350 | "secret": [ 351 | "sourceVault": { 352 | "id": "[resourceId(parameters('vaultResourceGroup'), 'Microsoft,KeyVault/vaults', parameters('vaultName'))]" 353 | }, 354 | "vaultCertificates": [ 355 | { 356 | "certificateUrl": "[parameters('secretUrl')]", 357 | "certificateStore": "My" 358 | } 359 | ] 360 | ], 361 | <%- end -%> 362 | <%- if ssh_key.nil? -%> 363 | "adminPassword": "[parameters('adminPassword')]", 364 | <%- end -%> 365 | "adminUsername": "[parameters('adminUsername')]" 366 | }, 367 | "storageProfile": { 368 | <%- if image_url.empty? and image_id.empty? -%> 369 | "imageReference": { 370 | "publisher": "[parameters('imagePublisher')]", 371 | "offer": "[parameters('imageOffer')]", 372 | "sku": "[parameters('imageSku')]", 373 | "version": "[parameters('imageVersion')]" 374 | }, 375 | <%- elsif !image_id.empty? -%> 376 | "imageReference": { 377 | "id": "[parameters('imageId')]" 378 | }, 379 | <%- end -%> 380 | <%- if use_ephemeral_osdisk -%> 381 | "osDisk": { 382 | "diffDiskSettings": { 383 | "option": "Local" 384 | }, 385 | "caching": "ReadOnly", 386 | "createOption": "FromImage" 387 | } 388 | <%- elsif use_managed_disks -%> 389 | "osDisk": { 390 | "name": "osdisk", 391 | <%- unless os_disk_size_gb.to_s.empty? -%> 392 | "diskSizeGB": "[parameters('osDiskSizeGB')]", 393 | <%- end -%> 394 | "managedDisk": { 395 | "storageAccountType": "[parameters('storageAccountType')]" 396 | }, 397 | "createOption": "FromImage" 398 | } 399 | <%- else -%> 400 | "osDisk": { 401 | "name": "osdisk", 402 | <%- if !image_url.empty? -%> 403 | <%- unless os_disk_size_gb.to_s.empty? -%> 404 | "diskSizeGB": "[parameters('osDiskSizeGB')]", 405 | <%- end -%> 406 | "image": { 407 | "uri": "[parameters('imageUrl')]" 408 | }, 409 | "osType": "[parameters('osType')]", 410 | <%- end -%> 411 | "vhd": { 412 | <%- if existing_storage_account_blob_url.empty? -%> 413 | "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName')), '2015-06-15').primaryEndpoints.blob, variables('vmStorageAccountContainerName'), '/',variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 414 | <%- else -%> 415 | <%- if existing_storage_account_container.empty? -%> 416 | "uri": "[concat(parameters('existingStorageAccountBlobURL'), '/', variables('vmStorageAccountContainerName'), '/', variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 417 | <%- else -%> 418 | "uri": "[concat(parameters('existingStorageAccountBlobURL'), '/', parameters('existingStorageAccountBlobContainer'), '/', variables('OSDiskName'),parameters('osDiskNameSuffix'),'.vhd')]" 419 | <%- end -%> 420 | <%- end -%> 421 | }, 422 | "caching": "ReadWrite", 423 | "createOption": "FromImage" 424 | } 425 | <%- end -%> 426 | <%- unless data_disks_for_vm_json.nil? -%> 427 | ,"dataDisks": 428 | <%= data_disks_for_vm_json %> 429 | <%- end -%> 430 | }, 431 | "networkProfile": { 432 | "networkInterfaces": [ 433 | { 434 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 435 | } 436 | ] 437 | }, 438 | "diagnosticsProfile": { 439 | <%- unless use_managed_disks -%> 440 | "bootDiagnostics": { 441 | "enabled": "[parameters('bootDiagnosticsEnabled')]", 442 | <%- if existing_storage_account_blob_url.empty? -%> 443 | "storageUri": "[reference(concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName')), '2015-06-15').primaryEndpoints.blob]" 444 | <%- else -%> 445 | "storageUri": "[parameters('existingStorageAccountBlobURL')]" 446 | <%- end -%> 447 | } 448 | <%- end -%> 449 | } 450 | }, 451 | <%- unless plan_json.nil? -%> 452 | "plan": <%= plan_json %>, 453 | <%- end -%> 454 | "identity": { 455 | "type": "[variables('vmIdentityType')]", 456 | "userAssignedIdentities": "[if(empty(parameters('userAssignedIdentities')), json('null'), parameters('userAssignedIdentities'))]" 457 | }, 458 | "tags": { 459 | <%= vm_tags unless vm_tags.empty? %> 460 | } 461 | } 462 | ] 463 | } 464 | --------------------------------------------------------------------------------