summaryrefslogtreecommitdiff
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
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>
-rw-r--r--Gemfile10
-rw-r--r--lib/chef.rb4
-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
-rw-r--r--spec/unit/knife/bootstrap_spec.rb1583
9 files changed, 2877 insertions, 497 deletions
diff --git a/Gemfile b/Gemfile
index 6abe88b86e..61cad77895 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,6 +13,16 @@ gem "ohai", git: "https://github.com/chef/ohai.git", branch: "master"
gem "chef-config", path: File.expand_path("../chef-config", __FILE__) if File.exist?(File.expand_path("../chef-config", __FILE__))
gem "cheffish", "~> 14"
+# The chef_core gems are sourced from git.
+# gem "chef_core", git: "https://github.com/chef/chef_core", branch: "chef-core-split"
+# gem "chef_core-actions", git: "https://github.com/chef/chef_core-actions", branch: "chef-core-split"
+# gem "chef_core-cliux", git: "https://github.com/chef/chef_core-cliux", branch: "chef-core-split"
+# Temporary for testing:
+#gem "train", path: "../train"
+# gem "chef_core", path: "../chef_core"
+# gem "chef_core-actions", path: "../chef_core-actions"
+# gem "chef_core-cliux", path: "../chef_core-cliux"
+
group(:omnibus_package) do
gem "appbundler"
gem "rb-readline"
diff --git a/lib/chef.rb b/lib/chef.rb
index 3d6b783253..ccfb57764d 100644
--- a/lib/chef.rb
+++ b/lib/chef.rb
@@ -16,7 +16,11 @@
# limitations under the License.
#
+
require "chef/version"
+
+require "chef_core/text"
+
require "chef/nil_argument"
require "chef/mash"
require "chef/exceptions"
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
diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb
index 258d193cf1..1c6943f2fe 100644
--- a/spec/unit/knife/bootstrap_spec.rb
+++ b/spec/unit/knife/bootstrap_spec.rb
@@ -22,33 +22,36 @@ Chef::Knife::Bootstrap.load_deps
require "net/ssh"
describe Chef::Knife::Bootstrap do
- before do
- allow(ChefConfig).to receive(:windows?) { false }
- end
+ let(:bootstrap_template) { nil }
+ let(:stderr) { StringIO.new }
+ let(:bootstrap_cli_options) { [ ] }
+ let(:base_os) { :linux }
+ let(:target_host) { double("TargetHost") }
+
let(:knife) do
Chef::Log.logger = Logger.new(StringIO.new)
Chef::Config[:knife][:bootstrap_template] = bootstrap_template unless bootstrap_template.nil?
k = Chef::Knife::Bootstrap.new(bootstrap_cli_options)
- k.merge_configs
-
allow(k.ui).to receive(:stderr).and_return(stderr)
allow(k).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false)
+ allow(k).to receive(:target_host).and_return target_host
+ k.merge_configs
k
end
- let(:stderr) { StringIO.new }
-
- let(:bootstrap_template) { nil }
-
- let(:bootstrap_cli_options) { [ ] }
+ before do
+ allow(target_host).to receive(:base_os).and_return base_os
+ end
- it "should use chef-full as default template" do
- expect(knife.bootstrap_template).to be_a_kind_of(String)
- expect(File.basename(knife.bootstrap_template)).to eq("chef-full")
+ context "#bootstrap_template" do
+ it "should default to chef-full" do
+ expect(knife.bootstrap_template).to be_a_kind_of(String)
+ expect(File.basename(knife.bootstrap_template)).to eq("chef-full")
+ end
end
- context "when using the chef-full default template" do
+ context "#render_template - when using the chef-full default template" do
let(:rendered_template) do
knife.merge_configs
knife.render_template
@@ -284,14 +287,14 @@ describe Chef::Knife::Bootstrap do
jsonfile.close
end
- context "when --json-attributes and --json-attribute-file were both passed" do
- it "raises a Chef::Exceptions::BootstrapCommandInputError with the proper error message" do
- knife.parse_options(["-j", '{"foo":{"bar":"baz"}}'])
- knife.parse_options(["--json-attribute-file", jsonfile.path])
- knife.merge_configs
- expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError)
- jsonfile.close
- end
+ it "raises a Chef::Exceptions::BootstrapCommandInputError with the proper error message" do
+ knife.parse_options(["-j", '{"foo":{"bar":"baz"}}'])
+ knife.parse_options(["--json-attribute-file", jsonfile.path])
+ knife.merge_configs
+ allow(knife).to receive(:validate_name_args!)
+
+ expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError)
+ jsonfile.close
end
end
end
@@ -317,13 +320,16 @@ describe Chef::Knife::Bootstrap do
subject(:knife) do
k = described_class.new
Chef::Config[:knife][:bootstrap_template] = template_file
+ allow(k).to receive(:target_host).and_return target_host
k.parse_options(options)
k.merge_configs
k
end
let(:options) { ["--bootstrap-no-proxy", setting, "-s", "foo"] }
+
let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) }
+
let(:rendered_template) do
knife.render_template
end
@@ -506,48 +512,153 @@ describe Chef::Knife::Bootstrap do
context "when client_d_dir is set" do
let(:client_d_dir) do
Chef::Util::PathHelper.cleanpath(
- File.join(File.dirname(__FILE__), "../../data/client.d_00")) end
+ File.join(File.dirname(__FILE__), "../../data/client.d_00")) end
+
+ it "creates /etc/chef/client.d" do
+ expect(rendered_template).to match("mkdir -p /etc/chef/client\.d")
+ end
+
+ context "a flat directory structure" do
+ it "escapes single-quotes" do
+ expect(rendered_template).to match("cat > /etc/chef/client.d/02-strings.rb <<'EOP'")
+ expect(rendered_template).to match("something '\\\\''/foo/bar'\\\\''")
+ end
+
+ it "creates a file 00-foo.rb" do
+ expect(rendered_template).to match("cat > /etc/chef/client.d/00-foo.rb <<'EOP'")
+ expect(rendered_template).to match("d6f9b976-289c-4149-baf7-81e6ffecf228")
+ end
+ it "creates a file bar" do
+ expect(rendered_template).to match("cat > /etc/chef/client.d/bar <<'EOP'")
+ expect(rendered_template).to match("1 / 0")
+ end
+ end
+
+ context "a nested directory structure" do
+ let(:client_d_dir) do
+ Chef::Util::PathHelper.cleanpath(
+ File.join(File.dirname(__FILE__), "../../data/client.d_01")) end
+ it "creates a file foo/bar.rb" do
+ expect(rendered_template).to match("cat > /etc/chef/client.d/foo/bar.rb <<'EOP'")
+ expect(rendered_template).to match("1 / 0")
+ end
+ end
+ end
+ end
+
+
+
+ describe "#connection_protocol" do
+ let(:host_descriptor) { "example.com" }
+ let(:config) { { } }
+ let(:knife_connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:config).and_return config
+ allow(knife).to receive(:host_descriptor).and_return host_descriptor
+ if knife_connection_protocol
+ Chef::Config[:knife][:connection_protocol] = knife_connection_protocol
+ end
+ end
+
+ context "when protocol is part of the host argument" do
+ let(:host_descriptor) { "winrm://myhost" }
+
+ it "returns the value provided by the host argument" do
+ expect(knife.connection_protocol).to eq "winrm"
+ end
+ end
+
+ context "when protocol is provided via the CLI flag" do
+ let(:config) { { connection_protocol: "winrm" } }
+ it "returns that value" do
+ expect(knife.connection_protocol).to eq "winrm"
+ end
+
+
+ end
+ context "when protocol is provided via the host argument and the CLI flag" do
+ let(:host_descriptor) { "ssh://example.com" }
+ let(:config) { { connection_protocol: "winrm" } }
- it "creates /etc/chef/client.d" do
- expect(rendered_template).to match("mkdir -p /etc/chef/client\.d")
+ it "returns the value provided by the host argument" do
+ expect(knife.connection_protocol).to eq "ssh"
end
+ end
- context "a flat directory structure" do
- it "escapes single-quotes" do
- expect(rendered_template).to match("cat > /etc/chef/client.d/02-strings.rb <<'EOP'")
- expect(rendered_template).to match("something '\\\\''/foo/bar'\\\\''")
+ context "when no explicit protocol is provided" do
+ let(:config) { {} }
+ let(:host_descriptor) { "example.com" }
+ let(:knife_connection_protocol) { "winrm" }
+ it "falls back to knife config" do
+ expect(knife.connection_protocol).to eq "winrm"
+ end
+ context "and there is no knife bootstrap_protocol" do
+ let(:knife_connection_protocol) { nil }
+ it "falls back to 'ssh'" do
+ expect(knife.connection_protocol).to eq "ssh"
end
+ end
+ end
- it "creates a file 00-foo.rb" do
- expect(rendered_template).to match("cat > /etc/chef/client.d/00-foo.rb <<'EOP'")
- expect(rendered_template).to match("d6f9b976-289c-4149-baf7-81e6ffecf228")
+ end
+
+ describe "#validate_protocol!" do
+ let(:host_descriptor) { "example.com" }
+ let(:config) { { } }
+ let(:connection_protocol) { "ssh" }
+ before do
+ allow(knife).to receive(:config).and_return config
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ allow(knife).to receive(:host_descriptor).and_return host_descriptor
+ end
+
+ context "when protocol is provided both in the URL and via --protocol" do
+
+ context "and they do not match" do
+ let(:connection_protocol) { "ssh" }
+ let(:config) { { connection_protocol: "winrm" } }
+ it "outputs an error and exits" do
+ expect(knife.ui).to receive(:error)
+ expect{ knife.validate_protocol! }.to raise_error SystemExit
end
- it "creates a file bar" do
- expect(rendered_template).to match("cat > /etc/chef/client.d/bar <<'EOP'")
- expect(rendered_template).to match("1 / 0")
+ end
+
+ context "and they do match" do
+ let(:connection_protocol) { "winrm" }
+ let(:config) { { connection_protocol: "winrm" } }
+ it "returns true" do
+ expect(knife.validate_protocol!).to eq true
end
end
+ end
- context "a nested directory structure" do
- let(:client_d_dir) do
- Chef::Util::PathHelper.cleanpath(
- File.join(File.dirname(__FILE__), "../../data/client.d_01")) end
- it "creates a file foo/bar.rb" do
- expect(rendered_template).to match("cat > /etc/chef/client.d/foo/bar.rb <<'EOP'")
- expect(rendered_template).to match("1 / 0")
+ context "and the protocol is supported" do
+
+ Chef::Knife::Bootstrap::SUPPORTED_CONNECTION_PROTOCOLS.each do |proto|
+ let(:connection_protocol) { proto }
+ it "returns true for #{proto}" do
+ expect(knife.validate_protocol!).to eq true
end
end
end
+
+ context "and the protocol is not supported" do
+ let(:connection_protocol) { "invalid" }
+ it "outputs an error and exits" do
+ expect(knife.ui).to receive(:error).with(/Unsupported protocol '#{connection_protocol}'/)
+ expect{ knife.validate_protocol! }.to raise_error SystemExit
+ end
+ end
end
- describe "handling policyfile options" do
+ describe "#validate_policy_options!" do
context "when only policy_name is given" do
let(:bootstrap_cli_options) { %w{ --policy-name my-app-server } }
it "returns an error stating that policy_name and policy_group must be given together" do
- expect { knife.validate_options! }.to raise_error(SystemExit)
+ expect { knife.validate_policy_options! }.to raise_error(SystemExit)
expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together")
end
@@ -558,7 +669,7 @@ describe Chef::Knife::Bootstrap do
let(:bootstrap_cli_options) { %w{ --policy-group staging } }
it "returns an error stating that policy_name and policy_group must be given together" do
- expect { knife.validate_options! }.to raise_error(SystemExit)
+ expect { knife.validate_policy_options! }.to raise_error(SystemExit)
expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together")
end
@@ -569,7 +680,7 @@ describe Chef::Knife::Bootstrap do
let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging --run-list cookbook } }
it "returns an error stating that policyfile and run_list are exclusive" do
- expect { knife.validate_options! }.to raise_error(SystemExit)
+ expect { knife.validate_policy_options! }.to raise_error(SystemExit)
expect(stderr.string).to include("ERROR: Policyfile options and --run-list are exclusive")
end
@@ -580,7 +691,7 @@ describe Chef::Knife::Bootstrap do
let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging } }
it "passes options validation" do
- expect { knife.validate_options! }.to_not raise_error
+ expect { knife.validate_policy_options! }.to_not raise_error
end
it "passes them into the bootstrap context" do
@@ -598,267 +709,1319 @@ describe Chef::Knife::Bootstrap do
# Arguably a bug in the plugin: it shouldn't be setting this to nil, but it
# worked before, so make it work now.
context "when a plugin sets the run list option to nil" do
-
before do
knife.config[:run_list] = nil
end
it "passes options validation" do
- expect { knife.validate_options! }.to_not raise_error
+ expect { knife.validate_policy_options! }.to_not raise_error
end
+ end
+ end
+ # TODO - this is the only cli option we validate the _option_ itself -
+ # so we'll know if someone accidentally deletes or renames use_sudo_password
+ # Is this worht keeping? If so, then it seems we should expand it
+ # to cover all options.
+ context "validating use_sudo_password option" do
+ it "use_sudo_password contains description and long params for help" do
+ expect(knife.options).to(have_key(:use_sudo_password)) \
+ && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq(""))\
+ && expect(knife.options[:use_sudo_password][:long].to_s).not_to(eq(""))
end
+ end
+
+
+ context "#connection_opts" do
+ let(:connection_protocol) { "ssh" }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+ context "behavioral test: " do
+ let(:expected_connection_opts) {
+ { base_opts: true,
+ ssh_identity_opts: true,
+ ssh_opts: true,
+ gateway_opts: true,
+ host_verify_opts: true,
+ sudo_opts: true,
+ winrm_opts: true }
+ }
+
+ it "queries and merges only expected configurations" do
+ expect(knife).to receive(:base_opts).and_return({ base_opts: true })
+ expect(knife).to receive(:host_verify_opts).and_return({ host_verify_opts: true })
+ expect(knife).to receive(:gateway_opts).and_return({ gateway_opts: true })
+ expect(knife).to receive(:sudo_opts).and_return({ sudo_opts: true })
+ expect(knife).to receive(:winrm_opts).and_return({ winrm_opts: true })
+ expect(knife).to receive(:ssh_opts).and_return({ ssh_opts: true })
+ expect(knife).to receive(:ssh_identity_opts).and_return({ ssh_identity_opts: true })
+ expect(knife.connection_opts).to match expected_connection_opts
+ end
+ end
+
+ context "functional test: " do
+ context "when protocol is winrm" do
+ let(:connection_protocol) { "winrm" }
+ # context "and neither CLI nor Chef::Config config entries have been provided"
+ # end
+ context "and all supported values are provided as Chef::Config entries" do
+ before do
+ # Set everything to easily identifiable and obviously fake values
+ # to verify that Chef::Config is being sourced instead of knife.config
+ Chef::Config[:knife][:max_wait] = 9999
+ Chef::Config[:knife][:winrm_user] = "winbob"
+ Chef::Config[:knife][:winrm_port] = 9999
+ Chef::Config[:knife][:ca_trust_file] = "trust.me"
+ Chef::Config[:knife][:kerberos_realm] = "realm"
+ Chef::Config[:knife][:kerberos_service] = "service"
+ Chef::Config[:knife][:winrm_auth_method] = "kerberos" # default is negotiate
+ Chef::Config[:knife][:winrm_basic_auth_only] = true
+ Chef::Config[:knife][:winrm_no_verify_cert] = true
+ Chef::Config[:knife][:winrm_session_timeout] = 9999
+ Chef::Config[:knife][:winrm_ssl] = true
+ Chef::Config[:knife][:winrm_ssl_peer_fingerprint] = "ABCDEF"
+ end
+
+ context "and unsupported Chef::Config options are given in Chef::Config, not in CLI" do
+ before do
+ Chef::Config[:knife][:password] = "blah"
+ Chef::Config[:knife][:winrm_password] = "blah"
+ end
+ it "does not include the corresponding option in the connection options" do
+ expect(knife.connection_opts.key?(:password)).to eq false
+ end
+ end
+
+ context "and no CLI options have been given" do
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "trust.me",
+ max_wait_until_ready: 9999,
+ operation_timeout: 9999,
+ ssl_peer_fingerprint: "ABCDEF",
+ winrm_transport: "kerberos",
+ winrm_basic_auth_only: true,
+ user: "winbob",
+ port: 9999,
+ self_signed: true,
+ ssl: true,
+ kerberos_realm: "realm",
+ kerberos_service: "service"
+ }
+ }
+
+ it "generates a config hash using the Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+
+ end
+
+ context "and some CLI options have been given" do
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "no trust",
+ max_wait_until_ready: 9999,
+ operation_timeout: 9999,
+ ssl_peer_fingerprint: "ABCDEF",
+ winrm_transport: "kerberos",
+ winrm_basic_auth_only: true,
+ user: "microsoftbob",
+ port: 12,
+ self_signed: true,
+ ssl: true,
+ kerberos_realm: "realm",
+ kerberos_service: "service",
+ password: "lobster"
+ }
+ }
+
+ before do
+ knife.config[:ca_trust_file] = "no trust"
+ knife.config[:connection_user] = "microsoftbob"
+ knife.config[:connection_port] = 12
+ knife.config[:winrm_port] = "13" # indirectly verify we're not looking for the wrong CLI flag
+ knife.config[:password] = "lobster"
+ end
+
+ it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+
+ context "and all CLI options have been given" do
+ before do
+ # We'll force kerberos vi knife.config because it
+ # causes additional options to populate - make sure
+ # Chef::Config is different so we can be sure that we didn't
+ # pull in the Chef::Config value
+ Chef::Config[:knife][:winrm_auth_method] = "negotiate"
+ knife.config[:password] = "blue"
+ knife.config[:max_wait] = 1000
+ knife.config[:connection_user] = "clippy"
+ knife.config[:connection_port] = 1000
+ knife.config[:winrm_port] = 1001 # We should not see this value get used
+
+ knife.config[:ca_trust_file] = "trust.the.internet"
+ knife.config[:kerberos_realm] = "otherrealm"
+ knife.config[:kerberos_service] = "otherservice"
+ knife.config[:winrm_auth_method] = "kerberos" # default is negotiate
+ knife.config[:winrm_basic_auth_only] = false
+ knife.config[:winrm_no_verify_cert] = false
+ knife.config[:winrm_session_timeout] = 1000
+ knife.config[:winrm_ssl] = false
+ knife.config[:winrm_ssl_peer_fingerprint] = "FEDCBA"
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "trust.the.internet",
+ max_wait_until_ready: 1000,
+ operation_timeout: 1000,
+ ssl_peer_fingerprint: "FEDCBA",
+ winrm_transport: "kerberos",
+ winrm_basic_auth_only: false,
+ user: "clippy",
+ port: 1000,
+ self_signed: false,
+ ssl: false,
+ kerberos_realm: "otherrealm",
+ kerberos_service: "otherservice",
+ password: "blue"
+ }
+ }
+ it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+ end # with underlying Chef::Config values
+
+ context "and no values are provided from Chef::Config or CLI" do
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log,
+ operation_timeout: 60,
+ self_signed: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ winrm_basic_auth_only: false,
+ winrm_transport: "negotiate"
+ }
+ }
+ it "populates appropriate defaults" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+ end # winrm
+
+ context "when protocol is ssh" do
+ let(:connection_protocol) { "ssh" }
+ # context "and neither CLI nor Chef::Config config entries have been provided"
+ # end
+ context "and all supported values are provided as Chef::Config entries" do
+ before do
+ # Set everything to easily identifiable and obviously fake values
+ # to verify that Chef::Config is being sourced instead of knife.config
+ Chef::Config[:knife][:max_wait] = 9999
+ Chef::Config[:knife][:ssh_user] = "sshbob"
+ Chef::Config[:knife][:ssh_port] = 9999
+ Chef::Config[:knife][:host_key_verify] = false
+ Chef::Config[:knife][:ssh_gateway_identity] = "/gateway.pem"
+ Chef::Config[:knife][:ssh_gateway] = "admin@mygateway.local:1234"
+ Chef::Config[:knife][:ssh_identity_file] = "/identity.pem"
+ Chef::Config[:knife][:use_sudo_password] = false # We have no password.
+ end
+
+ context "and no CLI options have been given" do
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 9999,
+ user: "sshbob",
+ bastion_host: "mygateway.local",
+ bastion_port: 1234,
+ bastion_user: "admin",
+ forward_agent: false,
+ keys_only: true,
+ key_files: ['/identity.pem', '/gateway.pem'],
+ sudo: false,
+ verify_host_key: false,
+ port: 9999
+ }
+ }
+
+ it "generates a correct config hash using the Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+
+ context "and unsupported Chef::Config options are given in Chef::Config, not in CLI" do
+ before do
+ Chef::Config[:knife][:password] = "blah"
+ Chef::Config[:knife][:ssh_password] = "blah"
+ Chef::Config[:knife][:preserve_home] = true
+ Chef::Config[:knife][:use_sudo] = true
+ Chef::Config[:knife][:ssh_forward_agent] = "blah"
+ end
+ it "does not include the corresponding option in the connection options" do
+ expect(knife.connection_opts.key?(:password)).to eq false
+ expect(knife.connection_opts.key?(:ssh_forward_agent)).to eq false
+ expect(knife.connection_opts.key?(:use_sudo)).to eq false
+ expect(knife.connection_opts.key?(:preserve_home)).to eq false
+ end
+ end
+
+ context "and some CLI options have been given" do
+ before do
+ knife.config[:connection_user] = "sshalice"
+ knife.config[:connection_port] = 12
+ knife.config[:ssh_port] = "13" # canary to indirectly verify we're not looking for the wrong CLI flag
+ knife.config[:password] = "feta cheese"
+ knife.config[:max_wait] = 150
+ knife.config[:use_sudo] = true
+ knife.config[:use_sudo_pasword] = true
+ knife.config[:ssh_forward_agent] = true
+ end
+
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 150, #cli
+ user: "sshalice", # cli
+ password: "feta cheese", # cli
+ bastion_host: "mygateway.local", # Config
+ bastion_port: 1234, # Config
+ bastion_user: "admin", # Config
+ forward_agent: true, # cli
+ keys_only: false, # implied false from config password present
+ key_files: ['/identity.pem', '/gateway.pem'], # Config
+ sudo: true, # ccli
+ verify_host_key: false, # Config
+ port: 12 # cli
+ }
+ }
+
+
+ it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+
+ context "and all CLI options have been given" do
+ before do
+ knife.config[:max_wait] = 150
+ knife.config[:connection_user] = "sshroot"
+ knife.config[:connection_port] = 1000
+ knife.config[:password] = "blah"
+ knife.config[:forward_agent] = true
+ knife.config[:use_sudo] = true
+ knife.config[:use_sudo_password] = true
+ knife.config[:preserve_home] = true
+ knife.config[:use_sudo_pasword] = true
+ knife.config[:ssh_forward_agent] = true
+ knife.config[:ssh_verify_host_key] = true
+ knife.config[:ssh_gateway_identity] = "/gateway-identity.pem"
+ knife.config[:ssh_gateway] = "me@example.com:10"
+ knife.config[:ssh_identity_file] = "/my-identity.pem"
+
+ # We'll set these as canaries - if one of these values shows up
+ # in a failed test, then the behavior of not pulling from these keys
+ # out of knife.config is broken:
+ knife.config[:ssh_user] = "do not use"
+ knife.config[:ssh_port] = 1001
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 150,
+ user: "sshroot",
+ password: "blah",
+ port: 1000,
+ bastion_host: "example.com",
+ bastion_port: 10,
+ bastion_user: "me",
+ forward_agent: true,
+ keys_only: false,
+ key_files: ['/my-identity.pem', '/gateway-identity.pem'],
+ sudo: true,
+ sudo_options: "-H",
+ sudo_password: "blah",
+ verify_host_key: true,
+ }
+ }
+ it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+ end
+ context "and no values are provided from Chef::Config or CLI" do
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ forward_agent: false,
+ key_files: [],
+ logger: Chef::Log,
+ keys_only: false,
+ sudo: false,
+ verify_host_key: true
+ }
+ }
+ it "populates appropriate defaults" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+
+ end #ssh
+ end #functional tests
+
+ end #connection_opts
+
+ context "#base_opts" do
+ let(:connection_protocol) { nil }
+
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "when determining knife config keys for user and port" do
+ let(:connection_protocol) { "fake" }
+ it "uses the protocol name to resolve the knife config keys" do
+ allow(knife).to receive(:config_value).with(:max_wait)
+
+ expect(knife).to receive(:config_value).with(:connection_port, :fake_port)
+ expect(knife).to receive(:config_value).with(:connection_user, :fake_user)
+ knife.base_opts
+ end
+ end
+
+ context "for all protocols" do
+ context "when password is provided" do
+ before do
+ knife.config[:connection_port] = 250
+ knife.config[:connection_user] = "test"
+ knife.config[:password] = "opscode"
+ end
+
+ let(:expected_opts) {
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log,
+ password: "opscode"
+ }
+ }
+ it "generates the correct options" do
+ expect(knife.base_opts).to eq expected_opts
+ end
+
+ end
+
+ context "when password is not provided" do
+ before do
+ knife.config[:connection_port] = 250
+ knife.config[:connection_user] = "test"
+ end
+
+ let(:expected_opts) {
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log
+ }
+ }
+ it "generates the correct options" do
+ expect(knife.base_opts).to eq expected_opts
+ end
+ end
+ end
end
- describe "when configuring the underlying knife ssh command" do
- context "from the command line" do
- let(:knife_ssh) do
- knife.name_args = ["foo.example.com"]
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_port] = "4001"
- knife.config[:ssh_password] = "open_sesame"
- Chef::Config[:knife][:ssh_user] = nil
- Chef::Config[:knife][:ssh_port] = nil
- knife.config[:forward_agent] = true
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- knife.knife_ssh
+ context "#host_verify_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ it "returns the expected configuration" do
+ knife.config[:winrm_no_verify_cert] = true
+ expect(knife.host_verify_opts).to eq( { self_signed: true } )
+ end
+ it "provides a correct default when no option given" do
+ expect(knife.host_verify_opts).to eq( { self_signed: false } )
end
+ end
- it "configures the hostname" do
- expect(knife_ssh.name_args.first).to eq("foo.example.com")
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ it "returns the expected configuration" do
+ knife.config[:ssh_verify_host_key] = false
+ expect(knife.host_verify_opts).to eq( { verify_host_key: false } )
end
+ it "provides a correct default when no option given" do
+ expect(knife.host_verify_opts).to eq( { verify_host_key: true } )
+ end
+ end
+ end
+
+ # TODO - test keys_only, password, config source behavior
+ context "#ssh_identity_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
- it "configures the ssh user" do
- expect(knife_ssh.config[:ssh_user]).to eq("rooty")
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ it "returns an empty hash" do
+ expect(knife.ssh_identity_opts).to eq({})
end
+ end
+
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ context "when an identity file is specified" do
+ before do
+ knife.config[:ssh_identity_file] = "/identity.pem"
+ end
+ it "generates the expected configuration" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: [ "/identity.pem" ],
+ keys_only: true
+ })
+ end
+ context "and a password is also specified" do
+ before do
+ knife.config[:password] = "blah"
+ end
+ it "generates the expected configuration (key, keys_only false)" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: [ "/identity.pem" ],
+ keys_only: false
+ })
+ end
+ end
+
+ context "and a gateway is not specified" do
+ context "but a gateway identity file is specified" do
+ it "does not include the gateway identity file in keys" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: ["/identity.pem"],
+ keys_only: true
+ })
+ end
- it "configures the ssh password" do
- expect(knife_ssh.config[:ssh_password]).to eq("open_sesame")
+ end
+
+ end
+
+ context "and a gatway is specified" do
+ before do
+ knife.config[:ssh_gateway] = "example.com"
+ end
+ context "and a gateway identity file is not specified" do
+ it "config includes only identity file and not gateway identity" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: [ "/identity.pem" ],
+ keys_only: true
+ })
+ end
+ end
+
+ context "and a gateway identity file is also specified" do
+ before do
+ knife.config[:ssh_gateway_identity] = "/gateway.pem"
+ end
+
+ it "generates the expected configuration (both keys, keys_only true)" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: [ "/identity.pem", "/gateway.pem" ],
+ keys_only: true
+ })
+ end
+ end
+ end
end
- it "configures the ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("4001")
+ context "when no identity file is specified" do
+ it "generates the expected configuration (no keys, keys_only false)" do
+ expect(knife.ssh_identity_opts).to eq( {
+ key_files: [ ],
+ keys_only: false
+ })
+ end
+ context "and a gateway with gateway identity file is specified" do
+ before do
+ knife.config[:ssh_gateway] = "host"
+ knife.config[:ssh_gateway_identity] = "/gateway.pem"
+ end
+
+ it "generates the expected configuration (gateway key, keys_only false)" do
+ expect(knife.ssh_identity_opts).to eq({
+ key_files: [ "/gateway.pem" ],
+ keys_only: false
+ })
+ end
+ end
+ end
+ end
+ end
+
+ context "#gateway_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ it "returns an empty hash" do
+ expect(knife.gateway_opts).to eq({})
+ end
+ end
+
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ context "and ssh_gateway with hostname, user and port provided" do
+ before do
+ knife.config[:ssh_gateway] = "testuser@gateway:9021"
+ end
+ it "returns a proper bastion host config subset" do
+ expect(knife.gateway_opts).to eq({
+ bastion_user: "testuser",
+ bastion_host: "gateway",
+ bastion_port: 9021
+ })
+ end
+ end
+ context "and ssh_gateway with only hostname is given" do
+ before do
+ knife.config[:ssh_gateway] = "gateway"
+ end
+ it "returns a proper bastion host config subset" do
+ expect(knife.gateway_opts).to eq({
+ bastion_user: nil,
+ bastion_host: "gateway",
+ bastion_port: nil
+ })
+ end
+ end
+ context "and ssh_gateway with hostname and user is is given" do
+ before do
+ knife.config[:ssh_gateway] = "testuser@gateway"
+ end
+ it "returns a proper bastion host config subset" do
+ expect(knife.gateway_opts).to eq({
+ bastion_user: "testuser",
+ bastion_host: "gateway",
+ bastion_port: nil
+ })
+ end
end
- it "configures the ssh agent forwarding" do
- expect(knife_ssh.config[:forward_agent]).to eq(true)
+ context "and ssh_gateway with hostname and port is is given" do
+ before do
+ knife.config[:ssh_gateway] = "gateway:11234"
+ end
+ it "returns a proper bastion host config subset" do
+ expect(knife.gateway_opts).to eq({
+ bastion_user: nil,
+ bastion_host: "gateway",
+ bastion_port: 11234
+ })
+ end
end
- it "configures the ssh identity file" do
- expect(knife_ssh.config[:ssh_identity_file]).to eq("~/.ssh/me.rsa")
+ context "and ssh_gateway is not provided" do
+ it "returns an empty hash" do
+ expect(knife.gateway_opts).to eq({})
+ end
end
end
+ end
- context "validating use_sudo_password" do
- before do
- knife.config[:ssh_password] = "password"
- allow(knife).to receive(:render_template).and_return("")
+ context "#sudo_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ it "returns an empty hash" do
+ expect(knife.sudo_opts).to eq({})
end
+ end
+
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ context "when use_sudo is set" do
+ before do
+ knife.config[:use_sudo] = true
+ end
+
+ it "returns a config that enables sudo" do
+ expect(knife.sudo_opts).to eq( { sudo: true} )
+ end
- it "use_sudo_password contains description and long params for help" do
- expect(knife.options).to(have_key(:use_sudo_password)) \
- && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq(""))\
- && expect(knife.options[:use_sudo_password][:long].to_s).not_to(eq(""))
+ context "when use_sudo_password is also set" do
+ before do
+ knife.config[:use_sudo_password] = true
+ knife.config[:password] = "opscode"
+ end
+ it "includes :password value in a sudo-enabled configuration" do
+ expect(knife.sudo_opts).to eq({
+ sudo: true,
+ sudo_password: "opscode"
+ })
+ end
+ end
+
+ context "when preserve_home is set" do
+ before do
+ knife.config[:preserve_home] = true
+ end
+ it "enables sudo with sudo_option to preserve home" do
+ expect(knife.sudo_opts).to eq({
+ sudo_options: "-H",
+ sudo: true
+ })
+ end
+ end
end
- it "uses the password from --ssh-password for sudo when --use-sudo-password is set" do
- knife.config[:use_sudo] = true
- knife.config[:use_sudo_password] = true
- expect(knife.ssh_command).to include("echo \'#{knife.config[:ssh_password]}\' | sudo -S")
+ context "when use_sudo is not set" do
+ before do
+ knife.config[:use_sudo_password] = true
+ knife.config[:preserve_home] = true
+ end
+ it "returns configuration for sudo off, ignoring other related options" do
+ expect(knife.sudo_opts).to eq( { sudo: false} )
+ end
end
+ end
+ end
+
+ context "#ssh_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
- it "should not honor --use-sudo-password when --use-sudo is not set" do
- knife.config[:use_sudo] = false
- knife.config[:use_sudo_password] = true
- expect(knife.ssh_command).not_to include("echo #{knife.config[:ssh_password]} | sudo -S")
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ context "when ssh_forward_agent has a value" do
+ before do
+ knife.config[:ssh_forward_agent] = true
+ end
+ it "returns a configuration hash with forward_agent set to true" do
+ expect(knife.ssh_opts).to eq({ forward_agent: true })
+ end
+ end
+ context "when ssh_forward_agent is not set" do
+ it "returns a configuration hash with forward_agent set to false" do
+ expect(knife.ssh_opts).to eq({ forward_agent: false })
+ end
end
end
- context "from the knife config file" do
- let(:knife_ssh) do
- knife.name_args = ["config.example.com"]
- Chef::Config[:knife][:ssh_user] = "curiosity"
- Chef::Config[:knife][:ssh_port] = "2430"
- Chef::Config[:knife][:forward_agent] = true
- Chef::Config[:knife][:ssh_identity_file] = "~/.ssh/you.rsa"
- Chef::Config[:knife][:ssh_gateway] = "towel.blinkenlights.nl"
- Chef::Config[:knife][:ssh_gateway_identity] = "~/.ssh/gateway.rsa"
- Chef::Config[:knife][:host_key_verify] = true
- allow(knife).to receive(:render_template).and_return("")
- knife.config = {}
- knife.merge_configs
- knife.knife_ssh
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ it "returns an empty has because ssh is not winrm" do
+ expect(knife.ssh_opts).to eq({})
end
+ end
- it "configures the ssh user" do
- expect(knife_ssh.config[:ssh_user]).to eq("curiosity")
+ end
+
+ context "#winrm_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "for winrm" do
+ let(:connection_protocol) { "winrm" }
+ let(:expected) { {
+ winrm_transport: "negotiate",
+ winrm_basic_auth_only: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ operation_timeout: 60,
+ }}
+
+ it "generates a correct configuration hash with expected defaults" do
+ expect(knife.winrm_opts).to eq expected
end
- it "configures the ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("2430")
+ context "with ssl_peer_fingerprint" do
+ let(:ssl_peer_fingerprint_expected) {
+ expected.merge({ ssl_peer_fingerprint: "ABCD"})
+ }
+
+ before do
+ knife.config[:winrm_ssl_peer_fingerprint] = "ABCD"
+ end
+
+ it "generates a correct options hash with ssl_peer_fingerprint from the config provided" do
+ expect(knife.winrm_opts).to eq ssl_peer_fingerprint_expected
+ end
end
- it "configures the ssh agent forwarding" do
- expect(knife_ssh.config[:forward_agent]).to eq(true)
+ context "with winrm_ssl" do
+ let(:ssl_expected) {
+ expected.merge({ ssl: true })
+ }
+ before do
+ knife.config[:winrm_ssl] = true
+ end
+
+ it "generates a correct options hash with ssl from the config provided" do
+ expect(knife.winrm_opts).to eq ssl_expected
+ end
end
- it "configures the ssh identity file" do
- expect(knife_ssh.config[:ssh_identity_file]).to eq("~/.ssh/you.rsa")
+ context "with winrm_auth_method" do
+ let(:winrm_auth_method_expected) {
+ expected.merge({ winrm_transport: "freeaccess" })
+ }
+
+ before do
+ knife.config[:winrm_auth_method] = "freeaccess"
+ end
+
+ it "generates a correct options hash with winrm_transport from the config provided" do
+ expect(knife.winrm_opts).to eq winrm_auth_method_expected
+ end
end
- it "configures the ssh gateway" do
- expect(knife_ssh.config[:ssh_gateway]).to eq("towel.blinkenlights.nl")
+ context "with ca_trust_file" do
+ let(:ca_trust_expected) {
+ expected.merge({ ca_trust_file: "/trust.me"})
+ }
+ before do
+ knife.config[:ca_trust_file] = "/trust.me"
+ end
+
+ it "generates a correct options hash with ca_trust_file from the config provided" do
+ expect(knife.winrm_opts).to eq ca_trust_expected
+ end
end
- it "configures the ssh gateway identity" do
- expect(knife_ssh.config[:ssh_gateway_identity]).to eq("~/.ssh/gateway.rsa")
+ context "with kerberos auth" do
+ let(:kerberos_expected) {
+ expected.merge({
+ kerberos_service: "testsvc",
+ kerberos_realm: "TESTREALM",
+ winrm_transport: "kerberos"
+ })
+ }
+
+ before do
+ knife.config[:winrm_auth_method] = "kerberos"
+ knife.config[:kerberos_service] = "testsvc"
+ knife.config[:kerberos_realm] = "TESTREALM"
+ end
+
+ it "generates a correct options hash containing kerberos auth configuration from the config provided" do
+ expect(knife.winrm_opts).to eq kerberos_expected
+ end
end
- it "configures the host key verify mode" do
- expect(knife_ssh.config[:host_key_verify]).to eq(true)
+ context "with winrm_basic_auth_only" do
+ before do
+ knife.config[:winrm_basic_auth_only] = true
+ end
+ let(:basic_auth_expected) {
+ expected.merge( { winrm_basic_auth_only: true } )
+ }
+ it "generates a correct options hash containing winrm_basic_auth_only from the config provided" do
+ expect(knife.winrm_opts).to eq basic_auth_expected
+ end
end
end
- describe "when falling back to password auth when host key auth fails" do
- let(:knife_ssh_with_password_auth) do
- knife.name_args = ["foo.example.com"]
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- k = knife.knife_ssh
- allow(k).to receive(:get_password).and_return("typed_in_password")
- allow(knife).to receive(:knife_ssh).and_return(k)
- knife.knife_ssh_with_password_auth
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ it "returns an empty hash because ssh is not winrm" do
+ expect(knife.winrm_opts).to eq({})
end
+ end
+ end
+ describe "#run" do
+ before do
+ allow(knife.client_builder).to receive(:client_path).and_return("/key.pem")
+ end
+
+ it "performs the steps we expect to run a bootstrap" do
+ expect(knife).to receive(:validate_name_args!).ordered
+ expect(knife).to receive(:validate_protocol!).ordered
+ expect(knife).to receive(:validate_first_boot_attributes!).ordered
+ expect(knife).to receive(:validate_winrm_transport_opts!).ordered
+ expect(knife).to receive(:validate_policy_options!).ordered
+ expect(knife).to receive(:winrm_warn_no_ssl_verification).ordered
+ expect(knife).to receive(:register_client).ordered
+ expect(knife).to receive(:connect!).ordered
+ expect(knife).to receive(:render_template).and_return "content"
+ expect(knife).to receive(:upload_bootstrap).with("content").and_return "/remote/path.sh"
+ expect(knife).to receive(:perform_bootstrap).with("/remote/path.sh")
+ expect(target_host).to receive(:del_file) # Make sure cleanup happens
+
+ knife.run
+
+ # Post-run verify expected state changes (not many directly in #run)
+ expect(knife.bootstrap_context.client_pem).to eq "/key.pem"
+ expect($stdout.sync).to eq true
+ end
+ end
+
+ describe "#register_client" do
+ let(:vault_handler_mock) { double("ChefVaultHandler") }
+ let(:client_builder_mock) { double("ClientBuilder") }
+ let(:node_name) { nil }
+ before do
+ allow(knife).to receive(:chef_vault_handler).and_return vault_handler_mock
+ allow(knife).to receive(:client_builder).and_return client_builder_mock
+ knife.config[:chef_node_name] = node_name
+ end
+
+ shared_examples_for "creating the client locally" do
+ context "when a valid node name is present" do
+ let(:node_name) { "test" }
+ before do
+ allow(client_builder_mock).to receive(:client).and_return "client"
+ end
+
+ it "runs client_builder and vault_handler" do
+ expect(client_builder_mock).to receive(:run)
+ expect(vault_handler_mock).to receive(:run).with("client")
+ knife.register_client
+ end
+ end
+
+ context "when no valid node name is present" do
+ let(:node_name) { nil }
+ it "shows an error and exits" do
+ expect(knife.ui).to receive(:error)
+ expect{knife.register_client}.to raise_error(SystemExit)
+ end
+ end
+ end
+ context "when chef_vault_handler says we're using vault" do
+ let(:vault_handler_mock) { double("ChefVaultHandler") }
+ before do
+ allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return true
+ end
+ it_behaves_like "creating the client locally"
+ end
+
+ context "when an non-existant validation key is specified in chef config" do
+ before do
+ Chef::Config[:validation_key] = "/blah"
+ allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false
+ allow(File).to receive(:exist?).with("/blah").and_return false
+ end
+ it_behaves_like "creating the client locally"
+ end
- it "prompts the user for a password " do
- expect(knife_ssh_with_password_auth.config[:ssh_password]).to eq("typed_in_password")
+ context "when a valid validation key is given and we're doing old-style client creation" do
+ before do
+ Chef::Config[:validation_key] = "/blah"
+ allow(File).to receive(:exist?).with("/blah").and_return true
+ allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false
end
- it "configures knife not to use the identity file that didn't work previously" do
- expect(knife_ssh_with_password_auth.config[:ssh_identity_file]).to be_nil
+ it "shows a message" do
+ expect(knife.ui).to receive(:info)
+ knife.register_client
end
end
end
- it "verifies that a server to bootstrap was given as a command line arg" do
- knife.name_args = nil
- expect { knife.run }.to raise_error(SystemExit)
- expect(stderr.string).to match(/ERROR:.+FQDN or ip/)
+ describe "#perform_bootstrap" do
+ let(:exit_status) { 0 }
+ let(:result_mock) { double("result", exit_status: exit_status, stderr: "A message") }
+
+ before do
+ allow(target_host).to receive(:hostname).and_return "testhost"
+ end
+ it "runs the remote script and logs the output" do
+ expect(knife.ui).to receive(:info).with(/Bootstrapping.*/)
+ expect(knife).to receive(:bootstrap_command).
+ with("/path.sh").
+ and_return("sh /path.sh")
+ expect(target_host).
+ to receive(:run_command).
+ with("sh /path.sh").
+ and_yield("output here").
+ and_return result_mock
+
+ expect(knife.ui).to receive(:msg).with(/testhost/)
+ knife.perform_bootstrap("/path.sh")
+ end
+ context "when the remote command fails" do
+ let(:exit_status) { 1 }
+ it "shows an error and exits" do
+ expect(knife.ui).to receive(:info).with(/Bootstrapping.*/)
+ expect(knife).to receive(:bootstrap_command).
+ with("/path.sh").
+ and_return("sh /path.sh")
+ expect(target_host).to receive(:run_command).with("sh /path.sh").and_return result_mock
+ expect{knife.perform_bootstrap("/path.sh")}.to raise_error(SystemExit)
+ end
+ end
end
- describe "when running the bootstrap" do
- let(:knife_ssh) do
- knife.name_args = ["foo.example.com"]
- knife.config[:chef_node_name] = "foo.example.com"
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- knife_ssh = knife.knife_ssh
- allow(knife).to receive(:knife_ssh).and_return(knife_ssh)
- knife_ssh
+
+ describe "#connect!" do
+ context "in the normal case" do
+ it "connects using the connection_opts and notifies the operator of progress" do
+ expect(knife.ui).to receive(:info).with(/Connecting to.*/)
+ expect(knife).to receive(:connection_opts).and_return( { opts: "here" })
+ expect(knife).to receive(:do_connect).with( { opts: "here" } )
+ knife.connect!
+ end
+ end
+
+ context "when a non-auth-failure occurs" do
+ let(:expected_error) { RuntimeError.new }
+ before do
+ allow(knife).to receive(:do_connect).and_raise(expected_error)
+ end
+ it "re-raises the exception" do
+ expect{knife.connect!}.to raise_error(expected_error)
+ end
end
- let(:client) { Chef::ApiClient.new }
- context "when running with a configured and present validation key" do
+ context "when an auth failure occurs" do
+ let(:expected_error) {
+ # TODO This is awkward and ugly. Requires some refactor of chef_core/error
+ # to make it not so. See comment in rescue block of connect! for details.
+ e = RuntimeError.new
+ interim = RuntimeError.new
+ actual = Net::SSH::AuthenticationFailed.new
+ allow(interim).to receive(:cause).and_return(actual)
+ allow(e).to receive(:cause).and_return(interim)
+ e
+ }
+
before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check
- allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
+ require 'net/ssh'
end
- it "configures the underlying ssh command and then runs it" do
- expect(knife_ssh).to receive(:run)
- knife.run
+ context "and password auth was used" do
+ before do
+ knife.config[:password] = "tryme"
+ end
+
+ it "re-raises the error so as not to resubmit the same failing password" do
+ expect(knife).to receive(:do_connect).and_raise(expected_error)
+ expect{knife.connect!}.to raise_error(expected_error)
+ end
end
- it "falls back to password based auth when auth fails the first time" do
- allow(knife).to receive(:puts)
+ context "and password auth was not used" do
+ before do
+ knife.config[:password] = nil
+ allow(target_host).to receive(:user).and_return "testuser"
+ end
- fallback_knife_ssh = knife_ssh.dup
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed.new("no ssh for you"))
- allow(knife).to receive(:knife_ssh_with_password_auth).and_return(fallback_knife_ssh)
- expect(fallback_knife_ssh).to receive(:run)
- knife.run
+ it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password" do
+ question_mock = double("question")
+ expect(knife).to receive(:do_connect).and_raise(expected_error)
+ expect(knife.ui).to receive(:warn).with(/Failed to auth.*/)
+ expect(knife.ui).to receive(:ask).and_yield(question_mock).and_return("newpassword")
+ # Ensure that we set echo off to prevent showing password on the screen
+ expect(question_mock).to receive(:echo=).with false
+ expect(knife).to receive(:do_connect) do |opts|
+ expect(opts[:password]).to eq "newpassword"
+ end
+ knife.connect!
+ end
end
+ end
+ end
+
+
+
+ it "verifies that a server to bootstrap was given as a command line arg" do
+ knife.name_args = nil
+ expect { knife.run }.to raise_error(SystemExit)
+ expect(stderr.string).to match(/ERROR:.+FQDN or ip/)
+ end
- it "raises the exception if config[:ssh_password] is set and an authentication exception is raised" do
- knife.config[:ssh_password] = "password"
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed)
- expect { knife.run }.to raise_error(Net::SSH::AuthenticationFailed)
+ describe "#bootstrap_context" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ it "creates a WindowsBootstrapContext" do
+ require 'chef/knife/core/windows_bootstrap_context'
+ expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::WindowsBootstrapContext
end
+ end
- it "creates the client and adds chef-vault items if vault_list is set" do
- knife.config[:bootstrap_vault_file] = "/not/our/responsibility/to/check/if/this/exists"
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+ context "under linux" do
+ let(:base_os) { :linux }
+ it "creates a BootstrapContext" do
+ require 'chef/knife/core/bootstrap_context'
+ expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::BootstrapContext
end
+ end
+ end
- it "creates the client and adds chef-vault items if vault_items is set" do
- knife.config[:bootstrap_vault_json] = '{ "vault" => "item" }'
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+
+ describe "#config_value" do
+ before do
+ knife.config[:test_key_a] = "a from cli"
+ knife.config[:test_key_b] = "b from cli"
+ Chef::Config[:knife][:test_key_a] = "a from Chef::Config"
+ Chef::Config[:knife][:test_key_c] = "c from Chef::Config"
+ Chef::Config[:knife][:alt_test_key_c] = "alt c from Chef::Config"
+ end
+
+ it "returns CLI value when key is only provided by the CLI" do
+ expect(knife.config_value(:test_key_b)).to eq "b from cli"
+ end
+
+ it "returns CLI value when key is provided by CLI and Chef::Config" do
+ expect(knife.config_value(:test_key_a)).to eq "a from cli"
+ end
+
+ it "returns Chef::Config value whent he key is only provided by Chef::Config" do
+ expect(knife.config_value(:test_key_c)).to eq "c from Chef::Config"
+ end
+
+ it "returns the Chef::Config value from the alternative key when the CLI key is not set" do
+ expect(knife.config_value(:test_key_c, :alt_test_key_c)).to eq "alt c from Chef::Config"
+ end
+
+ it "returns the default value when the key is not provided by CLI or Chef::Config" do
+ expect(knife.config_value(:missing_key, :missing_key, "found")).to eq "found"
+ end
+ end
+
+ describe "#upload_bootstrap" do
+ before do
+ allow(target_host).to receive(:temp_dir).and_return(temp_dir)
+ allow(target_host).to receive(:normalize_path) { |a| a }
+ end
+
+ let(:content) { "bootstrap script content" }
+ context "under Windows" do
+ let(:base_os) { :windows }
+ let(:temp_dir) { "C:/Temp/bootstrap" }
+ it "creates a bat file in the temp dir provided by target_host, using given content" do
+ expect(target_host).to receive(:save_as_remote_file).with(content, "C:/Temp/bootstrap/bootstrap.bat")
+ expect(knife.upload_bootstrap(content)).to eq "C:/Temp/bootstrap/bootstrap.bat"
end
+ end
- it "does old-style validation without creating a client key if vault_list+vault_items is not set" do
- expect(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).not_to receive(:run)
- expect(knife.chef_vault_handler).not_to receive(:run)
- knife.run
+ context "under Linux" do
+ let(:base_os) { :linux }
+ let(:temp_dir) { "/tmp/bootstrap" }
+ it "creates a 'sh file in the temp dir provided by target_host, using given content" do
+ expect(target_host).to receive(:save_as_remote_file).with(content, "/tmp/bootstrap/bootstrap.sh")
+ expect(knife.upload_bootstrap(content)).to eq "/tmp/bootstrap/bootstrap.sh"
end
+ end
+ end
- it "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ describe "#bootstrap_command" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ it "prefixes the command to run under cmd.exe" do
+ expect(knife.bootstrap_command("autoexec.bat")).to eq "cmd.exe /C autoexec.bat"
+ end
- expect { knife.run }.to raise_error(SystemExit)
+ end
+ context "under Linux" do
+ let(:base_os) { :linux }
+ it "prefixes the command to run under sh" do
+ expect(knife.bootstrap_command("bootstrap")).to eq "sh bootstrap"
end
end
+ end
- context "when the validation key is not present" do
- before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check
- allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false)
+
+ describe "#default_bootstrap_template" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ it "is windows-chef-client-msi" do
+ expect(knife.default_bootstrap_template).to eq "windows-chef-client-msi"
end
- it "creates the client (and possibly adds chef-vault items)" do
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+ end
+ context "under Linux" do
+ let(:base_os) { :linux }
+ it "is chef-full" do
+ expect(knife.default_bootstrap_template).to eq "chef-full"
end
+ end
+ end
+
+ describe "#do_connect" do
+ let(:host_descriptor) { "example.com" }
+ let(:target_host) { double("TargetHost") }
+ let(:resolver_mock) { double("TargetResolver", targets: [ target_host ]) }
+ before do
+ allow(knife).to receive(:host_descriptor).and_return host_descriptor
+ end
- it "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ it "resolves the target and connects it" do
+ expect(ChefCore::TargetResolver).to receive(:new).and_return resolver_mock
+ expect(target_host).to receive(:connect!)
+ knife.do_connect({})
+ end
+ end
- expect { knife.run }.to raise_error(SystemExit)
+ describe "validate_winrm_transport_opts!" do
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "when using ssh" do
+ let(:connection_protocol) { "ssh" }
+ it "returns true" do
+ expect(knife.validate_winrm_transport_opts!).to eq true
end
end
+ context "when using winrm" do
+ let(:connection_protocol) { "winrm" }
+ context "with plaintext auth" do
+ before do
+ knife.config[:winrm_auth_method] = "plaintext"
+ end
+ context "with ssl" do
+ before do
+ knife.config[:winrm_ssl] = true
+ end
+ it "will not error because we won't send anything in plaintext regardless" do
+ expect(knife.validate_winrm_transport_opts!).to eq true
+ end
+ end
+ context "without ssl" do
+ before do
+ knife.config[:winrm_ssl] = false
+ end
+ context "and no validation key exists" do
+ before do
+ Chef::Config[:validation_key] = "validation_key.pem"
+ allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false
+ end
- context "when the validation_key is nil" do
- before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check for some plugins
- Chef::Config[:validation_key] = nil
+ it "will error because we will generate and send a client key over the wire in plaintext" do
+ expect{knife.validate_winrm_transport_opts!}.to raise_error(SystemExit)
+ end
+
+ end
+ context "and a validation key exists" do
+ before do
+ Chef::Config[:validation_key] = "validation_key.pem"
+ allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true
+ end
+ # TODO - don't we still send validation key?
+ it "will not error because we don not send client key over the wire" do
+ expect(knife.validate_winrm_transport_opts!).to eq true
+ end
+ end
+ end
end
- it "creates the client and does not run client_builder or the chef_vault_handler" do
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).not_to receive(:run)
- expect(knife.chef_vault_handler).not_to receive(:run)
- knife.run
+ context "with other auth" do
+ before do
+ knife.config[:winrm_auth_method] = "kerberos"
+ end
+
+ context "and no validation key exists" do
+ before do
+
+ Chef::Config[:validation_key] = "validation_key.pem"
+ allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false
+ end
+
+ it "will not error because we're not using plaintext auth" do
+ expect(knife.validate_winrm_transport_opts!).to eq true
+ end
+ end
+ context "and a validation key exists" do
+ before do
+ Chef::Config[:validation_key] = "validation_key.pem"
+ allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true
+ end
+
+ it "will not error because a client key won't be sent over the wire in plaintext when a validation key is present" do
+ expect(knife.validate_winrm_transport_opts!).to eq true
+ end
+ end
+
end
+
end
+
end
- describe "specifying ssl verification" do
+ describe "#winrm_warn_no_ssl_verification" do
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ context "when using ssh" do
+ let(:connection_protocol) { "ssh" }
+ it "does not issue a warning" do
+ expect(knife.ui).to_not receive(:warn)
+ knife.winrm_warn_no_ssl_verification
+ end
+ end
+ context "when using winrm" do
+ let(:connection_protocol) { "winrm" }
+ context "winrm_no_verify_cert is set" do
+ before do
+ knife.config[:winrm_no_verify_cert] = true
+ end
+
+ context "and ca_trust_file is present" do
+ before do
+ knife.config[:ca_trust_file] = "file"
+ end
+ it "does not issue a warning" do
+ expect(knife.ui).to_not receive(:warn)
+ knife.winrm_warn_no_ssl_verification
+ end
+ end
+
+ context "and winrm_ssl_peer_fingerprint is present" do
+ before do
+ knife.config[:winrm_ssl_peer_fingerprint] = "ABCD"
+ end
+ it "does not issue a warning" do
+ expect(knife.ui).to_not receive(:warn)
+ knife.winrm_warn_no_ssl_verification
+ end
+ end
+ context "and neither ca_trust_file nor winrm_ssl_peer_fingerprint is present" do
+ it "issues a warning" do
+ expect(knife.ui).to receive(:warn)
+ knife.winrm_warn_no_ssl_verification
+ end
+ end
+ end
+ end
end
end
+
+