Class: VagrantPlugins::DSC::Provisioner

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant-dsc/provisioner.rb

Overview

DSC Provisioner Plugin.

Runs the [Desired State Configuration](technet.microsoft.com/en-au/library/dn249912.aspx) system on a guest Virtual Machine, enabling you to quickly configure & bootstrap a Windows Virtual Machine in a repeatable, reliable fashion - the Vagrant way.

Constant Summary collapse

PowerShell_VERSION =
4
DSC_GUEST_RUNNER_PATH =

Default path for storing the transient script runner This should be removed in cleanup

"c:/tmp/vagrant-dsc-runner.ps1"

Instance Method Summary collapse

Constructor Details

#initialize(machine, config) ⇒ Provisioner

Constructs the Provisioner Plugin.

Parameters:

  • machine (Machine)

    The guest machine that is to be provisioned.

  • config (Config)

    The Configuration object used by the Provisioner.



32
33
34
35
36
# File 'lib/vagrant-dsc/provisioner.rb', line 32

def initialize(machine, config)
  super

  @logger = Log4r::Logger.new("vagrant::provisioners::dsc")
end

Instance Method Details

#cleanupObject

Cleanup after a destroy action.

This is the method called when destroying a machine that allows for any state related to the machine created by the provisioner to be cleaned up.



165
166
167
# File 'lib/vagrant-dsc/provisioner.rb', line 165

def cleanup
  # Remove temp files? Or is this ONLY called in destroy (in which case those files will go anyway...)
end

#configure(root_config) ⇒ Object

Configures the Provisioner.

Parameters:

  • root_config (Config)

    The default configuration from the Vagrant hierarchy.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/vagrant-dsc/provisioner.rb', line 41

def configure(root_config)
  @logger.info("==> Configuring DSC")

  # Calculate the paths we're going to use based on the environment
  root_path = @machine.env.root_path
  @expanded_module_paths   = @config.expanded_module_paths(root_path)

  # Setup the module paths
  @module_paths = []
  @expanded_module_paths.each_with_index do |path, i|
    @module_paths << [path, File.join(config.temp_dir, "modules-#{i}")]
  end

  folder_opts = {}
  folder_opts[:type] = @config.synced_folder_type if @config.synced_folder_type
  folder_opts[:owner] = "root" if !@config.synced_folder_type

  # Share the manifests directory with the guest
  @logger.info("==> Sharing manifest #{File.expand_path(@config.manifests_path, root_path)} | #{manifests_guest_path} | #{folder_opts}")

  root_config.vm.synced_folder(
    File.expand_path(@config.manifests_path, root_path),
    manifests_guest_path, folder_opts)

  # Share the module paths
  @module_paths.each do |from, to|
    @logger.info("==> Sharing module folders #{from} | #{to}")
    root_config.vm.synced_folder(from, to, folder_opts)
  end
end

#generate_dsc_runner_scriptString

Generates a PowerShell DSC runner script from an ERB template

Returns:

  • (String)

    The interpolated PowerShell script.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/vagrant-dsc/provisioner.rb', line 205

def generate_dsc_runner_script
  path = File.expand_path("../templates/runner.ps1", __FILE__)

  script = Vagrant::Util::TemplateRenderer.render(path, options: {
      config: @config,
      module_paths: @module_paths.map { |k,v| v }.join(";"),
      mof_path: @config.mof_path,
      configuration_file: @config.configuration_file,
      configuration_file_path: "#{@config.manifests_path}/#{File.basename @config.configuration_file}",
      configuration_data_file_path: @config.configuration_data_file,
      configuration_name: @config.configuration_name,
      manifests_path: @config.manifests_path,
      temp_path: @config.temp_dir,
      module_install: @config.module_install.nil? ? "" : @config.module_install.join(";"),
      parameters: @config.configuration_params.map { |k,v| "#{k}" + (!v.nil? ? " \"#{v}\"": '') }.join(" ")
  })
end

#get_configuration_statusObject



148
149
150
151
# File 'lib/vagrant-dsc/provisioner.rb', line 148

def get_configuration_status
  status = @machine.communicate.shell.powershell("(Get-DscConfigurationStatus).Status")
  return status[:data][0][:stdout]
end

#get_guest_powershell_versionObject



138
139
140
141
# File 'lib/vagrant-dsc/provisioner.rb', line 138

def get_guest_powershell_version
  version = @machine.communicate.shell.powershell("$PSVersionTable.PSVersion.Major")
  return version[:data][0][:stdout]
end

#get_lcm_stateObject



143
144
145
146
# File 'lib/vagrant-dsc/provisioner.rb', line 143

def get_lcm_state
  state = @machine.communicate.shell.powershell("(Get-DscLocalConfigurationManager).LCMState")
  return state[:data][0][:stdout]
end

#install_dscObject

Install and Configure DSC where possible.

Operation is current unsupported, but is likely to be enabled as a flag when the plugin detects an unsupported OS.



198
199
200
# File 'lib/vagrant-dsc/provisioner.rb', line 198

def install_dsc
  raise DSCUnsupportedOperation,  :operation => "install_dsc"
end

#manifests_guest_pathObject

Local path (guest path) to the manifests directory.



170
171
172
# File 'lib/vagrant-dsc/provisioner.rb', line 170

def manifests_guest_path
    File.join(config.temp_dir, config.manifests_path)
end

#provisionObject

Provision the guest machine with DSC.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/vagrant-dsc/provisioner.rb', line 73

def provision
  @logger.info("==> Provisioning DSC man! #{Vagrant.source_root}")

  # If the machine has a wait for reboot functionality, then
  # do that (primarily Windows)
  if @machine.guest.capability?(:wait_for_reboot)
    @machine.guest.capability(:wait_for_reboot)
  end

  # Check that the shared folders are properly shared
  check = []
  check << manifests_guest_path
  @module_paths.each do |host_path, guest_path|
    check << guest_path
  end

  # Make sure the temporary directory is properly set up
  @machine.communicate.tap do |comm|
    comm.sudo("mkdir -p #{config.temp_dir}")
    comm.sudo("chmod 0777 #{config.temp_dir}")
  end

  verify_shared_folders(check)

  verify_dsc

  write_dsc_runner_script(generate_dsc_runner_script)

  begin
    run_dsc_apply
  rescue VagrantPlugins::CommunicatorWinRM::Errors::AuthenticationFailed
    # when install set a domain controller windows kills the active connection with a AuthenticationFailed.
    # The DSC job is still running and new connections are possible, so try to wait 
    @machine.ui.info(I18n.t("vagrant_dsc.errors.winrm_authorization_error_recover"))
  end
  
  wait_for_dsc_completion
end

#run_dsc_applyObject

Runs the DSC Configuration on the guest machine.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/vagrant-dsc/provisioner.rb', line 243

def run_dsc_apply

  @machine.ui.info(I18n.t(
    "vagrant_dsc.running_dsc",
    manifest: config.configuration_file))

  # A bit of an ugly dance, but this is how we get neat, colourised output and exit codes from a Powershell run
  error = false
  machine.communicate.shell.powershell("powershell -ExecutionPolicy Bypass -OutputFormat Text -file #{DSC_GUEST_RUNNER_PATH}") do |type, data|
    if !data.chomp.empty?
      error = true if type == :stderr
      if [:stderr, :stdout].include?(type)
        color = type == :stdout ? :green : :red
        # Remove the \r\n since the dsc output uses this if line is to long. A Line break is a simple \n
        data = data.gsub(/\r\n/,"") if type == :stdout
        @machine.ui.info( data.strip(), color: color, prefix: false)
      end
    end
  end

  error == false
end

#show_dsc_failure_messageObject



153
154
155
156
157
158
# File 'lib/vagrant-dsc/provisioner.rb', line 153

def show_dsc_failure_message
  dsc_error_ps = "Get-WinEvent \"Microsoft-Windows-Dsc/Operational\" | Where-Object {$_.LevelDisplayName -eq \"Error\" -and $_.Message.StartsWith(\"Job $((Get-DscConfigurationStatus).JobId)\" )} | foreach { $_.Message }"
  @machine.communicate.shell.powershell(dsc_error_ps) do |type,data|
    @machine.ui.error(data, prefix: false)
  end
end

#verify_binary(binary) ⇒ Object

Verify the DSC binary is executable on the guest machine.



186
187
188
189
190
191
192
# File 'lib/vagrant-dsc/provisioner.rb', line 186

def verify_binary(binary)
  @machine.communicate.sudo(
    "which #{binary}",
    error_class: DSCError,
    error_key: :dsc_not_detected,
    binary: binary)
end

#verify_dscObject

Verify that a current version of WMF/Powershell is enabled on the guest.



175
176
177
178
179
180
181
182
183
# File 'lib/vagrant-dsc/provisioner.rb', line 175

def verify_dsc
  verify_binary("Start-DscConfiguration")

  # Confirm WMF 4.0+ in $PSVersionTable
  @machine.communicate.test(
      "(($PSVersionTable | ConvertTo-json | ConvertFrom-Json).PSVersion.Major) -ge #{PowerShell_VERSION}",
      error_class: DSCError,
      error_key: :dsc_incorrect_PowerShell_version )
end

#verify_shared_folders(folders) ⇒ Object

Verify that the shared folders have been properly configured on the guest machine.



268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/vagrant-dsc/provisioner.rb', line 268

def verify_shared_folders(folders)
  folders.each do |folder|
    # Warm up PoSH communicator for new instances - any stderr results
    # in failure: https://github.com/mefellows/vagrant-dsc/issues/21
    @machine.communicate.test("test -d #{folder}", sudo: true)

    @logger.info("Checking for shared folder: #{folder}")
    if !@machine.communicate.test("test -d #{folder}", sudo: true)
      raise DSCError, :missing_shared_folders
    end
  end
end

#wait_for_dsc_completionObject

Waits for the completion of the dsc configuration if dsc needs reboots. This currntly only works for WMF5 and needs wait_for_reboot



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/vagrant-dsc/provisioner.rb', line 113

def wait_for_dsc_completion
  powershell_version = get_guest_powershell_version
  return if powershell_version.to_i < 5 || !@machine.guest.capability?(:wait_for_reboot)
  dsc_running = true

  while dsc_running
    case get_lcm_state
      when "PendingReboot"
        @machine.ui.info("DSC needs reboot. Wait for the completion of DSC.")
        @machine.guest.capability(:wait_for_reboot)

      # Do not Know a way to reattch to dsc job, therefore check periodically the state
      when "Busy"
        sleep 10
      else
        dsc_running = false
    end
  end

  if (get_configuration_status == "Failure")
    @machine.ui.error(I18n.t("failure_status"))
    show_dsc_failure_message
  end
end

#windows?Boolean

If on using WinRM, we can assume we are on Windows

Returns:

  • (Boolean)


282
283
284
# File 'lib/vagrant-dsc/provisioner.rb', line 282

def windows?
  @machine.config.vm.communicator == :winrm
end

#write_dsc_runner_script(script) ⇒ String

Writes the PowerShell DSC runner script to a location on the guest.

Parameters:

  • script (String)

    The PowerShell DSC runner script.

Returns:

  • (String)

    the Path to the uploaded location on the guest machine.



227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/vagrant-dsc/provisioner.rb', line 227

def write_dsc_runner_script(script)
  guest_script_path = DSC_GUEST_RUNNER_PATH
  file = Tempfile.new(["vagrant-dsc-runner", "ps1"])
  begin
    file.write(script)
    file.fsync
    file.close
    @machine.communicate.upload(file.path, guest_script_path)
  ensure
    file.close
    file.unlink
  end
  guest_script_path
end