lib/chef/knife/bootstrap_windows_base.rb in knife-windows-1.3.0 vs lib/chef/knife/bootstrap_windows_base.rb in knife-windows-1.4.0

- old
+ new

@@ -1,415 +1,449 @@ -# -# Author:: Seth Chisamore (<schisamo@opscode.com>) -# Copyright:: Copyright (c) 2011 Opscode, 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 'chef/knife' -require 'chef/knife/bootstrap' -require 'chef/encrypted_data_bag_item' -require 'chef/knife/core/windows_bootstrap_context' -require 'chef/knife/knife_windows_base' -# Chef 11 PathHelper doesn't have #home -#require 'chef/util/path_helper' - -class Chef - class Knife - module BootstrapWindowsBase - - include Chef::Knife::KnifeWindowsBase - - # :nodoc: - # Would prefer to do this in a rational way, but can't be done b/c of - # Mixlib::CLI's design :( - def self.included(includer) - includer.class_eval do - - deps do - require 'readline' - require 'chef/json_compat' - end - - option :chef_node_name, - :short => "-N NAME", - :long => "--node-name NAME", - :description => "The Chef node name for your new node" - - option :prerelease, - :long => "--prerelease", - :description => "Install the pre-release chef gems" - - option :bootstrap_version, - :long => "--bootstrap-version VERSION", - :description => "The version of Chef to install", - :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v } - - option :bootstrap_proxy, - :long => "--bootstrap-proxy PROXY_URL", - :description => "The proxy server for the node being bootstrapped", - :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p } - - option :bootstrap_no_proxy, - :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]", - :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode", - :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np } - - option :bootstrap_install_command, - :long => "--bootstrap-install-command COMMANDS", - :description => "Custom command to install chef-client", - :proc => Proc.new { |ic| Chef::Config[:knife][:bootstrap_install_command] = ic } - - # DEPR: Remove this option in Chef 13 - option :distro, - :short => "-d DISTRO", - :long => "--distro DISTRO", - :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.", - :proc => Proc.new { |v| - Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use --bootstrap-template option instead.") - v - } - - option :bootstrap_template, - :short => "-t TEMPLATE", - :long => "--bootstrap-template TEMPLATE", - :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates." - - # DEPR: Remove this option in Chef 13 - option :template_file, - :long => "--template-file TEMPLATE", - :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.", - :proc => Proc.new { |v| - Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use --bootstrap-template option instead.") - v - } - - option :run_list, - :short => "-r RUN_LIST", - :long => "--run-list RUN_LIST", - :description => "Comma separated list of roles/recipes to apply", - :proc => lambda { |o| o.split(",") }, - :default => [] - - option :hint, - :long => "--hint HINT_NAME[=HINT_FILE]", - :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.", - :proc => Proc.new { |h| - Chef::Config[:knife][:hints] ||= Hash.new - name, path = h.split("=") - Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new - } - - option :first_boot_attributes, - :short => "-j JSON_ATTRIBS", - :long => "--json-attributes", - :description => "A JSON string to be added to the first run of chef-client", - :proc => lambda { |o| JSON.parse(o) }, - :default => nil - - option :first_boot_attributes_from_file, - :long => "--json-attribute-file FILE", - :description => "A JSON file to be used to the first run of chef-client", - :proc => lambda { |o| Chef::JSONCompat.parse(File.read(o)) }, - :default => nil - - # Mismatch between option 'encrypted_data_bag_secret' and it's long value '--secret' is by design for compatibility - option :encrypted_data_bag_secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to decrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config.", - :default => false - - # Mismatch between option 'encrypted_data_bag_secret_file' and it's long value '--secret-file' is by design for compatibility - option :encrypted_data_bag_secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config." - - option :auth_timeout, - :long => "--auth-timeout MINUTES", - :description => "The maximum time in minutes to wait to for authentication over the transport to the node to succeed. The default value is 2 minutes.", - :default => 2 - - option :node_ssl_verify_mode, - :long => "--node-ssl-verify-mode [peer|none]", - :description => "Whether or not to verify the SSL cert for all HTTPS requests.", - :proc => Proc.new { |v| - valid_values = ["none", "peer"] - unless valid_values.include?(v) - raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}" - end - } - - option :node_verify_api_cert, - :long => "--[no-]node-verify-api-cert", - :description => "Verify the SSL cert for HTTPS requests to the Chef server API.", - :boolean => true - - option :msi_url, - :short => "-u URL", - :long => "--msi-url URL", - :description => "Location of the Chef Client MSI. The default templates will prefer to download from this location. The MSI will be downloaded from chef.io if not provided.", - :default => '' - - option :install_as_service, - :long => "--install-as-service", - :description => "Install chef-client as a Windows service", - :default => false - - option :bootstrap_vault_file, - :long => '--bootstrap-vault-file VAULT_FILE', - :description => 'A JSON file with a list of vault(s) and item(s) to be updated' - - option :bootstrap_vault_json, - :long => '--bootstrap-vault-json VAULT_JSON', - :description => 'A JSON string with the vault(s) and item(s) to be updated' - - option :bootstrap_vault_item, - :long => '--bootstrap-vault-item VAULT_ITEM', - :description => 'A single vault and item to update as "vault:item"', - :proc => Proc.new { |i| - (vault, item) = i.split(/:/) - Chef::Config[:knife][:bootstrap_vault_item] ||= {} - Chef::Config[:knife][:bootstrap_vault_item][vault] ||= [] - Chef::Config[:knife][:bootstrap_vault_item][vault].push(item) - Chef::Config[:knife][:bootstrap_vault_item] - } - - option :policy_name, - :long => "--policy-name POLICY_NAME", - :description => "Policyfile name to use (--policy-group must also be given)", - :default => nil - - option :policy_group, - :long => "--policy-group POLICY_GROUP", - :description => "Policy group name to use (--policy-name must also be given)", - :default => nil - - option :tags, - :long => "--tags TAGS", - :description => "Comma separated list of tags to apply to the node", - :proc => lambda { |o| o.split(/[\s,]+/) }, - :default => [] - end - end - - def default_bootstrap_template - "windows-chef-client-msi" - end - - def bootstrap_template - # The order here is important. We want to check if we have the new Chef 12 option is set first. - # Knife cloud plugins unfortunately all set a default option for the :distro so it should be at - # the end. - config[:bootstrap_template] || config[:template_file] || config[:distro] || default_bootstrap_template - end - - # TODO: This should go away when CHEF-2193 is fixed - def load_template(template=nil) - # Are we bootstrapping using an already shipped template? - - template = bootstrap_template - - # Use the template directly if it's a path to an actual file - if File.exists?(template) - Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}") - return IO.read(template).chomp - end - - # Otherwise search the template directories until we find the right one - bootstrap_files = [] - bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb") - bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir - ::Knife::Windows::PathHelper.all_homes('.chef', 'bootstrap', "#{template}.erb") { |p| bootstrap_files << p } - bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb")) - bootstrap_files.flatten! - - template = Array(bootstrap_files).find do |bootstrap_template| - Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}") - ::File.exists?(bootstrap_template) - end - - unless template - ui.info("Can not find bootstrap definition for #{config[:distro]}") - raise Errno::ENOENT - end - - Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}") - - IO.read(template).chomp - end - - def bootstrap_context - @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config) - end - - def load_correct_secret - knife_secret_file = Chef::Config[:knife][:encrypted_data_bag_secret_file] - knife_secret = Chef::Config[:knife][:encrypted_data_bag_secret] - cli_secret_file = config[:encrypted_data_bag_secret_file] - cli_secret = config[:encrypted_data_bag_secret] - - cli_secret_file = nil if cli_secret_file == knife_secret_file - cli_secret = nil if cli_secret == knife_secret - - cli_secret_file = Chef::EncryptedDataBagItem.load_secret(cli_secret_file) if cli_secret_file != nil - knife_secret_file = Chef::EncryptedDataBagItem.load_secret(knife_secret_file) if knife_secret_file != nil - - cli_secret_file || cli_secret || knife_secret_file || knife_secret - end - - def render_template(template=nil) - config[:secret] = load_correct_secret - Erubis::Eruby.new(template).evaluate(bootstrap_context) - end - - def bootstrap(proto=nil) - if Chef::Config[:knife][:encrypted_data_bag_secret_file] || Chef::Config[:knife][:encrypted_data_bag_secret] - warn_chef_config_secret_key - end - - validate_name_args! - - # adding respond_to? so this works with pre 12.4 chef clients - validate_options! if respond_to?(:validate_options!) - - @node_name = Array(@name_args).first - # back compat--templates may use this setting: - config[:server_name] = @node_name - - STDOUT.sync = STDERR.sync = true - - if Chef::VERSION.split('.').first.to_i == 11 && Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])) - ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.") - exit 1 - end - - if (defined?(chef_vault_handler) && chef_vault_handler.doing_chef_vault?) || - (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) - - unless locate_config_value(:chef_node_name) - ui.error("You must pass a node name with -N when bootstrapping with user credentials") - exit 1 - end - - client_builder.run - - if client_builder.respond_to?(:client) - chef_vault_handler.run(client_builder.client) - else - chef_vault_handler.run(node_name: config[:chef_node_name]) - end - - bootstrap_context.client_pem = client_builder.client_path - - else - ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...") - ui.info("Delete your validation key in order to use your user credentials instead") - ui.info("") - end - - wait_for_remote_response( config[:auth_timeout].to_i ) - ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") - # create a bootstrap.bat file on the node - # we have to run the remote commands in 2047 char chunks - create_bootstrap_bat_command do |command_chunk| - begin - render_command_result = run_command(command_chunk) - ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 - render_command_result - rescue SystemExit => e - raise unless e.success? - end - end - - # execute the bootstrap.bat file - bootstrap_command_result = run_command(bootstrap_command) - ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0 - - bootstrap_command_result - end - - protected - - # Default implementation -- override only if required by the transport - def wait_for_remote_response(wait_max_minutes) - end - - def bootstrap_command - @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" - end - - def bootstrap_render_banner_command(chunk_num) - "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" - end - - def escape_windows_batch_characters(line) - # TODO: The commands are going to get redirected - do we need to escape &? - line.gsub!(/[(<|>)^]/).each{|m| "^#{m}"} - end - - def create_bootstrap_bat_command() - chunk_num = 0 - bootstrap_bat = "" - banner = bootstrap_render_banner_command(chunk_num += 1) - render_template(load_template(config[:bootstrap_template])).each_line do |line| - escape_windows_batch_characters(line) - # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can - # confidently prefix every actual command with &&. - # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? - render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" - # Windows commands are limited to 8191 characters for machines running XP or higher but - # this includes the length of environment variables after they have been expanded. - # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner - # and once in every command redirection), we simply guess and set the max to 5000. - # TODO: When a more accurate method is available, fix this. - if bootstrap_bat.length + render_line.length + banner.length > 5000 - # Can't fit it into this chunk? - flush (if necessary) and then try. - # Do this first because banner.length might change (e.g. due to an extra digit) and - # prevent a fit. - unless bootstrap_bat.empty? - yield banner + bootstrap_bat - bootstrap_bat = "" - banner = bootstrap_render_banner_command(chunk_num += 1) - end - # Will this ever fit? - if render_line.length + banner.length > 5000 - raise "Command in bootstrap template too long by #{render_line.length + banner.length - 5000} characters : #{line}" - end - end - bootstrap_bat << render_line - end - raise "Bootstrap template was empty! Check #{config[:bootstrap_template]}" if bootstrap_bat.empty? - yield banner + bootstrap_bat - end - - def bootstrap_bat_file - @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" - end - - def warn_chef_config_secret_key - ui.info "* " * 40 - ui.warn(<<-WARNING) -\nSpecifying the encrypted data bag secret key using an 'encrypted_data_bag_secret' -entry in 'knife.rb' is deprecated. Please use the '--secret' or '--secret-file' -options of this command instead. - -#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this -behavior will be removed and any 'encrypted_data_bag_secret' entries in -'knife.rb' will be ignored completely. - WARNING - ui.info "* " * 40 - end - end - end -end +# +# Author:: Seth Chisamore (<schisamo@chef.io>) +# Copyright:: Copyright (c) 2011-2016 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 'chef/knife' +require 'chef/knife/bootstrap' +require 'chef/encrypted_data_bag_item' +require 'chef/knife/core/windows_bootstrap_context' +require 'chef/knife/knife_windows_base' +# Chef 11 PathHelper doesn't have #home +#require 'chef/util/path_helper' + +class Chef + class Knife + module BootstrapWindowsBase + + include Chef::Knife::KnifeWindowsBase + + # :nodoc: + # Would prefer to do this in a rational way, but can't be done b/c of + # Mixlib::CLI's design :( + def self.included(includer) + includer.class_eval do + + deps do + require 'readline' + require 'chef/json_compat' + end + + option :chef_node_name, + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The Chef node name for your new node" + + option :prerelease, + :long => "--prerelease", + :description => "Install the pre-release chef gems" + + option :bootstrap_version, + :long => "--bootstrap-version VERSION", + :description => "The version of Chef to install", + :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v } + + option :bootstrap_proxy, + :long => "--bootstrap-proxy PROXY_URL", + :description => "The proxy server for the node being bootstrapped", + :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p } + + option :bootstrap_no_proxy, + :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]", + :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode", + :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np } + + option :bootstrap_install_command, + :long => "--bootstrap-install-command COMMANDS", + :description => "Custom command to install chef-client", + :proc => Proc.new { |ic| Chef::Config[:knife][:bootstrap_install_command] = ic } + + # DEPR: Remove this option in Chef 13 + option :distro, + :short => "-d DISTRO", + :long => "--distro DISTRO", + :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.", + :proc => Proc.new { |v| + Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use --bootstrap-template option instead.") + v + } + + option :bootstrap_template, + :short => "-t TEMPLATE", + :long => "--bootstrap-template TEMPLATE", + :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates." + + # DEPR: Remove this option in Chef 13 + option :template_file, + :long => "--template-file TEMPLATE", + :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.", + :proc => Proc.new { |v| + Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use --bootstrap-template option instead.") + v + } + + option :run_list, + :short => "-r RUN_LIST", + :long => "--run-list RUN_LIST", + :description => "Comma separated list of roles/recipes to apply", + :proc => lambda { |o| o.split(",") }, + :default => [] + + option :hint, + :long => "--hint HINT_NAME[=HINT_FILE]", + :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.", + :proc => Proc.new { |h| + Chef::Config[:knife][:hints] ||= Hash.new + name, path = h.split("=") + Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new + } + + option :first_boot_attributes, + :short => "-j JSON_ATTRIBS", + :long => "--json-attributes", + :description => "A JSON string to be added to the first run of chef-client", + :proc => lambda { |o| JSON.parse(o) }, + :default => nil + + option :first_boot_attributes_from_file, + :long => "--json-attribute-file FILE", + :description => "A JSON file to be used to the first run of chef-client", + :proc => lambda { |o| Chef::JSONCompat.parse(File.read(o)) }, + :default => nil + + # Mismatch between option 'encrypted_data_bag_secret' and it's long value '--secret' is by design for compatibility + option :encrypted_data_bag_secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to decrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config.", + :default => false + + # Mismatch between option 'encrypted_data_bag_secret_file' and it's long value '--secret-file' is by design for compatibility + option :encrypted_data_bag_secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config." + + option :auth_timeout, + :long => "--auth-timeout MINUTES", + :description => "The maximum time in minutes to wait to for authentication over the transport to the node to succeed. The default value is 2 minutes.", + :default => 2 + + option :node_ssl_verify_mode, + :long => "--node-ssl-verify-mode [peer|none]", + :description => "Whether or not to verify the SSL cert for all HTTPS requests.", + :proc => Proc.new { |v| + valid_values = ["none", "peer"] + unless valid_values.include?(v) + raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}" + end + } + + option :node_verify_api_cert, + :long => "--[no-]node-verify-api-cert", + :description => "Verify the SSL cert for HTTPS requests to the Chef server API.", + :boolean => true + + option :msi_url, + :short => "-u URL", + :long => "--msi-url URL", + :description => "Location of the Chef Client MSI. The default templates will prefer to download from this location. The MSI will be downloaded from chef.io if not provided.", + :default => '' + + option :install_as_service, + :long => "--install-as-service", + :description => "Install chef-client as a Windows service", + :default => false + + option :bootstrap_vault_file, + :long => '--bootstrap-vault-file VAULT_FILE', + :description => 'A JSON file with a list of vault(s) and item(s) to be updated' + + option :bootstrap_vault_json, + :long => '--bootstrap-vault-json VAULT_JSON', + :description => 'A JSON string with the vault(s) and item(s) to be updated' + + option :bootstrap_vault_item, + :long => '--bootstrap-vault-item VAULT_ITEM', + :description => 'A single vault and item to update as "vault:item"', + :proc => Proc.new { |i| + (vault, item) = i.split(/:/) + Chef::Config[:knife][:bootstrap_vault_item] ||= {} + Chef::Config[:knife][:bootstrap_vault_item][vault] ||= [] + Chef::Config[:knife][:bootstrap_vault_item][vault].push(item) + Chef::Config[:knife][:bootstrap_vault_item] + } + + option :policy_name, + :long => "--policy-name POLICY_NAME", + :description => "Policyfile name to use (--policy-group must also be given)", + :default => nil + + option :policy_group, + :long => "--policy-group POLICY_GROUP", + :description => "Policy group name to use (--policy-name must also be given)", + :default => nil + + option :tags, + :long => "--tags TAGS", + :description => "Comma separated list of tags to apply to the node", + :proc => lambda { |o| o.split(/[\s,]+/) }, + :default => [] + end + end + + def default_bootstrap_template + "windows-chef-client-msi" + end + + def bootstrap_template + # The order here is important. We want to check if we have the new Chef 12 option is set first. + # Knife cloud plugins unfortunately all set a default option for the :distro so it should be at + # the end. + config[:bootstrap_template] || config[:template_file] || config[:distro] || default_bootstrap_template + end + + # TODO: This should go away when CHEF-2193 is fixed + def load_template(template=nil) + # Are we bootstrapping using an already shipped template? + + template = bootstrap_template + + # Use the template directly if it's a path to an actual file + if File.exists?(template) + Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}") + return IO.read(template).chomp + end + + # Otherwise search the template directories until we find the right one + bootstrap_files = [] + bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb") + bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir + ::Knife::Windows::PathHelper.all_homes('.chef', 'bootstrap', "#{template}.erb") { |p| bootstrap_files << p } + bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb")) + bootstrap_files.flatten! + + template = Array(bootstrap_files).find do |bootstrap_template| + Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}") + ::File.exists?(bootstrap_template) + end + + unless template + ui.info("Can not find bootstrap definition for #{config[:distro]}") + raise Errno::ENOENT + end + + Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}") + + IO.read(template).chomp + end + + def bootstrap_context + @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config) + end + + def load_correct_secret + knife_secret_file = Chef::Config[:knife][:encrypted_data_bag_secret_file] + knife_secret = Chef::Config[:knife][:encrypted_data_bag_secret] + cli_secret_file = config[:encrypted_data_bag_secret_file] + cli_secret = config[:encrypted_data_bag_secret] + + cli_secret_file = nil if cli_secret_file == knife_secret_file + cli_secret = nil if cli_secret == knife_secret + + cli_secret_file = Chef::EncryptedDataBagItem.load_secret(cli_secret_file) if cli_secret_file != nil + knife_secret_file = Chef::EncryptedDataBagItem.load_secret(knife_secret_file) if knife_secret_file != nil + + cli_secret_file || cli_secret || knife_secret_file || knife_secret + end + + def render_template(template=nil) + config[:secret] = load_correct_secret + Erubis::Eruby.new(template).evaluate(bootstrap_context) + end + + def bootstrap(proto=nil) + if Chef::Config[:knife][:encrypted_data_bag_secret_file] || Chef::Config[:knife][:encrypted_data_bag_secret] + warn_chef_config_secret_key + end + + bootstrap_architecture = Chef::Config[:knife][:bootstrap_architecture] + if bootstrap_architecture && ![:x86_64, :i386].include?(bootstrap_architecture.to_sym) + raise "Valid values for the knife config :bootstrap_architecture are i386 or x86_64. Supplied value is #{bootstrap_architecture}" + end + if Chef::Config[:knife][:architecture] + raise "Do not set :architecture in your knife config, use :bootstrap_architecture." + end + + validate_name_args! + + # adding respond_to? so this works with pre 12.4 chef clients + validate_options! if respond_to?(:validate_options!) + + @node_name = Array(@name_args).first + # back compat--templates may use this setting: + config[:server_name] = @node_name + + STDOUT.sync = STDERR.sync = true + + if Chef::VERSION.split('.').first.to_i == 11 && Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])) + ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.") + exit 1 + end + + if (defined?(chef_vault_handler) && chef_vault_handler.doing_chef_vault?) || + (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) + + unless locate_config_value(:chef_node_name) + ui.error("You must pass a node name with -N when bootstrapping with user credentials") + exit 1 + end + + client_builder.run + + if client_builder.respond_to?(:client) + chef_vault_handler.run(client_builder.client) + else + chef_vault_handler.run(node_name: config[:chef_node_name]) + end + + bootstrap_context.client_pem = client_builder.client_path + + else + ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...") + ui.info("Delete your validation key in order to use your user credentials instead") + ui.info("") + end + + wait_for_remote_response( config[:auth_timeout].to_i ) + + set_target_architecture(bootstrap_architecture) + + ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") + # create a bootstrap.bat file on the node + # we have to run the remote commands in 2047 char chunks + create_bootstrap_bat_command do |command_chunk| + begin + render_command_result = run_command(command_chunk) + ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0 + render_command_result + rescue SystemExit => e + raise unless e.success? + end + end + + # execute the bootstrap.bat file + bootstrap_command_result = run_command(bootstrap_command) + ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0 + + bootstrap_command_result + end + + protected + + # Default implementation -- override only if required by the transport + def wait_for_remote_response(wait_max_minutes) + end + + def bootstrap_command + @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}" + end + + def bootstrap_render_banner_command(chunk_num) + "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}" + end + + def escape_windows_batch_characters(line) + # TODO: The commands are going to get redirected - do we need to escape &? + line.gsub!(/[(<|>)^]/).each{|m| "^#{m}"} + end + + def create_bootstrap_bat_command() + chunk_num = 0 + bootstrap_bat = "" + banner = bootstrap_render_banner_command(chunk_num += 1) + render_template(load_template(config[:bootstrap_template])).each_line do |line| + escape_windows_batch_characters(line) + # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can + # confidently prefix every actual command with &&. + # TODO: Why does ^\n&& work directly through the commandline but not through SOAP? + render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})" + # Windows commands are limited to 8191 characters for machines running XP or higher but + # this includes the length of environment variables after they have been expanded. + # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner + # and once in every command redirection), we simply guess and set the max to 5000. + # TODO: When a more accurate method is available, fix this. + if bootstrap_bat.length + render_line.length + banner.length > 5000 + # Can't fit it into this chunk? - flush (if necessary) and then try. + # Do this first because banner.length might change (e.g. due to an extra digit) and + # prevent a fit. + unless bootstrap_bat.empty? + yield banner + bootstrap_bat + bootstrap_bat = "" + banner = bootstrap_render_banner_command(chunk_num += 1) + end + # Will this ever fit? + if render_line.length + banner.length > 5000 + raise "Command in bootstrap template too long by #{render_line.length + banner.length - 5000} characters : #{line}" + end + end + bootstrap_bat << render_line + end + raise "Bootstrap template was empty! Check #{config[:bootstrap_template]}" if bootstrap_bat.empty? + yield banner + bootstrap_bat + end + + def bootstrap_bat_file + @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\"" + end + + def warn_chef_config_secret_key + ui.info "* " * 40 + ui.warn(<<-WARNING) +\nSpecifying the encrypted data bag secret key using an 'encrypted_data_bag_secret' +entry in 'knife.rb' is deprecated. Please use the '--secret' or '--secret-file' +options of this command instead. + +#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this +behavior will be removed and any 'encrypted_data_bag_secret' entries in +'knife.rb' will be ignored completely. + WARNING + ui.info "* " * 40 + end + + # We allow the user to specify the desired architecture of Chef to install or we default + # to whatever the target system is. We assume that we are only bootstrapping 1 node at a time + # so we don't need to worry about multipe responses from this command. + def set_target_architecture(bootstrap_architecture) + session_results = relay_winrm_command("echo %PROCESSOR_ARCHITECTURE%") + if session_results.empty? || session_results[0].stdout.strip.empty? + raise "Response to 'echo %PROCESSOR_ARCHITECTURE%' command was invalid: #{session_results}" + end + current_architecture = session_results[0].stdout.strip == "X86" ? :i386 : :x86_64 + + if bootstrap_architecture.nil? + architecture = current_architecture + elsif bootstrap_architecture == :x86_64 && current_architecture == :i386 + raise "You specified bootstrap_architecture as x86_64 but the target machine is i386. A 64 bit program cannot run on a 32 bit machine." + else + architecture = bootstrap_architecture + end + + # The windows install script wants i686, not i386 + architecture = :i686 if architecture == :i386 + Chef::Config[:knife][:architecture] = architecture + end + end + end +end