diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/application/client.rb | 15 | ||||
-rw-r--r-- | lib/chef/client.rb | 47 | ||||
-rw-r--r-- | lib/chef/dsl/universal.rb | 1 | ||||
-rw-r--r-- | lib/chef/exceptions.rb | 2 | ||||
-rw-r--r-- | lib/chef/formatters/doc.rb | 1 | ||||
-rw-r--r-- | lib/chef/formatters/minimal.rb | 1 | ||||
-rw-r--r-- | lib/chef/guard_interpreter/default_guard_interpreter.rb | 4 | ||||
-rw-r--r-- | lib/chef/mixin/train_or_shell.rb | 74 | ||||
-rw-r--r-- | lib/chef/node_map.rb | 17 | ||||
-rw-r--r-- | lib/chef/provider/execute.rb | 4 | ||||
-rw-r--r-- | lib/chef/resource/execute.rb | 3 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 19 | ||||
-rw-r--r-- | lib/chef/train_transport.rb | 140 |
13 files changed, 318 insertions, 10 deletions
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 |