diff --git a/bin/resource b/bin/resource new file mode 100755 index 0000000..5879645 --- /dev/null +++ b/bin/resource @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +require 'cheffish/chef_run' + +def post(resource_type, name, properties) + chef_run = Cheffish::ChefRun.new + begin + r = chef_run.client.build_resource(resource_type, name) do + properties.each { |attr, value| public_send(attr, value) } + end + chef_run.client.add_resource(r) + chef_run.converge + puts "CODE: #{chef_run.updated? ? 201 : 200}" + puts "STDOUT: #{chef_run.stdout}\nSTDERR: #{chef_run.stderr}\nLOGS: #{chef_run.logs}" + rescue + puts "CODE: #{400}" + puts "ERROR: #{$!}\nBACKTRACE: #{$!.backtrace}\nSTDOUT: #{chef_run.stdout}\nSTDERR: #{chef_run.stderr}\nLOGS: #{chef_run.logs}" + end +end + +post(ARGV.shift, ARGV.shift, Hash[*ARGV]) diff --git a/cheffish.gemspec b/cheffish.gemspec index 470d220..37bc6c4 100644 --- a/cheffish.gemspec +++ b/cheffish.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.email = 'jkeiser@opscode.com' s.homepage = 'http://wiki.opscode.com/display/chef' - s.add_dependency 'chef-zero', '~> 4.0' + s.add_dependency 'chef-zero', '~> 4.2' s.add_dependency 'chef' , '~> 12.1' s.add_development_dependency 'rake' diff --git a/lib/chef/provider/private_key.rb b/lib/chef/provider/private_key.rb index 3a36b20..a1261da 100644 --- a/lib/chef/provider/private_key.rb +++ b/lib/chef/provider/private_key.rb @@ -209,6 +209,7 @@ def load_current_resource end rescue # If there's an error reading, we assume format and type are wrong and don't futz with them + Chef::Log.warn("Error reading #{new_path}: #{$!}") end else resource.action :delete diff --git a/lib/cheffish.rb b/lib/cheffish.rb index ae608e6..2331e1b 100644 --- a/lib/cheffish.rb +++ b/lib/cheffish.rb @@ -1,11 +1,3 @@ -require 'chef/run_list/run_list_item' -require 'cheffish/basic_chef_client' -require 'cheffish/server_api' -require 'chef/knife' -require 'chef/config_fetcher' -require 'chef/log' -require 'chef/application' - module Cheffish NAME_REGEX = /^[.\-[:alnum:]_]+$/ @@ -92,8 +84,10 @@ def self.get_private_key(name, config = profiled_config) def self.get_private_key_with_path(name, config = profiled_config) if config[:private_keys] && config[:private_keys][name] if config[:private_keys][name].is_a?(String) + Chef::Log.info("Got key #{name} from Chef::Config.private_keys.#{name}, which points at #{config[:private_keys[name]]}. Reading key from there ...") return [ IO.read(config[:private_keys][name]), config[:private_keys][name] ] else + Chef::Log.info("Got key #{name} raw from Chef::Config.private_keys.#{name}.") return [ config[:private_keys][name].to_pem, nil ] end elsif config[:private_key_paths] @@ -104,6 +98,7 @@ def self.get_private_key_with_path(name, config = profiled_config) if ext == '' || ext == '.pem' key_name = key[0..-(ext.length+1)] if key_name == name + Chef::Log.info("Reading key #{name} from file #{private_key_path}/#{key}") return [ IO.read("#{private_key_path}/#{key}"), "#{private_key_path}/#{key}" ] end end @@ -223,4 +218,11 @@ def remove_role(*roles) # Include all recipe objects so require 'cheffish' brings in the whole recipe DSL +require 'chef/run_list/run_list_item' +require 'cheffish/basic_chef_client' +require 'cheffish/server_api' +require 'chef/knife' +require 'chef/config_fetcher' +require 'chef/log' +require 'chef/application' require 'cheffish/recipe_dsl' diff --git a/lib/cheffish/basic_chef_client.rb b/lib/cheffish/basic_chef_client.rb index 9269d49..eb3e9af 100644 --- a/lib/cheffish/basic_chef_client.rb +++ b/lib/cheffish/basic_chef_client.rb @@ -13,7 +13,7 @@ module Cheffish class BasicChefClient include Chef::DSL::Recipe - def initialize(node = nil, events = nil) + def initialize(node = nil, events = nil, **chef_config) if !node node = Chef::Node.new node.name 'basic_chef_client' @@ -21,34 +21,53 @@ def initialize(node = nil, events = nil) node.automatic[:platform_version] = Cheffish::VERSION end - @event_catcher = BasicChefClientEvents.new - dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher) - dispatcher.register(events) if events - @run_context = Chef::RunContext.new(node, {}, dispatcher) - @updated = [] - @cookbook_name = 'basic_chef_client' + # Decide on the config we want for this chef client + @chef_config = chef_config + + with_chef_config do + @cookbook_name = 'basic_chef_client' + @event_catcher = BasicChefClientEvents.new + dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher) + case events + when nil + when Array + events.each { |e| dispatcher.register(e) } if events + else + dispatcher.register(events) + end + @run_context = Chef::RunContext.new(node, {}, dispatcher) + @updated = [] + @cookbook_name = 'basic_chef_client' + end end extend Forwardable # Stuff recipes need + attr_reader :chef_config attr_reader :run_context attr_accessor :cookbook_name attr_accessor :recipe_name def_delegators :@run_context, :resource_collection, :immediate_notifications, :delayed_notifications def add_resource(resource) - resource.run_context = run_context - run_context.resource_collection.insert(resource) + with_chef_config do + resource.run_context = run_context + run_context.resource_collection.insert(resource) + end end def load_block(&block) - @recipe_name = 'block' - instance_eval(&block) + with_chef_config do + @recipe_name = 'block' + instance_eval(&block) + end end def converge - Chef::Runner.new(self).converge + with_chef_config do + Chef::Runner.new(self).converge + end end def updates @@ -63,15 +82,21 @@ def updated? # add_resource() method. def self.build_resource(type, name, created_at=nil, &resource_attrs_block) created_at ||= caller[0] - result = BasicChefClient.new.build_resource(type, name, created_at, &resource_attrs_block) + result = BasicChefClient.new.tap do |client| + client.with_chef_config do + client.build_resource(type, name, created_at, &resource_attrs_block) + end + end result end def self.inline_resource(provider, provider_action, *resources, &block) events = ProviderEventForwarder.new(provider, provider_action) client = BasicChefClient.new(provider.node, events) - resources.each do |resource| - client.add_resource(resource) + client.with_chef_config do + resources.each do |resource| + client.add_resource(resource) + end end client.load_block(&block) if block client.converge @@ -85,6 +110,50 @@ def self.converge_block(node = nil, events = nil, &block) client.updated? end + def with_chef_config(&block) + old_chef_config = Chef::Config.save + if chef_config[:log_location] + old_loggers = Chef::Log.loggers + Chef::Log.init(chef_config[:log_location]) + end + if chef_config[:log_level] + old_level = Chef::Log.level + Chef::Log.level(chef_config[:log_level]) + end + # if chef_config[:stdout] + # old_stdout = $stdout + # $stdout = chef_config[:stdout] + # end + # if chef_config[:stderr] + # old_stderr = $stderr + # $stderr = chef_config[:stderr] + # end + begin + deep_merge_config(chef_config, Chef::Config) + block.call + ensure + # $stdout = old_stdout if chef_config[:stdout] + # $stderr = old_stderr if chef_config[:stderr] + if old_loggers + Chef::Log.logger = old_loggers.shift + old_loggers.each { |l| Chef::Log.loggers.push(l) } + elsif chef_config[:log_level] + Chef::Log.level = old_level + end + Chef::Config.restore(old_chef_config) + end + end + + def deep_merge_config(src, dest) + src.each do |name, value| + if value.is_a?(Hash) && dest[name].is_a?(Hash) + deep_merge_config(value, dest[name]) + else + dest[name] = value + end + end + end + class BasicChefClientEvents < Chef::EventDispatch::Base def initialize @updates = [] diff --git a/lib/cheffish/chef_run.rb b/lib/cheffish/chef_run.rb new file mode 100644 index 0000000..b63c8e3 --- /dev/null +++ b/lib/cheffish/chef_run.rb @@ -0,0 +1,142 @@ +require 'cheffish/basic_chef_client' + +module Cheffish + class ChefRun + # + # @param chef_config A hash with symbol keys that looks suspiciously similar to `Chef::Config`. + # Some possible options: + # - stdout: - where to stream stdout to + # - stderr: - where to stream stderr to + # - log_level: :debug|:info|:warn|:error|:fatal + # - log_location: - where to stream logs to + # - verbose_logging: true|false - true if you want verbose logging in :debug + # + def initialize(chef_config={}) + @chef_config = chef_config || {} + end + + attr_reader :chef_config + + class StringIOTee < StringIO + def initialize(*streams) + super() + @streams = streams.flatten.select { |s| !s.nil? } + end + + attr_reader :streams + + def write(*args, &block) + super + streams.each { |s| s.write(*args, &block) } + end + end + + def client + @client ||= begin + chef_config = self.chef_config.dup + chef_config[:log_level] ||= :debug if !chef_config.has_key?(:log_level) + chef_config[:verbose_logging] = false if !chef_config.has_key?(:verbose_logging) + chef_config[:stdout] = StringIOTee.new(chef_config[:stdout]) + chef_config[:stderr] = StringIOTee.new(chef_config[:stderr]) + chef_config[:log_location] = StringIOTee.new(chef_config[:log_location]) + @client = ::Cheffish::BasicChefClient.new(nil, + [ event_sink, Chef::Formatters.new(:doc, chef_config[:stdout], chef_config[:stderr]) ], + chef_config + ) + end + end + + def event_sink + @event_sink ||= EventSink.new + end + + # + # output + # + def stdout + @client ? client.chef_config[:stdout].string : nil + end + def stderr + @client ? client.chef_config[:stderr].string : nil + end + def logs + @client ? client.chef_config[:log_location].string : nil + end + + def resources + client.resource_collection + end + + def converge + begin + client.converge + @converged = true + rescue RuntimeError => e + @raised_exception = e + raise + end + end + + def reset + @client = nil + @converged = nil + @stdout = nil + @stderr = nil + @logs = nil + @raised_exception = nil + end + + def converged? + !!@converged + end + + def converge_failed? + @raised_exception.nil? ? false : true + end + + def updated? + client.updated? + end + + def up_to_date? + !client.updated? + end + + def output_for_failure_message + message = "" + if stdout && !stdout.empty? + message << "--- ---\n" + message << "--- Chef Client Output ---\n" + message << "--- ---\n" + message << stdout + message << "\n" if !stdout.end_with?("\n") + end + if stderr && !stderr.empty? + message << "--- ---\n" + message << "--- Chef Client Error Output ---\n" + message << "--- ---\n" + message << stderr + message << "\n" if !stderr.end_with?("\n") + end + if logs && !logs.empty? + message << "--- ---\n" + message << "--- Chef Client Logs ---\n" + message << "--- ---\n" + message << logs + end + message + end + + class EventSink + def initialize + @events = [] + end + + attr_reader :events + + def method_missing(method, *args) + @events << [ method, *args ] + end + end + end +end diff --git a/lib/cheffish/key_formatter.rb b/lib/cheffish/key_formatter.rb index 2c4011b..e9187e5 100644 --- a/lib/cheffish/key_formatter.rb +++ b/lib/cheffish/key_formatter.rb @@ -24,7 +24,7 @@ def self.decode(str, pass_phrase=nil, filename='') end key_format[:type] = type_of(key) - key_format[:size] = size_of(key) + key_format[:size] = size_of(key) if size_of(key) key_format[:pass_phrase] = pass_phrase if pass_phrase # TODO cipher, exponent @@ -102,8 +102,12 @@ def self.type_of(key) end def self.size_of(key) - # TODO DSA -- this is RSA only - key.n.num_bytes * 8 + case key.class + when OpenSSL::PKey::RSA + key.n.num_bytes * 8 + else + nil + end end end end diff --git a/lib/cheffish/rspec/chef_run_support.rb b/lib/cheffish/rspec/chef_run_support.rb index c5a6637..b74fc63 100644 --- a/lib/cheffish/rspec/chef_run_support.rb +++ b/lib/cheffish/rspec/chef_run_support.rb @@ -3,70 +3,24 @@ require 'cheffish/rspec/repository_support' require 'uri' require 'cheffish/basic_chef_client' +require 'cheffish/rspec/chef_run_wrapper' +require 'cheffish/rspec/recipe_run_wrapper' module Cheffish module RSpec module ChefRunSupport include ChefZero::RSpec - - def when_the_chef_12_server(*args, &block) - if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new('3.1') - when_the_chef_server(*args, :osc_compat => false, :single_org => false, &block) - end - end + include RepositorySupport def self.extended(klass) klass.class_eval do - extend RepositorySupport - - def rest - ::Chef::ServerAPI.new - end - - def get(path, *args) - if path[0] == '/' - path = URI.join(rest.url, path) - end - rest.get(path, *args) - end - - def chef_run - converge if !@converged - event_sink.events - end - - def event_sink - @event_sink ||= EventSink.new - end - - def basic_chef_client - @basic_chef_client ||= begin - ::Cheffish::BasicChefClient.new(nil, event_sink) - end - end - - def load_recipe(&block) - basic_chef_client.load_block(&block) - end - - def run_recipe(&block) - load_recipe(&block) - converge - end - - def reset_chef_client - @event_sink = nil - @basic_chef_client = nil - @converged = false - end + include ChefRunSupportInstanceMethods + end + end - def converge - if @converged - raise "Already converged! Cannot converge twice, that's bad mojo." - end - @converged = true - basic_chef_client.converge - end + def when_the_chef_12_server(*args, **options, &block) + if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new('3.1') + when_the_chef_server(*args, :osc_compat => false, :single_org => false, **options, &block) end end @@ -76,7 +30,7 @@ def with_recipe(&block) end after :each do - if !@converged + if !chef_client.converge_failed? && !chef_client.converged? raise "Never tried to converge!" end end @@ -89,18 +43,68 @@ def with_converge(&block) end end - class EventSink - def initialize - @events = [] + module ChefRunSupportInstanceMethods + def rest + ::Chef::ServerAPI.new end - attr_reader :events + def get(path, *args) + if path[0] == '/' + path = URI.join(rest.url, path) + end + rest.get(path, *args) + end - def method_missing(method, *args) - @events << [ method, *args ] + def chef_config + {} + end + + def expect_recipe(&recipe) + expect(recipe(&recipe)) + end + + def recipe(&recipe) + RecipeRunWrapper.new(chef_config, &recipe) + end + + def chef_client + @chef_client ||= ChefRunWrapper.new(chef_config) + end + + def chef_run + converge if !chef_client.converged? + event_sink.events + end + + def event_sink + chef_client.event_sink end - end + def basic_chef_client + chef_client.client + end + + def load_recipe(&recipe) + chef_client.client.load_block(&recipe) + end + + def run_recipe(&recipe) + load_recipe(&recipe) + converge + end + + def reset_chef_client + @event_sink = nil + @basic_chef_client = nil + end + + def converge + if chef_client.converged? + raise "Already converged! Cannot converge twice, that's bad mojo." + end + chef_client.converge + end + end end end end diff --git a/lib/cheffish/rspec/chef_run_wrapper.rb b/lib/cheffish/rspec/chef_run_wrapper.rb new file mode 100644 index 0000000..76e2470 --- /dev/null +++ b/lib/cheffish/rspec/chef_run_wrapper.rb @@ -0,0 +1,5 @@ +require 'cheffish/chef_run' + +module Cheffish::RSpec + ChefRunWrapper = Cheffish::ChefRun +end diff --git a/lib/cheffish/rspec/matchers.rb b/lib/cheffish/rspec/matchers.rb index bbb0ec3..a55826f 100644 --- a/lib/cheffish/rspec/matchers.rb +++ b/lib/cheffish/rspec/matchers.rb @@ -25,6 +25,20 @@ end end +RSpec::Matchers.define :be_idempotent do + match do |recipe| + @recipe = recipe + recipe.reset + recipe.converge + recipe.up_to_date? + end + + failure_message { + "#{@recipe} is not idempotent! Converging it a second time caused updates.\n#{@recipe.output_for_failure_message}" + } +end + + RSpec::Matchers.define :update_acls do |acl_paths, expected_acls| errors = [] diff --git a/lib/cheffish/rspec/recipe_run_wrapper.rb b/lib/cheffish/rspec/recipe_run_wrapper.rb new file mode 100644 index 0000000..f3bb566 --- /dev/null +++ b/lib/cheffish/rspec/recipe_run_wrapper.rb @@ -0,0 +1,22 @@ +require 'cheffish/rspec/chef_run_wrapper' + +module Cheffish + module RSpec + class RecipeRunWrapper < ChefRunWrapper + def initialize(chef_config, &recipe) + super(chef_config) + @recipe = recipe + end + + attr_reader :recipe + + def client + if !@client + super + @client.load_block(&recipe) + end + @client + end + end + end +end