summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2019-04-24 10:49:33 -0700
committerGitHub <noreply@github.com>2019-04-24 10:49:33 -0700
commitf99ecb0fe7d1be9d01ffc575590ec18014ca2f4b (patch)
tree861bd855664dff59cc9a1ad41b40da431af668b7
parentf492fe53eac1b74a0d184f0e9cf7412b70770e29 (diff)
parent8770eef5239bace3e2919accdc3cc672867c76a7 (diff)
downloadchef-f99ecb0fe7d1be9d01ffc575590ec18014ca2f4b.tar.gz
Merge pull request #8253 from chef/SUSTAINING-955/bootstrap-on-chef-core
Refactor bootstrapping to use train as the transport with full Windows bootstrap support
-rw-r--r--Gemfile.lock38
-rw-r--r--RELEASE_NOTES.md68
-rw-r--r--chef.gemspec1
-rw-r--r--docs/dev/design_documents/bootstrap_with_train.md150
-rw-r--r--lib/chef.rb13
-rw-r--r--lib/chef/knife.rb21
-rw-r--r--lib/chef/knife/bootstrap.rb790
-rw-r--r--lib/chef/knife/bootstrap/chef_vault_handler.rb1
-rw-r--r--lib/chef/knife/bootstrap/client_builder.rb1
-rw-r--r--lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb267
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb36
-rw-r--r--lib/chef/knife/core/ui.rb25
-rw-r--r--lib/chef/knife/core/windows_bootstrap_context.rb383
-rw-r--r--spec/data/trusted_certs_empty/.gitkeep0
-rw-r--r--spec/data/trusted_certs_empty/README.md1
-rw-r--r--spec/integration/knife/upload_spec.rb4
-rw-r--r--spec/support/shared/integration/integration_helper.rb1
-rw-r--r--spec/unit/knife/bootstrap_spec.rb1577
-rw-r--r--spec/unit/knife/core/bootstrap_context_spec.rb41
-rw-r--r--spec/unit/knife/core/windows_bootstrap_context_spec.rb267
-rw-r--r--spec/unit/knife_spec.rb20
21 files changed, 3296 insertions, 409 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 13be39ec98..526d7271c5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -32,6 +32,7 @@ PATH
bcrypt_pbkdf (~> 1.0)
bundler (>= 1.10)
chef-config (= 15.0.232)
+ chef-core (~> 0.0.3)
chef-zero (>= 14.0.11)
diff-lcs (~> 1.2, >= 1.2.4)
ed25519 (~> 1.2)
@@ -60,6 +61,7 @@ PATH
bcrypt_pbkdf (~> 1.0)
bundler (>= 1.10)
chef-config (= 15.0.232)
+ chef-core (~> 0.0.3)
chef-zero (>= 14.0.11)
diff-lcs (~> 1.2, >= 1.2.4)
ed25519 (~> 1.2)
@@ -111,7 +113,7 @@ GEM
specs:
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
- appbundler (0.12.3)
+ appbundler (0.12.4)
mixlib-cli (>= 1.4, < 3.0)
mixlib-shellout (>= 2.0, < 4.0)
ast (2.4.0)
@@ -122,6 +124,20 @@ GEM
debug_inspector (>= 0.0.1)
builder (3.2.3)
byebug (11.0.1)
+ chef-core (0.0.3)
+ chef-telemetry
+ mixlib-log
+ pastel
+ r18n-desktop
+ train-core (~> 2.0, >= 2.0.12)
+ tty-color
+ tty-cursor
+ tty-spinner
+ chef-telemetry (0.1.8)
+ chef-config
+ concurrent-ruby (~> 1.0)
+ ffi-yajl (~> 2.2)
+ http (~> 2.2)
chef-vault (3.4.3)
chef-zero (14.0.12)
ffi-yajl (~> 2.2)
@@ -133,11 +149,14 @@ GEM
chef-zero (~> 14.0)
net-ssh
coderay (1.1.2)
+ concurrent-ruby (1.1.5)
crack (0.4.3)
safe_yaml (~> 1.0.0)
debug_inspector (0.0.3)
diff-lcs (1.3)
docile (1.3.1)
+ domain_name (0.5.20180417)
+ unf (>= 0.0.5, < 1.0.0)
ed25519 (1.2.4)
equatable (0.5.0)
erubis (2.7.0)
@@ -163,6 +182,15 @@ GEM
hashie (3.6.0)
highline (1.7.10)
htmlentities (4.3.4)
+ http (2.2.2)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 1.0.1)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ http-form_data (1.0.3)
+ http_parser.rb (0.6.0)
httpclient (2.8.3)
iniparse (1.4.4)
inspec-core (4.1.4.preview)
@@ -258,6 +286,9 @@ GEM
binding_of_caller (>= 0.7)
pry (>= 0.9.11)
public_suffix (3.0.3)
+ r18n-core (3.2.0)
+ r18n-desktop (3.2.0)
+ r18n-core (= 3.2.0)
rack (2.0.7)
rainbow (3.0.0)
rake (12.3.2)
@@ -344,12 +375,17 @@ GEM
tty-screen (~> 0.6.4)
wisper (~> 2.0.0)
tty-screen (0.6.5)
+ tty-spinner (0.9.0)
+ tty-cursor (~> 0.6.0)
tty-table (0.10.0)
equatable (~> 0.5.0)
necromancer (~> 0.4.0)
pastel (~> 0.7.2)
strings (~> 0.1.0)
tty-screen (~> 0.6.4)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.6)
unicode-display_width (1.4.1)
unicode_utils (1.4.0)
uuidtools (2.1.5)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 58018ee63f..ef2e843bd5 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -44,6 +44,74 @@ The LC_ALL property in the locale resource has been deprecated as the usage of t
## Breaking Changes
+### Knife Bootstrap
+
+Knife bootstrap has been updated, and Windows bootstrap has been merged into core Chef's `knife bootstrap`. This marks the deprecation of the `knife-windows` plugin's `bootstrap` behavior.
+This change also addresses [CVE-2015-8559](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-8559): The `knife bootstrap` command in chef leaks the validator.pem private RSA key to /var/log/messages.
+
+*Important*: `knife bootstrap` works with all supported versions of Chef client. Older versions may continue to work as far back as 12.20
+
+In order to accomodate a combined bootstrap that supports both SSH and WinRM,
+CLI flags have been added, removed, or changed. Using the changed options will
+result in deprecation warnings, but `knife bootstrap` will accept those options
+unless otherwise noted.
+
+Using removed options will cause the command to fail.
+
+#### New Flags
+
+| Flag | Description |
+|-----:|:------------|
+| --max-wait SECONDS | Maximum time to wait for initial connection to be established. |
+| --winrm-basic-auth-only | Perform only Basic Authentication to the target WinRM node. |
+| --connection-protocol PROTOCOL|Connection protocol to use. Valid values are 'winrm' and 'ssh'. Default is 'ssh'. |
+| --connection-user | user to authenticate as, regardless of protocol |
+| --connection-password| Password to authenticate as, regardless of protocol |
+| --connection-port | port to connect to, regardless of protocol |
+
+#### Changed Flags
+
+| Flag | New Option | Notes |
+|-----:|:-----------|:------|
+| --[no-]host-key-verify |--[no-]ssh-verify-host-key| |
+| --forward-agent | --ssh-forward-agent| |
+| --session-timeout MINUTES | --session-timeout SECONDS|New for ssh, existing for winrm. The unit has changed from MINUTES to SECONDS for consistency with other timeouts.|
+| --ssh-password | --connection-password | |
+| --ssh-port | --connection-port | `knife[:ssh_port]` config setting remains available.
+| --ssh-user | --connection-user | `knife[:ssh_user]` config setting remains available.
+| --ssl-peer-fingerprint | --winrm-ssl-peer-fingerprint | |
+| --winrm-authentication-protocol=PROTO | --winrm-auth-method=AUTH-METHOD | Valid values: plaintext, kerberos, ssl, _negotiate_|
+| --winrm-password| --connection-password | |
+| --winrm-port| --connection-port | `knife[:winrm_port]` config setting remains available.|
+| --winrm-ssl-verify-mode MODE | --winrm-no-verify-cert | [1] Mode is not accepted. When flag is present, SSL cert will not be verified. Same as original mode of 'verify_none'. |
+| --winrm-transport TRANSPORT | --winrm-ssl | [1] Use this flag if the target host is accepts WinRM connections over SSL.
+| --winrm-user | --connection-user | `knife[:winrm_user]` config setting remains available.|
+
+1. These flags do not have an automatic mapping of old flag -> new flag. The
+ new flag must be used.
+
+#### Removed Flags
+
+| Flag | Notes |
+|-----:|:------|
+|--kerberos-keytab-file| This option existed but was not implemented.|
+|--winrm-codepage| This was used under knife-windows because bootstrapping was performed over a `cmd` shell. It is now invoked from `powershell`, so this option is no longer used.|
+|--winrm-shell|This option was ignored for bootstrap.|
+|--prerelease|Chef now releases all development builds to our current channel and does not perform pre-release gem releases.|
+|--install-as-service|Installing Chef client as a service is not supported|
+
+#### Usage Changes
+
+Instead of specifying protocol with `-o`, it's also possible to prefix
+the target hostname with the protocol in URL format. For example:
+
+```
+ knife bootstrap example.com -o ssh
+ knife bootstrap ssh://example.com
+ knife bootstrap example.com -o winrm
+ knife bootstrap winrm://example.com
+```
+
### Audit Mode
Chef's Audit mode was introduced in 2015 as a beta that needed to be enabled via client.rb. Its functionality has been superceded by InSpec and has been removed.
diff --git a/chef.gemspec b/chef.gemspec
index 63f9a8c331..32e12bad41 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 2.5.0"
s.add_dependency "chef-config", "= #{Chef::VERSION}"
+ s.add_dependency "chef-core", "~> 0.0.3"
s.add_dependency "mixlib-cli", ">= 1.7", "< 3.0"
s.add_dependency "mixlib-log", ">= 2.0.3", "< 4.0"
diff --git a/docs/dev/design_documents/bootstrap_with_train.md b/docs/dev/design_documents/bootstrap_with_train.md
new file mode 100644
index 0000000000..be4fdeb3ec
--- /dev/null
+++ b/docs/dev/design_documents/bootstrap_with_train.md
@@ -0,0 +1,150 @@
+# Bootstrap with Train
+
+Update `knife bootstrap` to use `train` as its backend via `chef-core`, and integrate Windows bootstrap support.
+
+## Motivation
+
+ As a Chef User,
+ I want to be able to bootstrap a system without logging secure data on that system
+ so that chef-client's keys are not exposed to anyone who can read the logs.
+
+ As a Chef User who adminsters Windows nodes,
+ I want to be able to bootstrap a system using the core Chef package
+ so that I don't have extra things to download first.
+
+ As a Chef Developer who works on bootstrap,
+ I want to be able to maintain one copy of the bootstrap logic
+ so that I don't have to spend time keeping a second copy in sync.
+
+## Summary
+
+The Windows bootstrap process has lived outside of core Chef for a long time.
+Switching to Train as the supporting back-end gives us the opportunity to merge
+the `knife-windows` bootstrap behavior into core knife. This will reduce the maintenance burden
+of maintaining what is a mostly-complete copy of bootstrap behaviors in `knife-windows`.
+
+This also addresses [CVE-2015-8559](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-8559), in which
+the bootstrap mechanism runs the full bootstrap script as an inline argument to bash/cmd.exe, resulting
+in sensitive data potentially getting logged on the remote system. Train provides a back-end that knows how to
+do file management and command execution over supported protocols. This allows us to upload the
+bootstrap script and execute it in a remote shell without exposing the contents in a way that could result
+in capturing them in system logs.
+
+## Anatomy of a Bootstrap
+
+Bootstrap follows this general flow:
+
+* validate CLI options are proper
+* register the new client if not using org validation key to create it
+* determine connection properties based on protocol
+* connect to the remote host
+* generate and upload the bootstrap script
+* remotely run the bootstrap script
+
+This change focuses on configuring the connection
+and executing the bootstrap script. The underlying bootstrap behavior itself
+remains largely unchanged.
+
+## Implementation
+
+### Remove Unsupported Behaviors
+
+We will also remove the following obsolete or unsupported behaviors:
+
+* `--prelease` flag - Chef hasn't been pre-released in quite some time.
+* `--install-as-service` - For many years we have suggested users not run chef-client as a service due to memory leaks in long running Ruby processes.
+* `--kerberos-keytab-file` - this is not implemented in the WinRM gem we use, and so was
+passed through to no effect.
+* remove explicit support for versions of Chef older than 12.8. Versions older than the supported
+ Chef client distributions will continue to be use at your own risk.
+* Remove support for Windows 2003 in the Windows bootstrap template as Chef does not support EOL Windows 2003 installs.
+
+### CLI Flag Changes
+
+As part of this change, CLI options from `knife bootstrap windows winrm` and `knife bootstrap`
+need to be merged. The majority will be untouched, but we'll also take this opportunity
+to make flag names more accurately describe what they're doing, and updating several options that are
+protocol-specific to be prefixed with the protocol (e.g. `--ssl-peer-fingerprint` to `--winrm-ssl-peer-fingerprint`)
+When a direct mapping exists, the original names will continue to work with backward
+compatibility and a deprecation warning if they have changed.
+
+#### New CLI Flags
+
+| Flag | Description |
+|-----:|:------------|
+| --max-wait SECONDS | Maximum time to wait for initial connection to be established. |
+| --winrm-basic-auth-only | Perform only Basic Authentication to the target WinRM node. |
+| --connection-protocol PROTOCOL|Connection protocol to use. Valid values are 'winrm' and 'ssh'. Default is 'ssh'. |
+| --connection-user | user to authenticate as, regardless of protocol |
+| --connection-password| Password to authenticate as, regardless of protocol |
+| --connection-port | port to connect to, regardless of protocol |
+
+`--connection-user`, `--connection-port`, and `--connection-password` replace their protocol-specific counterparts, since
+these are applicable to all supported transports. Their original knife config keys (`ssh\_user`, `ssh\_password`, etc.) remain
+available for use.
+
+Note that auth-related configuration may see further changes as work proceeds on credential set support for train.
+
+### Changed CLI Flags
+
+| Flag | New Option | Notes |
+|-----:|:-----------|:------|
+| --[no-]host-key-verify |--[no-]ssh-verify-host-key| |
+| --forward-agent | --ssh-forward-agent| |
+| --session-timeout MINUTES | --session-timeout SECONDS|New for ssh, existing for winrm. The unit has changed from MINUTES to SECONDS for consistency with other timeouts.|
+| --ssh-password | --connection-password | |
+| --ssh-port | --connection-port | `knife[:ssh_port]` config setting remains available.
+| --ssh-user | --connection-user | `knife[:ssh_user]` config setting remains available.
+| --ssl-peer-fingerprint | --winrm-ssl-peer-fingerprint | |
+| --winrm-authentication-protocol=PROTO | --winrm-auth-method=AUTH-METHOD | Valid values: plaintext, kerberos, ssl, _negotiate_|
+| --winrm-password| --connection-password | |
+| --winrm-port| --connection-port | `knife[:winrm_port]` config setting remains available.|
+| --winrm-ssl-verify-mode MODE | --winrm-no-verify-cert | [1] Mode is not accepted. When flag is present, SSL cert will not be verified. Same as original mode of 'verify_none'. |
+| --winrm-transport TRANSPORT | --winrm-ssl | [1] Use this flag if the target host is accepts WinRM connections over SSL.
+| --winrm-user | --connection-user | `knife[:winrm_user]` config setting remains available.|
+
+1. These flags do not have an automatic mapping of old flag -> new flag. The
+ new flag must be used.
+
+### Removed Flags
+
+| Flag | Notes |
+|-----:|:------|
+|--kerberos-keytab-file| This option existed but was not implemented.|
+|--winrm-codepage| This was used under `knife-windows` because bootstrapping was performed over a `cmd` shell. It is now invoked from `powershell`, so this option is no longer required.|
+|--winrm-shell| This option was ignored for bootstrap.|
+|--prerelease|Prerelease Chef hasn't existed for some time.|
+|--install-as-service|Installing Chef client as a service is not supported|
+
+### Conversion to ChefCore and Train
+
+CLI and knife options will be mapped to their train counterparts, and passed through to `TargetHost` to establish a connection.
+The TargetHost instance will be used for all upload and execution operations.
+
+Tests must ensure that options resolve correctly from the CLI, knife configuration, and defaults; and that they map to the corresponding
+`train` options.
+
+#### Validation
+
+Existing windows bootstrap validation checks should be preserved, unless they are superceded by related
+validations for ssh bootstrap.
+
+#### Context
+
+`WindowsBootstrapContext` will be moved into knife, with updates for namespacing as needed.
+
+#### Template
+
+`knife-windows/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb` will be moved into
+knife's bootstrap templates.
+
+### Future Improvements
+
+Because there are only two supported protocols for the near-term future,
+it does not add much benefit to split out the bootstrap CLI behavior based on
+protocol, so both are handled within the bootstrap command directly.
+
+If we want to support additional protocols, it will become unwieldy to continue with protocol `if`
+checks, and would be advisable to separate out protocol-specific behaviors
+into classes determined at runtime based on protocol.
+
diff --git a/lib/chef.rb b/lib/chef.rb
index 3d6b783253..c58f46debd 100644
--- a/lib/chef.rb
+++ b/lib/chef.rb
@@ -17,6 +17,19 @@
#
require "chef/version"
+
+# Ensure that this loads ahead of anything that
+# might cause rubygems to hit Gem.load_yaml, including
+# evaluating gemspecs. When load_yaml is invoked,
+# it stubs out the YAML::Syck namespace. This causes
+# r18n to break, which expects either YAML::Syck to be there
+# and fully defined (particularly, the Syck::PrivateType class),
+# or for it to not be there at all.
+#
+# When it's not - because it's a stub - r18n explodes on loading.
+# Ensuring chef_core/text and r18n are loaded first prevents this.
+require "chef_core/text"
+
require "chef/nil_argument"
require "chef/mash"
require "chef/exceptions"
diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb
index b5b32cb193..aac0c8e258 100644
--- a/lib/chef/knife.rb
+++ b/lib/chef/knife.rb
@@ -344,8 +344,15 @@ class Chef
# Chef::Config[:knife] would break the defaults in the cli that we would otherwise
# overwrite.
def config_file_settings
+ @key_sources = { cli: [], config: [] }
cli_keys.each_with_object({}) do |key, memo|
- memo[key] = Chef::Config[:knife][key] if Chef::Config[:knife].key?(key)
+ if config.key?(key)
+ @key_sources[:cli] << key
+ end
+ if Chef::Config[:knife].key?(key)
+ @key_sources[:config] << key
+ memo[key] = Chef::Config[:knife][key]
+ end
end
end
@@ -356,9 +363,15 @@ class Chef
def merge_configs
# other code may have a handle to the config object, so use Hash#replace to deliberately
# update-in-place.
- config.replace(
- default_config.merge(config_file_settings).merge(config)
- )
+ config.replace(default_config.merge(config_file_settings).merge(config))
+ end
+
+ # Return where a config key has been sourced,
+ # :cli, :config, or nil if the key is not set.
+ def config_source(key)
+ return :cli if @key_sources[:cli].include? key
+ return :config if @key_sources[:config].include? key
+ nil
end
# Catch-all method that does any massaging needed for various config
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index b9e09a15ba..b68fb1aa1e 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");
@@ -28,39 +28,91 @@ class Chef
class Bootstrap < Knife
include DataBagSecretOptions
- attr_accessor :client_builder
- attr_accessor :chef_vault_handler
-
- 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)"
+ SUPPORTED_CONNECTION_PROTOCOLS = %w{ssh winrm}.freeze
+ WINRM_AUTH_PROTOCOL_LIST = %w{plaintext kerberos ssl negotiate}.freeze
- option :ssh_user,
- short: "-x USERNAME",
- long: "--ssh-user USERNAME",
- description: "The ssh username",
- default: "root"
+ # Common connectivity options
+ option :connection_user,
+ short: "-U USERNAME",
+ long: "--connection-user USERNAME",
+ description: "Authenticate to the target host with this user account"
- option :ssh_password,
+ option :connection_password,
short: "-P PASSWORD",
- long: "--ssh-password PASSWORD",
- description: "The ssh password"
+ long: "--connection-password PASSWORD",
+ description: "Authenticate to the target host with this password"
- option :ssh_port,
+ option :connection_port,
short: "-p PORT",
- long: "--ssh-port PORT",
- description: "The ssh port",
- proc: Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
+ 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: #{SUPPORTED_CONNECTION_PROTOCOLS.join(" ")}"
+
+ option :max_wait,
+ short: "-W SECONDS",
+ long: "--max-wait SECONDS",
+ description: "The maximum time to wait for the initial connection to be established."
+
+ # WinRM Authentication
+ 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.",
+ boolean: true
+
+ 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 }
+
+ ## SSH Authentication
option :ssh_gateway,
short: "-G GATEWAY",
long: "--ssh-gateway GATEWAY",
@@ -72,9 +124,9 @@ class Chef
description: "The SSH identity file used for gateway authentication",
proc: Proc.new { |key| Chef::Config[:knife][:ssh_gateway_identity] = key }
- option :forward_agent,
+ option :ssh_forward_agent,
short: "-A",
- long: "--forward-agent",
+ long: "--ssh-forward-agent",
description: "Enable SSH agent forwarding",
boolean: true
@@ -83,58 +135,92 @@ class Chef
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 :ssh_verify_host_key,
+ long: "--[no-]ssh-verify-host-key",
+ description: "Verify host key, enabled by default.",
+ boolean: true
- option :prerelease,
- long: "--prerelease",
- description: "Install the pre-release chef gems"
+ #
+ # bootstrap options
+ #
+ # 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 Opscode",
+ 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",
@@ -142,22 +228,26 @@ class Chef
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",
@@ -165,18 +255,21 @@ class Chef
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
- option :host_key_verify,
- long: "--[no-]host-key-verify",
- description: "Verify host key, enabled by default.",
- boolean: true,
- default: true
+ # 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. We cannot always used the merged
+ # config, because in some cases the knife keys thIn 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.",
@@ -186,55 +279,55 @@ class Chef
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 }
+ option :msi_url, # Windows target only
+ 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: ""
+
+ # 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 }
+ 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 }
- 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
- }
-
- 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
-
+ # 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"',
@@ -246,6 +339,65 @@ class Chef
Chef::Config[:knife][:bootstrap_vault_item]
}
+ # OPTIONAL: This can be exposed as an class method on Knife
+ # subclasses instead - that would let us move deprecation handling
+ # up into the base clase.
+ DEPRECATED_FLAGS = {
+ # old_key: [:new_key, old_long, new_long]
+ auth_timeout: [:max_wait, "--max-wait SECONDS"],
+ host_key_verify: [:ssh_verify_host_key,
+ "--[no-]host-key-verify",
+ ],
+ ssh_user: [:connection_user,
+ "--ssh-user USER",
+ ],
+ ssh_password: [:connection_password,
+ "--ssh-password PASSWORD",
+ ],
+ ssh_port: [:connection_port,
+ "-ssh-port",
+ ],
+ ssl_peer_fingerprint: [:winrm_ssl_peer_fingerprint,
+ "--ssl-peer-fingerprint FINGERPRINT",
+ ],
+ winrm_user: [:connection_user,
+ "--winrm-user USER",
+ ],
+ winrm_password: [:connection_password,
+ "--winrm-password",
+ ],
+ winrm_port: [:connection_port,
+ "--winrm-port",
+ ],
+ winrm_authentication_protocol: [:winrm_auth_method,
+ "--winrm-authentication-protocol PROTOCOL",
+ ],
+ }.freeze
+
+ DEPRECATED_FLAGS.each do |flag, new_flag_config|
+ new_flag, old_long = new_flag_config
+ new_long = options[new_flag][:long]
+ new_flag_name = new_long.split(" ").first
+
+ option(flag, long: new_long,
+ description: "#{old_long} is deprecated. Use #{new_long} instead.",
+ boolean: options[new_flag][:boolean])
+ end
+
+ attr_accessor :client_builder
+ attr_accessor :chef_vault_handler
+ attr_reader :target_host
+
+ deps do
+ require "chef/json_compat"
+ require "tempfile"
+ require "chef_core/text" # i18n and standardized error structures
+ require "chef_core/target_host"
+ require "chef_core/target_resolver"
+ end
+
+ banner "knife bootstrap [PROTOCOL://][USER@]FQDN (options)"
+
def initialize(argv = [])
super
@client_builder = Chef::Knife::Bootstrap::ClientBuilder.new(
@@ -259,12 +411,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 +438,9 @@ 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 +480,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
@@ -350,49 +505,181 @@ class Chef
Erubis::Eruby.new(template).evaluate(bootstrap_context)
end
- def run
- if @config[:first_boot_attributes] && @config[:first_boot_attributes_from_file]
- raise Chef::Exceptions::BootstrapCommandInputError
+ # Check deprecated flags are used; map them to their new keys,
+ # and print a warning. Will not map a value to a new key if the
+ # CLI flag for that new key has also been specified.
+ # If both old and new flags are specified, this will warn
+ # and take the new flag value.
+ # This can be moved up to the base knife class if it's agreeable.
+ def warn_and_map_deprecated_flags
+ DEPRECATED_FLAGS.each do |old_key, new_flag_config|
+ new_key, = new_flag_config
+ if config.key?(old_key) && config_source(old_key) == :cli
+ # TODO - do we want the same warnings for knife config keys
+ # in absence of CLI keys?
+ if config.key?(new_key) && config_source(new_key) == :cli
+ new_key_name = "--#{new_key.to_s.tr("_", "-")}"
+ old_key_name = "--#{old_key.to_s.tr("_", "-")}"
+ ui.warn <<~EOM
+ You provided both #{new_key_name} and #{old_key_name}.
+ Using: '#{new_key_name.split(" ").first} #{config[new_key]}' because #{old_key_name} is deprecated.
+ EOM
+ else
+ config[new_key] = config[old_key]
+ unless Chef::Config[:silence_deprecation_warnings] == true
+ ui.warn options[old_key][:description]
+ end
+ end
+ end
end
+ end
+
+ def run
+ warn_and_map_deprecated_flags
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)}")
+ 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
- begin
- knife_ssh.run
- rescue Net::SSH::AuthenticationFailed
- if config[:ssh_password]
+ 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 +687,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 +697,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 +705,284 @@ 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 work around this issue you can use the flag `--winrm-no-verify-cert`
+ or add an entry like this to your knife configuration file:
+
+ # Verify all WinRM HTTPS connections
+ knife[:winrm_no_verify_cert] = true
+
+ You can also specify a ca_trust_file via --ca-trust-file,
+ or the expected fingerprint of the target host's certificate
+ 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[:connection_password] if config.key?(:connection_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"
+ # Fall back to the old knife config key name for back compat.
+ { 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?(:connection_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[:connection_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
+
+ 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
- command
+ # 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_sym
end
private
@@ -487,7 +1004,6 @@ class Chef
def incomplete_policyfile_options?
(!!config[:policy_name] ^ config[:policy_group])
end
-
end
end
end
diff --git a/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb
index 24ed0eb379..233350de73 100644
--- a/lib/chef/knife/bootstrap/chef_vault_handler.rb
+++ b/lib/chef/knife/bootstrap/chef_vault_handler.rb
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-require "chef/knife/bootstrap"
class Chef
class Knife
diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb
index 5fb0edc31b..09f4f67ced 100644
--- a/lib/chef/knife/bootstrap/client_builder.rb
+++ b/lib/chef/knife/bootstrap/client_builder.rb
@@ -20,7 +20,6 @@ require "chef/node"
require "chef/server_api"
require "chef/api_client/registration"
require "chef/api_client"
-require "chef/knife/bootstrap"
require "tmpdir"
class Chef
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..37fcf15682
--- /dev/null
+++ b/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb
@@ -0,0 +1,267 @@
+@rem
+@rem Author:: Seth Chisamore (<schisamo@chef.io>)
+@rem Copyright:: Copyright (c) 2011-2019 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
+
+: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 2019 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..355978c79d 100644
--- a/lib/chef/knife/core/bootstrap_context.rb
+++ b/lib/chef/knife/core/bootstrap_context.rb
@@ -161,14 +161,7 @@ class Chef
end
if Chef::Config[:fips]
- client_rb << <<-CONFIG.gsub(/^ {14}/, "")
- fips true
- require "chef/version"
- 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
+ client_rb << "fips true\n"
end
client_rb
@@ -194,28 +187,15 @@ class Chef
#
# chef version string to fetch the latest current version from omnitruck
- # If user is on X.Y.Z bootstrap will use the latest X release
- # X here can be 10 or 11
+ # If user is on X.Y.Z, bootstrap will use the latest X release
def latest_current_chef_version_string
- installer_version_string = nil
- if @config[:prerelease]
- installer_version_string = ["-p"]
- 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 -p to the installer string
- if chef_version_string.split(".").length > 3
- installer_version_string << "-p"
- end
- end
+ chef_version_string = if knife_config[:bootstrap_version]
+ knife_config[:bootstrap_version]
+ else
+ Chef::VERSION.split(".").first
+ end
- installer_version_string.join(" ")
+ "-v #{chef_version_string}"
end
def first_boot
diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb
index 3f0a697107..9801b8d033 100644
--- a/lib/chef/knife/core/ui.rb
+++ b/lib/chef/knife/core/ui.rb
@@ -76,7 +76,23 @@ class Chef
#
# @param message [String] the text string
def log(message)
- stderr.puts message
+ lines = message.split("\n")
+ first_line = lines.shift
+ stderr.puts first_line
+ # If the message is multiple lines,
+ # indent subsequent lines to align with the
+ # log type prefix ("ERROR: ", etc)
+ unless lines.empty?
+ prefix, = first_line.split(":", 2)
+ return if prefix.nil?
+ prefix_len = prefix.length
+ prefix_len -= 9 if color? # prefix includes 9 bytes of color escape sequences
+ prefix_len += 2 # include room to align to the ": " following PREFIX
+ padding = " " * prefix_len
+ lines.each do |line|
+ stderr.puts "#{padding}#{line}"
+ end
+ end
rescue Errno::EPIPE => e
raise e if @config[:verbosity] >= 2
exit 0
@@ -85,6 +101,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..c7b8666ed0
--- /dev/null
+++ b/lib/chef/knife/core/windows_bootstrap_context.rb
@@ -0,0 +1,383 @@
+#
+# 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
+ super(config, run_list, chef_config, secret)
+ end
+
+ def validation_key
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
+ IO.read(File.expand_path(@chef_config[:validation_key]))
+ else
+ false
+ end
+ end
+
+ def 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.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.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 << "fips true\n"
+ end
+
+ escape_and_echo(client_rb)
+ end
+
+ def get_log_location
+ if @chef_config[:config_log_location].equal?(:win_evt)
+ %Q{:#{@chef_config[:config_log_location]}\n}
+ elsif @chef_config[:config_log_location].equal?(:syslog)
+ raise "syslog is not supported for log_location on Windows OS\n"
+ elsif @chef_config[:config_log_location].equal?(STDOUT)
+ "STDOUT\n"
+ elsif @chef_config[:config_log_location].equal?(STDERR)
+ "STDERR\n"
+ elsif @chef_config[:config_log_location].nil? || @chef_config[:config_log_location].empty?
+ "STDOUT\n"
+ elsif @chef_config[:config_log_location]
+ %Q{"#{@chef_config[:config_log_location]}"\n}
+ else
+ "STDOUT\n"
+ end
+ end
+
+ def start_chef
+ 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
+ chef_version_string = if knife_config[:bootstrap_version]
+ knife_config[:bootstrap_version]
+ else
+ Chef::VERSION.split(".").first
+ end
+
+ "&v=#{chef_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_command('"') + "\n" + fallback_install_task_command
+ end
+
+ def bootstrap_directory
+ "C:\\chef"
+ end
+
+ def 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)
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
+ end
+
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
+ # This string should contain both the commands necessary to both create the files, as well as their content
+ def trusted_certs_content
+ content = ""
+ if @chef_config[:trusted_certs_dir]
+ Dir.glob(File.join(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}".tr("/", "\\")
+ if f.directory?
+ content << "mkdir #{file_on_node}\n"
+ else
+ content << "> #{file_on_node} (\n" +
+ escape_and_echo(IO.read(File.expand_path(f))) + "\n)\n"
+ end
+ end
+ end
+ end
+ content
+ end
+
+ def fallback_install_task_command
+ # This command will be executed by schtasks.exe in the batch
+ # code below. To handle tasks that contain arguments that
+ # need to be double quoted, schtasks allows the use of single
+ # quotes that will later be converted to double quotes
+ command = install_command("'")
+ <<~EOH
+ @set MSIERRORCODE=!ERRORLEVEL!
+ @if ERRORLEVEL 1 (
+ @echo WARNING: Failed to install 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/data/trusted_certs_empty/.gitkeep b/spec/data/trusted_certs_empty/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/spec/data/trusted_certs_empty/.gitkeep
diff --git a/spec/data/trusted_certs_empty/README.md b/spec/data/trusted_certs_empty/README.md
new file mode 100644
index 0000000000..e7e52627f1
--- /dev/null
+++ b/spec/data/trusted_certs_empty/README.md
@@ -0,0 +1 @@
+A directory with no certs. Used for testing directories with no certs during bootstrap.
diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb
index 3f42b08f21..766f91ab25 100644
--- a/spec/integration/knife/upload_spec.rb
+++ b/spec/integration/knife/upload_spec.rb
@@ -641,8 +641,8 @@ EOM
(right here) ------^
ERROR: /environments/x.json failed to write: Parse error reading JSON: parse error: premature EOF
- {
- (right here) ------^
+ {
+ (right here) ------^
EOH
warn = <<~EOH
diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb
index 5fc9de4de7..b6851f2d0e 100644
--- a/spec/support/shared/integration/integration_helper.rb
+++ b/spec/support/shared/integration/integration_helper.rb
@@ -19,6 +19,7 @@
require "tmpdir"
require "fileutils"
+require "chef_core/text"
require "chef/config"
require "chef/json_compat"
require "chef/server_api"
diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb
index 258d193cf1..f54c8ac1d6 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
@@ -441,11 +447,13 @@ describe Chef::Knife::Bootstrap do
end
end
- it "doesn't create /etc/chef/trusted_certs if :trusted_certs_dir is empty" do
- allow(Dir).to receive(:glob).and_call_original
- expect(Dir).to receive(:glob).with(File.join(trusted_certs_dir, "*.{crt,pem}")).and_return([])
- expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/trusted_certs})
+ context "when :trusted_cets_dir is empty" do
+ let(:trusted_certs_dir) { Chef::Util::PathHelper.cleanpath(File.join(File.dirname(__FILE__), "../../data/trusted_certs_empty")) }
+ it "doesn't create /etc/chef/trusted_certs if :trusted_certs_dir is empty" do
+ expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/trusted_certs})
+ end
end
+
end
context "when doing fips things" do
@@ -506,7 +514,7 @@ 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")
@@ -531,7 +539,7 @@ describe Chef::Knife::Bootstrap do
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
+ 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")
@@ -540,14 +548,116 @@ describe Chef::Knife::Bootstrap do
end
end
- describe "handling policyfile options" do
+ 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 "returns the value provided by the host argument" do
+ expect(knife.connection_protocol).to eq "ssh"
+ end
+ end
+
+ 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
+
+ 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
+ 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 "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 "#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 +668,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 +679,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 +690,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 +708,1342 @@ 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) do
+ { base_opts: true,
+ ssh_identity_opts: true,
+ ssh_opts: true,
+ gateway_opts: true,
+ host_verify_opts: true,
+ sudo_opts: true,
+ winrm_opts: true }
+ end
+
+ 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][:connection_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) do
+ {
+ 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",
+ }
+ end
+
+ 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) do
+ {
+ 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",
+ }
+ end
+
+ 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[:connection_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[:connection_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) do
+ {
+ 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",
+ }
+ end
+ 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) do
+ {
+ logger: Chef::Log,
+ operation_timeout: 60,
+ self_signed: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ winrm_basic_auth_only: false,
+ winrm_transport: "negotiate",
+ }
+ end
+ 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) do
+ {
+ 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,
+ }
+ end
+
+ 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[:connection_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) do
+ {
+ 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
+ }
+ 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
+ knife.config[:max_wait] = 150
+ knife.config[:connection_user] = "sshroot"
+ knife.config[:connection_port] = 1000
+ knife.config[:connection_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) do
+ {
+ 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,
+ }
+ end
+ 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) do
+ {
+ forward_agent: false,
+ key_files: [],
+ logger: Chef::Log,
+ keys_only: false,
+ sudo: false,
+ verify_host_key: true,
+ }
+ end
+ 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[:connection_password] = "opscode"
+ end
+
+ let(:expected_opts) do
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log,
+ password: "opscode",
+ }
+ end
+ 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) do
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log,
+ }
+ end
+ it "generates the correct options" do
+ expect(knife.base_opts).to eq expected_opts
+ end
+ end
+ end
+ end
+
+ 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
+
+ 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
- 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
+ # 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
+
+ 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[:connection_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
+
+ 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
- it "configures the hostname" do
- expect(knife_ssh.name_args.first).to eq("foo.example.com")
+ 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 user" do
- expect(knife_ssh.config[:ssh_user]).to eq("rooty")
+ 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
- it "configures the ssh password" do
- expect(knife_ssh.config[:ssh_password]).to eq("open_sesame")
+ 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
- it "configures the ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("4001")
+ 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
+
+ context "when use_sudo_password is also set" do
+ before do
+ knife.config[:use_sudo_password] = true
+ knife.config[:connection_password] = "opscode"
+ end
+ it "includes :connection_password value in a sudo-enabled configuration" do
+ expect(knife.sudo_opts).to eq({
+ sudo: true,
+ sudo_password: "opscode",
+ })
+ end
+ 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 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
- 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 "#ssh_opts" do
+ let(:connection_protocol) { nil }
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
+
+ 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
+
+ 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) do
+ {
+ winrm_transport: "negotiate",
+ winrm_basic_auth_only: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ operation_timeout: 60,
+ } end
+
+ it "generates a correct configuration hash with expected defaults" do
+ expect(knife.winrm_opts).to eq expected
+ end
+
+ context "with ssl_peer_fingerprint" do
+ let(:ssl_peer_fingerprint_expected) do
+ expected.merge({ ssl_peer_fingerprint: "ABCD" })
+ end
+
+ 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
+
+ context "with winrm_ssl" do
+ let(:ssl_expected) do
+ expected.merge({ ssl: true })
+ end
+ 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
+
+ context "with winrm_auth_method" do
+ let(:winrm_auth_method_expected) do
+ expected.merge({ winrm_transport: "freeaccess" })
+ end
+
+ 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
+
+ context "with ca_trust_file" do
+ let(:ca_trust_expected) do
+ expected.merge({ ca_trust_file: "/trust.me" })
+ end
+ 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
+
+ context "with kerberos auth" do
+ let(:kerberos_expected) do
+ expected.merge({
+ kerberos_service: "testsvc",
+ kerberos_realm: "TESTREALM",
+ winrm_transport: "kerberos",
+ })
+ end
+
+ before do
+ knife.config[:winrm_auth_method] = "kerberos"
+ knife.config[:kerberos_service] = "testsvc"
+ knife.config[:kerberos_realm] = "TESTREALM"
+ end
- it "configures the ssh user" do
- expect(knife_ssh.config[:ssh_user]).to eq("curiosity")
+ 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 ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("2430")
+ context "with winrm_basic_auth_only" do
+ before do
+ knife.config[:winrm_basic_auth_only] = true
+ end
+ let(:basic_auth_expected) do
+ expected.merge( { winrm_basic_auth_only: true } )
+ end
+ 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
- it "configures the ssh agent forwarding" do
- expect(knife_ssh.config[:forward_agent]).to eq(true)
+ 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 "configures the ssh identity file" do
- expect(knife_ssh.config[:ssh_identity_file]).to eq("~/.ssh/you.rsa")
+ it "performs the steps we expect to run a bootstrap" do
+ expect(knife).to receive(:warn_and_map_deprecated_flags).ordered
+ 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 "#warn_and_map_deprecated_flags" do
+ before do
+ Chef::Config[:silence_deprecation_warnings] = false
+ end
+
+ context "when a deprecated CLI flag is given on the CLI" do
+ before do
+ knife.config[:ssh_user] = "sshuser"
+ knife.merge_configs
+ end
+ it "maps the key value to the new key and points the human to the new flag" do
+ expect(knife.ui).to receive(:warn).with(/--ssh-user USER is deprecated. Use --connection-user USERNAME instead./)
+ knife.warn_and_map_deprecated_flags
+ expect(knife.config[:connection_user]).to eq "sshuser"
end
+ end
- it "configures the ssh gateway" do
- expect(knife_ssh.config[:ssh_gateway]).to eq("towel.blinkenlights.nl")
+ context "when a deprecated CLI flag is given on the CLI, along with its replacement" do
+ before do
+ knife.config[:ssh_user] = "sshuser"
+ knife.config[:connection_user] = "real-user"
+ knife.merge_configs
end
+ it "warns that both are provided and takes the non-deprecated value" do
+ expect(knife.ui).to receive(:warn).with(/You provided both --connection-user and --ssh-user.*'--connection-user real-user'/m)
+ knife.warn_and_map_deprecated_flags
+ expect(knife.config[:connection_user]).to eq "real-user"
+ end
+ end
+ end
- it "configures the ssh gateway identity" do
- expect(knife_ssh.config[:ssh_gateway_identity]).to eq("~/.ssh/gateway.rsa")
+ 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
- it "configures the host key verify mode" do
- expect(knife_ssh.config[:host_key_verify]).to eq(true)
+ 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
- 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 "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
- let(:client) { Chef::ApiClient.new }
- context "when running with a configured and present validation key" do
+ context "when a non-auth-failure occurs" do
+ let(:expected_error) { RuntimeError.new }
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)
+ 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
- it "configures the underlying ssh command and then runs it" do
- expect(knife_ssh).to receive(:run)
- knife.run
+ context "when an auth failure occurs" do
+ let(:expected_error) do
+ # 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
end
- it "falls back to password based auth when auth fails the first time" do
- allow(knife).to receive(:puts)
+ before do
+ require "net/ssh"
+ 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
+ context "and password auth was used" do
+ before do
+ knife.config[:connection_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 "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)
+ context "and password auth was not used" do
+ before do
+ knife.config.delete :connection_password
+ allow(target_host).to receive(:user).and_return "testuser"
+ end
+
+ 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 "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
+ 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
+
+ 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_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
+ 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
+
+ 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 "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
+ 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 "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ 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
- expect { knife.run }.to raise_error(SystemExit)
+ 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
+
+ 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
- it "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ 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 "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
+
+ describe "validate_winrm_transport_opts!" do
+ before do
+ allow(knife).to receive(:connection_protocol).and_return connection_protocol
+ end
- expect { knife.run }.to raise_error(SystemExit)
+ 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
diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb
index 5aa176557f..7b12177ab9 100644
--- a/spec/unit/knife/core/bootstrap_context_spec.rb
+++ b/spec/unit/knife/core/bootstrap_context_spec.rb
@@ -168,18 +168,6 @@ describe Chef::Knife::Core::BootstrapContext do
end
end
- describe "when a pre-release bootstrap_version is specified" do
- let(:chef_config) do
- {
- knife: { bootstrap_version: "11.12.4.rc.0" },
- }
- end
-
- it "should send the full version to the installer and set the pre-release flag" do
- expect(bootstrap_context.latest_current_chef_version_string).to eq("-v 11.12.4.rc.0 -p")
- end
- end
-
describe "when a bootstrap_version is not specified" do
it "should send the latest current to the installer" do
# Intentionally hard coded in order not to replicate the logic.
@@ -218,15 +206,12 @@ describe Chef::Knife::Core::BootstrapContext do
Chef::Config[:fips] = true
end
- it "adds the chef version check" do
- expect(bootstrap_context.config_content).to include <<-CONFIG.gsub(/^ {8}/, "")
- fips true
- require "chef/version"
- 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
+ after do
+ Chef::Config.reset!
+ end
+
+ it "sets fips mode in the client.rb" do
+ expect(bootstrap_context.config_content).to match(/fips true/)
end
end
@@ -256,20 +241,6 @@ describe Chef::Knife::Core::BootstrapContext do
end
end
- describe "prerelease" do
- it "isn't set in the config_content by default" do
- expect(bootstrap_context.config_content).not_to include("prerelease")
- end
-
- describe "when configured via cli" do
- let(:config) { { prerelease: true } }
-
- it "uses CLI value" do
- expect(bootstrap_context.latest_current_chef_version_string).to eq("-p")
- end
- end
- end
-
describe "#config_log_location" do
context "when config_log_location is nil" do
let(:chef_config) { { config_log_location: nil } }
diff --git a/spec/unit/knife/core/windows_bootstrap_context_spec.rb b/spec/unit/knife/core/windows_bootstrap_context_spec.rb
new file mode 100644
index 0000000000..3ab173f316
--- /dev/null
+++ b/spec/unit/knife/core/windows_bootstrap_context_spec.rb
@@ -0,0 +1,267 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright (c) 2014-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 "spec_helper"
+require "chef/knife/core/windows_bootstrap_context"
+describe Chef::Knife::Core::WindowsBootstrapContext do
+ let(:config) { {} }
+ let(:bootstrap_context) { Chef::Knife::Core::WindowsBootstrapContext.new(config, nil, Chef::Config, nil) }
+
+ describe "fips" do
+ before do
+ Chef::Config[:fips] = fips_mode
+ end
+
+ after do
+ Chef::Config.reset!
+ end
+
+ context "when fips is set" do
+ let(:fips_mode) { true }
+
+ it "sets fips mode in the client.rb" do
+ expect(bootstrap_context.config_content).to match(/fips true/)
+ end
+ end
+
+ context "when fips is not set" do
+ let(:fips_mode) { false }
+
+ it "sets fips mode in the client.rb" do
+ expect(bootstrap_context.config_content).not_to match(/fips true/)
+ end
+ end
+ end
+
+ describe "trusted_certs_script" do
+ let(:mock_cert_dir) { ::File.absolute_path(::File.join("spec", "assets", "fake_trusted_certs")) }
+ let(:script_output) { bootstrap_context.trusted_certs_script }
+ let(:crt_files) { ::Dir.glob(::File.join(mock_cert_dir, "*.crt")) }
+ let(:pem_files) { ::Dir.glob(::File.join(mock_cert_dir, "*.pem")) }
+ let(:other_files) { ::Dir.glob(::File.join(mock_cert_dir, "*")) - crt_files - pem_files }
+
+ before do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(trusted_certs_dir: mock_cert_dir))
+ end
+
+ it "should echo every .crt file in the trusted_certs directory" do
+ crt_files.each do |f|
+ echo_file = ::File.read(f).gsub(/^/, "echo.")
+ expect(script_output).to include(::File.join("trusted_certs", ::File.basename(f)))
+ expect(script_output).to include(echo_file)
+ end
+ end
+
+ it "should echo every .pem file in the trusted_certs directory" do
+ pem_files.each do |f|
+ echo_file = ::File.read(f).gsub(/^/, "echo.")
+ expect(script_output).to include(::File.join("trusted_certs", ::File.basename(f)))
+ expect(script_output).to include(echo_file)
+ end
+ end
+
+ it "should not echo files which aren't .crt or .pem files" do
+ other_files.each do |f|
+ echo_file = ::File.read(f).gsub(/^/, "echo.")
+ expect(script_output).to_not include(::File.join("trusted_certs", ::File.basename(f)))
+ expect(script_output).to_not include(echo_file)
+ end
+ end
+ end
+
+ describe "validation_key" do
+ before do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(validation_key: "C:\\chef\\key.pem"))
+ end
+
+ it "should return false if validation_key does not exist" do
+ allow(::File).to receive(:expand_path).with("C:\\chef\\key.pem").and_call_original
+ allow(::File).to receive(:exist?).and_return(false)
+ expect(bootstrap_context.validation_key).to eq(false)
+ end
+ end
+
+ describe "#get_log_location" do
+
+ context "when config_log_location value is nil" do
+ it "sets STDOUT in client.rb as default" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: nil))
+ expect(bootstrap_context.get_log_location).to eq("STDOUT\n")
+ end
+ end
+
+ context "when config_log_location value is empty" do
+ it "sets STDOUT in client.rb as default" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: ""))
+ expect(bootstrap_context.get_log_location).to eq("STDOUT\n")
+ end
+ end
+
+ context "when config_log_location value is STDOUT" do
+ it "sets STDOUT in client.rb" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: STDOUT))
+ expect(bootstrap_context.get_log_location).to eq("STDOUT\n")
+ end
+ end
+
+ context "when config_log_location value is STDERR" do
+ it "sets STDERR in client.rb" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: STDERR))
+ expect(bootstrap_context.get_log_location).to eq("STDERR\n")
+ end
+ end
+
+ context "when config_log_location value is path to a file" do
+ it "sets file path in client.rb" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: "C:\\chef\\chef.log"))
+ expect(bootstrap_context.get_log_location).to eq("\"C:\\chef\\chef.log\"\n")
+ end
+ end
+
+ context "when config_log_location value is :win_evt" do
+ it "sets :win_evt in client.rb" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: :win_evt))
+ expect(bootstrap_context.get_log_location).to eq(":win_evt\n")
+ end
+ end
+
+ context "when config_log_location value is :syslog" do
+ it "raise error with message and exit" do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_location: :syslog))
+ expect { bootstrap_context.get_log_location }.to raise_error("syslog is not supported for log_location on Windows OS\n")
+ end
+ end
+
+ end
+
+ describe "#config_content" do
+ before do
+ bootstrap_context.instance_variable_set(:@chef_config, Mash.new(config_log_level: :info,
+ config_log_location: STDOUT,
+ chef_server_url: "http://chef.example.com:4444",
+ validation_client_name: "chef-validator-testing",
+ file_cache_path: "c:/chef/cache",
+ file_backup_path: "c:/chef/backup",
+ cache_options: ({ path: "c:/chef/cache/checksums", skip_expires: true })
+ ))
+ end
+
+ it "generates the config file data" do
+ expected = <<~EXPECTED
+ echo.chef_server_url "http://chef.example.com:4444"
+ echo.validation_client_name "chef-validator-testing"
+ echo.file_cache_path "c:/chef/cache"
+ echo.file_backup_path "c:/chef/backup"
+ echo.cache_options ^({:path =^> "c:/chef/cache/checksums", :skip_expires =^> true}^)
+ echo.# Using default node name ^(fqdn^)
+ echo.log_level :info
+ echo.log_location STDOUT
+ EXPECTED
+ expect(bootstrap_context.config_content).to eq expected
+ end
+ end
+
+ describe "latest_current_windows_chef_version_query" do
+ it "returns the major version of the current version of Chef" do
+ stub_const("Chef::VERSION", "15.1.2")
+ expect(bootstrap_context.latest_current_windows_chef_version_query).to eq("&v=15")
+ end
+
+ end
+
+ describe "msi_url" do
+ context "when config option is not set" do
+ before do
+ expect(bootstrap_context).to receive(:latest_current_windows_chef_version_query).and_return("&v=something")
+ end
+
+ it "returns a chef.io msi url with minimal url parameters" do
+ reference_url = "https://www.chef.io/chef/download?p=windows&v=something"
+ expect(bootstrap_context.msi_url).to eq(reference_url)
+ end
+
+ it "returns a chef.io msi url with provided url parameters substituted" do
+ reference_url = "https://www.chef.io/chef/download?p=windows&pv=machine&m=arch&DownloadContext=ctx&v=something"
+ expect(bootstrap_context.msi_url("machine", "arch", "ctx")).to eq(reference_url)
+ end
+ end
+
+ context "when msi_url config option is set" do
+ let(:custom_url) { "file://something" }
+ let(:config) { { msi_url: custom_url, install: true } }
+
+ it "returns the overriden url" do
+ expect(bootstrap_context.msi_url).to eq(custom_url)
+ end
+
+ it "doesn't introduce any unnecessary query parameters if provided by the template" do
+ expect(bootstrap_context.msi_url("machine", "arch", "ctx")).to eq(custom_url)
+ end
+ end
+ end
+
+ describe "bootstrap_install_command for bootstrap through WinRM" do
+ context "when bootstrap_install_command option is passed on CLI" do
+ let(:bootstrap) { Chef::Knife::Bootstrap.new(["--bootstrap-install-command", "chef-client"]) }
+ before do
+ bootstrap.config[:bootstrap_install_command] = "chef-client"
+ end
+
+ it "sets the bootstrap_install_command option under Chef::Config::Knife object" do
+ expect(Chef::Config[:knife][:bootstrap_install_command]).to eq("chef-client")
+ end
+
+ after do
+ bootstrap.config.delete(:bootstrap_install_command)
+ Chef::Config[:knife].delete(:bootstrap_install_command)
+ end
+ end
+
+ context "when bootstrap_install_command option is not passed on CLI" do
+ let(:bootstrap) { Chef::Knife::Bootstrap.new([]) }
+ it "does not set the bootstrap_install_command option under Chef::Config::Knife object" do
+ expect(Chef::Config[:knife][:bootstrap_install_command]). to eq(nil)
+ end
+ end
+ end
+
+ describe "bootstrap_install_command for bootstrap through SSH" do
+ context "when bootstrap_install_command option is passed on CLI" do
+ let(:bootstrap) { Chef::Knife::Bootstrap.new(["--bootstrap-install-command", "chef-client"]) }
+ before do
+ bootstrap.config[:bootstrap_install_command] = "chef-client"
+ end
+
+ it "sets the bootstrap_install_command option under Chef::Config::Knife object" do
+ expect(Chef::Config[:knife][:bootstrap_install_command]).to eq("chef-client")
+ end
+
+ after do
+ bootstrap.config.delete(:bootstrap_install_command)
+ Chef::Config[:knife].delete(:bootstrap_install_command)
+ end
+ end
+
+ context "when bootstrap_install_command option is not passed on CLI" do
+ let(:bootstrap) { Chef::Knife::Bootstrap.new([]) }
+ it "does not set the bootstrap_install_command option under Chef::Config::Knife object" do
+ expect(Chef::Config[:knife][:bootstrap_install_command]). to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb
index ff89bc6f30..f48ab4019c 100644
--- a/spec/unit/knife_spec.rb
+++ b/spec/unit/knife_spec.rb
@@ -299,7 +299,7 @@ describe Chef::Knife do
expect(Chef::Config[:log_level]).to eql(:warn)
end
- it "prefers the default value if no config or command line value is present" do
+ it "prefers the default value if no config or command line value is present and reports the source as default" do
knife_command = KnifeSpecs::TestYourself.new([]) # empty argv
knife_command.configure_chef
expect(knife_command.config[:opt_with_default]).to eq("default-value")
@@ -310,18 +310,32 @@ describe Chef::Knife do
knife_command = KnifeSpecs::TestYourself.new([]) # empty argv
knife_command.configure_chef
expect(knife_command.config[:opt_with_default]).to eq("from-knife-config")
+ expect(knife_command.config_source(:opt_with_default)).to eq (:config)
end
- it "prefers a value from command line over Chef::Config and the default" do
+ it "correctly reports Chef::Config as the source when a a config entry comes from there" do
Chef::Config[:knife][:opt_with_default] = "from-knife-config"
+ knife_command = KnifeSpecs::TestYourself.new([]) # empty argv
+ knife_command.configure_chef
+ expect(knife_command.config_source(:opt_with_default)).to eq (:config)
+ end
+
+ it "prefers a value from command line over Chef::Config and the default and reports the source as CLI" do
knife_command = KnifeSpecs::TestYourself.new(["-D", "from-cli"])
knife_command.configure_chef
expect(knife_command.config[:opt_with_default]).to eq("from-cli")
+ expect(knife_command.config_source(:opt_with_default)).to eq (:cli)
+ end
+ it "correctly reports CLI as the source when a config entry comes from the CLI" do
+ knife_command = KnifeSpecs::TestYourself.new(["-D", "from-cli"])
+ knife_command.configure_chef
+ expect(knife_command.config_source(:opt_with_default)).to eq (:cli)
end
it "merges `listen` config to Chef::Config" do
- Chef::Knife.run(%w{test yourself --no-listen}, Chef::Application::Knife.options)
+ knife_command = Chef::Knife.run(%w{test yourself --no-listen}, Chef::Application::Knife.options)
expect(Chef::Config[:listen]).to be(false)
+ expect(knife_command.config_source(:listen)).to eq(:cli)
end
context "verbosity is one" do