summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McLellan <btm@loftninjas.org>2018-10-16 15:05:47 -0400
committerBryan McLellan <btm@loftninjas.org>2019-05-06 12:56:55 -0400
commit9d5f5c40362d1fd7b0323cf0880300d6165b3a94 (patch)
tree2bb91f44829455b5566c0de4ad6bb154dd610e4b
parent7560313217c851c5b018b27d6ea4bae8c3af0ff1 (diff)
downloadchef-9d5f5c40362d1fd7b0323cf0880300d6165b3a94.tar.gz
Target Mode initial implementation
Signed-off-by: Bryan McLellan <btm@chef.io>
-rw-r--r--Gemfile.lock2
-rw-r--r--chef-config/lib/chef-config/config.rb33
-rw-r--r--chef.gemspec2
-rw-r--r--lib/chef/application/client.rb15
-rw-r--r--lib/chef/client.rb47
-rw-r--r--lib/chef/dsl/universal.rb1
-rw-r--r--lib/chef/exceptions.rb2
-rw-r--r--lib/chef/formatters/doc.rb1
-rw-r--r--lib/chef/formatters/minimal.rb1
-rw-r--r--lib/chef/guard_interpreter/default_guard_interpreter.rb4
-rw-r--r--lib/chef/mixin/train_or_shell.rb74
-rw-r--r--lib/chef/node_map.rb17
-rw-r--r--lib/chef/provider/execute.rb4
-rw-r--r--lib/chef/resource/execute.rb3
-rw-r--r--lib/chef/run_context.rb19
-rw-r--r--lib/chef/train_transport.rb140
-rw-r--r--spec/unit/node_map_spec.rb6
17 files changed, 355 insertions, 16 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 3fc4ba443c..f66c654f0c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -55,6 +55,7 @@ PATH
syslog-logger (~> 1.6)
train-core (~> 2.0, >= 2.0.12)
tty-screen (~> 0.6)
+ train-core
uuidtools (~> 2.1.5)
chef (15.0.263-universal-mingw32)
addressable
@@ -85,6 +86,7 @@ PATH
syslog-logger (~> 1.6)
train-core (~> 2.0, >= 2.0.12)
tty-screen (~> 0.6)
+ train-core
uuidtools (~> 2.1.5)
win32-api (~> 1.5.3)
win32-certstore (~> 0.3)
diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb
index 0c257a49b5..910f0e024d 100644
--- a/chef-config/lib/chef-config/config.rb
+++ b/chef-config/lib/chef-config/config.rb
@@ -293,10 +293,11 @@ module ChefConfig
# the cache path.
unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root)
secondary_cache_path = PathHelper.join(user_home, ".chef")
+ secondary_cache_path = target_mode? ? "#{secondary_cache_path}/#{target_mode.host}" : secondary_cache_path
ChefConfig.logger.trace("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}")
secondary_cache_path
else
- primary_cache_path
+ target_mode? ? "#{primary_cache_path}/#{target_mode.host}" : primary_cache_path
end
end
end
@@ -435,6 +436,22 @@ module ChefConfig
# * Chef 11 mode doesn't expose RBAC objects
default :osc_compat, false
end
+
+ # RFCxxx Target Mode support, value is the name of a remote device to Chef against
+ # --target exists as a shortcut to enabling target_mode and setting the host
+ configurable(:target)
+
+ config_context :target_mode do
+ config_strict_mode false # we don't want to have to add all train configuration keys here
+ default :enabled, false
+ default :protocol, "ssh"
+ # typical additional keys: host, user, password
+ end
+
+ def self.target_mode?
+ target_mode.enabled
+ end
+
default :chef_server_url, "https://localhost:443"
default(:chef_server_root) do
@@ -625,7 +642,15 @@ module ChefConfig
# `node_name` of the client.
#
# If chef-zero is enabled, this defaults to nil (no authentication).
- default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") }
+ default(:client_key) do
+ if chef_zero.enabled
+ nil
+ elsif target_mode?
+ platform_specific_path("/etc/chef/#{target_mode.host}/client.pem")
+ else
+ platform_specific_path("/etc/chef/client.pem")
+ end
+ end
# A credentials file may contain a complete client key, rather than the path
# to one.
@@ -645,7 +670,9 @@ module ChefConfig
# This secret is used to decrypt encrypted data bag items.
default(:encrypted_data_bag_secret) do
- if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret"))
+ if target_mode? && File.exist?(platform_specific_path("/etc/chef/#{target_mode.host}/encrypted_data_bag_secret"))
+ platform_specific_path("/etc/chef/#{target_mode.host}/encrypted_data_bag_secret")
+ elsif File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret"))
platform_specific_path("/etc/chef/encrypted_data_bag_secret")
else
nil
diff --git a/chef.gemspec b/chef.gemspec
index ebd6d192bb..9cccc1744a 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -38,6 +38,8 @@ Gem::Specification.new do |s|
s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4"
s.add_dependency "ffi-libarchive"
s.add_dependency "chef-zero", ">= 14.0.11"
+ s.add_dependency "train-core"
+
s.add_dependency "plist", "~> 3.2"
s.add_dependency "iniparse", "~> 1.4"
s.add_dependency "addressable"
diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb
index 390acfba04..2acf180ca4 100644
--- a/lib/chef/application/client.rb
+++ b/lib/chef/application/client.rb
@@ -298,6 +298,15 @@ class Chef::Application::Client < Chef::Application
description: "Use cached cookbooks without overwriting local differences from the #{Chef::Dist::SERVER_PRODUCT}.",
boolean: false
+ option :target,
+ short: "-t TARGET",
+ long: "--target TARGET",
+ description: "Target the Chef Client run against a remote system or device",
+ proc: lambda { |target|
+ Chef::Log.warn "-- EXPERIMENTAL -- Target mode activated, resources and dsl may change without warning -- EXPERIMENTAL --"
+ target
+ }
+
IMMEDIATE_RUN_SIGNAL = "1".freeze
RECONFIGURE_SIGNAL = "H".freeze
@@ -348,6 +357,12 @@ class Chef::Application::Client < Chef::Application
Chef::Config.chef_zero.host = config[:chef_zero_host] if config[:chef_zero_host]
Chef::Config.chef_zero.port = config[:chef_zero_port] if config[:chef_zero_port]
+ if config[:target] || Chef::Config.target
+ Chef::Config.target_mode.enabled = true
+ Chef::Config.target_mode.host = config[:target] || Chef::Config.target
+ Chef::Config.node_name = Chef::Config.target_mode.host unless Chef::Config.node_name
+ end
+
if Chef::Config[:daemonize]
Chef::Config[:interval] ||= 1800
end
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 0c4acc0a8d..7b0303a9e3 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -55,6 +55,8 @@ require "chef/mixin/deprecation"
require "ohai"
require "rbconfig"
require "chef/dist"
+require "ostruct"
+require "forwardable"
class Chef
# == Chef::Client
@@ -65,6 +67,7 @@ class Chef
extend Chef::Mixin::Deprecation
+ extend Forwardable
#
# The status of the Chef run.
#
@@ -136,6 +139,9 @@ class Chef
attr_reader :events
attr_reader :logger
+
+ def_delegator :@run_context, :transport_connection
+
#
# Creates a new Chef::Client.
#
@@ -151,8 +157,11 @@ class Chef
@json_attribs = json_attribs || {}
@logger = args.delete(:logger) || Chef::Log.with_child
- @ohai = Ohai::System.new(logger: logger)
-
+ @ohai = if Chef::Config.target_mode?
+ OpenStruct.new(data: Mash.new)
+ else
+ Ohai::System.new(logger: logger)
+ end
event_handlers = configure_formatters + configure_event_loggers
event_handlers += Array(Chef::Config[:event_handlers])
@@ -244,9 +253,15 @@ class Chef
logger.info("*** #{Chef::Dist::PRODUCT} #{Chef::VERSION} ***")
logger.info("Platform: #{RUBY_PLATFORM}")
logger.info "#{Chef::Dist::CLIENT.capitalize} pid: #{Process.pid}"
+ logger.info "Targeting node: #{Chef::Config.target_mode.host}" if Chef::Config.target_mode?
logger.debug("#{Chef::Dist::CLIENT.capitalize} request_id: #{request_id}")
enforce_path_sanity
- run_ohai
+
+ if Chef::Config.target_mode?
+ get_ohai_data_remotely
+ else
+ run_ohai
+ end
unless Chef::Config[:solo_legacy_mode]
register
@@ -556,6 +571,32 @@ class Chef
end
#
+ # Populate the minimal ohai attributes defined in #run_ohai with data train collects.
+ #
+ # Eventually ohai may support colleciton of data.
+ #
+ def get_ohai_data_remotely
+ ohai.data[:fqdn] = if transport_connection.respond_to?(:hostname)
+ transport_connection.hostname
+ else
+ Chef::Config[:target_mode][:host]
+ end
+ if transport_connection.respond_to?(:os)
+ ohai.data[:platform] = transport_connection.os.name
+ ohai.data[:platform_version] = transport_connection.os.release
+ ohai.data[:os] = transport_connection.os.family_hierarchy[1]
+ ohai.data[:platform_family] = transport_connection.os.family
+ end
+ # train does not collect these specifically
+ # ohai.data[:machinename] = nil
+ # ohai.data[:hostname] = nil
+ # ohai.data[:os_version] = nil # kernel version
+
+ ohai.data[:ohai_time] = Time.now.to_f
+ events.ohai_completed(node)
+ end
+
+ #
# Run ohai plugins. Runs all ohai plugins unless minimal_ohai is specified.
#
# Sends the ohai_completed event when finished.
diff --git a/lib/chef/dsl/universal.rb b/lib/chef/dsl/universal.rb
index eb90acfa2c..256bc2820d 100644
--- a/lib/chef/dsl/universal.rb
+++ b/lib/chef/dsl/universal.rb
@@ -25,6 +25,7 @@ require "chef/mixin/powershell_exec"
require "chef/mixin/powershell_out"
require "chef/mixin/shell_out"
require "chef/mixin/lazy_module_include"
+require "chef/mixin/train_or_shell"
class Chef
module DSL
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 4ab9434906..f109b9406e 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -132,7 +132,7 @@ class Chef
# Can't find a Resource of this type that is valid on this platform.
class NoSuchResourceType < NameError
def initialize(short_name, node)
- super "Cannot find a resource for #{short_name} on #{node[:platform]} version #{node[:platform_version]}"
+ super "Cannot find a resource for #{short_name} on #{node[:platform]} version #{node[:platform_version]} with target_mode? #{Chef::Config.target_mode?}"
end
end
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
index 1fdbb66b1b..d1849a72d9 100644
--- a/lib/chef/formatters/doc.rb
+++ b/lib/chef/formatters/doc.rb
@@ -42,6 +42,7 @@ class Chef
def run_start(version, run_status)
puts_line "Starting #{Chef::Dist::PRODUCT}, version #{version}"
+ puts_line "Targeting node: #{Chef::Config.target_mode.host}" if Chef::Config.target_mode?
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb
index 6b12be71b7..1a755afd94 100644
--- a/lib/chef/formatters/minimal.rb
+++ b/lib/chef/formatters/minimal.rb
@@ -29,6 +29,7 @@ class Chef
# Called at the very start of a Chef Run
def run_start(version, run_status)
puts_line "Starting #{Chef::Dist::PRODUCT}, version #{version}"
+ puts_line "Targeting node: #{Chef::Config.target_mode.host}" if Chef::Config.target_mode?
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/guard_interpreter/default_guard_interpreter.rb b/lib/chef/guard_interpreter/default_guard_interpreter.rb
index c4c09ac47a..402f05f288 100644
--- a/lib/chef/guard_interpreter/default_guard_interpreter.rb
+++ b/lib/chef/guard_interpreter/default_guard_interpreter.rb
@@ -17,11 +17,13 @@
#
require "chef/mixin/shell_out"
+require "chef/mixin/train_or_shell"
class Chef
class GuardInterpreter
class DefaultGuardInterpreter
include Chef::Mixin::ShellOut
+ include Chef::Mixin::TrainOrShell
protected
@@ -33,7 +35,7 @@ class Chef
public
def evaluate
- result = shell_out(@command, default_env: false, **@command_opts)
+ result = train_or_shell(@command, default_env: false, **@command_opts)
Chef::Log.debug "Command failed: #{result.stderr}" unless result.status.success?
result.status.success?
# Timeout fails command rather than chef-client run, see:
diff --git a/lib/chef/mixin/train_or_shell.rb b/lib/chef/mixin/train_or_shell.rb
new file mode 100644
index 0000000000..c976094b14
--- /dev/null
+++ b/lib/chef/mixin/train_or_shell.rb
@@ -0,0 +1,74 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright 2018, 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 "ostruct"
+
+class Chef
+ module Mixin
+ module TrainOrShell
+ #
+ # Train #run_command returns a Train::Extras::CommandResult which
+ # includes `exit_status` but Mixlib::Shellout returns exitstatus
+ # This wrapper makes the result look like Mixlib::ShellOut to make it
+ # easier to swap providers from #shell_out to #train_or_shell
+ #
+ def train_to_shellout_result(stdout, stderr, exit_status)
+ status = OpenStruct.new(success?: ( exit_status == 0 ))
+ OpenStruct.new(stdout: stdout, stderr: stderr, exitstatus: exit_status, status: status)
+ end
+
+ def train_or_shell(*args, **opts)
+ if Chef::Config.target_mode?
+ result = run_context.transport_connection.run_command(args)
+ train_to_shellout_result(result.stdout, result.stderr, result.exit_status)
+ else
+ shell_out(*args, opts)
+ end
+ end
+
+ def train_or_shell!(*args, **opts)
+ if Chef::Config.target_mode?
+ result = run_context.transport_connection.run_command(*args)
+ raise Mixlib::ShellOut::ShellCommandFailed, "Unexpected exit status of #{result.exit_status} running #{args}" if result.exit_status != 0
+ train_to_shellout_result(result.stdout, result.stderr, result.exit_status)
+ else
+ shell_out!(*args, opts)
+ end
+ end
+
+ def train_or_powershell(*args, **opts)
+ if Chef::Config.target_mode?
+ run_context.transport_connection.run_command(args)
+ train_to_shellout_result(result.stdout, result.stderr, result.exit_status)
+ else
+ powershell_out(*args)
+ end
+ end
+
+ def train_or_powershell!(*args, **opts)
+ if Chef::Config.target_mode?
+ result = run_context.transport_connection.run_command(args)
+ raise Mixlib::ShellOut::ShellCommandFailed, "Unexpected exit status of #{result.exit_status} running #{args}" if result.exit_status != 0
+ train_to_shellout_result(result.stdout, result.stderr, result.exit_status)
+ else
+ powershell_out!(*args)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb
index 50a763f686..e66e249a97 100644
--- a/lib/chef/node_map.rb
+++ b/lib/chef/node_map.rb
@@ -66,7 +66,7 @@ class Chef
#
# @return [NodeMap] Returns self for possible chaining
#
- def set(key, klass, platform: nil, platform_version: nil, platform_family: nil, os: nil, canonical: nil, override: nil, allow_cookbook_override: false, __core_override__: false, chef_version: nil, &block) # rubocop:disable Lint/UnderscorePrefixedVariableName
+ def set(key, klass, platform: nil, platform_version: nil, platform_family: nil, os: nil, canonical: nil, override: nil, allow_cookbook_override: false, __core_override__: false, chef_version: nil, target_mode: nil, &block) # rubocop:disable Lint/UnderscorePrefixedVariableName
new_matcher = { klass: klass }
new_matcher[:platform] = platform if platform
new_matcher[:platform_version] = platform_version if platform_version
@@ -77,6 +77,7 @@ class Chef
new_matcher[:override] = override if override
new_matcher[:cookbook_override] = allow_cookbook_override
new_matcher[:core_override] = __core_override__
+ new_matcher[:target_mode] = target_mode
if chef_version && Chef::VERSION !~ chef_version
return map
@@ -269,11 +270,21 @@ class Chef
end
end
+ # Succeeds if:
+ # - we are in target mode, and the target_mode filter is true
+ # - we are not in target mode
+ #
+ def matches_target_mode?(filters)
+ return true unless Chef::Config.target_mode?
+ !!filters[:target_mode]
+ end
+
def filters_match?(node, filters)
matches_black_white_list?(node, filters, :os) &&
matches_black_white_list?(node, filters, :platform_family) &&
matches_black_white_list?(node, filters, :platform) &&
- matches_version_list?(node, filters, :platform_version)
+ matches_version_list?(node, filters, :platform_version) &&
+ matches_target_mode?(filters)
end
def block_matches?(node, block)
@@ -295,6 +306,8 @@ class Chef
# "provides" lines with identical filters sort by class name (ascending).
#
def compare_matchers(key, new_matcher, matcher)
+ cmp = compare_matcher_properties(new_matcher[:target_mode], matcher[:target_mode])
+ return cmp if cmp != 0
cmp = compare_matcher_properties(new_matcher[:block], matcher[:block])
return cmp if cmp != 0
cmp = compare_matcher_properties(new_matcher[:platform_version], matcher[:platform_version])
diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb
index 02db7cb593..8b5530d672 100644
--- a/lib/chef/provider/execute.rb
+++ b/lib/chef/provider/execute.rb
@@ -25,7 +25,7 @@ class Chef
class Execute < Chef::Provider
extend Forwardable
- provides :execute
+ provides :execute, target_mode: true
def_delegators :new_resource, :command, :returns, :environment, :user, :domain, :password, :group, :cwd, :umask, :creates, :elevated, :default_env
@@ -55,7 +55,7 @@ class Chef
converge_by("execute #{description}") do
begin
- shell_out!(command, opts)
+ train_or_shell!(command, opts)
rescue Mixlib::ShellOut::ShellCommandFailed
if sensitive?
ex = Mixlib::ShellOut::ShellCommandFailed.new("Command execution failed. STDOUT/STDERR suppressed for sensitive resource")
diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb
index de3b7e044a..295cf357cb 100644
--- a/lib/chef/resource/execute.rb
+++ b/lib/chef/resource/execute.rb
@@ -24,7 +24,8 @@ class Chef
class Resource
class Execute < Chef::Resource
resource_name :execute
- provides :execute
+ provides :execute, target_mode: true
+
description "Use the execute resource to execute a single command. Commands that"\
" are executed with this resource are (by their nature) not idempotent,"\
" as they are typically unique to the environment in which they are run."\
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index dc322b254f..403a30f6fb 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -25,6 +25,7 @@ require "chef/log"
require "chef/recipe"
require "chef/run_context/cookbook_compiler"
require "chef/event_dispatch/events_output_stream"
+require "chef/train_transport"
require "forwardable"
class Chef
@@ -590,6 +591,22 @@ class Chef
reboot_info.size > 0
end
+ # Remote transport from Train
+ #
+ # @return [Train::Plugins::Transport] The child class for our train transport.
+ #
+ def transport
+ @transport ||= Chef::TrainTransport.build_transport(logger)
+ end
+
+ # Remote connection object from Train
+ #
+ # @return [Train::Plugins::Transport::BaseConnection]
+ #
+ def transport_connection
+ transport.connection
+ end
+
#
# Create a child RunContext.
#
@@ -650,6 +667,8 @@ class Chef
rest_clean
rest_clean=
unreachable_cookbook?
+ transport
+ transport_connection
}
def initialize(parent_run_context)
diff --git a/lib/chef/train_transport.rb b/lib/chef/train_transport.rb
new file mode 100644
index 0000000000..420851d3a5
--- /dev/null
+++ b/lib/chef/train_transport.rb
@@ -0,0 +1,140 @@
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright 2018, 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-config/mixin/credentials"
+require "train"
+
+class Chef
+ class TrainTransport
+ #
+ # Returns a RFC099 credentials profile as a hash
+ #
+ def self.load_credentials(profile)
+ extend ChefConfig::Mixin::Credentials
+
+ # Tomlrb.load_file returns a hash with keys as strings
+ credentials = parse_credentials_file
+ if contains_split_fqdn?(credentials, profile)
+ Chef::Log.warn("Credentials file #{credentials_file_path} contains target '#{profile}' as a Hash, expected a string.")
+ Chef::Log.warn("Hostnames must be surrounded by single quotes, e.g. ['host.example.org']")
+ end
+
+ # host names must be specified in credentials file as ['foo.example.org'] with quotes
+ if !credentials.nil? && !credentials[profile].nil?
+ Mash.from_hash(credentials[profile]).symbolize_keys
+ else
+ nil
+ end
+ end
+
+ # Toml creates hashes when a key is separated by periods, e.g.
+ # [host.example.org] => { host: { example: { org: {} } } }
+ #
+ # Returns true if the above example is true
+ #
+ # A hostname has to be specified as ['host.example.org']
+ # This will be a common mistake so we should catch it
+ #
+ def self.contains_split_fqdn?(hash, fqdn)
+ n = 0
+ matches = 0
+ fqdn_split = fqdn.split(".")
+
+ # if the top level of the hash matches the first part of the fqdn, continue
+ if hash.key?(fqdn_split[n])
+ matches += 1
+ until n == fqdn_split.length - 1
+ # if we still have fqdn elements but ran out of depth, return false
+ return false if !hash[fqdn_split[n]].is_a?(Hash)
+ if hash[fqdn_split[n]].key?(fqdn_split[n + 1])
+ matches += 1
+ return true if matches == fqdn_split.length
+ end
+ hash = hash[fqdn_split[n]]
+ n += 1
+ end
+ end
+ false
+ end
+
+ # ChefConfig::Mixin::Credentials.credentials_file_path is designed around knife,
+ # overriding it here.
+ #
+ # Credentials file preference:
+ #
+ # 1) target_mode.credentials_file
+ # 2) /etc/chef/TARGET_MODE_HOST/credentials
+ # 3) #credentials_file_path from parent ($HOME/.chef/credentials)
+ #
+ def self.credentials_file_path
+ tm_config = Chef::Config.target_mode
+ profile = tm_config.host
+
+ credentials_file =
+ if tm_config.credentials_file
+ if File.exists?(tm_config.credentials_file)
+ tm_config.credentials_file
+ else
+ raise ArgumentError, "Credentials file specified for target mode does not exist: '#{tm_config.credentials_file}'"
+ end
+ elsif File.exists?(Chef::Config.platform_specific_path("/etc/chef/#{profile}/credentials"))
+ Chef::Config.platform_specific_path("/etc/chef/#{profile}/credentials")
+ else
+ super
+ end
+ if credentials_file
+ Chef::Log.debug("Loading credentials file '#{credentials_file}' for target '#{profile}'")
+ else
+ Chef::Log.debug("No credentials file found for target '#{profile}'")
+ end
+
+ credentials_file
+ end
+
+ def self.build_transport(logger = Chef::Log.with_child(subsystem: "transport"))
+ # TODO: Consider supporting parsing the protocol from a URI passed to `--target`
+ #
+ train_config = Hash.new
+
+ # Load the target_mode config context from Chef::Config, and place any valid settings into the train configuration
+ tm_config = Chef::Config.target_mode
+ protocol = tm_config.protocol
+ train_config = tm_config.to_hash.select { |k| Train.options(protocol).key?(k) }
+ Chef::Log.trace("Using target mode options from Chef config file: #{train_config.keys.join(', ')}") if train_config
+
+ # Load the credentials file, and place any valid settings into the train configuration
+ credentials = load_credentials(tm_config.host)
+ if credentials
+ valid_settings = credentials.select { |k| Train.options(protocol).key?(k) }
+ valid_settings[:enable_password] = credentials[:enable_password] if credentials.key?(:enable_password)
+ train_config.merge!(valid_settings)
+ Chef::Log.trace("Using target mode options from credentials file: #{valid_settings.keys.join(', ')}") if valid_settings
+ end
+
+ train_config[:logger] = logger
+
+ # Train handles connection retries for us
+ Train.create(protocol, train_config)
+ rescue SocketError => e # likely a dns failure, not caught by train
+ e.message.replace "Error connecting to #{train_config[:target]} - #{e.message}"
+ raise e
+ rescue Train::PluginLoadError
+ logger.error("Invalid target mode protocol: #{protocol}")
+ exit(false)
+ end
+ end
+end
diff --git a/spec/unit/node_map_spec.rb b/spec/unit/node_map_spec.rb
index 9c161f3893..d486d913aa 100644
--- a/spec/unit/node_map_spec.rb
+++ b/spec/unit/node_map_spec.rb
@@ -145,14 +145,14 @@ describe Chef::NodeMap do
describe "deleting classes" do
it "deletes a class and removes the mapping completely" do
node_map.set(:thing, Bar)
- expect( node_map.delete_class(Bar) ).to include({ thing: [{ klass: Bar, cookbook_override: false, core_override: false }] })
+ expect( node_map.delete_class(Bar) ).to include({ thing: [{ klass: Bar, cookbook_override: false, core_override: false, target_mode: nil }] })
expect( node_map.get(node, :thing) ).to eql(nil)
end
it "deletes a class and leaves the mapping that still has an entry" do
node_map.set(:thing, Bar)
node_map.set(:thing, Foo)
- expect( node_map.delete_class(Bar) ).to eql({ thing: [{ klass: Bar, cookbook_override: false, core_override: false }] })
+ expect( node_map.delete_class(Bar) ).to eql({ thing: [{ klass: Bar, cookbook_override: false, core_override: false, target_mode: nil }] })
expect( node_map.get(node, :thing) ).to eql(Foo)
end
@@ -160,7 +160,7 @@ describe Chef::NodeMap do
node_map.set(:thing1, Bar)
node_map.set(:thing2, Bar)
node_map.set(:thing2, Foo)
- expect( node_map.delete_class(Bar) ).to eql({ thing1: [{ klass: Bar, cookbook_override: false, core_override: false }], thing2: [{ klass: Bar, cookbook_override: false, core_override: false }] })
+ expect( node_map.delete_class(Bar) ).to eql({ thing1: [{ klass: Bar, cookbook_override: false, core_override: false, target_mode: nil }], thing2: [{ klass: Bar, cookbook_override: false, core_override: false, target_mode: nil }] })
expect( node_map.get(node, :thing1) ).to eql(nil)
expect( node_map.get(node, :thing2) ).to eql(Foo)
end