summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc A. Paradise <marc.paradise@gmail.com>2019-03-05 06:07:15 -0500
committerMarc A. Paradise <marc.paradise@gmail.com>2019-03-19 14:25:11 -0400
commitdf56179cee3c41ab7291fe0e112101850f5b4e5a (patch)
tree31ab226211a776238782201e4f43bda07c1f9a28
parent43fd40e799ec9d58ffe390d25acb61e02f45b64b (diff)
downloadchef-df56179cee3c41ab7291fe0e112101850f5b4e5a.tar.gz
initial Windows bootstrap support
Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
-rw-r--r--lib/chef/knife/bootstrap.rb128
-rw-r--r--lib/chef/knife/bootstrap/options.rb350
-rw-r--r--lib/chef/knife/core/ui.rb7
-rw-r--r--lib/chef/knife/core/windows_bootstrap_context.rb413
4 files changed, 692 insertions, 206 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index e0b2124f68..e27b4228f7 100644
--- a/lib/chef/knife/bootstrap.rb
+++ b/lib/chef/knife/bootstrap.rb
@@ -38,7 +38,6 @@ class Chef
attr_reader :target_host
deps do
- require "chef/knife/core/bootstrap_context"
require "chef/json_compat"
require "tempfile"
require "chef_core/text" # i18n and standardized error structures
@@ -71,7 +70,11 @@ class Chef
#
# @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
@@ -89,11 +92,6 @@ class Chef
end
end
- def user_name
- if host_descriptor
- @user_name ||= host_descriptor.split("@").reverse[1]
- end
- end
def bootstrap_template
# Allow passing a bootstrap template or use the default
@@ -138,12 +136,18 @@ class Chef
end
def bootstrap_context
- @bootstrap_context ||= Knife::Core::BootstrapContext.new(
- config,
- config[:run_list],
- Chef::Config,
- secret
- )
+ #require "pry"; binding.pry
+ @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
@@ -181,7 +185,6 @@ class Chef
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")
@@ -191,33 +194,41 @@ class Chef
ui.info("Connecting to #{ui.color(server_name, :bold)}")
begin
- # TODO live stream output may take some doing, and knife ssh does it already
- @target_host = ChefCore::TargetHost.new(server_name, ssh_opts)
+ # 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, config[:protocol] || "ssh",
+ connection_opts, max_expanded_targets: 1)
+ @target_host = resolver.targets.first
+ # rescue: TargetResolverError
target_host.connect!
+ # TODO Creating bootstrap context needs a live connection to query OS info
+ unless client_builder.client_path.nil?
+ bootstrap_context.client_pem = client_builder.client_path
+ end
bootstrap_path = render_and_upload_bootstrap
- r = target_host.run_command(ssh_command(bootstrap_path))
+ r = target_host.run_command(bootstrap_command(bootstrap_path))
if r.exit_status != 0
- ui.error("The following error occurred on on #{server_name}:")
+ ui.error("The following error occurred on #{server_name}:")
ui.error(r.stderr)
exit 1
end
- # TODO - woudl be nice to pull in chef-cdore error printing, but that'll change expected output
# TODO mp 2019-02-22 this *should* be the same behavior under train without
# forcing the behavior here, but we need to verify that.
#
# rescue Net::SSH::AuthenticationFailed
- # if config[:ssh_password]
+ # if config[:password]
# raise
# else
- # ui.info("Failed to authenticate #{knife_ssh.config[:ssh_user]} - trying password auth")
+ # ui.info("Failed to authenticate #{knife_ssh.config[:user]} - trying password auth")
# knife_ssh_with_password_auth.run
# def knife_ssh_with_password_auth
# # prompt for a password then return a knife ssh object with that password set
# # and with ssh_identity_file set to nil
# ssh = knife_ssh
# ssh.config[:ssh_identity_file] = nil
- # ssh.config[:ssh_password] = ssh.get_password
+ # ssh.config[:password] = ssh.get_password
# ssh
# end
# end
@@ -229,9 +240,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
@@ -253,41 +261,59 @@ class Chef
true
end
- # setup a Chef::Knife::Ssh object using the passed config options
+ def connection_protocol
+
+ end
+
+
+ # Createa configuration object based on setup a Chef::Knife::Ssh object using the passed config options
#
- # @return Chef::Knife::Ssh
- def ssh_opts
+ # @return a configuration hash suitable for connecting to the remote host.
+ def connection_opts
+ # Mapping of our options ot train options - they're pretty similar with removal of
+ # the ssh- prefix, but there's more to corre2ct
+ # TODO - is now the time to change flag names for consistency?
opts = {
- # TODO based on khife ssh, we will set :keys_only to true if a key is present.
- host: server_name,
- port: config[:ssh_port],
- user: user_name || config[:ssh_user],
+ port: config[:port], # Default if it's not in the connection string
+ user: config[:user], # "
+ password: config[:password], # TODO - check if we need to exclude if not set, diff behavior for nil?
key_files: config[:ssh_identity_file],
- logger: Chef::Log
+ logger: Chef::Log,
+ # WinRM options - they will be ignored for ssh
+ # TODO train will throw if this is not valid, should be OK as-is
+ winrm_transport: config[:winrm_transport],
+ self_signed: config[:winrm_self_signed_cert],
+ ssl: config[:winrm_ssl]
}
- if config[:ssh_password]
- opts[:password] = config[:ssh_password]
- end
+
if config[:use_sudo]
opts[:sudo] = true
+ # TODO this preserves original logic - we're using the provided password for sudo
+ # if sudo is enabled. Note that train supports a separate sudo password.
+ # TODO - check original, what if password was not given? Where do we validate?
if opts[:use_sudo_password]
- opts[:sudo_password] = config[:ssh_password]
+ opts[:sudo_password] = config[:password]
end
if opts[:preserve_home]
opts[:sudo_options] = "-H"
end
end
- opts
- # TODO - looks like we can password, or we can sudo password, but we can't
- # do both currently in bootstrap. train permits both if we want to add the option
- # TODO - we can now allow a custom sudo_command
- #
+ if config[:password]
+ opts[:password] = config[:password]
+ end
+
+ if config[:ssh_identity_file]
+ # TODO - to get the matching original knife bootstrap fallback behavior of prompting for password
+ # when we don't provide it, I think we'll want to _not_ do this here - we should get automatic
+ # keyboard-interactive auth if we don't set this and key fails.
+ opts[:keys_only] = true
+ end
# 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
- # TODO train appears to false this to always false. We'll need to make it an option.
+ # TODO train appears to false this to always false. We'll need to make it an option.
# ssh.config[:host_key_verify] = config[:host_key_verify]
# ssh.config[:on_error] = true
# TODO: proxy command
@@ -301,20 +327,28 @@ class Chef
# short: "-e",
# long: "--exit-on-error",
# description: "Immediately exit if an error is encountered.",
+ opts
end
+
+
def render_and_upload_bootstrap
content = render_template
- remote_path = target_host.normalize_path(File.join(target_host.temp_dir, "bootstrap.sh"))
+ 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 ssh command for bootrapping
+ # build the command string for bootrapping
# @return String
- def ssh_command(remote_path)
- "sh #{remote_path} "
+ def bootstrap_command(remote_path)
+ if target_host.base_os == :windows
+ "cmd.exe /C #{remote_path}"
+ else
+ "sh #{remote_path} "
+ end
end
private
diff --git a/lib/chef/knife/bootstrap/options.rb b/lib/chef/knife/bootstrap/options.rb
index d4ad714afa..7e6b356d75 100644
--- a/lib/chef/knife/bootstrap/options.rb
+++ b/lib/chef/knife/bootstrap/options.rb
@@ -15,31 +15,34 @@
# limitations under the License.
#
-module Chef
- module Knife
+class Chef
+ class Knife
class Bootstrap
module Options
def self.included(klass)
- # SSH - :host
- klass.option :ssh_user,
- short: "-x USERNAME",
- long: "--ssh-user USERNAME",
- description: "The ssh username",
- default: "root"
-
- # SSH - :password
- klass.option :ssh_password,
+ # Common connectivity options
+ klass.option :user, # TODO - deprecate ssh_user which this replaces
+ short: "-u USERNAME",
+ long: "--user USERNAME",
+ description: "The remote user to connect as"
+
+ klass.option :password, # TODO - deprecate ssh_password
short: "-P PASSWORD",
long: "--ssh-password PASSWORD",
- description: "The ssh password"
+ description: "The password of the remote user."
- # SSH :port
- klass.option :ssh_port,
+ klass.option :port,
short: "-p PORT",
long: "--ssh-port PORT",
- description: "The ssh port",
+ description: "The port on the target node to connect to.",
+
proc: Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
+ klass.option :protocol,
+ short: "-o PROTOCOL",
+ long: "--protocol PROTOCOL",
+ description: "The protocol to use to connect to the target node. Supports ssh and winrm."
+
# TODO SSH train gives bastion_host which seeems to map to getway/gateway_identity -
# though not exactly.
klass.option :ssh_gateway,
@@ -72,51 +75,53 @@ module Chef
long: "--ssh-identity-file IDENTITY_FILE",
description: "The SSH identity file used for authentication"
- klass.option :chef_node_name,
- short: "-N NAME",
- long: "--node-name NAME",
- description: "The Chef node name for your new node"
+ # ssh options - train options[:verify_host_key]
+ klass.option :host_key_verify,
+ long: "--[no-]host-key-verify",
+ description: "Verify host key, enabled by default.",
+ boolean: true,
+ default: true
+ # argument to installer in chef-full, via bootstrap_context
klass.option :prerelease,
long: "--prerelease",
description: "Install the pre-release chef gems"
- # client.rb
+ # client.rb content via chef-full/bootstrap_context
klass.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
+ # client.rb content via chef-full/bootstrap_context
klass.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
+ # client.rb content via bootstrap_context
klass.option :bootstrap_proxy_user,
long: "--bootstrap-proxy-user PROXY_USER",
description: "The proxy authentication username for the node being bootstrapped"
- # client.rb
+ # client.rb content via bootstrap_context
klass.option :bootstrap_proxy_pass,
long: "--bootstrap-proxy-pass PROXY_PASS",
description: "The proxy authentication password for the node being bootstrapped"
- # client.rb
+ # client.rb content via bootstrap_context
klass.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 klass.option is used internally by Opscode",
+ description: "Do not proxy locations for the node being bootstrapped; this klass.option is used internally by Chef",
proc: Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np }
- # client.rb
+ # client.rb content via bootstrap_context
klass.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."
-
- # bootstrap_context - client.rb
+ # client.rb content via bootstrap_context
klass.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.",
@@ -128,137 +133,164 @@ module Chef
v
}
- # bootstrap_context - client.rb
- klass.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, prefixes to ssh command. train: [:sudo] - auto prefixes everything
- klass.option :use_sudo,
- long: "--sudo",
- description: "Execute the bootstrap via sudo",
- boolean: true
-
- # runtime - prefixes to ssh command string
- klass.option :preserve_home,
- long: "--sudo-preserve-home",
- description: "Preserve non-root user HOME environment variable with sudo",
- boolean: true
-
- # runtime - prefixes to ssh command string
- klass.option :use_sudo_password,
- long: "--use-sudo-password",
- description: "Execute the bootstrap via sudo with password",
- boolean: false
-
- # runtime - client_builder - set runlist when creating node
- klass.option :run_list,
- short: "-r RUN_LIST",
- long: "--run-list RUN_LIST",
- description: "Comma separated list of roles/recipes to apply",
- proc: lambda { |o| o.split(/[\s,]+/) },
- default: []
-
- # runtime - client_builder - set policy name when creating node
- klass.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
- klass.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
- klass.option :tags,
- long: "--tags TAGS",
- description: "Comma separated list of tags to apply to the node",
- proc: lambda { |o| o.split(/[\s,]+/) },
- default: []
-
- # runtime - bootstrap template
- klass.option :first_boot_attributes,
- short: "-j JSON_ATTRIBS",
- long: "--json-attributes",
- description: "A JSON string to be added to the first run of chef-client",
- proc: lambda { |o| Chef::JSONCompat.parse(o) },
- default: nil
-
- # runtime - bootstrap template
- klass.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
-
- # ssh options - train options[:verify_host_key]
- klass.option :host_key_verify,
- long: "--[no-]host-key-verify",
- description: "Verify host key, enabled by default.",
- boolean: true,
- default: true
-
-
- # bootstrap template
- # Create ohai hints in /etc/hef/ohai/hints, fname=hintname, content=value
- klass.option :hint,
- long: "--hint HINT_NAME[=HINT_FILE]",
- description: "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
- proc: Proc.new { |h|
- Chef::Config[:knife][:hints] ||= Hash.new
- name, path = h.split("=")
- Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new
- }
-
- # bootstrap overrides that change bootstrap behavior - runs on target
- klass.option :bootstrap_url,
- long: "--bootstrap-url URL",
- description: "URL to a custom installation script",
- proc: Proc.new { |u| Chef::Config[:knife][:bootstrap_url] = u }
-
- klass.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 }
-
- klass.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 }
-
- # runtime on target - can this go away with switch to train + actions - uses mixlib-install.
- klass.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 }
-
- # runtime - can this go away with switch to train + actions - uses mixlib-install.
- klass.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 }
-
- klass.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"
-
- klass.option :bootstrap_vault_json,
- long: "--bootstrap-vault-json VAULT_JSON",
- description: "A JSON string with the vault(s) and item(s) to be updated"
-
- klass.option :bootstrap_vault_item,
- long: "--bootstrap-vault-item VAULT_ITEM",
- description: 'A single vault and item to update as "vault:item"',
- proc: Proc.new { |i|
- (vault, item) = i.split(/:/)
- Chef::Config[:knife][:bootstrap_vault_item] ||= {}
- Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
- Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
- Chef::Config[:knife][:bootstrap_vault_item]
- }
+ # bootstrap_context - client.rb
+ klass.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, prefixes to ssh command. train: [:sudo] - auto prefixes everything
+ klass.option :use_sudo,
+ long: "--sudo",
+ description: "Execute the bootstrap via sudo",
+ boolean: true
+
+ # runtime - prefixes to ssh command string
+ klass.option :preserve_home,
+ long: "--sudo-preserve-home",
+ description: "Preserve non-root user HOME environment variable with sudo",
+ boolean: true
+
+ # runtime - prefixes to ssh command string
+ klass.option :use_sudo_password,
+ long: "--use-sudo-password",
+ description: "Execute the bootstrap via sudo with password",
+ boolean: false
+
+ # runtime - client_builder
+ klass.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
+ klass.option :run_list,
+ short: "-r RUN_LIST",
+ long: "--run-list RUN_LIST",
+ description: "Comma separated list of roles/recipes to apply",
+ proc: lambda { |o| o.split(/[\s,]+/) },
+ default: []
+
+ # runtime - client_builder - set policy name when creating node
+ klass.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
+ klass.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
+ klass.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
+ klass.option :first_boot_attributes,
+ short: "-j JSON_ATTRIBS",
+ long: "--json-attributes",
+ description: "A JSON string to be added to the first run of chef-client",
+ proc: lambda { |o| Chef::JSONCompat.parse(o) },
+ default: nil
+
+ # bootstrap template
+ klass.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
+
+ # bootstrap template
+ # Create ohai hints in /etc/chef/ohai/hints, fname=hintname, content=value
+ klass.option :hint,
+ long: "--hint HINT_NAME[=HINT_FILE]",
+ description: "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
+ proc: Proc.new { |h|
+ Chef::Config[:knife][:hints] ||= Hash.new
+ name, path = h.split("=")
+ Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new
+ }
+
+ # bootstrap override: url of a an installer shell script touse in place of omnitruck
+ # TODO - this replaces --msi-url out of knife windows bootstrap
+ klass.option :bootstrap_url,
+ long: "--bootstrap-url URL",
+ description: "URL to a custom installation script",
+ proc: Proc.new { |u| Chef::Config[:knife][:bootstrap_url] = u }
+
+
+ # bootstrap override: Do this instead of our own setup.sh from omnitruck. Causes bootstrap_url to be ignored.
+ klass.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
+ klass.option :bootstrap_preinstall_command,
+ long: "--bootstrap-preinstall-command COMMANDS",
+ description: "Custom commands to run before installing chef-client",
+ proc: Proc.new { |preic| Chef::Config[:knife][:bootstrap_preinstall_command] = preic }
+
+ # bootstrap template
+ klass.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
+ klass.option :bootstrap_curl_options,
+ long: "--bootstrap-curl-options OPTIONS",
+ description: "Add options to curl when install chef-client",
+ proc: Proc.new { |co| Chef::Config[:knife][:bootstrap_curl_options] = co }
+
+ # chef_vault_handler
+ klass.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
+ klass.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
+ klass.option :bootstrap_vault_item,
+ long: "--bootstrap-vault-item VAULT_ITEM",
+ description: 'A single vault and item to update as "vault:item"',
+ proc: Proc.new { |i|
+ (vault, item) = i.split(/:/)
+ Chef::Config[:knife][:bootstrap_vault_item] ||= {}
+ Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
+ Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
+ Chef::Config[:knife][:bootstrap_vault_item]
+ }
+
+ # TODO
+ # Windows only
+ klass.option :install_as_service,
+ :long => "--install-as-service",
+ :description => "Install chef-client as a Windows service. (Windows only)",
+ :default => false
+ # TODO
+ # Windows only
+ klass.option :winrm_self_signed_cert,
+ long: "--winrm-self-signed-cert",
+ :description => "Expect a self-signed certificate when transport is 'ssl'. Defaults to false.",
+ :default => false
+ klass.option :winrm_transport,
+ long: "--winrm-transport TRANSPORT",
+ :description => "Specify WinRM transport. Supported values are ssl, plaintext, Defaults to 'negotiate'.",
+ :default => "negotiate"
+
+ klass.option :winrm_ssl,
+ long: "--winrm-ssl",
+ :description => "Connect to WinRM over HTTPS. Defaults to false",
+ :default => false
end
end
diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb
index 3f0a697107..c696378912 100644
--- a/lib/chef/knife/core/ui.rb
+++ b/lib/chef/knife/core/ui.rb
@@ -85,6 +85,13 @@ class Chef
alias :info :log
alias :err :log
+ # Print a Debug
+ #
+ # @param message [String] the text string
+ def debug(message)
+ log("#{color('DEBUG:', :blue, :bold)} #{message}")
+ end
+
# Print a warning message
#
# @param message [String] the text string
diff --git a/lib/chef/knife/core/windows_bootstrap_context.rb b/lib/chef/knife/core/windows_bootstrap_context.rb
new file mode 100644
index 0000000000..93a7c85bcd
--- /dev/null
+++ b/lib/chef/knife/core/windows_bootstrap_context.rb
@@ -0,0 +1,413 @@
+#
+# 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
+ attr_accessor :client_pem
+
+ def initialize(config, run_list, chef_config, secret=nil)
+ @config = config
+ @run_list = run_list
+ @chef_config = chef_config
+ @secret = secret
+ # Compatibility with Chef 12 and Chef 11 versions
+ begin
+ # Pass along the secret parameter for Chef 12
+ super(config, run_list, chef_config, secret)
+ rescue ArgumentError
+ # The Chef 11 base class only has parameters for initialize
+ super(config, run_list, chef_config)
+ end
+ end
+
+ def validation_key
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
+ IO.read(File.expand_path(@chef_config[:validation_key]))
+ else
+ false
+ end
+ end
+
+ def secret
+ escape_and_echo(@config[:secret])
+ end
+
+ def trusted_certs_script
+ @trusted_certs_script ||= trusted_certs_content
+ end
+
+ def config_content
+ client_rb = <<-CONFIG
+chef_server_url "#{@chef_config[:chef_server_url]}"
+validation_client_name "#{@chef_config[:validation_client_name]}"
+file_cache_path "c:/chef/cache"
+file_backup_path "c:/chef/backup"
+cache_options ({:path => "c:/chef/cache/checksums", :skip_expires => true})
+ CONFIG
+ if @config[:chef_node_name]
+ client_rb << %Q{node_name "#{@config[:chef_node_name]}"\n}
+ else
+ client_rb << "# Using default node name (fqdn)\n"
+ end
+
+ if @chef_config[:config_log_level]
+ client_rb << %Q{log_level :#{@chef_config[:config_log_level]}\n}
+ else
+ client_rb << "log_level :info\n"
+ end
+
+ client_rb << "log_location #{get_log_location}"
+
+ # We configure :verify_api_cert only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if !@config[:node_verify_api_cert].nil? || knife_config.has_key?(:verify_api_cert)
+ value = @config[:node_verify_api_cert].nil? ? knife_config[:verify_api_cert] : @config[:node_verify_api_cert]
+ client_rb << %Q{verify_api_cert #{value}\n}
+ end
+
+ # We configure :ssl_verify_mode only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if @config[:node_ssl_verify_mode] || knife_config.has_key?(:ssl_verify_mode)
+ value = case @config[:node_ssl_verify_mode]
+ when "peer"
+ :verify_peer
+ when "none"
+ :verify_none
+ when nil
+ knife_config[:ssl_verify_mode]
+ else
+ nil
+ end
+
+ if value
+ client_rb << %Q{ssl_verify_mode :#{value}\n}
+ end
+ end
+
+ if @config[:ssl_verify_mode]
+ client_rb << %Q{ssl_verify_mode :#{knife_config[:ssl_verify_mode]}\n}
+ end
+
+ if knife_config[:bootstrap_proxy]
+ client_rb << "\n"
+ client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n} if knife_config[:bootstrap_no_proxy]
+ end
+
+ if knife_config[:bootstrap_no_proxy]
+ client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n}
+ end
+
+ if @config[:secret]
+ client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n}
+ end
+
+ unless trusted_certs_script.empty?
+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n}
+ end
+
+ if Chef::Config[:fips]
+ client_rb << <<-CONFIG
+fips true
+chef_version = ::Chef::VERSION.split(".")
+unless chef_version[0].to_i > 12 || (chef_version[0].to_i == 12 && chef_version[1].to_i >= 8)
+ raise "FIPS Mode requested but not supported by this client"
+end
+CONFIG
+ end
+
+ escape_and_echo(client_rb)
+ end
+
+ def get_log_location
+ if @chef_config[:config_log_location].equal?(:win_evt)
+ %Q{:#{@chef_config[:config_log_location]}\n}
+ elsif @chef_config[:config_log_location].equal?(:syslog)
+ raise "syslog is not supported for log_location on Windows OS\n"
+ elsif (@chef_config[:config_log_location].equal?(STDOUT))
+ "STDOUT\n"
+ elsif (@chef_config[:config_log_location].equal?(STDERR))
+ "STDERR\n"
+ elsif @chef_config[:config_log_location].nil? || @chef_config[:config_log_location].empty?
+ "STDOUT\n"
+ elsif @chef_config[:config_log_location]
+ %Q{"#{@chef_config[:config_log_location]}"\n}
+ else
+ "STDOUT\n"
+ end
+ end
+
+ def start_chef
+ bootstrap_environment_option = bootstrap_environment.nil? ? '' : " -E #{bootstrap_environment}"
+ start_chef = "SET \"PATH=%SystemRoot%\\system32;%SystemRoot%;%SystemRoot%\\System32\\Wbem;%SYSTEMROOT%\\System32\\WindowsPowerShell\\v1.0\\;C:\\ruby\\bin;C:\\opscode\\chef\\bin;C:\\opscode\\chef\\embedded\\bin\"\n"
+ start_chef << "chef-client -c c:/chef/client.rb -j c:/chef/first-boot.json#{bootstrap_environment_option}\n"
+ end
+
+ def latest_current_windows_chef_version_query
+ installer_version_string = nil
+ if @config[:prerelease]
+ installer_version_string = "&prerelease=true"
+ else
+ chef_version_string = if knife_config[:bootstrap_version]
+ knife_config[:bootstrap_version]
+ else
+ Chef::VERSION.split(".").first
+ end
+
+ installer_version_string = "&v=#{chef_version_string}"
+
+ # If bootstrapping a pre-release version add the prerelease query string
+ if chef_version_string.split(".").length > 3
+ installer_version_string << "&prerelease=true"
+ end
+ end
+
+ installer_version_string
+ end
+
+ def win_wget
+ # I tried my best to figure out how to properly url decode and switch / to \
+ # but this is VBScript - so I don't really care that badly.
+ win_wget = <<-WGET
+url = WScript.Arguments.Named("url")
+path = WScript.Arguments.Named("path")
+proxy = null
+'* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all
+'* / into \. Also assume that file:/// is a local absolute path and that file://<foo>
+'* is possibly a network file path.
+If InStr(url, "file://") = 1 Then
+url = Unescape(url)
+If InStr(url, "file:///") = 1 Then
+sourcePath = Mid(url, Len("file:///") + 1)
+Else
+sourcePath = Mid(url, Len("file:") + 1)
+End If
+sourcePath = Replace(sourcePath, "/", "\\")
+
+Set objFSO = CreateObject("Scripting.FileSystemObject")
+If objFSO.Fileexists(path) Then objFSO.DeleteFile path
+objFSO.CopyFile sourcePath, path, true
+Set objFSO = Nothing
+
+Else
+Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
+Set wshShell = CreateObject( "WScript.Shell" )
+Set objUserVariables = wshShell.Environment("USER")
+
+rem http proxy is optional
+rem attempt to read from HTTP_PROXY env var first
+On Error Resume Next
+
+If NOT (objUserVariables("HTTP_PROXY") = "") Then
+proxy = objUserVariables("HTTP_PROXY")
+
+rem fall back to named arg
+ElseIf NOT (WScript.Arguments.Named("proxy") = "") Then
+proxy = WScript.Arguments.Named("proxy")
+End If
+
+If NOT isNull(proxy) Then
+rem setProxy method is only available on ServerXMLHTTP 6.0+
+Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
+objXMLHTTP.setProxy 2, proxy
+End If
+
+On Error Goto 0
+
+objXMLHTTP.open "GET", url, false
+objXMLHTTP.send()
+If objXMLHTTP.Status = 200 Then
+Set objADOStream = CreateObject("ADODB.Stream")
+objADOStream.Open
+objADOStream.Type = 1
+objADOStream.Write objXMLHTTP.ResponseBody
+objADOStream.Position = 0
+Set objFSO = Createobject("Scripting.FileSystemObject")
+If objFSO.Fileexists(path) Then objFSO.DeleteFile path
+Set objFSO = Nothing
+objADOStream.SaveToFile path
+objADOStream.Close
+Set objADOStream = Nothing
+End If
+Set objXMLHTTP = Nothing
+End If
+WGET
+ escape_and_echo(win_wget)
+ end
+
+ def win_wget_ps
+ win_wget_ps = <<-WGET_PS
+param(
+ [String] $remoteUrl,
+ [String] $localPath
+)
+
+$ProxyUrl = $env:http_proxy;
+$webClient = new-object System.Net.WebClient;
+
+if ($ProxyUrl -ne '') {
+ $WebProxy = New-Object System.Net.WebProxy($ProxyUrl,$true)
+ $WebClient.Proxy = $WebProxy
+}
+
+$webClient.DownloadFile($remoteUrl, $localPath);
+WGET_PS
+
+ escape_and_echo(win_wget_ps)
+ end
+
+ def install_chef
+ # The normal install command uses regular double quotes in
+ # the install command, so request such a string from install_command
+ install_chef = install_command('"') + "\n" + fallback_install_task_command
+ end
+
+ def bootstrap_directory
+ bootstrap_directory = "C:\\chef"
+ end
+
+ def local_download_path
+ local_download_path = "%TEMP%\\chef-client-latest.msi"
+ end
+
+ def msi_url(machine_os=nil, machine_arch=nil, download_context=nil)
+ # The default msi path has a number of url query parameters - we attempt to substitute
+ # such parameters in as long as they are provided by the template.
+
+ if @config[:msi_url].nil? || @config[:msi_url].empty?
+ url = "https://www.chef.io/chef/download?p=windows"
+ url += "&pv=#{machine_os}" unless machine_os.nil?
+ url += "&m=#{machine_arch}" unless machine_arch.nil?
+ url += "&DownloadContext=#{download_context}" unless download_context.nil?
+ url += latest_current_windows_chef_version_query
+ else
+ @config[:msi_url]
+ end
+ end
+
+ def first_boot
+ escape_and_echo(super.to_json)
+ end
+
+ # escape WIN BATCH special chars
+ # and prefixes each line with an
+ # echo
+ def escape_and_echo(file_contents)
+ file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1')
+ end
+
+ private
+
+ def install_command(executor_quote)
+ if @config[:install_as_service]
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} ADDLOCAL=#{executor_quote}ChefClientFeature,ChefServiceFeature#{executor_quote}"
+ else
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
+ end
+ end
+
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
+ # This string should contain both the commands necessary to both create the files, as well as their content
+ def trusted_certs_content
+ content = ""
+ if @chef_config[:trusted_certs_dir]
+ Dir.glob(File.join(Chef::Util::PathHelper.escape_glob_dir(@chef_config[:trusted_certs_dir]), "*.{crt,pem}")).each do |cert|
+ content << "> #{bootstrap_directory}/trusted_certs/#{File.basename(cert)} (\n" +
+ escape_and_echo(IO.read(File.expand_path(cert))) + "\n)\n"
+ end
+ end
+ content
+ end
+
+ def client_d_content
+ content = ""
+ if @chef_config[:client_d_dir] && File.exist?(@chef_config[:client_d_dir])
+ root = Pathname(@chef_config[:client_d_dir])
+ root.find do |f|
+ relative = f.relative_path_from(root)
+ if f != root
+ file_on_node = "#{bootstrap_directory}/client.d/#{relative}".gsub("/","\\")
+ if f.directory?
+ content << "mkdir #{file_on_node}\n"
+ else
+ content << "> #{file_on_node} (\n" +
+ escape_and_echo(IO.read(File.expand_path(f))) + "\n)\n"
+ end
+ end
+ end
+ end
+ content
+ end
+
+ def fallback_install_task_command
+ # This command will be executed by schtasks.exe in the batch
+ # code below. To handle tasks that contain arguments that
+ # need to be double quoted, schtasks allows the use of single
+ # quotes that will later be converted to double quotes
+ command = install_command('\'')
+<<-EOH
+ @set MSIERRORCODE=!ERRORLEVEL!
+ @if ERRORLEVEL 1 (
+ @echo WARNING: Failed to install Chef Client MSI package in remote context with status code !MSIERRORCODE!.
+ @echo WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614
+ @set OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log"
+ @move "%CHEF_CLIENT_MSI_LOG_PATH%" "!OLDLOGLOCATION!" > NUL
+ @echo WARNING: Saving installation log of failure at !OLDLOGLOCATION!
+ @echo WARNING: Retrying installation with local context...
+ @schtasks /create /f /sc once /st 00:00:00 /tn chefclientbootstraptask /ru SYSTEM /rl HIGHEST /tr \"cmd /c #{command} & sleep 2 & waitfor /s %computername% /si chefclientinstalldone\"
+
+ @if ERRORLEVEL 1 (
+ @echo ERROR: Failed to create Chef Client installation scheduled task with status code !ERRORLEVEL! > "&2"
+ ) else (
+ @echo Successfully created scheduled task to install Chef Client.
+ @schtasks /run /tn chefclientbootstraptask
+ @if ERRORLEVEL 1 (
+ @echo ERROR: Failed to execut Chef Client installation scheduled task with status code !ERRORLEVEL!. > "&2"
+ ) else (
+ @echo Successfully started Chef Client installation scheduled task.
+ @echo Waiting for installation to complete -- this may take a few minutes...
+ waitfor chefclientinstalldone /t 600
+ if ERRORLEVEL 1 (
+ @echo ERROR: Timed out waiting for Chef Client package to install
+ ) else (
+ @echo Finished waiting for Chef Client package to install.
+ )
+ @schtasks /delete /f /tn chefclientbootstraptask > NUL
+ )
+ )
+ ) else (
+ @echo Successfully installed Chef Client package.
+ )
+EOH
+ end
+ end
+ end
+ end
+end