kitchen-oci 1.15.1 → 1.16.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e3fbf95593303d5f9538c59a7f07eb03dd1871b05b12ca0a82e5d2ed9b529b4
4
- data.tar.gz: d0c4e60f05ebaf18293ce93bad6f6a52d5d9657de9a0d34b4bb1d61ddaf3d57e
3
+ metadata.gz: 54d56f663b88d438c3b210755b53c04b1103445cd3f7736dd783679cda016bb7
4
+ data.tar.gz: d21f780ff87721d8027d2a4cbcbee1c449644515d61b36207d52d0478651be0a
5
5
  SHA512:
6
- metadata.gz: dac56f0b424e0b74ca273de0242d5f0883c95eee61c4e086a7cfa66c9382b9b933f5eb907099c83860f1a44c64c10277a0a656e56c755658a44b548b824c2875
7
- data.tar.gz: 19adf812199ecbba332985656ffbcc67252107f3f3d1ae2d9fbd1401a9c05e00342bf3d0ba2285f76388ba3075b111de0cb54dab43b94de8424c9ff3359aa04c
6
+ metadata.gz: 810d92317048752b4d2ad40b952aa8656153c14243fb19fbde92610c6dbc668094d49af4f130f6f3ab4b584e85199d08369ecb35d0f48bd627483b4d272254d9
7
+ data.tar.gz: b17e5a0695d27d9fab17fb3e415820bb2e62a81124a83bd506e6552f89bfc01d816e66fd3bf681a87629e661d3fed3d3c4b3902c697db854aabdef97136c61ab
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Author:: Justin Steele (<justin.steele@oracle.com>)
5
+ #
6
+ # Copyright:: (C) 2024, Stephen Pearson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ module Kitchen
21
+ module Driver
22
+ class Oci
23
+ # Api class that defines the various API classes used to interact with OCI
24
+ class Api
25
+ attr_reader :oci_config, :config
26
+ def initialize(oci_config, config)
27
+ @oci_config = oci_config
28
+ @config = config
29
+ end
30
+
31
+ def compute
32
+ generic_api(OCI::Core::ComputeClient)
33
+ end
34
+
35
+ def network
36
+ generic_api(OCI::Core::VirtualNetworkClient)
37
+ end
38
+
39
+ def dbaas
40
+ generic_api(OCI::Database::DatabaseClient)
41
+ end
42
+
43
+ def identity
44
+ generic_api(OCI::Identity::IdentityClient)
45
+ end
46
+
47
+ def blockstorage
48
+ generic_api(OCI::Core::BlockstorageClient)
49
+ end
50
+
51
+ private
52
+
53
+ def generic_api(klass)
54
+ params = {}
55
+ params[:proxy_settings] = api_proxy if api_proxy
56
+ params[:signer] = if config[:use_instance_principals]
57
+ OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
58
+ elsif config[:use_token_auth]
59
+ token_signer
60
+ end
61
+ params[:config] = oci_config unless config[:use_instance_principals]
62
+ klass.new(**params.compact)
63
+ end
64
+
65
+ def token_signer
66
+ pkey_content = oci_config.key_content || File.read(oci_config.key_file).strip
67
+ pkey = OpenSSL::PKey::RSA.new(pkey_content, oci_config.pass_phrase)
68
+
69
+ token = File.read(oci_config.security_token_file).strip
70
+ OCI::Auth::Signers::SecurityTokenSigner.new(token, pkey)
71
+ end
72
+
73
+ def proxy_config
74
+ if config[:proxy_url]
75
+ URI.parse(config[:proxy_url])
76
+ else
77
+ URI.parse("http://example.com").find_proxy
78
+ end
79
+ end
80
+
81
+ def api_proxy
82
+ prx = proxy_config
83
+ return unless prx
84
+
85
+ if prx.user
86
+ OCI::ApiClientProxySettings.new(prx.host, prx.port, prx.user,
87
+ prx.password)
88
+ else
89
+ OCI::ApiClientProxySettings.new(prx.host, prx.port)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Author:: Justin Steele (<justin.steele@oracle.com>)
4
+ #
5
+ # Copyright (C) 2024, Stephen Pearson
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Kitchen
20
+ module Driver
21
+ class Oci
22
+ # generic class for blockstorage
23
+ class Blockstorage < Oci
24
+ require_relative "api"
25
+ require_relative "config"
26
+ require_relative "models/iscsi"
27
+ require_relative "models/paravirtual"
28
+
29
+ attr_accessor :config, :state, :oci, :api, :volume_state, :volume_attachment_state
30
+
31
+ def initialize(config, state, oci, api, action = :create)
32
+ super()
33
+ @config = config
34
+ @state = state
35
+ @oci = oci
36
+ @api = api
37
+ @volume_state = {}
38
+ @volume_attachment_state = {}
39
+ oci.compartment if action == :create
40
+ end
41
+
42
+ def create_volume(volume)
43
+ info("Creating <#{volume[:name]}>...")
44
+ result = api.blockstorage.create_volume(volume_details(volume))
45
+ response = volume_response(result.data.id)
46
+ info("Finished creating <#{volume[:name]}>.")
47
+ [response, final_state(response)]
48
+ end
49
+
50
+ def attach_volume(volume_details, server_id)
51
+ info("Attaching <#{volume_details.display_name}>...")
52
+ attach_volume = api.compute.attach_volume(attachment_details(volume_details, server_id))
53
+ response = attachment_response(attach_volume.data.id)
54
+ info("Finished attaching <#{volume_details.display_name}>.")
55
+ final_state(response)
56
+ end
57
+
58
+ def delete_volume(volume)
59
+ info("Deleting <#{volume[:display_name]}>...")
60
+ api.blockstorage.delete_volume(volume[:id])
61
+ api.blockstorage.get_volume(volume[:id])
62
+ .wait_until(:lifecycle_state, OCI::Core::Models::Volume::LIFECYCLE_STATE_TERMINATED)
63
+ info("Finished deleting <#{volume[:display_name]}>.")
64
+ end
65
+
66
+ def detatch_volume(volume_attachment)
67
+ info("Detaching <#{attachment_name(volume_attachment)}>...")
68
+ api.compute.detach_volume(volume_attachment[:id])
69
+ api.compute.get_volume_attachment(volume_attachment[:id])
70
+ .wait_until(:lifecycle_state, OCI::Core::Models::VolumeAttachment::LIFECYCLE_STATE_DETACHED)
71
+ info("Finished detaching <#{attachment_name(volume_attachment)}>.")
72
+ end
73
+
74
+ def detatch_and_delete
75
+ state[:volume_attachments].each do |att|
76
+ detatch_volume(att)
77
+ end
78
+
79
+ state[:volumes].each do |vol|
80
+ delete_volume(vol)
81
+ end
82
+ end
83
+
84
+ def final_state(response)
85
+ case response
86
+ when OCI::Core::Models::Volume
87
+ final_volume_state(response)
88
+ when OCI::Core::Models::VolumeAttachment
89
+ final_volume_attachment_state(response)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def volume_response(volume_id)
96
+ api.blockstorage.get_volume(volume_id)
97
+ .wait_until(:lifecycle_state, OCI::Core::Models::Volume::LIFECYCLE_STATE_AVAILABLE).data
98
+ end
99
+
100
+ def attachment_response(attachment_id)
101
+ api.compute.get_volume_attachment(attachment_id)
102
+ .wait_until(:lifecycle_state, OCI::Core::Models::VolumeAttachment::LIFECYCLE_STATE_ATTACHED).data
103
+ end
104
+
105
+ def volume_details(volume)
106
+ OCI::Core::Models::CreateVolumeDetails.new(
107
+ compartment_id: oci.compartment,
108
+ availability_domain: config[:availability_domain],
109
+ display_name: volume[:name],
110
+ size_in_gbs: volume[:size_in_gbs],
111
+ vpus_per_gb: volume[:vpus_per_gb] || 10
112
+ )
113
+ end
114
+
115
+ def attachment_name(attachment)
116
+ attachment[:display_name].gsub(/(?:paravirtual|iscsi)-/, "")
117
+ end
118
+
119
+ def final_volume_state(response)
120
+ volume_state.store(:id, response.id)
121
+ volume_state.store(:display_name, response.display_name)
122
+ volume_state
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Author:: Justin Steele (<justin.steele@oracle.com>)
5
+ #
6
+ # Copyright (C) 2024, Stephen Pearson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require_relative "api"
21
+
22
+ module Kitchen
23
+ module Driver
24
+ class Oci
25
+ # Config class that defines the oci config that will be used for the API calls
26
+ class Config
27
+ attr_reader :config
28
+
29
+ def initialize(driver_config)
30
+ setup_driver_config(driver_config)
31
+ @config = oci_config
32
+ end
33
+
34
+ def oci_config
35
+ # OCI::Config is missing this and we're definitely using compartment and security_token_file if specified in the config
36
+ OCI::Config.class_eval { attr_accessor :security_token_file } if @driver_config[:use_token_auth]
37
+ conf = config_loader(config_file_location: @driver_config[:oci_config_file], profile_name: @driver_config[:oci_profile_name])
38
+ @driver_config[:oci_config].each do |key, value|
39
+ conf.send("#{key}=", value) unless value.nil? || value.empty?
40
+ end
41
+ conf
42
+ end
43
+
44
+ def compartment
45
+ @compartment ||= @compartment_id
46
+ return @compartment if @compartment
47
+
48
+ raise "must specify either compartment_id or compartment_name" unless [@compartment_id, @compartment_name].any?
49
+
50
+ @compartment ||= compartment_id_by_name(@compartment_name)
51
+ raise "compartment not found" unless @compartment
52
+ end
53
+
54
+ private
55
+
56
+ def setup_driver_config(config)
57
+ @driver_config = config
58
+ @compartment_id = config[:compartment_id]
59
+ @compartment_name = config[:compartment_name]
60
+ end
61
+
62
+ def config_loader(opts = {})
63
+ OCI::ConfigFileLoader.load_config(**opts.compact)
64
+ rescue OCI::ConfigFileLoader::Errors::ConfigFileNotFoundError
65
+ OCI::Config.new
66
+ end
67
+
68
+ def tenancy
69
+ if @driver_config[:use_instance_principals]
70
+ sign = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
71
+ sign.instance_variable_get "@tenancy_id"
72
+ else
73
+ config.tenancy
74
+ end
75
+ end
76
+
77
+ def compartment_id_by_name(name)
78
+ api = Oci::Api.new(config, @driver_config).identity
79
+ all_compartments(api, config.tenancy).select { |c| c.name == name }&.first&.id
80
+ end
81
+
82
+ def all_compartments(api, tenancy, compartments = [], page = nil)
83
+ current_compartments = api.list_compartments(tenancy, page: page)
84
+ next_page = current_compartments.next_page
85
+ compartments << current_compartments.data
86
+ all_compartments(api, tenancy, compartments, next_page) unless next_page.nil?
87
+ compartments.flatten
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Author:: Justin Steele (<justin.steele@oracle.com>)
5
+ #
6
+ # Copyright (C) 2024, Stephen Pearson
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ module Kitchen
21
+ module Driver
22
+ class Oci
23
+ # generic class for instance models
24
+ class Instance < Oci
25
+ require_relative "api"
26
+ require_relative "config"
27
+ require_relative "models/compute"
28
+ require_relative "models/dbaas"
29
+
30
+ attr_accessor :config, :state, :oci, :api
31
+
32
+ def initialize(config, state, oci, api, action)
33
+ super()
34
+ @config = config
35
+ @state = state
36
+ @oci = oci
37
+ @api = api
38
+ end
39
+
40
+ def compartment_id
41
+ launch_details.compartment_id = oci.compartment
42
+ end
43
+
44
+ def availability_domain
45
+ launch_details.availability_domain = config[:availability_domain]
46
+ end
47
+
48
+ def defined_tags
49
+ launch_details.defined_tags = config[:defined_tags]
50
+ end
51
+
52
+ def shape
53
+ launch_details.shape = config[:shape]
54
+ end
55
+
56
+ def freeform_tags
57
+ launch_details.freeform_tags = process_freeform_tags
58
+ end
59
+
60
+ def final_state(state, instance_id)
61
+ state.store(:server_id, instance_id)
62
+ state.store(:hostname, instance_ip(instance_id))
63
+ state
64
+ end
65
+
66
+ private
67
+
68
+ def public_ip_allowed?
69
+ subnet = api.network.get_subnet(config[:subnet_id]).data
70
+ !subnet.prohibit_public_ip_on_vnic
71
+ end
72
+
73
+ def random_password(special_chars)
74
+ (Array.new(5) { special_chars.sample } +
75
+ Array.new(5) { ("a".."z").to_a.sample } +
76
+ Array.new(5) { ("A".."Z").to_a.sample } +
77
+ Array.new(5) { ("0".."9").to_a.sample }).shuffle.join
78
+ end
79
+
80
+ def random_string(length)
81
+ Array.new(length) { ("a".."z").to_a.sample }.join
82
+ end
83
+
84
+ def random_number(length)
85
+ Array.new(length) { ("0".."9").to_a.sample }.join
86
+ end
87
+
88
+ def process_freeform_tags
89
+ tags = %w{run_list policyfile}
90
+ fft = config[:freeform_tags]
91
+ tags.each do |tag|
92
+ unless fft[tag.to_sym].nil? || fft[tag.to_sym].empty?
93
+ fft[tag] =
94
+ prov[tag.to_sym].join(",")
95
+ end
96
+ end
97
+ fft[:kitchen] = true
98
+ fft
99
+ end
100
+
101
+ def user_data
102
+ case config[:user_data]
103
+ when Array
104
+ Base64.encode64(multi_part_user_data.close.string).delete("\n")
105
+ when String
106
+ Base64.encode64(config[:user_data]).delete("\n")
107
+ end
108
+ end
109
+
110
+ def multi_part_user_data
111
+ boundary = "MIMEBOUNDARY_#{random_string(20)}"
112
+ msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"",
113
+ "MIME-Version: 1.0", ""]
114
+ msg += mime_parts(boundary)
115
+ txt = "#{msg.join("\n")}\n"
116
+ gzip = Zlib::GzipWriter.new(StringIO.new)
117
+ gzip << txt
118
+ end
119
+
120
+ def mime_parts(boundary)
121
+ msg = []
122
+ config[:user_data].each do |m|
123
+ msg << "--#{boundary}"
124
+ msg << "Content-Disposition: attachment; filename=\"#{m[:filename]}\""
125
+ msg << "Content-Transfer-Encoding: 7bit"
126
+ msg << "Content-Type: text/#{m[:type]}" << "Mime-Version: 1.0" << ""
127
+ msg << read_part(m) << ""
128
+ end
129
+ msg << "--#{boundary}--"
130
+ msg
131
+ end
132
+
133
+ def read_part(part)
134
+ if part[:path]
135
+ content = File.read part[:path]
136
+ elsif part[:inline]
137
+ content = part[:inline]
138
+ else
139
+ raise "Invalid user data"
140
+ end
141
+ content.split("\n")
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Author:: Justin Steele (<justin.steele@oracle.com>)
4
+ #
5
+ # Copyright (C) 2024, Stephen Pearson
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Kitchen
20
+ module Driver
21
+ class Oci
22
+ module Models
23
+ # Compute instance model
24
+ class Compute < Instance # rubocop:disable Metrics/ClassLength
25
+ attr_accessor :launch_details
26
+
27
+ def initialize(config, state, oci, api, action = :create)
28
+ super
29
+ @launch_details = OCI::Core::Models::LaunchInstanceDetails.new
30
+ end
31
+
32
+ def launch
33
+ process_windows_options
34
+ response = api.compute.launch_instance(launch_instance_details)
35
+ instance_id = response.data.id
36
+ api.compute.get_instance(instance_id).wait_until(:lifecycle_state, OCI::Core::Models::Instance::LIFECYCLE_STATE_RUNNING )
37
+ final_state(state, instance_id)
38
+ end
39
+
40
+ def terminate
41
+ api.compute.terminate_instance(state[:server_id])
42
+ api.compute.get_instance(state[:server_id]).wait_until(:lifecycle_state, OCI::Core::Models::Instance::LIFECYCLE_STATE_TERMINATING)
43
+ end
44
+
45
+ private
46
+
47
+ def launch_instance_details # rubocop:disable Metrics/MethodLength
48
+ compartment_id
49
+ availability_domain
50
+ defined_tags
51
+ shape
52
+ freeform_tags
53
+ hostname_display_name
54
+ instance_source_details
55
+ instance_metadata
56
+ preemptible_instance_config
57
+ shape_config
58
+ launch_details
59
+ end
60
+
61
+ def hostname_display_name
62
+ display_name = hostname
63
+ launch_details.display_name = display_name
64
+ launch_details.create_vnic_details = create_vnic_details(display_name)
65
+ end
66
+
67
+ def hostname
68
+ [config[:hostname_prefix], random_string(6)].compact.join("-")
69
+ end
70
+
71
+ def preemptible_instance_config
72
+ return unless config[:preemptible_instance]
73
+
74
+ launch_details.preemptible_instance_config = OCI::Core::Models::PreemptibleInstanceConfigDetails.new(
75
+ preemption_action:
76
+ OCI::Core::Models::TerminatePreemptionAction.new(
77
+ type: "TERMINATE", preserve_boot_volume: true
78
+ )
79
+ )
80
+ end
81
+
82
+ def shape_config
83
+ return if config[:shape_config].empty?
84
+
85
+ launch_details.shape_config = OCI::Core::Models::LaunchInstanceShapeConfigDetails.new(
86
+ ocpus: config[:shape_config][:ocpus],
87
+ memory_in_gbs: config[:shape_config][:memory_in_gbs],
88
+ baseline_ocpu_utilization: config[:shape_config][:baseline_ocpu_utilization] || "BASELINE_1_1"
89
+ )
90
+ end
91
+
92
+ def instance_source_details
93
+ launch_details.source_details = OCI::Core::Models::InstanceSourceViaImageDetails.new(
94
+ sourceType: "image",
95
+ imageId: config[:image_id],
96
+ bootVolumeSizeInGBs: config[:boot_volume_size_in_gbs]
97
+ )
98
+ end
99
+
100
+ def create_vnic_details(name)
101
+ OCI::Core::Models::CreateVnicDetails.new(
102
+ assign_public_ip: public_ip_allowed?,
103
+ display_name: name,
104
+ hostname_label: name,
105
+ nsg_ids: config[:nsg_ids],
106
+ subnetId: config[:subnet_id]
107
+ )
108
+ end
109
+
110
+ def pubkey
111
+ File.readlines(config[:ssh_keypath]).first.chomp
112
+ end
113
+
114
+ def instance_metadata
115
+ launch_details.metadata = metadata
116
+ end
117
+
118
+ def metadata
119
+ md = {}
120
+ inject_powershell
121
+ config[:custom_metadata]&.each { |k, v| md.store(k, v) }
122
+ md.store("ssh_authorized_keys", pubkey)
123
+ md.store("user_data", user_data) if config[:user_data] && !config[:user_data].empty?
124
+ md
125
+ end
126
+
127
+ def vnics(instance_id)
128
+ vnic_attachments(instance_id).map { |att| api.network.get_vnic(att.vnic_id).data }
129
+ end
130
+
131
+ def vnic_attachments(instance_id)
132
+ att = api.compute.list_vnic_attachments(oci.compartment, instance_id: instance_id).data
133
+ raise "Could not find any VNIC attachments" unless att.any?
134
+
135
+ att
136
+ end
137
+
138
+ def instance_ip(instance_id)
139
+ vnic = vnics(instance_id).select(&:is_primary).first
140
+ if public_ip_allowed?
141
+ config[:use_private_ip] ? vnic.private_ip : vnic.public_ip
142
+ else
143
+ vnic.private_ip
144
+ end
145
+ end
146
+
147
+ def process_windows_options
148
+ return unless windows_state?
149
+
150
+ state.store(:username, config[:winrm_user])
151
+ state.store(:password, config[:winrm_password] || random_password(%w{@ - ( ) .}))
152
+ end
153
+
154
+ def windows_state?
155
+ config[:setup_winrm] && config[:password].nil? && state[:password].nil?
156
+ end
157
+
158
+ def winrm_ps1
159
+ filename = File.join(__dir__, %w{.. .. .. .. .. tpl setup_winrm.ps1.erb})
160
+ tpl = ERB.new(File.read(filename))
161
+ tpl.result(binding)
162
+ end
163
+
164
+ def inject_powershell
165
+ return unless config[:setup_winrm]
166
+
167
+ data = winrm_ps1
168
+ config[:user_data] ||= []
169
+ config[:user_data] << {
170
+ type: "x-shellscript",
171
+ inline: data,
172
+ filename: "setup_winrm.ps1",
173
+ }
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end