diff options
author | Marc A. Paradise <marc.paradise@gmail.com> | 2019-03-05 06:07:15 -0500 |
---|---|---|
committer | Marc A. Paradise <marc.paradise@gmail.com> | 2019-03-19 14:25:11 -0400 |
commit | df56179cee3c41ab7291fe0e112101850f5b4e5a (patch) | |
tree | 31ab226211a776238782201e4f43bda07c1f9a28 | |
parent | 43fd40e799ec9d58ffe390d25acb61e02f45b64b (diff) | |
download | chef-df56179cee3c41ab7291fe0e112101850f5b4e5a.tar.gz |
initial Windows bootstrap support
Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
-rw-r--r-- | lib/chef/knife/bootstrap.rb | 128 | ||||
-rw-r--r-- | lib/chef/knife/bootstrap/options.rb | 350 | ||||
-rw-r--r-- | lib/chef/knife/core/ui.rb | 7 | ||||
-rw-r--r-- | lib/chef/knife/core/windows_bootstrap_context.rb | 413 |
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 |