summaryrefslogtreecommitdiff
path: root/knife/lib/chef/knife/core/windows_bootstrap_context.rb
diff options
context:
space:
mode:
Diffstat (limited to 'knife/lib/chef/knife/core/windows_bootstrap_context.rb')
-rw-r--r--knife/lib/chef/knife/core/windows_bootstrap_context.rb406
1 files changed, 406 insertions, 0 deletions
diff --git a/knife/lib/chef/knife/core/windows_bootstrap_context.rb b/knife/lib/chef/knife/core/windows_bootstrap_context.rb
new file mode 100644
index 0000000000..0ff209a0b7
--- /dev/null
+++ b/knife/lib/chef/knife/core/windows_bootstrap_context.rb
@@ -0,0 +1,406 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Copyright:: Copyright (c) Chef Software Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require_relative "bootstrap_context"
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ module Core
+ # Instances of BootstrapContext are the context objects (i.e., +self+) for
+ # bootstrap templates. For backwards compatibility, they +must+ set the
+ # following instance variables:
+ # * @config - a hash of knife's config values
+ # * @run_list - the run list for the node to bootstrap
+ #
+ class WindowsBootstrapContext < BootstrapContext
+ attr_accessor :config
+ attr_accessor :chef_config
+ attr_accessor :secret
+
+ 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 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 encrypted_data_bag_secret
+ escape_and_echo(@secret)
+ end
+
+ def trusted_certs_script
+ @trusted_certs_script ||= trusted_certs_content
+ end
+
+ 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(ChefConfig::Config.var_chef_dir(windows: true))}\\\\cache"
+ file_backup_path "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.var_chef_dir(windows: true))}\\\\backup"
+ 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 config[:config_log_level]
+ client_rb << %Q{log_level :#{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 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 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 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
+ End If
+ Set objXMLHTTP = Nothing
+ End If
+ WGET
+ escape_and_echo(win_wget)
+ end
+
+ 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
+ }
+
+ $webClient.DownloadFile($remoteUrl, $localPath);
+ WGET_PS
+
+ escape_and_echo(win_wget_ps)
+ end
+
+ 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 clean_etc_chef_file(path)
+ ChefConfig::PathHelper.cleanpath(etc_chef_file(path), windows: true)
+ end
+
+ def etc_chef_file(path)
+ "#{bootstrap_directory}/#{path}"
+ end
+
+ def bootstrap_directory
+ ChefConfig::Config.etc_chef_dir(windows: true)
+ end
+
+ def local_download_path
+ "%TEMP%\\#{ChefUtils::Dist::Infra::CLIENT}-latest.msi"
+ end
+
+ # Build a URL to query www.chef.io 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 = "https://www.chef.io/chef/download?p=windows"
+ 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 += "&channel=#{config[:channel]}"
+ url += "&v=#{version_to_install}"
+ else
+ config[:msi_url]
+ end
+ end
+
+ def first_boot
+ escape_and_echo(super.to_json)
+ end
+
+ # escape WIN BATCH special chars
+ # and prefixes each line with an
+ # echo
+ def escape_and_echo(file_contents)
+ file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1')
+ end
+
+ private
+
+ 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
+
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
+ # This string should contain both the commands necessary to both create the files, as well as their content
+ 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 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 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
+ end
+ end
+ end
+end