Class: Kitchen::Provisioner::Dsc
- Inherits:
-
Base
- Object
- Base
- Kitchen::Provisioner::Dsc
- Defined in:
- lib/kitchen/provisioner/dsc.rb
Overview
Applies PowerShell Desired State Configuration to a Test Kitchen instance.
The provisioner runs across the four Test Kitchen phases:
- #install_command configures the Local Configuration Manager on the system under test.
- #init_command creates the remote configuration directory and, on WMF 5, installs any modules requested from a PowerShell gallery.
- #create_sandbox stages DSC resources and the configuration script on the workstation, ready for upload.
- #prepare_command compiles the configuration into a MOF on the system under test and #run_command applies it.
Two project layouts are supported, chosen automatically by
#powershell_module?: module style, where the kitchen root is itself a
PowerShell module (identified by a <module name>.psd1 manifest), and
repository style, where DSC resources live in a modules directory.
Instance Attribute Summary collapse
-
#tmp_dir ⇒ String?
Path to a scratch directory on the system under test.
Instance Method Summary collapse
-
#configuration_data_assignment ⇒ String
private
private
PowerShell that assigns
:configuration_datato its variable. -
#configuration_data_variable ⇒ String
private
private
Name of the PowerShell variable holding configuration data.
-
#create_sandbox ⇒ void
Stages DSC resources and the configuration script into the sandbox.
-
#ensure_array(thing) ⇒ Array
private
private
Wraps a scalar in an array so
:configuration_namemay be given either as a single name or as a list. -
#finalize_config!(instance) ⇒ self
Resolves the Local Configuration Manager settings before the instance is used.
-
#init_command ⇒ String
Builds the command that prepares the system under test for upload.
-
#install_command ⇒ String
Builds the command that configures the Local Configuration Manager.
-
#install_module_script ⇒ String?
private
private
PowerShell that installs every requested gallery module.
-
#install_modules? ⇒ Boolean
private
private
Whether gallery modules should be installed during #init_command.
-
#lcm ⇒ DscLcmConfiguration::LcmBase
private
private
The Local Configuration Manager configuration for the target WMF version.
-
#list_files(path) ⇒ Array<String>
private
private
Lists the files to stage from a module-style project.
-
#module_name ⇒ String
private
private
The PowerShell module name implied by the project directory.
-
#nuget_force_bootstrap ⇒ String?
private
private
PowerShell that bootstraps the NuGet package provider.
-
#pad(depth = 0) ⇒ String
private
private
Indentation used when rendering PowerShell hashtables.
-
#powershell_module? ⇒ Boolean
private
private
Whether the kitchen root is itself a PowerShell module.
-
#powershell_module_params(module_specification_hash) ⇒ String
private
private
Renders a module specification hash as
install-moduleparameters. -
#powershell_modules ⇒ Array<String>
private
private
Builds one
install-moduleline per entry in:modules_from_gallery. -
#prepare_command ⇒ String
Builds the command that compiles configurations into MOF documents.
-
#prepare_configuration_script ⇒ void
private
private
Copies the DSC configuration script into the sandbox.
-
#prepare_repo_style_directory ⇒ void
private
private
Stages a repository-style project into the sandbox.
-
#prepare_resource_style_directory ⇒ void
private
private
Stages a module-style project into the sandbox.
-
#ps_hash(obj, depth = 0) ⇒ String
private
private
Renders a Ruby object as a PowerShell literal.
-
#psmodule_repository_name ⇒ String
private
private
The PowerShellGet repository name to install modules from.
-
#register_psmodule_repository ⇒ String?
private
private
PowerShell that registers a private gallery as a package source.
-
#run_command ⇒ String
Builds the command that applies the compiled MOF documents.
-
#sandboxed_configuration_script ⇒ String
private
private
Path of the configuration script relative to the sandbox and to
:root_pathon the system under test. -
#setup_config_directory_script ⇒ String
private
private
PowerShell that creates the remote directory holding the configuration script.
-
#wrap_powershell_code(code) ⇒ String
private
private
Wraps generated PowerShell for execution by the transport.
Instance Attribute Details
#tmp_dir ⇒ String?
Returns path to a scratch directory on the system under test. Set by consumers that need somewhere to write intermediate files; unused by the provisioner itself.
53 54 55 |
# File 'lib/kitchen/provisioner/dsc.rb', line 53 def tmp_dir @tmp_dir end |
Instance Method Details
#configuration_data_assignment ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
PowerShell that assigns :configuration_data to its variable.
399 400 401 |
# File 'lib/kitchen/provisioner/dsc.rb', line 399 def configuration_data_assignment "$" + configuration_data_variable + " = " + ps_hash(config[:configuration_data]) end |
#configuration_data_variable ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Name of the PowerShell variable holding configuration data.
391 392 393 |
# File 'lib/kitchen/provisioner/dsc.rb', line 391 def configuration_data_variable config[:configuration_data_variable].nil? ? "ConfigurationData" : config[:configuration_data_variable] end |
#create_sandbox ⇒ void
This method returns an undefined value.
Stages DSC resources and the configuration script into the sandbox.
The sandbox is the local directory Test Kitchen uploads to the system under test. Which staging strategy runs depends on whether the project is laid out as a PowerShell module or as a repository of modules.
134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/kitchen/provisioner/dsc.rb', line 134 def create_sandbox super info("Staging DSC Resource Modules for copy to the SUT") if powershell_module? prepare_resource_style_directory else prepare_repo_style_directory end info("Staging DSC configuration script for copy to the SUT") prepare_configuration_script end |
#ensure_array(thing) ⇒ Array (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Wraps a scalar in an array so :configuration_name may be given either
as a single name or as a list.
561 562 563 564 565 566 567 |
# File 'lib/kitchen/provisioner/dsc.rb', line 561 def ensure_array(thing) if thing.is_a?(Array) thing else [thing] end end |
#finalize_config!(instance) ⇒ self
Resolves the Local Configuration Manager settings before the instance is used.
Replaces the caller's partial :dsc_local_configuration_manager hash
with the fully defaulted settings for the configured WMF version, so
later phases and kitchen diagnose see the values that will actually be
applied.
83 84 85 86 |
# File 'lib/kitchen/provisioner/dsc.rb', line 83 def finalize_config!(instance) config[:dsc_local_configuration_manager] = lcm.lcm_config super(instance) end |
#init_command ⇒ String
Builds the command that prepares the system under test for upload.
Always creates the directory the configuration script will be copied
into. On WMF 5 with :modules_from_gallery set, it also bootstraps
PackageManagement and installs those modules.
115 116 117 118 119 120 121 |
# File 'lib/kitchen/provisioner/dsc.rb', line 115 def init_command script = <<~EOH #{setup_config_directory_script} #{install_module_script if install_modules?} EOH wrap_powershell_code(script) end |
#install_command ⇒ String
Builds the command that configures the Local Configuration Manager.
Runs during Test Kitchen's install phase, before any configuration is
compiled, since the LCM controls how DSC behaves for the rest of the
run.
96 97 98 99 100 101 102 103 104 105 |
# File 'lib/kitchen/provisioner/dsc.rb', line 96 def install_command full_lcm_configuration_script = <<-EOH #{lcm.lcm_configuration_script} $null = SetupLCM Set-DscLocalConfigurationManager -Path ./SetupLCM | out-null EOH wrap_powershell_code(full_lcm_configuration_script) end |
#install_module_script ⇒ String? (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
PowerShell that installs every requested gallery module.
365 366 367 368 369 370 371 372 373 |
# File 'lib/kitchen/provisioner/dsc.rb', line 365 def install_module_script return if config[:modules_from_gallery].nil? <<-EOH #{nuget_force_bootstrap} #{register_psmodule_repository} #{powershell_modules.join("\n")} EOH end |
#install_modules? ⇒ Boolean (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Whether gallery modules should be installed during #init_command.
Gallery installation depends on PowerShellGet, which ships with WMF 5.
381 382 383 384 |
# File 'lib/kitchen/provisioner/dsc.rb', line 381 def install_modules? config[:dsc_local_configuration_manager_version] == "wmf5" && !config[:modules_from_gallery].nil? end |
#lcm ⇒ DscLcmConfiguration::LcmBase (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The Local Configuration Manager configuration for the target WMF version.
Note that DscLcmConfiguration::Factory only recognizes "4",
"wmf4_with_update", "5" and "wmf5"; every other value, including
this provisioner's own "wmf4" default, yields the base LCM
configuration.
265 266 267 268 269 270 271 |
# File 'lib/kitchen/provisioner/dsc.rb', line 265 def lcm @lcm ||= begin lcm_version = config[:dsc_local_configuration_manager_version] lcm_config = config[:dsc_local_configuration_manager] DscLcmConfiguration::Factory.create(lcm_version, lcm_config) end end |
#list_files(path) ⇒ Array<String> (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Lists the files to stage from a module-style project.
Directories are excluded because the copy recreates them as needed, and repository housekeeping files are excluded because they are not part of the module.
436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/kitchen/provisioner/dsc.rb', line 436 def list_files(path) base_directory_content = Dir.glob(File.join(path, "*")) nested_directory_content = Dir.glob(File.join(path, "*/**/*")) all_directory_content = [base_directory_content, nested_directory_content].flatten ignore_files = ["Gemfile", "Gemfile.lock", "README.md", "LICENSE.txt"] all_directory_content.reject do |f| debug("Enumerating #{f}") ignore_files.include?(File.basename(f)) || File.directory?(f) end end |
#module_name ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The PowerShell module name implied by the project directory.
452 453 454 |
# File 'lib/kitchen/provisioner/dsc.rb', line 452 def module_name File.basename(config[:kitchen_root]) end |
#nuget_force_bootstrap ⇒ String? (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
PowerShell that bootstraps the NuGet package provider.
PackageManagement cannot install from a gallery until the NuGet provider is present, and its interactive bootstrap prompt would hang a converge.
328 329 330 331 332 333 |
# File 'lib/kitchen/provisioner/dsc.rb', line 328 def nuget_force_bootstrap return unless config[:nuget_force_bootstrap] info("Bootstrapping the nuget package provider for PowerShell PackageManagement.") "install-packageprovider nuget -force -forcebootstrap | out-null" end |
#pad(depth = 0) ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Indentation used when rendering PowerShell hashtables.
510 511 512 |
# File 'lib/kitchen/provisioner/dsc.rb', line 510 def pad(depth = 0) " " * depth end |
#powershell_module? ⇒ Boolean (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Whether the kitchen root is itself a PowerShell module.
Detected by a <module name>.psd1 manifest sitting beside the project,
which selects module-style staging over repository-style staging.
422 423 424 425 |
# File 'lib/kitchen/provisioner/dsc.rb', line 422 def powershell_module? = File.join(config[:kitchen_root], "#{module_name}.psd1") File.exist?() end |
#powershell_module_params(module_specification_hash) ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Renders a module specification hash as install-module parameters.
A Force key is dropped because -force is already appended to every
install-module call, and PowerShell rejects a duplicated parameter. A
Repository key is added from the configured gallery unless the caller
supplied one.
293 294 295 296 297 298 299 300 |
# File 'lib/kitchen/provisioner/dsc.rb', line 293 def powershell_module_params(module_specification_hash) keys = module_specification_hash.keys.reject { |k| k.to_s.casecmp("force") == 0 } unless keys.any? { |k| k.to_s.downcase == "repository" } keys.push(:repository) module_specification_hash[:repository] = psmodule_repository_name end keys.map { |key| "-#{key} #{module_specification_hash[key]}" }.join(" ") end |
#powershell_modules ⇒ Array<String> (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Builds one install-module line per entry in :modules_from_gallery.
Entries may be plain module names or hashes of install-module
parameters.
309 310 311 312 313 314 315 316 317 318 |
# File 'lib/kitchen/provisioner/dsc.rb', line 309 def powershell_modules Array(config[:modules_from_gallery]).map do |powershell_module| params = if powershell_module.is_a? Hash powershell_module_params(powershell_module) else "-name '#{powershell_module}' -Repository #{psmodule_repository_name}" end "install-module #{params} -force | out-null" end end |
#prepare_command ⇒ String
Builds the command that compiles configurations into MOF documents.
Copies the uploaded modules onto the PSModulePath, loads the
configuration script, then compiles each name in :configuration_name
into c:/configurations/<name>. Any leftover MOF from a previous
converge is removed first so a failed compile cannot be silently applied.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/kitchen/provisioner/dsc.rb', line 155 def prepare_command info("Moving DSC Resources onto PSModulePath") # +@ makes an explicitly mutable buffer under frozen_string_literal. scripts = +<<-EOH if (Test-Path (join-path #{config[:root_path]} 'modules')) { dir ( join-path #{config[:root_path]} 'modules/*') -directory | copy-item -destination $env:programfiles/windowspowershell/modules/ -recurse -force } $ConfigurationScriptPath = Join-path #{config[:root_path]} #{sandboxed_configuration_script} if (-not (test-path $ConfigurationScriptPath)) { throw "Failed to find $ConfigurationScriptPath" } invoke-expression (get-content $ConfigurationScriptPath -raw) EOH ensure_array(config[:configuration_name]).each do |configuration| info("Generating the MOF script for the configuration #{configuration}") stage_resources_and_generate_mof_script = <<-EOH if(Test-Path c:/configurations/#{configuration}) { Remove-Item -Recurse -Force c:/configurations/#{configuration} } $Error.clear() if (-not (test-path 'c:/configurations')) { mkdir 'c:/configurations' | out-null } if (-not (get-command #{configuration})) { throw "Failed to create a configuration command #{configuration}" } #{configuration_data_assignment unless config[:configuration_data].nil?} try{ $null = #{configuration} -outputpath c:/configurations/#{configuration} #{"-configurationdata $" + configuration_data_variable} } catch{ } if($Error -ne $null) { $Error[-1] exit 1 } EOH scripts << stage_resources_and_generate_mof_script end debug("Shelling out: #{scripts}") wrap_powershell_code(scripts) end |
#prepare_configuration_script ⇒ void (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Copies the DSC configuration script into the sandbox.
546 547 548 549 550 551 552 553 |
# File 'lib/kitchen/provisioner/dsc.rb', line 546 def prepare_configuration_script configuration_script_file = File.join(config[:configuration_script_folder], config[:configuration_script]) configuration_script_path = File.join(config[:kitchen_root], configuration_script_file) sandbox_configuration_script_path = File.join(sandbox_path, sandboxed_configuration_script) FileUtils.mkdir_p(File.dirname(sandbox_configuration_script_path)) debug("Moving #{configuration_script_path} to #{sandbox_configuration_script_path}") FileUtils.cp(configuration_script_path, sandbox_configuration_script_path) end |
#prepare_repo_style_directory ⇒ void (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Stages a repository-style project into the sandbox.
A missing modules directory is not an error: a project may ship only a configuration script and rely on resources already present on the node.
484 485 486 487 488 489 490 491 492 493 494 |
# File 'lib/kitchen/provisioner/dsc.rb', line 484 def prepare_repo_style_directory module_path = File.join(config[:kitchen_root], config[:modules_path]) sandbox_module_path = File.join(sandbox_path, "modules") if Dir.exist?(module_path) debug("Moving #{module_path} to #{sandbox_module_path}") FileUtils.cp_r(module_path, sandbox_module_path) else debug("The modules path #{module_path} was not found. Not moving to #{sandbox_module_path}.") end end |
#prepare_resource_style_directory ⇒ void (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Stages a module-style project into the sandbox.
The whole kitchen root is copied to modules/<module name> so the
module lands on the system under test's PSModulePath under the name
DSC expects.
464 465 466 467 468 469 470 471 472 473 474 475 |
# File 'lib/kitchen/provisioner/dsc.rb', line 464 def prepare_resource_style_directory sandbox_base_module_path = File.join(sandbox_path, "modules/#{module_name}") base = config[:kitchen_root] list_files(base).each do |src| dest = File.join(sandbox_base_module_path, src.sub("#{base}/", "")) FileUtils.mkdir_p(File.dirname(dest)) debug("Staging #{src} ") debug(" at #{dest}") FileUtils.cp(src, dest, preserve: true) end end |
#ps_hash(obj, depth = 0) ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Renders a Ruby object as a PowerShell literal.
Hashes become hashtables and arrays become arrays; every other value is rendered as a double-quoted string, so Ruby booleans and integers reach DSC quoted.
528 529 530 531 532 533 534 535 536 537 538 539 |
# File 'lib/kitchen/provisioner/dsc.rb', line 528 def ps_hash(obj, depth = 0) if obj.is_a?(Hash) obj.map do |k, v| %{#{pad(depth + 2)}#{ps_hash(k)} = #{ps_hash(v, depth + 2)}} end.join(";\n").insert(0, "@{\n").insert(-1, "\n#{pad(depth)}}") elsif obj.is_a?(Array) array_string = obj.map { |v| ps_hash(v, depth + 4) }.join(",") "#{pad(depth)}@(\n#{array_string}\n)" else %{"#{obj}"} end end |
#psmodule_repository_name ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The PowerShellGet repository name to install modules from.
341 342 343 344 345 346 |
# File 'lib/kitchen/provisioner/dsc.rb', line 341 def psmodule_repository_name return "PSGallery" if config[:gallery_name].nil? && config[:gallery_uri].nil? return "testing" if config[:gallery_name].nil? config[:gallery_name] end |
#register_psmodule_repository ⇒ String? (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
PowerShell that registers a private gallery as a package source.
353 354 355 356 357 358 |
# File 'lib/kitchen/provisioner/dsc.rb', line 353 def register_psmodule_repository return if config[:gallery_uri].nil? info("Registering a new PowerShellGet Repository - #{psmodule_repository_name}") "register-packagesource -providername PowerShellGet -name '#{psmodule_repository_name}' -location '#{config[:gallery_uri]}' -force -trusted" end |
#run_command ⇒ String
Builds the command that applies the compiled MOF documents.
A DSC resource may require a reboot to finish. Rather than failing, the
generated script reboots the node and exits 35, and this method opts the
instance into retrying that exit code so the converge resumes once the
node is back. Explicit :retry_on_exit_code and :max_retries settings
are left untouched.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/kitchen/provisioner/dsc.rb', line 226 def run_command config[:retry_on_exit_code] = [35] if config[:retry_on_exit_code].empty? config[:max_retries] = 3 if config[:max_retries] == 1 scripts = +"" ensure_array(config[:configuration_name]).each do |configuration| info("Running the configuration #{configuration}") run_configuration_script = <<-EOH $job = start-dscconfiguration -Path c:/configurations/#{configuration} -force $job | wait-job $verbose_output = $job.childjobs[0].verbose $verbose_output if ($verbose_output -match 'A reboot is required to progress further. Please reboot the system.') { "A reboot is required to continue." shutdown /r /t 15 exit 35 } $dsc_errors = $job.childjobs[0].Error if ($dsc_errors -ne $null) { $dsc_errors exit 1 } EOH scripts << run_configuration_script end debug("Shelling out: #{scripts}") wrap_powershell_code(scripts) end |
#sandboxed_configuration_script ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Path of the configuration script relative to the sandbox and to
:root_path on the system under test.
501 502 503 |
# File 'lib/kitchen/provisioner/dsc.rb', line 501 def sandboxed_configuration_script File.join("configuration", config[:configuration_script]) end |
#setup_config_directory_script ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
PowerShell that creates the remote directory holding the configuration script.
278 279 280 |
# File 'lib/kitchen/provisioner/dsc.rb', line 278 def setup_config_directory_script "mkdir (split-path (join-path #{config[:root_path]} #{sandboxed_configuration_script})) -force | out-null" end |
#wrap_powershell_code(code) ⇒ String (private)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Wraps generated PowerShell for execution by the transport.
Progress streams are silenced first: WinRM relays them as output, which makes converge logs unreadable and can slow long-running resources.
411 412 413 |
# File 'lib/kitchen/provisioner/dsc.rb', line 411 def wrap_powershell_code(code) wrap_shell_code(["$ProgressPreference = 'SilentlyContinue';", code].join("\n")) end |