├── CODEOWNERS
├── .github
├── CODEOWNERS
├── workflows
│ ├── lint.yml
│ └── publish.yml
└── dependabot.yml
├── .mdlrc
├── .release-please-manifest.json
├── .vscode
└── settings.json
├── .rubocop.yml
├── .markdownlint.yaml
├── .gitignore
├── lib
└── kitchen
│ └── driver
│ ├── azurerm_version.rb
│ ├── azure_credentials.rb
│ └── azurerm.rb
├── CODE_OF_CONDUCT.md
├── renovate.json
├── templates
├── empty.erb
├── internal.erb
└── public.erb
├── Gemfile
├── release-please-config.json
├── Rakefile
├── spec
├── fixtures
│ └── azure_credentials
├── spec_helper.rb
└── unit
│ └── kitchen
│ └── driver
│ ├── azure_credentials_spec.rb
│ └── azurerm_spec.rb
├── commit-msg
├── kitchen-azurerm.gemspec
├── LICENSE
├── CHANGELOG.md
└── README.md
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @test-kitchen/maintainers
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @test-kitchen/maintainers
2 |
--------------------------------------------------------------------------------
/.mdlrc:
--------------------------------------------------------------------------------
1 | rules "~MD013", "~MD024", "~MD026", "~MD036"
2 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "1.13.3"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rubocop.path": "chefstyle.bat"
3 | }
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | ---
2 | require:
3 | - cookstyle/chefstyle
4 |
5 | AllCops:
6 | TargetRubyVersion: 3.2
7 |
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | default: true
3 | MD004: false
4 | MD012: false
5 | MD013: false
6 | MD024: false
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sw[opn]
2 | Gemfile.lock
3 | *.gem
4 | /docs/examples
5 | coverage
6 | *.log
7 | *kitchen.local.yml
8 | .kitchen/
9 |
--------------------------------------------------------------------------------
/lib/kitchen/driver/azurerm_version.rb:
--------------------------------------------------------------------------------
1 | module Kitchen
2 | module Driver
3 | AZURERM_VERSION = "1.13.3".freeze
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | Please refer to the Chef Community Code of Conduct at
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 :cookstyle do
16 | gem "cookstyle", "~> 8.4"
17 | end
18 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v5
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 |
--------------------------------------------------------------------------------
/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.2"
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.3](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.13.2...v1.13.3) (2025-08-15)
8 |
9 |
10 | ### Bug Fixes
11 |
12 | * Require Ruby 3.2 ([#294](https://github.com/test-kitchen/kitchen-azurerm/issues/294)) ([a21cdcf](https://github.com/test-kitchen/kitchen-azurerm/commit/a21cdcf373c503bc27c014eaf58090d721a14857))
13 |
14 | ## [1.13.2](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.13.1...v1.13.2) (2025-03-03)
15 |
16 |
17 | ### Bug Fixes
18 |
19 | * 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))
20 |
21 | ## [1.13.1](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.13.0...v1.13.1) (2024-06-21)
22 |
23 |
24 | ### Bug Fixes
25 |
26 | * release please configs ([#273](https://github.com/test-kitchen/kitchen-azurerm/issues/273)) ([8633756](https://github.com/test-kitchen/kitchen-azurerm/commit/8633756a593dc68087e1a6bb6905e88330bf1e54))
27 |
28 | ## [1.13.0](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.12.0...v1.13.0) (2023-11-27)
29 |
30 |
31 | ### Features
32 |
33 | * add configurable vm prefix ([#264](https://github.com/test-kitchen/kitchen-azurerm/issues/264)) ([4b09973](https://github.com/test-kitchen/kitchen-azurerm/commit/4b099731f1132739aaaf203cc417d254feb6862e))
34 | * 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))
35 |
36 | ### [1.12.0](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.11.0...v1.11.1) (2023-05-08)
37 |
38 | ### Features
39 |
40 | * Azure sdk namespace updates ([#258](https://github.com/test-kitchen/kitchen-azurerm/issues/258)) ([d3041f1](https://github.com/test-kitchen/kitchen-azurerm/commit/d3041f19dd68e4c3ea00631e9fa5d3a63ea92a76))
41 |
42 | ### [1.11.0](http3://g11hub.com/test-kitchen/kitchen-azurerm/compare/v1.10.6...v1.11.0) (2023-04-11)
43 |
44 | ### Features
45 |
46 | * 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))
47 |
48 |
49 | ### [1.10.7](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.6...v1.10.7) (2022-04-20)
50 |
51 |
52 | ### Features
53 |
54 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339))
55 |
56 |
57 | ### Bug Fixes
58 |
59 | * 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))
60 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f))
61 | * publish workflow ([#247](https://github.com/test-kitchen/kitchen-azurerm/issues/247)) ([a68d380](https://github.com/test-kitchen/kitchen-azurerm/commit/a68d380bd44e7e4de7abb177034ba4109880dcef))
62 | * 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))
63 |
64 | ### [1.10.5](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.4...v1.10.5) (2022-04-13)
65 |
66 | ### Features
67 |
68 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339))
69 |
70 | ### Bug Fixes
71 |
72 | * 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))
73 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f))
74 |
75 | ### [1.10.4](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.3...v1.10.4) (2022-04-04)
76 |
77 | ### Bug Fixes
78 |
79 | * AZURERM_VERSION ([38b9475](https://github.com/test-kitchen/kitchen-azurerm/commit/38b9475da1ad421ea7ce927463c8a9e20761a56f))
80 |
81 | ### [1.10.3](https://github.com/test-kitchen/kitchen-azurerm/compare/v1.10.2...v1.10.3) (2022-04-04)
82 |
83 | ### Features
84 |
85 | * Add release please releaser ([#238](https://github.com/test-kitchen/kitchen-azurerm/issues/238)) ([32cf4b8](https://github.com/test-kitchen/kitchen-azurerm/commit/32cf4b84a1a864d6f5272bd53b438d74bc141339))
86 |
87 | ## [1.10.2] - 2022-04-04
88 |
89 | * move warning about missing credentials into debug by @jasonwbarnett in
90 | * Deprecation/positional arguments by @damacus in
91 | * Publish gem to GitHub by @damacus in
92 |
93 | ## [1.10.1] - 2022-03-10
94 |
95 | * Rollback #228 by [@jasonwbarnett](https://github.com/jasonwbarnett) in #234
96 |
97 | ## [1.10.0] - 2022-02.28
98 |
99 | * Add a new `store_deployment_credentials_in_state` configuration option to skip storing sensitive data in the state [@jasonwbarnett](https://github.com/jasonwbarnett)
100 |
101 | ## [1.9.0] - 2022-02.04
102 |
103 | * Support setting the VM availability zone with a new `zone` config. [@pkazi](https://github.com/pkazi)
104 | * Drop support for EOL Ruby 2.5
105 |
106 | ## [1.8.0] - 2021-08.27
107 |
108 | * Increase max OS volume size from 1023 to 2048 [@jasonwbarnett](https://github.com/jasonwbarnett)
109 |
110 | ## [1.7.0] - 2021-07.02
111 |
112 | * Support Test Kitchen 3.0
113 |
114 | ## [1.6.0] - 2021-03.19
115 |
116 | * 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)
117 |
118 | ## [1.5.3] - 2021-02-24
119 |
120 | * Additional fixes for `public_ip_sku` to update the default behavior to match pre-1.5.0 behavior [@collinmcneese](https://github.com/collinmcneese)
121 |
122 | ## [1.5.2] - 2021-02-18
123 |
124 | * Fix using `storage_account_type` config option to set data disk storage types - [@reasland](https://github.com/reasland)
125 |
126 | ## [1.5.1] - 2021-02-18
127 |
128 | * Populate publicIPSKU in template only if provided by kitchen config [@collinmcneese](https://github.com/collinmcneese)
129 |
130 | ## [1.5.0] - 2021-02-11
131 |
132 | * 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)
133 |
134 | ## [1.4.0] - 2020-09-29
135 |
136 | * Resolved an issue where VM state was persisted before VM is provisioned
137 | * Added linting and unit testing for each pull request via GitHub Actions
138 | * Resolved issues where resource groups where not being destroyed
139 | * Set 'az login' the default authentication mechanism
140 | * Added new `use_fqdn_hostname` config option to set Test Kitchen to communicate using the instance's FQDN
141 | * Resolved an issue where username was not being added to Test Kitchen's state
142 |
143 | ## [1.3.0] - 2020-09-09
144 |
145 | * Improve performance by loading dependencies only when we need them (@mwrock)
146 |
147 | ## [1.2.0] - 2020-08-20
148 |
149 | * Add support for deletion or preservation of resource group tags with a new `destroy_explicit_resource_group_tags` config that defaults to `true` (@StylusEaterChef)
150 | * Optimize our requires to make load the gem a tiny bit faster (@tas50)
151 |
152 | ## [1.1.0] - 2020-08-19
153 |
154 | * Update error messages to mention `kitchen.yml` not `.kitchen.yml` (@tas50)
155 | * Update the default password we generate to be 25 characters to avoid failures on newer Windows releases (@StylusEaterChef)
156 | * Remove `simplecov` development dependency (@tas50)
157 | * Updated Readme to be more explicit about credentials settings (@Vasu1105)
158 | * Remove tags in readme that could possibly confuse users (@jasonwbarnett)
159 | * Fix Azure SP documentation link and give an example on how to setup (@StylusEaterChef)
160 | * Update installation instructions not to mention ChefDK (@tas50)
161 |
162 | ## [1.0.0] - 2020-05-06
163 |
164 | * Add more specs and refactor Credentials [PR #135](https://github.com/test-kitchen/kitchen-azurerm/pull/135) (@jasonwbarnett)
165 | * Fix using `user_assigned_identities` config [PR #136](https://github.com/test-kitchen/kitchen-azurerm/pull/136) (@zanecodes)
166 |
167 | ## [0.17.0] - 2020-04-23
168 |
169 | * Add MSI Support [PR #134](https://github.com/test-kitchen/kitchen-azurerm/pull/134) (@jasonwbarnett)
170 |
171 | ## [0.16.0] - 2020-04-22
172 |
173 | * Add support for marketplace plan information [PR #132](https://github.com/test-kitchen/kitchen-azurerm/pull/132) (@jasonwbarnett)
174 |
175 | ## [0.15.2] - 2020-03-23
176 |
177 | * Fix require_relative for azure_credentials [PR #129](https://github.com/test-kitchen/kitchen-azurerm/pull/129) (@jasonwbarnett)
178 | * Refactor Kitchen::Driver::Credentials class [PR #128](https://github.com/test-kitchen/kitchen-azurerm/pull/128) (@jasonwbarnett)
179 | * Default password is now generated rather than hard-coded [#124](https://github.com/test-kitchen/kitchen-azurerm/pull/124) (@stuartpreston)
180 | * Add retry logic when checking deployment state [#125](https://github.com/test-kitchen/kitchen-azurerm/pull/125x) (@albertvaka)
181 | * Only add password to deployment template if ssh_key is not set [#126](https://github.com/test-kitchen/kitchen-azurerm/pull/126) (@KSerrania)
182 |
183 | ## [0.15.1] - 2020-01-14
184 |
185 | * Use require_relative instead of require [PR #123](https://github.com/test-kitchen/kitchen-azurerm/pull/123) (@tas50)
186 |
187 | ## [0.15.0] - 2019-11-29
188 |
189 | * Enable WinRM HTTP listener by default [PR #121](https://github.com/test-kitchen/kitchen-azurerm/pull/121) (@sean-nixon)
190 | * Default subscription_id to AZURE_SUBSCRIPTION_ID environment variable if not supplied[df79c787fa299cb6eff4a2fd7807fe28ce2bc725](https://github.com/test-kitchen/kitchen-azurerm/commit/df79c787fa299cb6eff4a2fd7807fe28ce2bc725) (@stuartpreston)
191 | * Allow nic name to be passed in as a parameter [PR #112](https://github.com/test-kitchen/kitchen-azurerm/pull/112) (@libertymutual)
192 | * Support for creating VM with Azure KeyVault certificate [PR #120](https://github.com/test-kitchen/kitchen-azurerm/pull/120) (@javgallegos)
193 |
194 | ## [0.14.9] - 2019-07-30
195 |
196 | * Support [Ephemeral OS Disk](https://azure.microsoft.com/en-us/updates/azure-ephemeral-os-disk-now-generally-available/), (@stuartpreston)
197 |
198 | ## [0.14.8] - 2018-12-30
199 |
200 | * 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)
201 | * Apply vm_tags to all resources in resource group [PR #105](https://github.com/test-kitchen/kitchen-azurerm/pull/105) (@josh-hetland)
202 |
203 | ## [0.14.7] - 2018-12-18
204 |
205 | * Updating Azure SDK dependencies, [PR #104](https://github.com/test-kitchen/kitchen-azurerm/pull/104) (@stuartpreston)
206 |
207 | ## [0.14.6] - 2018-12-11
208 |
209 | * Support tags at Resource Group level, [PR #102](https://github.com/test-kitchen/kitchen-azurerm/pull/102) (@pgryzan-chefio)
210 | * 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)
211 |
212 | ## [0.14.5] - 2018-09-30
213 |
214 | * Support Shared Image Gallery (preview Azure feature) (@zanecodes)
215 |
216 | ## [0.14.4] - 2018-08-10
217 |
218 | * Adding capability to execute ARM template after VM deployment, ```post_deployment_template``` and ```post_deployment_parameters``` added (@sebastiankasprzak)
219 |
220 | ## [0.14.3] - 2018-07-16
221 |
222 | * 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)
223 |
224 | ## [0.14.2] - 2018-07-09
225 |
226 | * 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)
227 |
228 | ## [0.14.1] - 2018-05-10
229 |
230 | * Support for soverign clouds with latest Azure SDK for Ruby, fixes [#79](https://github.com/test-kitchen/kitchen-azurerm/issues/79)
231 | * Raise error when subscription_id is not available, fixes [#74](https://github.com/test-kitchen/kitchen-azurerm/issues/74)
232 |
233 | ## [0.14.0] - 2018-04-10
234 |
235 | * Update Azure SDK to latest version, upgrade to latest build tools
236 |
237 | ## [0.13.0] - 2017-12-26
238 |
239 | * Switch to new Microsoft telemetry system [#73](https://github.com/test-kitchen/kitchen-azurerm/issues/73)
240 |
241 | ## [0.12.4] - 2017-11-17
242 |
243 | * Adding `explicit_resource_group_name` property to driver configuration
244 |
245 | ## [0.12.3] - 2017-10-18
246 |
247 | * Pinning to version 0.14.0 of Microsoft Azure SDK for Ruby, avoid namespace changes
248 |
249 | ## [0.12.2] - 2017-09-20
250 |
251 | * 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)
252 |
253 | ## [0.12.1] - 2017-09-10
254 |
255 | * Fix for undefined local variable when using pre_deployment_template [#65](https://github.com/test-kitchen/kitchen-azurerm/issue/65)
256 |
257 | ## [0.12.0] - 2017-09-01
258 |
259 | * Additional managed disks can be specified in configuration and left unformatted or formatted on Windows(@stuartpreston)
260 | * Added `azure_resource_group_prefix` and `azure_resource_group_suffix` parameter (@stuartpreston)
261 |
262 | ## [0.11.0] - 2017-07-20
263 |
264 | * Pin to latest ARM SDK and constants [#59](https://github.com/test-kitchen/kitchen-azurerm/pull/59) (@smurawski)
265 |
266 | ## [0.10.0] - 2017-07-03
267 |
268 | * Support for custom images (@elconas)
269 | * Support for custom-data (Linux only) (@elconas)
270 | * Support for custom OS sizes (@elconas)
271 |
272 | ## [0.9.1] - 2017-05-25
273 |
274 | * Support for Managed Disks enabled by default (@stuartpreston)
275 | * Add ```use_managed_disks``` driver_config parameter (@stuartpreston)
276 |
277 | ## [0.9.0] - 2017-04-28
278 |
279 | * Support for AzureUSGovernment, AzureChina and AzureGermanCloud environments
280 | * Add ```azure_environment``` driver_config parameter (@stuartpreston)
281 |
282 | ## [0.8.1] - 2017-02-28
283 |
284 | * Adding provider identifier tag to all created resources (@stuartpreston)
285 |
286 | ## [0.8.0] - 2017-01-16
287 |
288 | * [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)
289 | * [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)
290 | * [Resource Tags can now be applied to the created VMsPR](https://github.com/pendrica/kitchen-azurerm/pull/38) (@liamkirwan)
291 |
292 | ## [0.7.2] - 2016-11-03
293 |
294 | * Bug: When repeating a completed deployment, deployment would fail with a nil error on resource_name (@stuartpreston)
295 |
296 | ## [0.7.1] - 2016-09-17
297 |
298 | * Bug: WinRM is not enabled where the platform name does not contain 'nano' (@stuartpreston)
299 |
300 | ## [0.7.0] - 2016-09-15
301 |
302 | * Support creation of Windows Nano Server (ignoring automatic WinRM setting application) (@stuartpreston)
303 |
304 | ## [0.6.0] - 2016-08-22
305 |
306 | * Supports latest autogenerated resources from Azure SDK for Ruby (0.5.0) (@stuartpreston)
307 | * Removes unnecessary direct depdendencies on older ms_rest libraries (@stuartpreston)
308 | * ssh_key will be used in preference to password if both are supplied (@stuartpreston)
309 |
310 | ## [0.5.0] - 2016-08-07
311 |
312 | * Adding support for internal (e.g. ExpressRoute/VPN) access to created VM (@stuartpreston)
313 |
314 | ## [0.4.1] - 2016-07-01
315 |
316 | * Adding explicit depdendency on concurrent-ruby gem (@stuartpreston)
317 |
318 | ## [0.4.0] - 2016-06-26
319 |
320 | * Adding capability to execute ARM template prior to VM deployment, ```pre_deployment_template``` and ```pre_deployment_parameters``` added (@stuartpreston)
321 |
322 | ## [0.3.6] - 2016-05-10
323 |
324 | * Remove version pin on inifile gem dependency, compatible with newer ChefDK (@stuartpreston)
325 |
326 | ## [0.3.5] - 2016-03-21
327 |
328 | * Remove transport name restriction on SSH key upload (allow rsync support) (@stuartpreston)
329 | * Support SSH public keys with newlines as generated by ssh-keygen (@stuartpreston)
330 |
331 | ## [0.3.4] - 2016-03-19
332 |
333 | * Additional diagnostics when Azure Resource Group fails to create successfully (@stuartpreston)
334 |
335 | ## [0.3.3] - 2016-03-07
336 |
337 | * Pinning ms_rest_azure dependencies to avoid errors when using latest ms_rest_azure library (@stuartpreston)
338 |
339 | ## [0.3.2] - 2016-03-07
340 |
341 | * Breaking: Linux machines are now created using a temporary sshkey (~/.ssh/id_kitchen-azurerm) instead of password (@stuartpreston)
342 | * Real error message shown if credentials are incorrect (@stuartpreston)
343 |
344 | ## [0.2.4] - 2016-01-26
345 |
346 | * Support Premium Storage and Boot Diagnostics (@stuartpreston)
347 | * If deployment fails, show the message from the failing operation (@stuartpreston)
348 | * Updated Windows 2008 R2 example (@stuartpreston)
349 |
350 | ## [0.2.3] - 2015-12-17
351 |
352 | * ```kitchen create``` can now be executed multiple times, updating an existing deployment if an error occurs (@smurawski)
353 |
354 | ## [0.2.2] - 2015-12-10
355 |
356 | * Add an option for users to specify a custom script for WinRM (support Windows 2008 R2) (@andrewelizondo)
357 | * Add azure_management_url parameter for Azure Stack support (@andrewelizondo)
358 |
359 | ## [0.2.1] - 2015-10-06
360 |
361 | * Pointing to updated Azure SDK for Ruby, supports Linux
362 |
363 | ## [0.2.0] - 2015-09-29
364 |
365 | * Logs should be sent to info, not stdout (@stuartpreston)
366 | * Added WinRM support, enables WinRM and WinRM/s and configures server for Basic/Negotiate authentication (@stuartpreston)
367 | * Store server_id earlier so it can be retrieved if resources fail to create in Azure (@stuartpreston)
368 |
369 | ## [0.1.3] - 2015-09-23
370 |
371 | * Support *nix by changing the driver name to lowercase 'azurerm', remove Chef references (@gadgetmg)
372 |
373 | ## [0.1.2] - 2015-09-23
374 |
375 | * Initial release, supports provision of all public image types in Azure (@stuartpreston)
376 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kitchen-azurerm
2 |
3 | [](https://badge.fury.io/rb/kitchen-azurerm)
4 | [](https://github.com/test-kitchen/kitchen-azurerm/actions/workflows/lint.yml)
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 |
--------------------------------------------------------------------------------
/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 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 |
--------------------------------------------------------------------------------