class Chef::Knife::Core::WindowsBootstrapContext


* @run_list - the run list for the node to bootstrap
* @config - a hash of knife’s config values
following instance variables:
bootstrap templates. For backwards compatibility, they must set the
Instances of BootstrapContext are the context objects (i.e., self) for

def bootstrap_directory

def bootstrap_directory
  ChefConfig::Config.etc_chef_dir(windows: true)
end

def clean_etc_chef_file(path)

def clean_etc_chef_file(path)
  ChefConfig::PathHelper.cleanpath(etc_chef_file(path), windows: true)
end

def client_d_content

def client_d_content
  content = ""
  if chef_config[:client_d_dir] && File.exist?(chef_config[:client_d_dir])
    root = Pathname(chef_config[:client_d_dir])
    root.find do |f|
      relative = f.relative_path_from(root)
      if f != root
        file_on_node = "#{bootstrap_directory}/client.d/#{relative}".tr("/", "\\")
        if f.directory?
          content << "mkdir #{file_on_node}\n"
        else
          content << "> #{file_on_node} (\n" +
            escape_and_echo(IO.read(File.expand_path(f))) + "\n)\n"
        end
      end
    end
  end
  content
end

def config_content

def config_content
  # The windows: true / windows: false in the block that follows is more than a bit weird.  The way to read this is that we need

  # the e.g. var_chef_dir to be rendered for the windows value ("C:\chef"), but then we are rendering into a file to be read by

  # ruby, so we don't actually care about forward-vs-backslashes and by rendering into unix we avoid having to deal with the

  # double-backwhacking of everything.  So we expect to see:

  #

  # file_cache_path "C:/chef"

  #

  # Which is mildly odd, but should be entirely correct as far as ruby cares.

  #

  client_rb = <<~CONFIG

    chef_server_url  "#{chef_config[:chef_server_url]}"
    validation_client_name "#{chef_config[:validation_client_name]}"
    file_cache_path   "#{ChefConfig::PathHelper.escapepath(chef_config[:windows_bootstrap_file_cache_path] || "")}"
    file_backup_path  "#{ChefConfig::PathHelper.escapepath(chef_config[:windows_bootstrap_file_backup_path] || "")}"
    cache_options     ({:path => "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\cache\\\\checksums", :skip_expires => true})
  CONFIG

  unless chef_config[:chef_license].nil?
    client_rb << "chef_license \"#{chef_config[:chef_license]}\"\n"
  end
  if config[:chef_node_name]
    client_rb << %Q{node_name "#{config[:chef_node_name]}"\n}
  else
    client_rb << "# Using default node name (fqdn)\n"
  end
  if chef_config[:config_log_level]
    client_rb << %Q{log_level :#{chef_config[:config_log_level]}\n}
  else
    client_rb << "log_level        :auto\n"
  end
  client_rb << "log_location       #{get_log_location}"
  # We configure :verify_api_cert only when it's overridden on the CLI

  # or when specified in the knife config.

  if !config[:node_verify_api_cert].nil? || config.key?(:verify_api_cert)
    value = config[:node_verify_api_cert].nil? ? config[:verify_api_cert] : config[:node_verify_api_cert]
    client_rb << %Q{verify_api_cert #{value}\n}
  end
  # We configure :ssl_verify_mode only when it's overridden on the CLI

  # or when specified in the knife config.

  if config[:node_ssl_verify_mode] || config.key?(:ssl_verify_mode)
    value = case config[:node_ssl_verify_mode]
            when "peer"
              :verify_peer
            when "none"
              :verify_none
            when nil
              config[:ssl_verify_mode]
            else
              nil
            end
    if value
      client_rb << %Q{ssl_verify_mode :#{value}\n}
    end
  end
  if config[:ssl_verify_mode]
    client_rb << %Q{ssl_verify_mode :#{config[:ssl_verify_mode]}\n}
  end
  if config[:bootstrap_proxy]
    client_rb << "\n"
    client_rb << %Q{http_proxy        "#{config[:bootstrap_proxy]}"\n}
    client_rb << %Q{https_proxy       "#{config[:bootstrap_proxy]}"\n}
    client_rb << %Q{no_proxy          "#{config[:bootstrap_no_proxy]}"\n} if config[:bootstrap_no_proxy]
  end
  if config[:bootstrap_no_proxy]
    client_rb << %Q{no_proxy       "#{config[:bootstrap_no_proxy]}"\n}
  end
  if secret
    client_rb << %Q{encrypted_data_bag_secret "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\encrypted_data_bag_secret"\n}
  end
  unless trusted_certs_script.empty?
    client_rb << %Q{trusted_certs_dir "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\trusted_certs"\n}
  end
  if chef_config[:fips]
    client_rb << "fips true\n"
  end
  escape_and_echo(client_rb)
end

def encrypted_data_bag_secret

def encrypted_data_bag_secret
  escape_and_echo(@secret)
end

def escape_and_echo(file_contents)

echo
and prefixes each line with an
escape WIN BATCH special chars
def escape_and_echo(file_contents)
  file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1')
end

def etc_chef_file(path)

def etc_chef_file(path)
  "#{bootstrap_directory}/#{path}"
end

def fallback_install_task_command

def fallback_install_task_command
  # This command will be executed by schtasks.exe in the batch

  # code below. To handle tasks that contain arguments that

  # need to be double quoted, schtasks allows the use of single

  # quotes that will later be converted to double quotes

  command = install_command("'")
  <<~EOH

    @set MSIERRORCODE=!ERRORLEVEL!
    @if ERRORLEVEL 1 (
        @echo WARNING: Failed to install #{ChefUtils::Dist::Infra::PRODUCT} MSI package in remote context with status code !MSIERRORCODE!.
        @echo WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614
        @set OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log"
        @move "%CHEF_CLIENT_MSI_LOG_PATH%" "!OLDLOGLOCATION!" > NUL
        @echo WARNING: Saving installation log of failure at !OLDLOGLOCATION!
        @echo WARNING: Retrying installation with local context...
        @schtasks /create /f  /sc once /st 00:00:00 /tn chefclientbootstraptask /ru SYSTEM /rl HIGHEST /tr \"cmd /c #{command} & sleep 2 & waitfor /s %computername% /si chefclientinstalldone\"
        @if ERRORLEVEL 1 (
            @echo ERROR: Failed to create #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL! > "&2"
        ) else (
            @echo Successfully created scheduled task to install #{ChefUtils::Dist::Infra::PRODUCT}.
            @schtasks /run /tn chefclientbootstraptask
            @if ERRORLEVEL 1 (
                @echo ERROR: Failed to execute #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL!. > "&2"
            ) else (
                @echo Successfully started #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task.
                @echo Waiting for installation to complete -- this may take a few minutes...
                waitfor chefclientinstalldone /t 600
                if ERRORLEVEL 1 (
                    @echo ERROR: Timed out waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install
                ) else (
                    @echo Finished waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install.
                )
                @schtasks /delete /f /tn chefclientbootstraptask > NUL
            )
        )
    ) else (
        @echo Successfully installed #{ChefUtils::Dist::Infra::PRODUCT} package.
    )
  EOH

end

def first_boot

def first_boot
  escape_and_echo(super.to_json)
end

def get_log_location

def get_log_location
  if chef_config[:config_log_location].equal?(:win_evt)
    %Q{:#{chef_config[:config_log_location]}\n}
  elsif chef_config[:config_log_location].equal?(:syslog)
    raise "syslog is not supported for log_location on Windows OS\n"
  elsif chef_config[:config_log_location].equal?(STDOUT)
    "STDOUT\n"
  elsif chef_config[:config_log_location].equal?(STDERR)
    "STDERR\n"
  elsif chef_config[:config_log_location].nil? || chef_config[:config_log_location].empty?
    "STDOUT\n"
  elsif chef_config[:config_log_location]
    %Q{"#{chef_config[:config_log_location]}"\n}
  else
    "STDOUT\n"
  end
end

def initialize(config, run_list, chef_config, secret = nil)

def initialize(config, run_list, chef_config, secret = nil)
  @config       = config
  @run_list     = run_list
  @chef_config  = chef_config
  @secret       = secret
  super(config, run_list, chef_config, secret)
end

def install_chef

def install_chef
  # The normal install command uses regular double quotes in

  # the install command, so request such a string from install_command

  install_command('"') + "\n" + fallback_install_task_command
end

def install_command(executor_quote)

def install_command(executor_quote)
  "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
end

def local_download_path

def local_download_path
  "%TEMP%\\#{ChefUtils::Dist::Infra::CLIENT}-latest.msi"
end

def msi_url(machine_os = nil, machine_arch = nil, download_context = nil)

Build a URL that will redirect to the correct Chef Infra msi download.
def msi_url(machine_os = nil, machine_arch = nil, download_context = nil)
  if config[:msi_url].nil? || config[:msi_url].empty?
    url = if config[:license_id] && !config[:omnitruck_url].empty?
            format(config[:omnitruck_url], config[:channel] + "/chef/download") + "&p=windows"
          else
            "https://omnitruck.chef.io/chef/download?p=windows&channel=#{config[:channel]}"
          end
    url += "&pv=#{machine_os}" unless machine_os.nil?
    url += "&m=#{machine_arch}" unless machine_arch.nil?
    url += "&DownloadContext=#{download_context}" unless download_context.nil?
    url += "&v=#{version_to_install}"
  else
    config[:msi_url]
  end
end

def start_chef

def start_chef
  c_opscode_dir = ChefConfig::PathHelper.cleanpath(ChefConfig::Config.c_opscode_dir, windows: true)
  client_rb = clean_etc_chef_file("client.rb")
  first_boot = clean_etc_chef_file("first-boot.json")
  bootstrap_environment_option = bootstrap_environment.nil? ? "" : " -E #{bootstrap_environment}"
  start_chef = "SET \"PATH=%SYSTEM32%;%SystemRoot%;%SYSTEM32%\\Wbem;%SYSTEM32%\\WindowsPowerShell\\v1.0\\;C:\\ruby\\bin;#{c_opscode_dir}\\bin;#{c_opscode_dir}\\embedded\\bin\;%PATH%\"\n"
  start_chef << "#{ChefUtils::Dist::Infra::CLIENT} -c #{client_rb} -j #{first_boot}#{bootstrap_environment_option}\n"
end

def trusted_certs_content

This string should contain both the commands necessary to both create the files, as well as their content
Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
def trusted_certs_content
  content = ""
  if chef_config[:trusted_certs_dir]
    Dir.glob(File.join(ChefConfig::PathHelper.escape_glob_dir(chef_config[:trusted_certs_dir]), "*.{crt,pem}")).each do |cert|
      content << "> #{bootstrap_directory}/trusted_certs/#{File.basename(cert)} (\n" +
        escape_and_echo(IO.read(File.expand_path(cert))) + "\n)\n"
    end
  end
  content
end

def trusted_certs_script

def trusted_certs_script
  @trusted_certs_script ||= trusted_certs_content
end

def validation_key

def validation_key
  if File.exist?(File.expand_path(chef_config[:validation_key]))
    IO.read(File.expand_path(chef_config[:validation_key]))
  else
    false
  end
end

def win_wget

def win_wget
  # I tried my best to figure out how to properly url decode and switch / to \

  # but this is VBScript - so I don't really care that badly.

  win_wget = <<~WGET

    url = WScript.Arguments.Named("url")
    path = WScript.Arguments.Named("path")
    proxy = null
    '* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all
    '* / into \.  Also assume that file:/// is a local absolute path and that file://<foo>
    '* is possibly a network file path.
    If InStr(url, "file://") = 1 Then
    url = Unescape(url)
    If InStr(url, "file:///") = 1 Then
    sourcePath = Mid(url, Len("file:///") + 1)
    Else
    sourcePath = Mid(url, Len("file:") + 1)
    End If
    sourcePath = Replace(sourcePath, "/", "\\")
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    If objFSO.Fileexists(path) Then objFSO.DeleteFile path
    objFSO.CopyFile sourcePath, path, true
    Set objFSO = Nothing
    Else
    Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
    Set wshShell = CreateObject( "WScript.Shell" )
    Set objUserVariables = wshShell.Environment("USER")
    rem http proxy is optional
    rem attempt to read from HTTP_PROXY env var first
    On Error Resume Next
    If NOT (objUserVariables("HTTP_PROXY") = "") Then
    proxy = objUserVariables("HTTP_PROXY")
    rem fall back to named arg
    ElseIf NOT (WScript.Arguments.Named("proxy") = "") Then
    proxy = WScript.Arguments.Named("proxy")
    End If
    If NOT isNull(proxy) Then
    rem setProxy method is only available on ServerXMLHTTP 6.0+
    Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    objXMLHTTP.setProxy 2, proxy
    End If
    On Error Goto 0
    objXMLHTTP.open "GET", url, false
    objXMLHTTP.send()
    If objXMLHTTP.Status = 200 Then
    Set objADOStream = CreateObject("ADODB.Stream")
    objADOStream.Open
    objADOStream.Type = 1
    objADOStream.Write objXMLHTTP.ResponseBody
    objADOStream.Position = 0
    Set objFSO = Createobject("Scripting.FileSystemObject")
    If objFSO.Fileexists(path) Then objFSO.DeleteFile path
    Set objFSO = Nothing
    objADOStream.SaveToFile path
    objADOStream.Close
    Set objADOStream = Nothing
    ElseIf objXMLHTTP.Status = 400 Then
    errorBody = objXMLHTTP.ResponseText
    WScript.Echo "Error: 400 BadRequest"
    WScript.Echo "Error Body:"
    WScript.Echo errorBody
    Else
    WScript.Echo "An error occurred while downloading the file:"
    errorBody = objXMLHTTP.ResponseText
    WScript.Echo "Status: "
    WScript.Echo objXMLHTTP.Status
    WScript.Echo "Status Text: "
    WScript.Echo errorBody
    End If
    Set objXMLHTTP = Nothing
    End If
  WGET

  escape_and_echo(win_wget)
end

def win_wget_ps

def win_wget_ps
  win_wget_ps = <<~WGET_PS

    param(
       [String] $remoteUrl,
       [String] $localPath
    )
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $ProxyUrl = $env:http_proxy;
    $webClient = new-object System.Net.WebClient;
    if ($ProxyUrl -ne '') {
      $WebProxy = New-Object System.Net.WebProxy($ProxyUrl,$true)
      $WebClient.Proxy = $WebProxy
    }
    try {
      $webClient.DownloadFile($remoteUrl, $localPath);
      Write-Host "Download complete. The file has been saved to $localPath."
    } catch [System.Net.WebException] {
      $response = $_.Exception.Response
      if ($response.StatusCode -eq [System.Net.HttpStatusCode]::BadRequest) {
        $streamReader = New-Object System.IO.StreamReader($response.GetResponseStream())
        $errorBody = $streamReader.ReadToEnd()
        $streamReader.Dispose()
        Write-Host "Error: 400 BadRequest"
        Write-Host "Error Body:"
        Write-Host $errorBody
      }
      else {
        Write-Host "An error occurred while downloading the file:"
        Write-Host $_.Exception.Message
      }
      Exit 1
    }
  WGET_PS

  escape_and_echo(win_wget_ps)
end