summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
authorMarc A. Paradise <marc.paradise@gmail.com>2019-02-20 10:55:01 -0500
committerMarc A. Paradise <marc.paradise@gmail.com>2019-04-24 13:25:58 -0400
commitdf96e7420dd1c258c794d2181d911add3eea8c47 (patch)
treee09cf59c7f3f5b3291846122a18e9c4f08e5f2f0 /lib/chef
parentf492fe53eac1b74a0d184f0e9cf7412b70770e29 (diff)
downloadchef-df96e7420dd1c258c794d2181d911add3eea8c47.tar.gz
Bootstrap via chef_core
Make bootstrap use train via chef_core/TargetHost This commit implements usage of TargetHost instead of knife_ssh. TargetHost is a platform-independent representation of a Train connection. It abstracts common operations (such as file upload/download, permissions, temp directories, command executation, etc) and connection error handling. Moving to TargetHost and train gives us the ability to execute commands on the bootstrap target; instead of running sh -c 'long-command-string-containing-secrets', we'll now upload the bootstrap script to a temporary directory on the bootstrap target and execute it there. Incorporating WinRM support directly (allowing core support for Windows bootstraps, without the knife-windows plugin) will follow. THis also improves unit test coverage of the bootstrap module, and clarifies functional versus behavior tests in that space Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
Diffstat (limited to 'lib/chef')
-rw-r--r--lib/chef/knife/bootstrap.rb727
-rw-r--r--lib/chef/knife/bootstrap/options.rb358
-rw-r--r--lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb271
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb2
-rw-r--r--lib/chef/knife/core/ui.rb7
-rw-r--r--lib/chef/knife/core/windows_bootstrap_context.rb412
6 files changed, 1490 insertions, 287 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index b9e09a15ba..aa5d1f502a 100644
--- a/lib/chef/knife/bootstrap.rb
+++ b/lib/chef/knife/bootstrap.rb
@@ -1,6 +1,6 @@
#
# Author:: Adam Jacob (<adam@chef.io>)
-# Copyright:: Copyright 2010-2016, Chef Software Inc.
+# Copyright:: Copyright 2010-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,229 +22,31 @@ require "erubis"
require "chef/knife/bootstrap/chef_vault_handler"
require "chef/knife/bootstrap/client_builder"
require "chef/util/path_helper"
+require "chef/knife/bootstrap/options"
class Chef
class Knife
class Bootstrap < Knife
include DataBagSecretOptions
+ # Command line flags and options for bootstrap - there's a large number of them
+ # so we'll keep this file a little smaller by splitting them out.
+ include Bootstrap::Options
+
+ SUPPORTED_CONNECTION_PROTOCOLS = %w{ssh winrm}
attr_accessor :client_builder
attr_accessor :chef_vault_handler
+ attr_reader :target_host
deps do
- require "chef/knife/core/bootstrap_context"
require "chef/json_compat"
require "tempfile"
- require "highline"
- require "net/ssh"
- require "net/ssh/multi"
- require "chef/knife/ssh"
- Chef::Knife::Ssh.load_deps
- end
-
- banner "knife bootstrap [SSH_USER@]FQDN (options)"
-
- option :ssh_user,
- short: "-x USERNAME",
- long: "--ssh-user USERNAME",
- description: "The ssh username",
- default: "root"
-
- option :ssh_password,
- short: "-P PASSWORD",
- long: "--ssh-password PASSWORD",
- description: "The ssh password"
-
- option :ssh_port,
- short: "-p PORT",
- long: "--ssh-port PORT",
- description: "The ssh port",
- proc: Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
-
- option :ssh_gateway,
- short: "-G GATEWAY",
- long: "--ssh-gateway GATEWAY",
- description: "The ssh gateway",
- proc: Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
-
- option :ssh_gateway_identity,
- long: "--ssh-gateway-identity SSH_GATEWAY_IDENTITY",
- description: "The SSH identity file used for gateway authentication",
- proc: Proc.new { |key| Chef::Config[:knife][:ssh_gateway_identity] = key }
-
- option :forward_agent,
- short: "-A",
- long: "--forward-agent",
- description: "Enable SSH agent forwarding",
- boolean: true
-
- option :ssh_identity_file,
- short: "-i IDENTITY_FILE",
- long: "--ssh-identity-file IDENTITY_FILE",
- description: "The SSH identity file used for authentication"
-
- 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: lambda { |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_proxy_user,
- long: "--bootstrap-proxy-user PROXY_USER",
- description: "The proxy authentication username for the node being bootstrapped"
-
- option :bootstrap_proxy_pass,
- long: "--bootstrap-proxy-pass PROXY_PASS",
- description: "The proxy authentication password for the node being bootstrapped"
-
- 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_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."
-
- option :use_sudo,
- long: "--sudo",
- description: "Execute the bootstrap via sudo",
- boolean: true
-
- option :preserve_home,
- long: "--sudo-preserve-home",
- description: "Preserve non-root user HOME environment variable with sudo",
- boolean: true
-
- option :use_sudo_password,
- long: "--use-sudo-password",
- description: "Execute the bootstrap via sudo with password",
- boolean: false
-
- 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(/[\s,]+/) },
- default: []
-
- 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: []
-
- 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| Chef::JSONCompat.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
-
- option :host_key_verify,
- long: "--[no-]host-key-verify",
- description: "Verify host key, enabled by default.",
- boolean: true,
- default: true
-
- 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 :bootstrap_url,
- long: "--bootstrap-url URL",
- description: "URL to a custom installation script",
- proc: Proc.new { |u| Chef::Config[:knife][:bootstrap_url] = u }
-
- 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 }
-
- option :bootstrap_preinstall_command,
- long: "--bootstrap-preinstall-command COMMANDS",
- description: "Custom commands to run before installing chef-client",
- proc: Proc.new { |preic| Chef::Config[:knife][:bootstrap_preinstall_command] = preic }
-
- option :bootstrap_wget_options,
- long: "--bootstrap-wget-options OPTIONS",
- description: "Add options to wget when installing chef-client",
- proc: Proc.new { |wo| Chef::Config[:knife][:bootstrap_wget_options] = wo }
-
- option :bootstrap_curl_options,
- long: "--bootstrap-curl-options OPTIONS",
- description: "Add options to curl when install chef-client",
- proc: Proc.new { |co| Chef::Config[:knife][:bootstrap_curl_options] = co }
-
- 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 = %w{none peer}
- unless valid_values.include?(v)
- raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}"
- end
- v
- }
+ require "chef_core/text" # i18n and standardized error structures
+ require "chef_core/target_host"
+ require "chef_core/target_resolver"
+ 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 :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]
- }
+ banner "knife bootstrap [PROTOCOL://][USER@]FQDN (options)"
def initialize(argv = [])
super
@@ -259,12 +61,16 @@ class Chef
)
end
- # The default bootstrap template to use to bootstrap a server This is a public API hook
- # which knife plugins use or inherit and override.
+ # The default bootstrap template to use to bootstrap a server.
+ # This is a public API hook which knife plugins use or inherit and override.
#
# @return [String] Default bootstrap template
def default_bootstrap_template
- "chef-full"
+ if target_host.base_os == :windows
+ "windows-chef-client-msi"
+ else
+ "chef-full"
+ end
end
def host_descriptor
@@ -282,15 +88,10 @@ class Chef
end
end
- def user_name
- if host_descriptor
- @user_name ||= host_descriptor.split("@").reverse[1]
- end
- end
+ # @return [String] The CLI specific bootstrap template or the default
def bootstrap_template
# Allow passing a bootstrap template or use the default
- # @return [String] The CLI specific bootstrap template or the default
config[:bootstrap_template] || default_bootstrap_template
end
@@ -330,13 +131,18 @@ class Chef
@secret ||= encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
end
+ # Establish bootstrap context for template rendering.
+ # Requires target_host to be a live connection in order to determine
+ # the correct platform.
def bootstrap_context
- @bootstrap_context ||= Knife::Core::BootstrapContext.new(
- config,
- config[:run_list],
- Chef::Config,
- secret
- )
+ @bootstrap_context ||=
+ if target_host.base_os == :windows
+ require "chef/knife/core/windows_bootstrap_context"
+ Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config, secret)
+ else
+ require "chef/knife/core/bootstrap_context"
+ Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret)
+ end
end
def first_boot_attributes
@@ -351,48 +157,153 @@ class Chef
end
def run
- if @config[:first_boot_attributes] && @config[:first_boot_attributes_from_file]
- raise Chef::Exceptions::BootstrapCommandInputError
- end
-
validate_name_args!
- validate_options!
+ validate_protocol!
+ validate_first_boot_attributes!
+ validate_winrm_transport_opts!
+ validate_policy_options!
+
+ winrm_warn_no_ssl_verification
$stdout.sync = true
+ register_client
+ connect!
+ unless client_builder.client_path.nil?
+ bootstrap_context.client_pem = client_builder.client_path
+ end
+ content = render_template
+ bootstrap_path = upload_bootstrap(content)
+ perform_bootstrap(bootstrap_path)
+ ensure
+ target_host.del_file(bootstrap_path) if target_host && bootstrap_path
+ end
+ def register_client
# chef-vault integration must use the new client-side hawtness, otherwise to use the
# new client-side hawtness, just delete your validation key.
+ # 2019-04-01 TODO
+ # TODO - should this raise if config says to use vault because json/file/item exists
+ # but we still have a validation key? That means we can't use the new client hawtness,
+ # but we also don't tell the operator that their requested vault operations
+ # won't be performed
if chef_vault_handler.doing_chef_vault? ||
- (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
+ (Chef::Config[:validation_key] &&
+ !File.exist?(File.expand_path(Chef::Config[:validation_key])))
unless config[:chef_node_name]
ui.error("You must pass a node name with -N when bootstrapping with user credentials")
exit 1
end
-
client_builder.run
-
chef_vault_handler.run(client_builder.client)
-
- 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("")
+ ui.info <<~EOM
+ Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}..."
+ Delete your validation key in order to use your user credentials instead
+ EOM
+
end
+ end
- ui.info("Connecting to #{ui.color(server_name, :bold)}")
- begin
- knife_ssh.run
- rescue Net::SSH::AuthenticationFailed
- if config[:ssh_password]
+ def perform_bootstrap(remote_bootstrap_script_path)
+ ui.info("Bootstrapping #{ui.color(server_name, :bold)}")
+ cmd = bootstrap_command(remote_bootstrap_script_path)
+ r = target_host.run_command(cmd) do |data|
+ ui.msg("#{ui.color(" [#{target_host.hostname}]", :cyan)} #{data}")
+ end
+ if r.exit_status != 0
+ ui.error("The following error occurred on #{server_name}:")
+ ui.error(r.stderr)
+ exit 1
+ end
+ end
+
+ def connect!
+
+ ui.info("Connecting to #{ui.color(server_name, :bold)}")
+ opts = connection_opts.dup
+ do_connect(opts)
+ rescue => e
+ # Ugh. TODO: Train raises a Train::Transports::SSHFailed for a number of different errors. chef_core makes that
+ # a more general ConnectionFailed, with an error code based on the specific error text/reason provided from trainm.
+ # This means we have to look three layers into the exception to find out what actually happened instead of just
+ # looking at the exception type
+ #
+ # It doesn't help to provide our own error if it does't let the caller know what they need to identify the problem.
+ # Let's update chef_core to be a bit smarter about resolving the errors to an appropriate exception type
+ # (eg ChefCore::ConnectionFailed::AuthError or similar) that will work across protocols, instead of just a single
+ # ConnectionFailure type
+ #
+
+ if e.cause && e.cause.cause && e.cause.cause.class == Net::SSH::AuthenticationFailed
+ if opts[:password]
raise
else
- ui.info("Failed to authenticate #{knife_ssh.config[:ssh_user]} - trying password auth")
- knife_ssh_with_password_auth.run
+ ui.warn("Failed to authenticate #{opts[:user]} to #{server_name} - trying password auth")
+ password = ui.ask("Enter password for #{opts[:user]}@#{server_name} - trying password auth") do |q|
+ q.echo = false
+ end
+ end
+ opts.merge! force_ssh_password_opts(password)
+ do_connect(opts)
+ else
+ raise
+ end
+ end
+
+ def connection_protocol
+ return @connection_protocol if @connection_protocol
+ from_url = host_descriptor =~ /^(.*):\/\// ? $1 : nil
+ from_cli = config[:connection_protocol]
+ from_knife = Chef::Config[:knife][:connection_protocol]
+ @connection_protocol = from_url || from_cli || from_knife || "ssh"
+ end
+
+ def do_connect(conn_options)
+ # Resolve the given host name to a TargetHost instance. We will limit
+ # the number of hosts to 1 (effectivly eliminating wildcard support) since
+ # we only support running bootstrap against one host at a time.
+ resolver = ChefCore::TargetResolver.new(host_descriptor, connection_protocol,
+ conn_options, max_expanded_targets: 1)
+ @target_host = resolver.targets.first
+ target_host.connect!
+ target_host
+ end
+
+ # Fail if both first_boot_attributes and first_boot_attributes_from_file
+ # are set.
+ def validate_first_boot_attributes!
+ if @config[:first_boot_attributes] && @config[:first_boot_attributes_from_file]
+ raise Chef::Exceptions::BootstrapCommandInputError
+ end
+ true
+ end
+
+ # Fail if using plaintext auth without ssl because
+ # this can expose keys in plaintext on the wire.
+ # TODO test for this method
+ # TODO check that the protoocol is valid.
+ def validate_winrm_transport_opts!
+ return true if connection_protocol != "winrm"
+
+
+ if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
+ if (config_value(:winrm_auth_method) == "plaintext" &&
+ config_value(:winrm_ssl) != true)
+ ui.error <<~EOM
+ Validatorless bootstrap over unsecure winrm channels could expose your
+ key to network sniffing.
+
+ Please use a 'winrm_auth_method' other than 'plaintext',
+ or enable ssl on #{server_name} then use the --ssl flag
+ to connect.
+ EOM
+
+ exit 1
end
end
+ true
end
# fail if the server_name is nil
@@ -400,9 +311,6 @@ class Chef
if server_name.nil?
ui.error("Must pass an FQDN or ip to bootstrap")
exit 1
- elsif server_name == "windows"
- # catches "knife bootstrap windows" when that command is not installed
- ui.warn("'knife bootstrap windows' specified, but the knife-windows plugin is not installed. Please install 'knife-windows' if you are attempting to bootstrap a Windows node via WinRM.")
end
end
@@ -413,7 +321,7 @@ class Chef
# * Policyfile options are set and --run-list is set as well
#
# @return [TrueClass] If options are valid.
- def validate_options!
+ def validate_policy_options!
if incomplete_policyfile_options?
ui.error("--policy-name and --policy-group must be specified together")
exit 1
@@ -421,51 +329,299 @@ class Chef
ui.error("Policyfile options and --run-list are exclusive")
exit 1
end
- true
end
- # setup a Chef::Knife::Ssh object using the passed config options
+ # Ensure a valid protocol is provided for target host connection
#
- # @return Chef::Knife::Ssh
- def knife_ssh
- ssh = Chef::Knife::Ssh.new
- ssh.ui = ui
- ssh.name_args = [ server_name, ssh_command ]
- ssh.config[:ssh_user] = user_name || config[:ssh_user]
- ssh.config[:ssh_password] = config[:ssh_password]
- ssh.config[:ssh_port] = config[:ssh_port]
- ssh.config[:ssh_gateway] = config[:ssh_gateway]
- ssh.config[:ssh_gateway_identity] = config[:ssh_gateway_identity]
- ssh.config[:forward_agent] = config[:forward_agent]
- ssh.config[:ssh_identity_file] = config[:ssh_identity_file]
- ssh.config[:manual] = true
- ssh.config[:host_key_verify] = config[:host_key_verify]
- ssh.config[:on_error] = true
- ssh
- end
-
- # prompt for a password then return a knife ssh object with that password set
- # and with ssh_identity_file set to nil
+ # The method call will cause the program to exit(1) if:
+ # * Conflicting protocols are given via the target URI and the --protocol option
+ # * The protocol is not a supported protocol
+ #
+ # @return [TrueClass] If options are valid.
+ def validate_protocol!
+ from_cli = config[:connection_protocol]
+ if (from_cli && connection_protocol != from_cli)
+ # Hanging indent to align with the ERROR: prefix
+ ui.error <<~EOM
+ The URL '#{host_descriptor}' indicates protocol is '#{connection_protocol}'
+ while the --protocol flag specifies '#{from_cli}'. Please include
+ only one or the other.
+ EOM
+ exit 1
+ end
+
+ unless SUPPORTED_CONNECTION_PROTOCOLS.include?(connection_protocol)
+ ui.error <<~EOM
+ Unsupported protocol '#{connection_protocol}'.
+
+ Supported protocols are: #{SUPPORTED_CONNECTION_PROTOCOLS.join(" ")}
+ EOM
+ exit 1
+ end
+ true
+ end
+
+ def winrm_warn_no_ssl_verification
+ return if connection_protocol != "winrm"
+
+ # REVIEWER NOTE
+ # The original check from knife plugin did not include winrm_ssl_peer_fingerprint
+ # Reference:
+ # https://github.com/chef/knife-windows/blob/92d151298142be4a4750c5b54bb264f8d5b81b8a/lib/chef/knife/winrm_knife_base.rb#L271-L273
+ # TODO Seems like we should also do a similar warning if ssh_verify_host == false
+ if config_value(:ca_trust_file).nil? &&
+ config_value(:winrm_no_verify_cert) &&
+ config_value(:winrm_ssl_peer_fingerprint).nil?
+ ui.warn <<~WARN
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ SSL validation of HTTPS requests for the WinRM transport is disabled.
+ HTTPS WinRM # connections are still encrypted, but knife is not able
+ to detect forged replies # or spoofing attacks.
+
+ To fix this issue add an entry like this to your knife configuration file:
+
+ # Verify all WinRM HTTPS connections (default, recommended)
+ knife[:winrm_no_verify_cert] = false
+
+ You can also specify a ca_trust_file via --ca-trust-file,
+ or the expected fingerprint of the target host via
+ --winrm-ssl-peer-fingerprint.
+ WARN
+ end
+ end
+
+ #
+
+ # Create a configuration hash for TargetHost to connect
+ # to the remote host via Train.
#
- # @return Chef::Knife::Ssh
- def knife_ssh_with_password_auth
- ssh = knife_ssh
- ssh.config[:ssh_identity_file] = nil
- ssh.config[:ssh_password] = ssh.get_password
- ssh
+ # @return a configuration hash suitable for connecting to the remote
+ # host via TargetHost.
+ def connection_opts
+ return @connection_opts unless @connection_opts.nil?
+ @connection_opts = {}
+ @connection_opts.merge! base_opts
+ @connection_opts.merge! host_verify_opts
+ @connection_opts.merge! gateway_opts
+ @connection_opts.merge! sudo_opts
+ @connection_opts.merge! winrm_opts
+ @connection_opts.merge! ssh_opts
+ @connection_opts.merge! ssh_identity_opts
+ @connection_opts
end
- # build the ssh dommand for bootrapping
- # @return String
- def ssh_command
- command = render_template
+ # Common configuration for all protocols
+ def base_opts
+ #
+ port = config_value(:connection_port,
+ knife_key_for_protocol(connection_protocol, :port))
+ user = config_value(:connection_user,
+ knife_key_for_protocol(connection_protocol, :user))
+ {}.tap do |opts|
+ opts[:logger] = Chef::Log
+ # We do not store password in Chef::Config, so only use CLI `config` here
+ opts[:password] = config[:password] if config.key?(:password)
+ opts[:user] = user if user
+ opts[:max_wait_until_ready] = config_value(:max_wait) unless config_value(:max_wait).nil?
+ # TODO - when would we need to provide rdp_port vs port? Or are they not mutually exclusive?
+ opts[:port] = port if port
+ end
+ end
+
+ def host_verify_opts
+ case connection_protocol
+ when "winrm"
+ { self_signed: config_value(:winrm_no_verify_cert) === true }
+ when "ssh"
+ # TODO is this a safe footgun to provide? Seems a security risk
+ # if someone forgets to If someone forgets ssh_verify_host_key is
+ # is in knife config, s- setting ssh_verify_host_key to true
+ # in knife.rb, and forgetting it's there?
+ { verify_host_key: config_value(:ssh_verify_host_key,
+ :host_key_verify, true) === true }
+ else
+ {}
+ end
+ end
+
+ def ssh_opts
+ opts = {}
+ return opts if connection_protocol == "winrm"
+ opts[:forward_agent] = (config_value(:ssh_forward_agent) === true)
+ opts
+ end
+ def ssh_identity_opts
+ opts = {}
+ return opts if connection_protocol == "winrm"
+ identity_file = config_value(:ssh_identity_file)
+ if identity_file
+ opts[:key_files] = [identity_file]
+ # We only set keys_only based on the explicit ssh_identity_file;
+ # someone may use a gateway key and still expect password auth
+ # on the target. Similarly, someone may have a default key specified
+ # in knife config, but have provided a password on the CLI.
+
+ # REVIEW NOTE: this is a new behavior. Originally, ssh_identity_file
+ # could only be populated from CLI options, so there was no need to check
+ # for this. We will also set keys_only to false only if there are keys
+ # and no password.
+ # If both are present, train(via net/ssh) will prefer keys, falling back to password.
+ # Reference: https://github.com/chef/chef/blob/master/lib/chef/knife/ssh.rb#L272
+ opts[:keys_only] = config.key?(:password) == false
+ else
+ opts[:key_files] = []
+ opts[:keys_only] = false
+ end
+
+ gateway_identity_file = config_value(:ssh_gateway) ? config_value(:ssh_gateway_identity) : nil
+ unless gateway_identity_file.nil?
+ opts[:key_files] << gateway_identity_file
+ end
+
+ opts
+ end
+
+ def gateway_opts
+ opts = {}
+ if config_value(:ssh_gateway)
+ split = config_value(:ssh_gateway).split("@", 2)
+ if split.length == 1
+ gw_host = split[0]
+ else
+ gw_user = split[0]
+ gw_host = split[1]
+ end
+ gw_host, gw_port = gw_host.split(":", 2)
+ # TODO - validate convertable port in config validation?
+ gw_port = Integer(gw_port) rescue nil
+ opts[:bastion_host] = gw_host
+ opts[:bastion_user] = gw_user
+ opts[:bastion_port] = gw_port
+ end
+ opts
+ end
+
+
+ # use_sudo - tells bootstrap to use the sudo command to run bootstrap
+ # use_sudo_password - tells bootstrap to use the sudo command to run bootstrap
+ # and to use the password specified with --password
+ # TODO: I'd like to make our sudo options sane:
+ # --sudo (bool) - use sudo
+ # --sudo-password PASSWORD (default: :password) - use this password for sudo
+ # --sudo-options "opt,opt,opt" to pass into sudo
+ # --sudo-command COMMAND sudo command other than sudo
+ # REVIEW NOTE: knife bootstrap did not pull sudo values from Chef::Config,
+ # should we change that for consistency?
+ def sudo_opts
+ return {} if connection_protocol == "winrm"
+ opts = { sudo: false }
if config[:use_sudo]
- sudo_prefix = config[:use_sudo_password] ? "echo '#{config[:ssh_password]}' | sudo -S " : "sudo "
- command = config[:preserve_home] ? "#{sudo_prefix} #{command}" : "#{sudo_prefix} -H #{command}"
+ opts[:sudo] = true
+ if config[:use_sudo_password]
+ opts[:sudo_password] = config[:password]
+ end
+ if config[:preserve_home]
+ opts[:sudo_options] = "-H"
+ end
+ end
+ opts
+ end
+
+ def winrm_opts
+ return {} unless connection_protocol == "winrm"
+ auth_method = config_value(:winrm_auth_method, :winrm_auth_method, "negotiate")
+ opts = {
+ winrm_transport: auth_method, # winrm gem and train calls auth method 'transport'
+ winrm_basic_auth_only: config_value(:winrm_basic_auth_only) || false,
+ ssl: config_value(:winrm_ssl) === true,
+ ssl_peer_fingerprint: config_value(:winrm_ssl_peer_fingerprint)
+ }
+
+ if auth_method == "kerberos"
+ opts[:kerberos_service] = config_value(:kerberos_service) if config_value(:kerberos_service)
+ opts[:kerberos_realm] = config_value(:kerberos_realm) if config_value(:kerberos_service)
+ end
+
+ if config_value(:ca_trust_file)
+ opts[:ca_trust_file] = config_value(:ca_trust_file)
+ end
+
+ opts[:operation_timeout] = config_value(:winrm_session_timeout) || 60
+
+ opts
+ end
+
+
+ # Config overrides to force password auth.
+ def force_ssh_password_opts(password)
+ {
+ password: password,
+ non_interactive: false,
+ keys_only: false,
+ key_files: [],
+ auth_methods: [:password, :keyboard_interactive]
+ }
+ end
+
+ # Looks up configuration entries, first in the class member
+ # `config` which contains options populated from CLI flags.
+ # If the entry is not found there, Chef::Config[:knife][KEY]
+ # is checked.
+ #
+ # knife_config_key should be specified if the knife config lookup
+ # key is different from the CLI flag lookup key.
+ #
+ def config_value(key, knife_config_key = nil, default = nil)
+ if config.key? key
+ config[key]
+ else
+ lookup_key = knife_config_key || key
+ if Chef::Config[:knife].key?(lookup_key)
+ Chef::Config[:knife][lookup_key]
+ else
+ default
+ end
end
+ end
- command
+ # Tells us where a config value has come from ,
+ # :cli_config, :knife_config, :not_found
+ def config_source(key, knife_config_key = nil)
+ return :cli_config if config.key? key
+ return :knife_config if config.key?(key) || config.key?(knife_config_key)
+ :not_found
+ end
+
+
+ def upload_bootstrap(content)
+ script_name = target_host.base_os == :windows ? "bootstrap.bat" : "bootstrap.sh"
+ remote_path = target_host.normalize_path(File.join(target_host.temp_dir, script_name))
+ target_host.save_as_remote_file(content, remote_path)
+ remote_path
+ end
+
+
+ # build the command string for bootrapping
+ # @return String
+ def bootstrap_command(remote_path)
+ if target_host.base_os == :windows
+ "cmd.exe /C #{remote_path}"
+ else
+ "sh #{remote_path}"
+ end
+ end
+
+
+ # To avoid cluttering the CLI options, some flags (such as port and user)
+ # are shared between protocols. However, there is still a need to allow the operator
+ # to specify defaults separately, since they may not be the same values for different protocols.
+ #
+ # These keys are available in Chef::Config, and are prefixed with the protocol name.
+ # For example, :user CLI option will map to :winrm_user and :ssh_user Chef::Config keys,
+ # based on the connection protocol in use.
+ def knife_key_for_protocol(protocol, option)
+ "#{connection_protocol}_#{option.to_s}".to_sym
end
private
@@ -487,7 +643,6 @@ class Chef
def incomplete_policyfile_options?
(!!config[:policy_name] ^ config[:policy_group])
end
-
end
end
end
diff --git a/lib/chef/knife/bootstrap/options.rb b/lib/chef/knife/bootstrap/options.rb
new file mode 100644
index 0000000000..350aef0959
--- /dev/null
+++ b/lib/chef/knife/bootstrap/options.rb
@@ -0,0 +1,358 @@
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright 2019, 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.
+#
+
+class Chef
+ class Knife
+ class Bootstrap
+ module Options
+ # REVIEWER: let's talk about which protocols we want to support.
+ # TODO this should be available from train, which we can make our source of truth.
+ # TODO - we don't actually validate that the protocol is valid...
+ WINRM_AUTH_PROTOCOL_LIST = %w{plaintext kerberos ssl negotiate}
+
+ #TODO - missing - authtimeout (minutes)
+ #TODO - missing impl - session-timeout minutes
+ def self.included(includer)
+ includer.class_eval do
+
+ # Common connectivity options
+ option :connection_user,
+ short: "-U USERNAME",
+ long: "--connection-user USERNAME",
+ description: "Authenticate to the target host with this user account"
+
+ option :password,
+ short: "-P PASSWORD",
+ long: "--connection-password PASSWORD",
+ description: "Authenticate to the target host with this password"
+
+ option :connection_port,
+ short: "-p PORT",
+ long: "--connection-port PORT",
+ description: "The port on the target node to connect to."
+
+ option :connection_protocol,
+ short: "-o PROTOCOL",
+ long: "--connection-protocol PROTOCOL",
+ description: "The protocol to use to connect to the target node. Supports ssh and winrm."
+
+ option :max_wait,
+ short: "-W SECONDS",
+ long: "--max-wait SECONDS",
+ description: "The maximum time to wait for the initial connection to be established."
+
+ ## SSH options
+
+ option :ssh_gateway,
+ short: "-G GATEWAY",
+ long: "--ssh-gateway GATEWAY",
+ description: "The ssh gateway",
+ proc: Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
+
+ option :ssh_gateway_identity,
+ long: "--ssh-gateway-identity SSH_GATEWAY_IDENTITY",
+ description: "The SSH identity file used for gateway authentication",
+ proc: Proc.new { |key| Chef::Config[:knife][:ssh_gateway_identity] = key }
+
+ # SSH train ssh: options[:forward_agent]
+ option :ssh_forward_agent,
+ short: "-A",
+ long: "--ssh-forward-agent",
+ description: "Enable SSH agent forwarding",
+ boolean: true
+
+ # SSH train: options[key_files]
+ option :ssh_identity_file,
+ short: "-i IDENTITY_FILE",
+ long: "--ssh-identity-file IDENTITY_FILE",
+ description: "The SSH identity file used for authentication"
+
+ # ssh options - train options[:verify_host_key]
+ option :ssh_verify_host_key,
+ long: "--ssh-[no-]verify-host-key",
+ description: "Verify host key, enabled by default.",
+ boolean: true
+
+ # argument to installer in chef-full, via bootstrap_context
+ option :prerelease,
+ long: "--prerelease",
+ description: "Install the pre-release chef gems"
+
+ # client.rb content via chef-full/bootstrap_context
+ option :bootstrap_version,
+ long: "--bootstrap-version VERSION",
+ description: "The version of Chef to install",
+ proc: lambda { |v| Chef::Config[:knife][:bootstrap_version] = v }
+
+ # client.rb content via chef-full/bootstrap_context
+ 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 }
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_proxy_user,
+ long: "--bootstrap-proxy-user PROXY_USER",
+ description: "The proxy authentication username for the node being bootstrapped"
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_proxy_pass,
+ long: "--bootstrap-proxy-pass PROXY_PASS",
+ description: "The proxy authentication password for the node being bootstrapped"
+
+ # client.rb content via bootstrap_context
+ 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 Chef",
+ proc: Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np }
+
+ # client.rb content via bootstrap_context
+ 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."
+
+ # client.rb content via bootstrap_context
+ 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 = %w{none peer}
+ unless valid_values.include?(v)
+ raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}"
+ end
+ v
+ }
+
+ # bootstrap_context - client.rb
+ 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
+
+ # runtime - sudo settings (train handles sudo)
+ option :use_sudo,
+ long: "--sudo",
+ description: "Execute the bootstrap via sudo",
+ boolean: true
+
+ # runtime - sudo settings (train handles sudo)
+ option :preserve_home,
+ long: "--sudo-preserve-home",
+ description: "Preserve non-root user HOME environment variable with sudo",
+ boolean: true
+
+ # runtime - sudo settings (train handles sudo)
+ option :use_sudo_password,
+ long: "--use-sudo-password",
+ description: "Execute the bootstrap via sudo with password",
+ boolean: false
+
+ # runtime - client_builder
+ option :chef_node_name,
+ short: "-N NAME",
+ long: "--node-name NAME",
+ description: "The Chef node name for your new node"
+
+ # runtime - client_builder - set runlist when creating node
+ 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(/[\s,]+/) },
+ default: []
+
+ # runtime - client_builder - set policy name when creating node
+ option :policy_name,
+ long: "--policy-name POLICY_NAME",
+ description: "Policyfile name to use (--policy-group must also be given)",
+ default: nil
+
+ # runtime - client_builder - set policy group when creating node
+ option :policy_group,
+ long: "--policy-group POLICY_GROUP",
+ description: "Policy group name to use (--policy-name must also be given)",
+ default: nil
+
+ # runtime - client_builder - node tags
+ option :tags,
+ long: "--tags TAGS",
+ description: "Comma separated list of tags to apply to the node",
+ proc: lambda { |o| o.split(/[\s,]+/) },
+ default: []
+
+ # bootstrap template
+ 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| Chef::JSONCompat.parse(o) },
+ default: nil
+
+ # bootstrap template
+ 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
+
+ # Note that several of the below options are used by bootstrap template,
+ # but only from the passed-in knife config; it does not use the
+ # config from the CLI for those values. In those cases, the option
+ # will have a proc that assigns the value into Chef::Config[:knife]
+
+ # bootstrap template
+ # Create ohai hints in /etc/chef/ohai/hints, fname=hintname, content=value
+ 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
+ }
+
+ # bootstrap override: url of a an installer shell script touse in place of omnitruck
+ # Note that the bootstrap template _only_ references this out of Chef::Config, and not from
+ # the provided options to knife bootstrap, so we set the Chef::Config option here.
+ option :bootstrap_url,
+ long: "--bootstrap-url URL",
+ description: "URL to a custom installation script",
+ proc: Proc.new { |u| Chef::Config[:knife][:bootstrap_url] = u }
+
+
+ # bootstrap override: Do this instead of our own setup.sh from omnitruck. Causes bootstrap_url to be ignored.
+ 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 }
+
+ # bootstrap template: Run this command first in the bootstrap script
+ option :bootstrap_preinstall_command,
+ long: "--bootstrap-preinstall-command COMMANDS",
+ description: "Custom commands to run before installing chef-client",
+ proc: Proc.new { |preic| Chef::Config[:knife][:bootstrap_preinstall_command] = preic }
+
+ # bootstrap template
+ option :bootstrap_wget_options,
+ long: "--bootstrap-wget-options OPTIONS",
+ description: "Add options to wget when installing chef-client",
+ proc: Proc.new { |wo| Chef::Config[:knife][:bootstrap_wget_options] = wo }
+
+ # bootstrap template
+ option :bootstrap_curl_options,
+ long: "--bootstrap-curl-options OPTIONS",
+ description: "Add options to curl when install chef-client",
+ proc: Proc.new { |co| Chef::Config[:knife][:bootstrap_curl_options] = co }
+
+ # chef_vault_handler
+ 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"
+
+ # chef_vault_handler
+ option :bootstrap_vault_json,
+ long: "--bootstrap-vault-json VAULT_JSON",
+ description: "A JSON string with the vault(s) and item(s) to be updated"
+
+ # chef_vault_handler
+ 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]
+ }
+
+ # Windows only
+
+ # bootstrap template
+ option :install_as_service,
+ :long => "--install-as-service",
+ :description => "Install chef-client as a Windows service. (Windows only)",
+ :default => false
+
+ # bootstrap template
+ option :msi_url,
+ :short => "-m 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 (windows).",
+ :default => ''
+
+ option :winrm_ssl_peer_fingerprint,
+ :long => "--winrm-ssl-peer-fingerprint FINGERPRINT",
+ :description => "SSL certificate fingerprint expected from the target."
+
+ option :ca_trust_file,
+ :short => "-f CA_TRUST_PATH",
+ :long => "--ca-trust-file CA_TRUST_PATH",
+ :description => "The Certificate Authority (CA) trust file used for SSL transport"
+
+ option :winrm_no_verify_cert,
+ long: "--winrm-no-verify-cert",
+ description: "Do not verify the SSL certificate of the target node for WinRM."
+
+
+ option :winrm_ssl,
+ long: "--winrm-ssl",
+ description: "Connect to WinRM using SSL"
+
+ option :winrm_auth_method,
+ :short => "-w AUTH-METHOD",
+ :long => "--winrm-auth-method AUTH-METHOD",
+ :description => "The WinRM authentication method to use. Valid choices are #{WINRM_AUTH_PROTOCOL_LIST}",
+ :proc => Proc.new { |protocol| Chef::Config[:knife][:winrm_auth_method] = protocol }
+
+ option :winrm_basic_auth_only,
+ long: "--winrm-basic-auth-only",
+ description: "For WinRM basic authentication when using the 'ssl' auth method",
+ boolean: true
+
+ # This option was provided in knife bootstrap windows winrm,
+ # but it is ignored in knife-windows/WinrmSession, and so remains unimplemeneted here.
+ # option :kerberos_keytab_file,
+ # :short => "-T KEYTAB_FILE",
+ # :long => "--keytab-file KEYTAB_FILE",
+ # :description => "The Kerberos keytab file used for authentication",
+ # :proc => Proc.new { |keytab| Chef::Config[:knife][:kerberos_keytab_file] = keytab }
+
+ option :kerberos_realm,
+ :short => "-R KERBEROS_REALM",
+ :long => "--kerberos-realm KERBEROS_REALM",
+ :description => "The Kerberos realm used for authentication",
+ :proc => Proc.new { |protocol| Chef::Config[:knife][:kerberos_realm] = protocol }
+
+ option :kerberos_service,
+ :short => "-S KERBEROS_SERVICE",
+ :long => "--kerberos-service KERBEROS_SERVICE",
+ :description => "The Kerberos service used for authentication",
+ :proc => Proc.new { |protocol| Chef::Config[:knife][:kerberos_service] = protocol }
+
+ option :winrm_session_timeout,
+ :long => "--winrm-session-timeout SECONDS",
+ :description => "The number of seconds to wait for each WinRM operation to be acknowledged while running bootstrap",
+ :proc => Proc.new { |protocol| Chef::Config[:knife][:winrm_session_timeout] = protocol }
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb b/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb
new file mode 100644
index 0000000000..c30a22bd94
--- /dev/null
+++ b/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb
@@ -0,0 +1,271 @@
+@rem
+@rem Author:: Seth Chisamore (<schisamo@chef.io>)
+@rem Copyright:: Copyright (c) 2011-2017 Chef Software, Inc.
+@rem License:: Apache License, Version 2.0
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem http://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@rem Use delayed environment expansion so that ERRORLEVEL can be evaluated with the
+@rem !ERRORLEVEL! syntax which evaluates at execution of the line of script, not when
+@rem the line is read. See help for the /E switch from cmd.exe /? .
+@setlocal ENABLEDELAYEDEXPANSION
+
+<%= "SETX HTTP_PROXY \"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] %>
+
+@set BOOTSTRAP_DIRECTORY=<%= bootstrap_directory %>
+@echo Checking for existing directory "%BOOTSTRAP_DIRECTORY%"...
+@if NOT EXIST %BOOTSTRAP_DIRECTORY% (
+ @echo Existing directory not found, creating.
+ @mkdir %BOOTSTRAP_DIRECTORY%
+) else (
+ @echo Existing directory found, skipping creation.
+)
+
+> <%= bootstrap_directory %>\wget.vbs (
+ <%= win_wget %>
+)
+
+> <%= bootstrap_directory %>\wget.ps1 (
+ <%= win_wget_ps %>
+)
+
+@rem Determine the version and the architecture
+
+@FOR /F "usebackq tokens=1-8 delims=.[] " %%A IN (`ver`) DO (
+@set WinMajor=%%D
+@set WinMinor=%%E
+@set WinBuild=%%F
+)
+
+@echo Detected Windows Version %WinMajor%.%WinMinor% Build %WinBuild%
+
+@set LATEST_OS_VERSION_MAJOR=10
+@set LATEST_OS_VERSION_MINOR=1
+
+@if /i %WinMajor% GTR %LATEST_OS_VERSION_MAJOR% goto VersionUnknown
+@if /i %WinMajor% EQU %LATEST_OS_VERSION_MAJOR% (
+ @if /i %WinMinor% GTR %LATEST_OS_VERSION_MINOR% goto VersionUnknown
+)
+
+goto Version%WinMajor%.%WinMinor%
+
+:VersionUnknown
+@rem If this is an unknown version of windows set the default
+@set MACHINE_OS=2012r2
+@echo Warning: Unknown version of Windows, assuming default of Windows %MACHINE_OS%
+goto architecture_select
+
+:Version6.0
+@set MACHINE_OS=2008
+goto architecture_select
+
+:Version5.2
+@set MACHINE_OS=2003r2
+goto architecture_select
+
+:Version6.1
+@set MACHINE_OS=2008r2
+goto architecture_select
+
+:Version6.2
+@set MACHINE_OS=2012
+goto architecture_select
+
+@rem Currently Windows Server 2012 R2 is treated as equivalent to Windows Server 2012
+:Version6.3
+@set MACHINE_OS=2012r2
+goto architecture_select
+
+:Version10.0
+@set MACHINE_OS=2016
+goto architecture_select
+
+@rem Currently Windows Server 2016 R2 is treated as equivalent to Windows Server 2016
+:Version10.1
+goto Version10.0
+
+:architecture_select
+<% if knife_config[:architecture] %>
+ @set MACHINE_ARCH=<%= knife_config[:architecture] %>
+
+ <% if knife_config[:architecture] == "x86_64" %>
+ IF "%PROCESSOR_ARCHITECTURE%"=="x86" IF not defined PROCESSOR_ARCHITEW6432 (
+ echo You specified bootstrap_architecture as x86_64 but the target machine is i386. A 64 bit program cannot run on a 32 bit machine. > "&2"
+ echo Exiting without bootstrapping. > "&2"
+ exit /b 1
+ )
+ <% end %>
+<% else %>
+ @set MACHINE_ARCH=x86_64
+ IF "%PROCESSOR_ARCHITECTURE%"=="x86" IF not defined PROCESSOR_ARCHITEW6432 @set MACHINE_ARCH=i686
+<% end %>
+goto chef_installed
+
+:chef_installed
+@echo Checking for existing chef installation
+WHERE chef-client >nul 2>nul
+If !ERRORLEVEL!==0 (
+ @echo Existing Chef installation detected, skipping download
+ goto key_create
+) else (
+ @echo No existing installation of chef detected
+ goto install
+)
+
+:install
+@rem If user has provided the custom installation command for chef-client then execute it
+<% if @chef_config[:knife][:bootstrap_install_command] %>
+ <%= @chef_config[:knife][:bootstrap_install_command] %>
+<% else %>
+ @rem Install Chef using chef-client MSI installer
+
+ @set "LOCAL_DESTINATION_MSI_PATH=<%= local_download_path %>"
+ @set "CHEF_CLIENT_MSI_LOG_PATH=%TEMP%\chef-client-msi%RANDOM%.log"
+
+ @rem Clear any pre-existing downloads
+ @echo Checking for existing downloaded package at "%LOCAL_DESTINATION_MSI_PATH%"
+ @if EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ @echo Found existing downloaded package, deleting.
+ @del /f /q "%LOCAL_DESTINATION_MSI_PATH%"
+ @if ERRORLEVEL 1 (
+ echo Warning: Failed to delete pre-existing package with status code !ERRORLEVEL! > "&2"
+ )
+ ) else (
+ echo No existing downloaded packages to delete.
+ )
+
+ @rem If there is somehow a name collision, remove pre-existing log
+ @if EXIST "%CHEF_CLIENT_MSI_LOG_PATH%" del /f /q "%CHEF_CLIENT_MSI_LOG_PATH%"
+
+ @echo Attempting to download client package using PowerShell if available...
+ @set "REMOTE_SOURCE_MSI_URL=<%= msi_url('%MACHINE_OS%', '%MACHINE_ARCH%', 'PowerShell') %>"
+ @set powershell_download=powershell.exe -ExecutionPolicy Unrestricted -InputFormat None -NoProfile -NonInteractive -File <%= bootstrap_directory %>\wget.ps1 "%REMOTE_SOURCE_MSI_URL%" "%LOCAL_DESTINATION_MSI_PATH%"
+ @echo !powershell_download!
+ @call !powershell_download!
+
+ @set DOWNLOAD_ERROR_STATUS=!ERRORLEVEL!
+
+ @if ERRORLEVEL 1 (
+ @echo Failed PowerShell download with status code !DOWNLOAD_ERROR_STATUS! > "&2"
+ @if !DOWNLOAD_ERROR_STATUS!==0 set DOWNLOAD_ERROR_STATUS=2
+ ) else (
+ @rem Sometimes the error level is not set even when the download failed,
+ @rem so check for the file to be sure it is there -- if it is not, we will retry
+ @if NOT EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ echo Failed download: download completed, but downloaded file not found > "&2"
+ set DOWNLOAD_ERROR_STATUS=2
+ ) else (
+ echo Download via PowerShell succeeded.
+ )
+ )
+
+ @if NOT %DOWNLOAD_ERROR_STATUS%==0 (
+ @echo Warning: Failed to download "%REMOTE_SOURCE_MSI_URL%" to "%LOCAL_DESTINATION_MSI_PATH%"
+ @echo Warning: Retrying download with cscript ...
+
+ @if EXIST "%LOCAL_DESTINATION_MSI_PATH%" del /f /q "%LOCAL_DESTINATION_MSI_PATH%"
+
+ @set "REMOTE_SOURCE_MSI_URL=<%= msi_url('%MACHINE_OS%', '%MACHINE_ARCH%') %>"
+ cscript /nologo <%= bootstrap_directory %>\wget.vbs /url:"%REMOTE_SOURCE_MSI_URL%" /path:"%LOCAL_DESTINATION_MSI_PATH%"
+
+ @if NOT ERRORLEVEL 1 (
+ @rem Sometimes the error level is not set even when the download failed,
+ @rem so check for the file to be sure it is there.
+ @if NOT EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ echo Failed download: download completed, but downloaded file not found > "&2"
+ echo Exiting without bootstrapping due to download failure. > "&2"
+ exit /b 1
+ ) else (
+ echo Download via cscript succeeded.
+ )
+ ) else (
+ echo Failed to download "%REMOTE_SOURCE_MSI_URL%" with status code !ERRORLEVEL!. > "&2"
+ echo Exiting without bootstrapping due to download failure. > "&2"
+ exit /b 1
+ )
+ )
+
+ @echo Installing downloaded client package...
+
+ <%= install_chef %>
+
+ @if ERRORLEVEL 1 (
+ echo Chef-client package failed to install with status code !ERRORLEVEL!. > "&2"
+ echo See installation log for additional detail: %CHEF_CLIENT_MSI_LOG_PATH%. > "&2"
+ ) else (
+ @echo Installation completed successfully
+ del /f /q "%CHEF_CLIENT_MSI_LOG_PATH%"
+ )
+
+<% end %>
+
+:key_create
+@endlocal
+
+@echo off
+
+<% if client_pem -%>
+> <%= bootstrap_directory %>\client.pem (
+ <%= escape_and_echo(::File.read(::File.expand_path(client_pem))) %>
+)
+<% end -%>
+
+echo Writing validation key...
+
+<% if validation_key -%>
+> <%= bootstrap_directory %>\validation.pem (
+ <%= escape_and_echo(validation_key) %>
+)
+<% end -%>
+
+echo Validation key written.
+@echo on
+
+<% if @config[:secret] -%>
+> <%= bootstrap_directory %>\encrypted_data_bag_secret (
+ <%= secret %>
+)
+<% end -%>
+
+<% unless trusted_certs_script.empty? -%>
+mkdir <%= bootstrap_directory %>\trusted_certs
+<%= trusted_certs_script %>
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir <%= bootstrap_directory %>\ohai\hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+> <%= bootstrap_directory %>\ohai\hints\<%= name %>.json (
+ <%= escape_and_echo(hash.to_json) %>
+)
+<% end -%>
+<% end -%>
+
+> <%= bootstrap_directory %>\client.rb (
+ <%= config_content %>
+)
+
+> <%= bootstrap_directory %>\first-boot.json (
+ <%= first_boot %>
+)
+
+<% unless client_d.empty? -%>
+ mkdir <%= bootstrap_directory %>\client.d
+ <%= client_d %>
+<% end -%>
+
+@echo Starting chef to bootstrap the node...
+<%= start_chef %>
diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb
index bd094e5021..287fe0a50f 100644
--- a/lib/chef/knife/core/bootstrap_context.rb
+++ b/lib/chef/knife/core/bootstrap_context.rb
@@ -161,7 +161,7 @@ class Chef
end
if Chef::Config[:fips]
- client_rb << <<-CONFIG.gsub(/^ {14}/, "")
+ client_rb << <<~CONFIG
fips true
require "chef/version"
chef_version = ::Chef::VERSION.split(".")
diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb
index 3f0a697107..c696378912 100644
--- a/lib/chef/knife/core/ui.rb
+++ b/lib/chef/knife/core/ui.rb
@@ -85,6 +85,13 @@ class Chef
alias :info :log
alias :err :log
+ # Print a Debug
+ #
+ # @param message [String] the text string
+ def debug(message)
+ log("#{color('DEBUG:', :blue, :bold)} #{message}")
+ end
+
# Print a warning message
#
# @param message [String] the text string
diff --git a/lib/chef/knife/core/windows_bootstrap_context.rb b/lib/chef/knife/core/windows_bootstrap_context.rb
new file mode 100644
index 0000000000..6db017ca2f
--- /dev/null
+++ b/lib/chef/knife/core/windows_bootstrap_context.rb
@@ -0,0 +1,412 @@
+#
+# 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/core/bootstrap_context'
+require 'chef/util/path_helper'
+
+class Chef
+ class Knife
+ module Core
+ # Instances of BootstrapContext are the context objects (i.e., +self+) for
+ # bootstrap templates. For backwards compatability, they +must+ set the
+ # following instance variables:
+ # * @config - a hash of knife's config values
+ # * @run_list - the run list for the node to boostrap
+ #
+ class WindowsBootstrapContext < BootstrapContext
+
+ def initialize(config, run_list, chef_config, secret=nil)
+ @config = config
+ @run_list = run_list
+ @chef_config = chef_config
+ @secret = secret
+ # Compatibility with Chef 12 and Chef 11 versions
+ begin
+ # Pass along the secret parameter for Chef 12
+ super(config, run_list, chef_config, secret)
+ rescue ArgumentError
+ # The Chef 11 base class only has parameters for initialize
+ super(config, run_list, chef_config)
+ end
+ 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 secret
+ escape_and_echo(@config[:secret])
+ end
+
+ def trusted_certs_script
+ @trusted_certs_script ||= trusted_certs_content
+ end
+
+ def config_content
+ client_rb = <<~CONFIG
+ chef_server_url "#{@chef_config[:chef_server_url]}"
+ validation_client_name "#{@chef_config[:validation_client_name]}"
+ file_cache_path "c:/chef/cache"
+ file_backup_path "c:/chef/backup"
+ cache_options ({:path => "c:/chef/cache/checksums", :skip_expires => true})
+ CONFIG
+ 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? || knife_config.has_key?(:verify_api_cert)
+ value = @config[:node_verify_api_cert].nil? ? knife_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] || knife_config.has_key?(:ssl_verify_mode)
+ value = case @config[:node_ssl_verify_mode]
+ when "peer"
+ :verify_peer
+ when "none"
+ :verify_none
+ when nil
+ knife_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 :#{knife_config[:ssl_verify_mode]}\n}
+ end
+
+ if knife_config[:bootstrap_proxy]
+ client_rb << "\n"
+ client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n} if knife_config[:bootstrap_no_proxy]
+ end
+
+ if knife_config[:bootstrap_no_proxy]
+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n}
+ end
+
+ if @config[:secret]
+ client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n}
+ end
+
+ unless trusted_certs_script.empty?
+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n}
+ end
+
+ if Chef::Config[:fips]
+ client_rb << <<~CONFIG
+ fips true
+ chef_version = ::Chef::VERSION.split(".")
+ unless chef_version[0].to_i > 12 || (chef_version[0].to_i == 12 && chef_version[1].to_i >= 8)
+ raise "FIPS Mode requested but not supported by this client"
+ end
+ CONFIG
+ 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
+ bootstrap_environment_option = bootstrap_environment.nil? ? '' : " -E #{bootstrap_environment}"
+ start_chef = "SET \"PATH=%SystemRoot%\\system32;%SystemRoot%;%SystemRoot%\\System32\\Wbem;%SYSTEMROOT%\\System32\\WindowsPowerShell\\v1.0\\;C:\\ruby\\bin;C:\\opscode\\chef\\bin;C:\\opscode\\chef\\embedded\\bin\"\n"
+ start_chef << "chef-client -c c:/chef/client.rb -j c:/chef/first-boot.json#{bootstrap_environment_option}\n"
+ end
+
+ def latest_current_windows_chef_version_query
+ installer_version_string = nil
+ if @config[:prerelease]
+ installer_version_string = "&prerelease=true"
+ else
+ chef_version_string = if knife_config[:bootstrap_version]
+ knife_config[:bootstrap_version]
+ else
+ Chef::VERSION.split(".").first
+ end
+
+ installer_version_string = "&v=#{chef_version_string}"
+
+ # If bootstrapping a pre-release version add the prerelease query string
+ if chef_version_string.split(".").length > 3
+ installer_version_string << "&prerelease=true"
+ end
+ end
+
+ installer_version_string
+ 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
+ )
+
+ $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_chef = install_command('"') + "\n" + fallback_install_task_command
+ end
+
+ def bootstrap_directory
+ bootstrap_directory = "C:\\chef"
+ end
+
+ def local_download_path
+ local_download_path = "%TEMP%\\chef-client-latest.msi"
+ end
+
+ def msi_url(machine_os=nil, machine_arch=nil, download_context=nil)
+ # The default msi path has a number of url query parameters - we attempt to substitute
+ # such parameters in as long as they are provided by the template.
+
+ if @config[:install].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 += latest_current_windows_chef_version_query
+ 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)
+ if @config[:install_as_service]
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} ADDLOCAL=#{executor_quote}ChefClientFeature,ChefServiceFeature#{executor_quote}"
+ else
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
+ end
+ 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(Chef::Util::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}".gsub("/","\\")
+ 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 Chef Client 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 Chef Client installation scheduled task with status code !ERRORLEVEL! > "&2"
+ ) else (
+ @echo Successfully created scheduled task to install Chef Client.
+ @schtasks /run /tn chefclientbootstraptask
+ @if ERRORLEVEL 1 (
+ @echo ERROR: Failed to execut Chef Client installation scheduled task with status code !ERRORLEVEL!. > "&2"
+ ) else (
+ @echo Successfully started Chef Client 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 Chef Client package to install
+ ) else (
+ @echo Finished waiting for Chef Client package to install.
+ )
+ @schtasks /delete /f /tn chefclientbootstraptask > NUL
+ )
+ )
+ ) else (
+ @echo Successfully installed Chef Client package.
+ )
+ EOH
+ end
+ end
+ end
+ end
+end