diff options
Diffstat (limited to 'knife/lib/chef/knife/core/windows_bootstrap_context.rb')
-rw-r--r-- | knife/lib/chef/knife/core/windows_bootstrap_context.rb | 406 |
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 |