summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2019-05-06 11:46:42 -0700
committerGitHub <noreply@github.com>2019-05-06 11:46:42 -0700
commitc23a9d65f4bf5aba2fe7cf384e04f7703631fb0b (patch)
treeae33e0ce0c34a20834f8dfdc3083ecb49015159b
parent3ba19c97144832d44a1e77c3183b8b8c9b46f004 (diff)
parent2b12cda0693ad20780156d35a5ae316a90c5174b (diff)
downloadchef-c23a9d65f4bf5aba2fe7cf384e04f7703631fb0b.tar.gz
Merge pull request #7758 from chef/btm/target-mode
Initial target_mode implementation
-rw-r--r--chef-config/lib/chef-config/config.rb33
-rw-r--r--chef-config/spec/unit/config_spec.rb64
-rw-r--r--chef.gemspec1
-rw-r--r--lib/chef/application/client.rb15
-rw-r--r--lib/chef/client.rb39
-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.rb7
-rw-r--r--lib/chef/resource/execute.rb3
-rw-r--r--lib/chef/run_context.rb19
-rw-r--r--lib/chef/train_transport.rb129
-rw-r--r--spec/unit/node_map_spec.rb40
-rw-r--r--spec/unit/train_transport_spec.rb79
18 files changed, 511 insertions, 18 deletions
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-config/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb
index 0e17753185..378cc4a4a1 100644
--- a/chef-config/spec/unit/config_spec.rb
+++ b/chef-config/spec/unit/config_spec.rb
@@ -248,9 +248,10 @@ RSpec.describe ChefConfig::Config do
end
describe "default values" do
+ let(:system_drive) { ChefConfig::Config.env["SYSTEMDRIVE"] } if is_windows
let :primary_cache_path do
if is_windows
- "#{ChefConfig::Config.env['SYSTEMDRIVE']}\\chef"
+ "#{system_drive}\\chef"
else
"/var/chef"
end
@@ -275,6 +276,35 @@ RSpec.describe ChefConfig::Config do
allow(ChefConfig::Config).to receive(:path_accessible?).and_return(false)
end
+ describe "ChefConfig::Config[:client_key]" do
+ let(:path_to_client_key) { to_platform("/etc/chef") + ChefConfig::PathHelper.path_separator }
+
+ it "sets the default path to the client key" do
+ expect(ChefConfig::Config.client_key).to eq(path_to_client_key + "client.pem")
+ end
+
+ context "when target mode is enabled" do
+ let(:target_mode_host) { "fluffy.kittens.org" }
+
+ before do
+ ChefConfig::Config.target_mode.enabled = true
+ ChefConfig::Config.target_mode.host = target_mode_host
+ end
+
+ it "sets the default path to the client key with the target host name" do
+ expect(ChefConfig::Config.client_key).to eq(path_to_client_key + target_mode_host + ChefConfig::PathHelper.path_separator + "client.pem")
+ end
+ end
+
+ context "when local mode is enabled" do
+ before { ChefConfig::Config[:local_mode] = true }
+
+ it "returns nil" do
+ expect(ChefConfig::Config.client_key).to be_nil
+ end
+ end
+ end
+
describe "ChefConfig::Config[:fips]" do
let(:fips_enabled) { false }
@@ -370,16 +400,32 @@ RSpec.describe ChefConfig::Config do
end
describe "ChefConfig::Config[:cache_path]" do
+ let(:target_mode_host) { "fluffy.kittens.org" }
+ let(:target_mode_primary_cache_path) { "#{primary_cache_path}/#{target_mode_host}" }
+ let(:target_mode_secondary_cache_path) { "#{secondary_cache_path}/#{target_mode_host}" }
+
before do
if is_windows
- allow(File).to receive(:expand_path).and_return("#{ChefConfig::Config.env["SYSTEMDRIVE"]}/Path/To/Executable")
+ allow(File).to receive(:expand_path).and_return("#{system_drive}/Path/To/Executable")
end
end
+
context "when /var/chef exists and is accessible" do
- it "defaults to /var/chef" do
+ before do
allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true)
+ end
+
+ it "defaults to /var/chef" do
expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path)
end
+
+ context "and target mode is enabled" do
+ it "cache path includes the target host name" do
+ ChefConfig::Config.target_mode.enabled = true
+ ChefConfig::Config.target_mode.host = target_mode_host
+ expect(ChefConfig::Config[:cache_path]).to eq(target_mode_primary_cache_path)
+ end
+ end
end
context "when /var/chef does not exist and /var is accessible" do
@@ -399,13 +445,23 @@ RSpec.describe ChefConfig::Config do
end
context "when /var/chef exists and is not accessible" do
- it "defaults to $HOME/.chef" do
+ before do
allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(true)
allow(File).to receive(:readable?).with(to_platform("/var/chef")).and_return(true)
allow(File).to receive(:writable?).with(to_platform("/var/chef")).and_return(false)
+ end
+ it "defaults to $HOME/.chef" do
expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path)
end
+
+ context "and target mode is enabled" do
+ it "cache path defaults to $HOME/.chef with the target host name" do
+ ChefConfig::Config.target_mode.enabled = true
+ ChefConfig::Config.target_mode.host = target_mode_host
+ expect(ChefConfig::Config[:cache_path]).to eq(target_mode_secondary_cache_path)
+ end
+ end
end
context "when chef is running in local mode" do
diff --git a/chef.gemspec b/chef.gemspec
index 7c365eeeb4..975d51dba3 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -39,6 +39,7 @@ 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 "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 31932b812c..2c63a54f33 100644
--- a/lib/chef/application/client.rb
+++ b/lib/chef/application/client.rb
@@ -301,6 +301,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 #{Chef::Dist::PRODUCT} 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
@@ -351,6 +360,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..a11662b7d8 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -55,6 +55,7 @@ require "chef/mixin/deprecation"
require "ohai"
require "rbconfig"
require "chef/dist"
+require "forwardable"
class Chef
# == Chef::Client
@@ -65,6 +66,7 @@ class Chef
extend Chef::Mixin::Deprecation
+ extend Forwardable
#
# The status of the Chef run.
#
@@ -136,6 +138,9 @@ class Chef
attr_reader :events
attr_reader :logger
+
+ def_delegator :@run_context, :transport_connection
+
#
# Creates a new Chef::Client.
#
@@ -244,9 +249,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 +567,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..2d84ac317b 100644
--- a/lib/chef/provider/execute.rb
+++ b/lib/chef/provider/execute.rb
@@ -19,16 +19,19 @@
require "chef/log"
require "chef/provider"
require "forwardable"
+require "chef/mixin/train_or_shell"
class Chef
class Provider
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
+ include Chef::Mixin::TrainOrShell
+
def load_current_resource
current_resource = Chef::Resource::Execute.new(new_resource.name)
current_resource
@@ -55,7 +58,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..9db5f8fbf3
--- /dev/null
+++ b/lib/chef/train_transport.rb
@@ -0,0 +1,129 @@
+# 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?
+ credentials[profile].map { |k, v| [k.to_sym, v] }.to_h # return symbolized keys to match Train.options()
+ 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)
+ fqdn.split(".").reduce(hash) do |h, k|
+ v = h[k]
+ if Hash === v
+ v
+ else
+ break false
+ end
+ end
+ 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..7c867857dc 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
@@ -210,6 +210,40 @@ describe Chef::NodeMap do
end
end
+ # When in target mode, only match when target_mode is explicitly supported
+ context "when target mode is enabled" do
+ before do
+ allow(Chef::Config).to receive(:target_mode?).and_return(true)
+ end
+
+ it "returns the value when target_mode matches" do
+ node_map.set(:something, :network, target_mode: true)
+ expect(node_map.get(node, :something)).to eql(:network)
+ end
+
+ it "returns nil when target_mode does not match" do
+ node_map.set(:something, :local, target_mode: false)
+ expect(node_map.get(node, :something)).to eql(nil)
+ end
+ end
+
+ # When not in target mode, match regardless of target_mode filter
+ context "when target mode is not enabled" do
+ before do
+ allow(Chef::Config).to receive(:target_mode?).and_return(false)
+ end
+
+ it "returns the value if target_mode matches" do
+ node_map.set(:something, :local, target_mode: true)
+ expect(node_map.get(node, :something)).to eql(:local)
+ end
+
+ it "returns the value if target_mode does not match" do
+ node_map.set(:something, :local, target_mode: false)
+ expect(node_map.get(node, :something)).to eql(:local)
+ end
+ end
+
describe "locked mode" do
context "while unlocked" do
it "allows setting the same key twice" do
diff --git a/spec/unit/train_transport_spec.rb b/spec/unit/train_transport_spec.rb
new file mode 100644
index 0000000000..b56c7e1104
--- /dev/null
+++ b/spec/unit/train_transport_spec.rb
@@ -0,0 +1,79 @@
+#
+# Author:: Bryan McLellan (<btm@loftninjas.org>)
+# Copyright:: Copyright 2019, Chef Software Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "spec_helper"
+
+describe Chef::TrainTransport do
+ describe "load_credentials" do
+ let(:transport) { Chef::TrainTransport.new }
+ let(:good_credentials) { { "switch.cisco.com" => { "user" => "cisco", "password" => "cisco", "enable_password" => "secret" } } }
+
+ before do
+ allow(Chef::TrainTransport).to receive(:parse_credentials_file).and_return(good_credentials)
+ end
+
+ it "matches credentials when they exist" do
+ expect(Chef::TrainTransport.load_credentials("switch.cisco.com")[:user]).to eq("cisco")
+ expect(Chef::TrainTransport.load_credentials("switch.cisco.com")[:password]).to eq("cisco")
+ expect(Chef::TrainTransport.load_credentials("switch.cisco.com")[:enable_password]).to eq("secret")
+ end
+
+ it "returns nil if there is no match" do
+ expect(Chef::TrainTransport.load_credentials("router.unicorns.com")).to be_nil
+ end
+
+ # [foo.example.org] => {"foo"=>{"example"=>{"org"=>{}}}}
+ # ['foo.example.org'] => {"foo.example.org"=>{}}
+ it "warns if the host has been split by toml" do
+ allow(Chef::TrainTransport).to receive(:parse_credentials_file).and_return({ "foo" => { "example" => { "org" => {} } } })
+ expect(Chef::Log).to receive(:warn).with(/as a Hash/)
+ expect(Chef::Log).to receive(:warn).with(/Hostnames must be surrounded by single quotes/)
+ expect(Chef::TrainTransport.load_credentials("foo.example.org")).to be_nil
+ end
+ end
+
+ describe "credentials_file_path" do
+
+ context "when a file path is specified by a config" do
+ let(:cred_file_path) { "/somewhere/credentials" }
+
+ before do
+ tm_config = double("Config Context", host: "foo.example.org", credentials_file: cred_file_path)
+ allow(Chef::Config).to receive(:target_mode).and_return(tm_config)
+ end
+
+ it "returns the path if it exists" do
+ allow(File).to receive(:exists?).with(cred_file_path).and_return(true)
+ expect(Chef::TrainTransport.credentials_file_path).to eq(cred_file_path)
+ end
+
+ it "raises an error if it does not exist" do
+ allow(File).to receive(:exists?).with(cred_file_path).and_return(false)
+ expect { Chef::TrainTransport.credentials_file_path }.to raise_error(ArgumentError, /does not exist/)
+ end
+ end
+
+ it "returns the path to the default config file if it exists" do
+ cred_file_path = Chef::Config.platform_specific_path("/etc/chef/foo.example.org/credentials")
+ tm_config = double("Config Context", host: "foo.example.org", credentials_file: nil)
+ allow(Chef::Config).to receive(:target_mode).and_return(tm_config)
+ allow(File).to receive(:exists?).with(cred_file_path).and_return(true)
+ expect(Chef::TrainTransport.credentials_file_path).to eq(cred_file_path)
+ end
+ end
+end