diff --git a/.gitignore b/.gitignore index 98d5c33e2ed..7ade6f67f1b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,8 @@ chef/pkg chef-server/pkg chef/log chef-server/log +log +couchdb.stderr +couchdb.stdout +features/data/tmp/** *.swp diff --git a/CHANGELOG b/CHANGELOG index 8ddecc18800..b75b9a9e684 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,42 @@ +Fri Feb 13 12:26:07 PST 2009 +Release Notes - Chef - Version 0.5.4 +http://tickets.opscode.com/ + +** Bug + * [CHEF-48] - Invalid default recipe causes merb 500 error + * [CHEF-64] - chef-server pukes if you type an invalid url in the openid login + * [CHEF-72] - Templates used in definitions searched for only the cookbook they are used in + * [CHEF-76] - Search queries return empty results occationally + * [CHEF-77] - Indexer broken - theoretically creates index, but cannot read them + * [CHEF-82] - user provider doesn't handle 'shadow' not being installed correctly + * [CHEF-87] - File specificity (preferred file) is broken by dotfiles + * [CHEF-89] - remote_file doesn't support being passed a URL as a source, but the documentation argues otherwise - solo only + * [CHEF-90] - Search in recipes does not allow for attribute selection, even though the REST API does. + * [CHEF-92] - When loading the prior resource we should never load its action + * [CHEF-94] - Definitions should allow access to the node object within the parameter setting block + * [CHEF-95] - not_if's string behaviour is broken, closed stream + * [CHEF-96] - group resource doesn't if members is empty so it always tried to add them + * [CHEF-97] - not_if and only_if cause exceptions in popen4 + * [CHEF-108] - @@seen_recipes is a class variable, this makes chef-client and chef-solo *not* run any recipes after the first run in daemon mode + * [CHEF-110] - interval / splay needs to be supported outside of daemonized mode for chef-client + * [CHEF-111] - user provider mistakenly attempts to modify the user even if no changes are required + * [CHEF-114] - when not given an interval on the command line, chef-client runs in a tight loop driving server load up + * [CHEF-117] - Can't setgid if you have already setuid-ed + * [CHEF-123] - User provider fails to correctly compare a numeric GID to a string GID + * [CHEF-124] - Chef-server should set reload_classes false + * [CHEF-125] - chef-server init.rb should set Merb log_stream to the location supplied by chef/server.rb + +** Improvement + * [CHEF-71] - service resource :supports attribute too rubyish and unlike :action + * [CHEF-73] - When specifying a custom gem source for a gem_package, also include rubyforge in the list of sources so gem dependencies can be installed + * [CHEF-106] - refactor search, move attributes to search function : chef/chef-server/lib/chef/search.rb, chef/chef-server/lib/controllers/search.rb + * [CHEF-107] - more informative message for info log on package upgrade + * [CHEF-127] - cron resource should log to info for update/add instead of debug + +** New Feature + * [CHEF-59] - Package resource need Redhat provider + * [CHEF-91] - Chef Client should reload the configuration on SIGHUP + Sat Jan 31 18:52:41 PST 2009 Release Notes - Chef - Version 0.5.2 http://tickets.opscode.com/ diff --git a/NOTICE b/NOTICE index 165a89d0d57..74f868edfc1 100644 --- a/NOTICE +++ b/NOTICE @@ -9,7 +9,7 @@ Contributors and Copyright holders: * Copyright 2008, Arjuna Christensen * Copyright 2008, Bryan McLellan * Copyright 2008, Ezra Zygmuntowicz - * Copyright 2008, Sean Cribbs + * Copyright 2009, Sean Cribbs * Copyright 2009, Christopher Brown Chef incorporates code modified from Open4 (http://www.codeforpeople.com/lib/ruby/open4/), which was written by Ara T. Howard. diff --git a/Rakefile b/Rakefile index e1786bb0fef..c6d81b291ee 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,6 @@ gems = %w[chef chefserverslice chef-server] +require 'rubygems' +require 'cucumber/rake/task' desc "Build the chef gems" task :gem do @@ -28,7 +30,88 @@ task :spec do end end -namespace :dev do +def start_dev_environment(type="normal") + @couchdb_server_pid = nil + @chef_server_pid = nil + @chef_indexer_pid = nil + @stompserver_pid = nil + + ccid = fork + if ccid + @couchdb_server_pid = ccid + else + exec("couchdb") + end + + scid = fork + if scid + @stompserver_pid = scid + else + exec("stompserver") + end + + mcid = fork + if mcid # parent + @chef_indexer_pid = mcid + else # child + case type + when "normal" + exec("chef-indexer -l debug") + when "features" + exec("chef-indexer -c #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug") + end + end + + mcid = fork + if mcid # parent + @chef_server_pid = mcid + else # child + case type + when "normal" + exec("chef-server -l debug -N -c 2") + when "features" + exec("chef-server -C #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug -N -c 2") + + end + end + + puts "Running Chef at #{@chef_server_pid}" + puts "Running Chef Indexer at #{@chef_indexer_pid}" + puts "Running CouchDB at #{@couchdb_server_pid}" + puts "Running Stompserver at #{@stompserver_pid}" +end + +def stop_dev_environment + puts "Stopping CouchDB" + Process.kill("KILL", @couchdb_server_pid) + puts "Stopping Stomp server" + Process.kill("KILL", @stompserver_pid) + puts "Stopping Chef Server" + Process.kill("INT", @chef_server_pid) + puts "Stopping Chef Indexer" + Process.kill("INT", @chef_indexer_pid) + puts "\nCouchDB, Stomp, Chef Server and Chef Indexer killed - have a nice day!" +end + +def wait_for_ctrlc + puts "Hit CTRL-C to destroy development environment" + trap("CHLD", "IGNORE") + trap("INT") do + stop_dev_environment + exit 1 + end + while true + sleep 10 + end +end + +desc "Run a Devel instance of Chef" +task :dev => "dev:install" do + start_dev_environment + wait_for_ctrlc +end + +namespace :dev do desc "Install a Devel instance of Chef with the example-repository" task :install do gems.each do |dir| @@ -36,4 +119,20 @@ namespace :dev do end Dir.chdir("example-repository") { sh("rake install") } end + + + desc "Install a test instance of Chef for doing features against" + task :features do + gems.each do |dir| + Dir.chdir(dir) { sh "rake install" } + end + start_dev_environment("features") + wait_for_ctrlc + end +end + +Cucumber::Rake::Task.new(:features) do |t| + t.step_pattern = 'features/steps/**/*.rb' + supportdir = 'features/support' + t.cucumber_opts = "--format pretty -r #{supportdir}" end diff --git a/chef-server/Rakefile b/chef-server/Rakefile index 64192a945a8..c5a05d3b8f3 100644 --- a/chef-server/Rakefile +++ b/chef-server/Rakefile @@ -10,7 +10,7 @@ require 'chef' include FileUtils GEM = "chef-server" -CHEF_SERVER_VERSION = "0.5.3" +CHEF_SERVER_VERSION = "0.5.5" AUTHOR = "Opscode" EMAIL = "chef@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" diff --git a/chef-server/chef-server.gemspec b/chef-server/chef-server.gemspec new file mode 100644 index 00000000000..24728c3075f --- /dev/null +++ b/chef-server/chef-server.gemspec @@ -0,0 +1,58 @@ +Gem::Specification.new do |s| + s.name = %q{chef-server} + s.version = "0.5.5" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.authors = ["Adam Jacob"] + s.date = %q{2009-01-15} + s.description = %q{A systems integration framework, built to bring the benefits of configuration management to your entire infrastructure.} + s.email = %q{adam@opscode.com} + s.executables = ["chef-indexer", "chef-server"] + s.extra_rdoc_files = ["README.txt", "LICENSE", "NOTICE"] + s.files = ["LICENSE", "README.txt", "Rakefile", "lib/chef", "lib/chef/search.rb", "lib/chef/search_index.rb", "lib/controllers", "lib/controllers/application.rb", "lib/controllers/cookbook_attributes.rb", "lib/controllers/cookbook_definitions.rb", "lib/controllers/cookbook_files.rb", "lib/controllers/cookbook_libraries.rb", "lib/controllers/cookbook_recipes.rb", "lib/controllers/cookbook_templates.rb", "lib/controllers/cookbooks.rb", "lib/controllers/exceptions.rb", "lib/controllers/nodes.rb", "lib/controllers/openid_consumer.rb", "lib/controllers/openid_register.rb", "lib/controllers/openid_server.rb", "lib/controllers/search.rb", "lib/controllers/search_entries.rb", "lib/helpers", "lib/helpers/cookbooks_helper.rb", "lib/helpers/global_helpers.rb", "lib/helpers/nodes_helper.rb", "lib/helpers/openid_server_helpers.rb", "lib/init.rb", "lib/public", "lib/public/images", "lib/public/images/indicator.gif", "lib/public/images/merb.jpg", "lib/public/javascript", "lib/public/javascript/chef.js", "lib/public/jquery", "lib/public/jquery/jquery-1.2.6.min.js", "lib/public/jquery/jquery.jeditable.mini.js", "lib/public/stylesheets", "lib/public/stylesheets/master.css", "lib/views", "lib/views/cookbook_templates", "lib/views/cookbook_templates/index.html.haml", "lib/views/cookbooks", "lib/views/cookbooks/_attribute_file.html.haml", "lib/views/cookbooks/_syntax_highlight.html.haml", "lib/views/cookbooks/attribute_files.html.haml", "lib/views/cookbooks/index.html.haml", "lib/views/cookbooks/show.html.haml", "lib/views/exceptions", "lib/views/exceptions/bad_request.json.erb", "lib/views/exceptions/internal_server_error.html.erb", "lib/views/exceptions/not_acceptable.html.erb", "lib/views/exceptions/not_found.html.erb", "lib/views/layout", "lib/views/layout/application.html.haml", "lib/views/nodes", "lib/views/nodes/_action.html.haml", "lib/views/nodes/_node.html.haml", "lib/views/nodes/_resource.html.haml", "lib/views/nodes/compile.html.haml", "lib/views/nodes/index.html.haml", "lib/views/nodes/show.html.haml", "lib/views/openid_consumer", "lib/views/openid_consumer/index.html.haml", "lib/views/openid_consumer/start.html.haml", "lib/views/openid_login", "lib/views/openid_login/index.html.haml", "lib/views/openid_register", "lib/views/openid_register/index.html.haml", "lib/views/openid_register/show.html.haml", "lib/views/openid_server", "lib/views/openid_server/decide.html.haml", "lib/views/search", "lib/views/search/_search_form.html.haml", "lib/views/search/index.html.haml", "lib/views/search/show.html.haml", "lib/views/search_entries", "lib/views/search_entries/index.html.haml", "lib/views/search_entries/show.html.haml", "bin/chef-indexer", "bin/chef-server", "NOTICE"] + s.has_rdoc = true + s.homepage = %q{http://wiki.opscode.com/display/chef} + s.require_paths = ["lib"] + s.rubygems_version = %q{1.2.0} + s.summary = %q{A systems integration framework, built to bring the benefits of configuration management to your entire infrastructure.} + + if s.respond_to? :specification_version then + current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION + s.specification_version = 2 + + if current_version >= 3 then + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, [">= 0"]) + else + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + end + else + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + end +end diff --git a/chef-server/lib/views/exceptions/bad_request.html.haml b/chef-server/lib/views/exceptions/bad_request.html.haml new file mode 100644 index 00000000000..c70c26a6587 --- /dev/null +++ b/chef-server/lib/views/exceptions/bad_request.html.haml @@ -0,0 +1,2 @@ +- request.exceptions.each do |exception| + = exception.message \ No newline at end of file diff --git a/chef/Rakefile b/chef/Rakefile index 9a52dec46a8..faa66847a49 100644 --- a/chef/Rakefile +++ b/chef/Rakefile @@ -4,7 +4,7 @@ require 'rake/rdoctask' require './tasks/rspec.rb' GEM = "chef" -CHEF_VERSION = "0.5.3" +CHEF_VERSION = "0.5.5" AUTHOR = "Adam Jacob" EMAIL = "adam@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" diff --git a/chef/bin/chef-client b/chef/bin/chef-client index e217d11a5be..32a3c45fe0b 100755 --- a/chef/bin/chef-client +++ b/chef/bin/chef-client @@ -57,6 +57,13 @@ opts = OptionParser.new do |opts| end opts.parse!(ARGV) +trap("INT") { Chef.fatal!("SIGINT received, stopping", 2) } +trap("HUP") { + Chef::Log.info("SIGHUP received, reloading configuration") + Chef::Config.from_file(config[:config_file]) + Chef::Config.configure { |c| c.merge!(config) } +} + unless File.exists?(config[:config_file]) and File.readable?(config[:config_file]) Chef.fatal!("I cannot find or read the config file: #{config[:config_file]}", 1) end diff --git a/chef/chef.gemspec b/chef/chef.gemspec index 430123a498d..5273922e205 100644 --- a/chef/chef.gemspec +++ b/chef/chef.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{chef} - s.version = "0.5.3" + s.version = "0.5.5" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Jacob"] diff --git a/chef/lib/chef.rb b/chef/lib/chef.rb index 934fdc87aa4..1123a0ea4a1 100644 --- a/chef/lib/chef.rb +++ b/chef/lib/chef.rb @@ -27,7 +27,7 @@ Dir[File.join(File.dirname(__FILE__), 'chef/mixin/**/*.rb')].sort.each { |lib| require lib } class Chef - VERSION = '0.5.3' + VERSION = '0.5.5' class << self def fatal!(msg, err = -1) diff --git a/chef/lib/chef/config.rb b/chef/lib/chef/config.rb index b4160120441..325156b4211 100644 --- a/chef/lib/chef/config.rb +++ b/chef/lib/chef/config.rb @@ -53,6 +53,8 @@ class Config :log_location => STDOUT, :openid_providers => nil, :ssl_verify_mode => :verify_none, + :ssl_client_cert => "", + :ssl_client_key => "", :rest_timeout => 60, :couchdb_url => "http://localhost:5984", :registration_url => "http://localhost:4000", diff --git a/chef/lib/chef/mixin/generate_url.rb b/chef/lib/chef/mixin/generate_url.rb index abb28d6df0b..9ebe22b8327 100644 --- a/chef/lib/chef/mixin/generate_url.rb +++ b/chef/lib/chef/mixin/generate_url.rb @@ -24,7 +24,7 @@ module GenerateURL def generate_cookbook_url(url, cookbook, type, node, args=nil) new_url = nil - if url =~ /^http/ + if url =~ /^(http|https):\/\// new_url = url else new_url = "cookbooks/#{cookbook}/#{type}?" diff --git a/chef/lib/chef/mixin/template.rb b/chef/lib/chef/mixin/template.rb index 11329dbaa74..73a6ffbd95f 100644 --- a/chef/lib/chef/mixin/template.rb +++ b/chef/lib/chef/mixin/template.rb @@ -26,14 +26,59 @@ module Template # Render a template with Erubis. Takes a template as a string, and a # context hash. def render_template(template, context) - eruby = Erubis::Eruby.new(template) - output = eruby.evaluate(context) + begin + eruby = Erubis::Eruby.new(template) + output = eruby.evaluate(context) + rescue Object => e + raise TemplateError.new(e, template, context) + end final_tempfile = Tempfile.new("chef-rendered-template") final_tempfile.print(output) final_tempfile.close final_tempfile end + class TemplateError < RuntimeError + attr_reader :original_exception, :context + SOURCE_CONTEXT_WINDOW = 2 unless defined? SOURCE_CONTEXT_WINDOW + + def initialize(original_exception, template, context) + @original_exception, @template, @context = original_exception, template, context + end + + def message + @original_exception.message + end + + def line_number + @line_number ||= $1.to_i if original_exception.backtrace.find {|line| line =~ /\(erubis\):(\d+)/ } + end + + def source_location + "on line ##{line_number}" + end + + def source_listing + @source_listing ||= begin + line_index = line_number - 1 + beginning_line = line_index <= SOURCE_CONTEXT_WINDOW ? 0 : line_index - SOURCE_CONTEXT_WINDOW + source_size = SOURCE_CONTEXT_WINDOW * 2 + 1 + lines = @template.split(/\n/) + contextual_lines = lines[beginning_line, source_size] + output = [] + contextual_lines.each_with_index do |line, index| + line_number = (index+beginning_line+1).to_s.rjust(3) + output << "#{line_number}: #{line}" + end + output.join("\n") + end + end + + def to_s + "\n\n#{self.class} (#{message}) #{source_location}:\n\n" + + "#{source_listing}\n\n #{original_exception.backtrace.join("\n ")}\n\n" + end + end end end end diff --git a/chef/lib/chef/provider/package.rb b/chef/lib/chef/provider/package.rb index 5b323f2bdd0..badc7aaacd5 100644 --- a/chef/lib/chef/provider/package.rb +++ b/chef/lib/chef/provider/package.rb @@ -71,7 +71,8 @@ def action_install def action_upgrade if @current_resource.version != @candidate_version - Chef::Log.info("Upgrading #{@new_resource} version from #{@current_resource.version} to #{@candidate_version}") + orig_version = @current_resource.version || "uninstalled" + Chef::Log.info("Upgrading #{@new_resource} version from #{orig_version} to #{@candidate_version}") status = upgrade_package(@new_resource.package_name, @candidate_version) if status @new_resource.updated = true diff --git a/chef/lib/chef/provider/package/dpkg.rb b/chef/lib/chef/provider/package/dpkg.rb new file mode 100644 index 00000000000..0cad2d00e8e --- /dev/null +++ b/chef/lib/chef/provider/package/dpkg.rb @@ -0,0 +1,110 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' + +class Chef + class Provider + class Package + class Dpkg < Chef::Provider::Package::Apt + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @new_resource.version(nil) + + # We only -need- source for action install + if @new_resource.source + unless ::File.exists?(@new_resource.source) + raise Chef::Exception::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" + end + + # Get information from the package if supplied + Chef::Log.debug("Checking dpkg status for #{@new_resource.package_name}") + status = popen4("dpkg-deb -W #{@new_resource.source}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /([\w\d]+)\t([\w\d.-]+)/ + @current_resource.package_name($1) + @new_resource.version($2) + end + end + end + else + # if the source was not set, and we're installing, fail + if @new_resource.action.include?(:install) + raise Chef::Exception::Package, "Source for package #{@new_resource.name} required for action install" + end + end + + # Check to see if it is installed + package_installed = nil + Chef::Log.debug("Checking install state for #{@current_resource.package_name}") + status = popen4("dpkg -s #{@current_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /^Status: install ok installed/ + package_installed = true + when /^Version: (.+)$/ + if package_installed + Chef::Log.debug("Current version is #{$1}") + @current_resource.version($1) + end + end + end + end + + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exception::Package, "dpkg failed - #{status.inspect}!" + end + + @current_resource + end + + def install_package(name, version) + run_command( + :command => "dpkg -i #{@new_resource.source}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def remove_package(name, version) + run_command( + :command => "dpkg -r #{@new_resource.package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def purge_package(name, version) + run_command( + :command => "dpkg -P #{@new_resource.package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + end + end + end +end diff --git a/chef/lib/chef/provider/remote_directory.rb b/chef/lib/chef/provider/remote_directory.rb index fb490bbb1df..010e0b29cdb 100644 --- a/chef/lib/chef/provider/remote_directory.rb +++ b/chef/lib/chef/provider/remote_directory.rb @@ -53,7 +53,7 @@ def do_recursive full_dir = ::File.dirname(full_path) unless ::File.directory?(full_dir) new_dir = Chef::Resource::Directory.new(full_dir, nil, @node) - new_dir.cookbook_name = @new_resource.cookbook_name + new_dir.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name new_dir.mode(@new_resource.mode) new_dir.group(@new_resource.group) new_dir.owner(@new_resource.owner) @@ -67,7 +67,7 @@ def do_recursive end remote_file = Chef::Resource::RemoteFile.new(full_path, nil, @node) - remote_file.cookbook_name = @new_resource.cookbook_name + remote_file.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name remote_file.source(::File.join(@new_resource.source, remote_file_source)) remote_file.mode(@new_resource.files_mode) if @new_resource.files_mode remote_file.group(@new_resource.files_group) if @new_resource.files_group diff --git a/chef/lib/chef/provider/remote_file.rb b/chef/lib/chef/provider/remote_file.rb index 8ceed5f74dd..8fd5eb01049 100644 --- a/chef/lib/chef/provider/remote_file.rb +++ b/chef/lib/chef/provider/remote_file.rb @@ -91,6 +91,7 @@ def get_from_uri(source) uri = URI.parse(source) if uri.absolute r = Chef::REST.new(source) + Chef::Log.debug("Downloading from absolute URI: #{source}") r.get_rest(source, true).open end rescue URI::InvalidURIError @@ -101,6 +102,7 @@ def get_from_server(source, current_checksum) unless Chef::Config[:solo] r = Chef::REST.new(Chef::Config[:remotefile_url]) url = generate_url(source, "files", :checksum => current_checksum) + Chef::Log.debug("Downloading from server: #{url}") r.get_rest(url, true).open end end diff --git a/chef/lib/chef/resource/remote_directory.rb b/chef/lib/chef/resource/remote_directory.rb index 478cdc6528c..1628af3a333 100644 --- a/chef/lib/chef/resource/remote_directory.rb +++ b/chef/lib/chef/resource/remote_directory.rb @@ -34,6 +34,7 @@ def initialize(name, collection=nil, node=nil) @files_group = nil @files_mode = 0644 @allowed_actions.push(:create, :delete) + @cookbook = nil end def source(args=nil) @@ -76,6 +77,14 @@ def files_owner(arg=nil) ) end + def cookbook(args=nil) + set_or_return( + :cookbook, + args, + :kind_of => String + ) + end + end end end \ No newline at end of file diff --git a/chef/lib/chef/rest.rb b/chef/lib/chef/rest.rb index 32242625893..bac6788e526 100644 --- a/chef/lib/chef/rest.rb +++ b/chef/lib/chef/rest.rb @@ -123,6 +123,10 @@ def run_request(method, url, data=false, limit=10, raw=false) if Chef::Config[:ssl_verify_mode] == :verify_none http.verify_mode = OpenSSL::SSL::VERIFY_NONE end + if File.exists?(Chef::Config[:ssl_client_cert]) + http.cert = OpenSSL::X509::Certificate.new(File.read(Chef::Config[:ssl_client_cert])) + http.key = OpenSSL::PKey::RSA.new(File.read(Chef::Config[:ssl_client_key])) + end end http.read_timeout = Chef::Config[:rest_timeout] headers = Hash.new diff --git a/chef/spec/unit/mixin/template_spec.rb b/chef/spec/unit/mixin/template_spec.rb index 443ca28e1df..86e0b592321 100644 --- a/chef/spec/unit/mixin/template_spec.rb +++ b/chef/spec/unit/mixin/template_spec.rb @@ -19,42 +19,72 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) class TinyTemplateClass; include Chef::Mixin::Template; end - +require 'cgi' describe Chef::Mixin::Template, "render_template" do - before(:each) do - @template = "abcnews" - @context = { :fine => "dear" } - @eruby = mock(:erubis, { :evaluate => "elvis costello" }) - Erubis::Eruby.stub!(:new).and_return(@eruby) - @tempfile = mock(:tempfile, { :print => true, :close => true }) - Tempfile.stub!(:new).and_return(@tempfile) - @tiny_template = TinyTemplateClass.new - end - - it "should create a new Erubis object from the template" do - Erubis::Eruby.should_receive(:new).with("abcnews").and_return(@eruby) - @tiny_template.render_template(@template, @context) + before :each do + @template = TinyTemplateClass.new end - - it "should evaluate the template with the provided context" do - @eruby.should_receive(:evaluate).with(@context).and_return(true) - @tiny_template.render_template(@template, @context) + + it "should render the template evaluated in the given context" do + @template.render_template("<%= @foo %>", { :foo => "bar" }).open.read.should == "bar" end - it "should create a tempfile for the resulting file" do - Tempfile.should_receive(:new).and_return(@tempfile) - @tiny_template.render_template(@template, @context) + it "should return a file" do + @template.render_template("abcdef", {}).should be_kind_of(File) end - it "should print the contents of the resulting template to the tempfile" do - @tempfile.should_receive(:print).with("elvis costello").and_return(true) - @tiny_template.render_template(@template, @context) - end + describe "when an exception is raised in the template" do + def do_raise + @context = {:chef => "cool"} + @template.render_template("foo\nbar\nbaz\n<%= this_is_not_defined %>\nquin\nqunx\ndunno", @context) + end - it "should close the tempfile" do - @tempfile.should_receive(:close).and_return(true) - @tiny_template.render_template(@template, @context) + it "should catch and re-raise the exception as a TemplateError" do + lambda { do_raise }.should raise_error(Chef::Mixin::Template::TemplateError) + end + + describe "the raised TemplateError" do + before :each do + begin + do_raise + rescue Chef::Mixin::Template::TemplateError => e + @exception = e + end + end + + it "should have the original exception" do + @exception.original_exception.should be + @exception.original_exception.message.should =~ /undefined local variable or method `this_is_not_defined'/ + end + + it "should determine the line number of the exception" do + @exception.line_number.should == 4 + end + + it "should provide a source listing of the template around the exception" do + @exception.source_listing.should == " 2: bar\n 3: baz\n 4: <%= this_is_not_defined %>\n 5: quin\n 6: qunx" + end + + it "should provide the evaluation context of the template" do + @exception.context.should == @context + end + + it "should defer the message to the original exception" do + @exception.message.should =~ /undefined local variable or method `this_is_not_defined'/ + end + + it "should provide a nice source location" do + @exception.source_location.should == "on line #4" + end + + it "should create a pretty output for the terminal" do + @exception.to_s.should =~ /Chef::Mixin::Template::TemplateError/ + @exception.to_s.should =~ /undefined local variable or method `this_is_not_defined'/ + @exception.to_s.should include(" 2: bar\n 3: baz\n 4: <%= this_is_not_defined %>\n 5: quin\n 6: qunx") + @exception.to_s.should include(@exception.original_exception.backtrace.first) + end + end end end diff --git a/chef/spec/unit/provider/package/dpkg_spec.rb b/chef/spec/unit/provider/package/dpkg_spec.rb new file mode 100644 index 00000000000..390609aca90 --- /dev/null +++ b/chef/spec/unit/provider/package/dpkg_spec.rb @@ -0,0 +1,177 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# 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 File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec_helper")) + +describe Chef::Provider::Package::Dpkg, "load_current_resource" do + before(:each) do + @node = mock("Chef::Node", :null_object => true) + @new_resource = mock("Chef::Resource::Package", + :null_object => true, + :name => "wget", + :version => nil, + :package_name => "wget", + :updated => nil, + :source => "/tmp/wget_1.11.4-1ubuntu1_amd64.deb" + ) + @current_resource = mock("Chef::Resource::Package", + :null_object => true, + :name => "wget", + :version => nil, + :package_name => nil, + :updated => nil + ) + + @provider = Chef::Provider::Package::Dpkg.new(@node, @new_resource) + Chef::Resource::Package.stub!(:new).and_return(@current_resource) + + @stdin = mock("STDIN", :null_object => true) + @stdout = mock("STDOUT", :null_object => true) + @status = mock("Status", :exitstatus => 0) + @stderr = mock("STDERR", :null_object => true) + @pid = mock("PID", :null_object => true) + @provider.stub!(:popen4).and_return(@status) + + ::File.stub!(:exists?).and_return(true) + end + + it "should create a current resource with the name of the new_resource" do + Chef::Resource::Package.should_receive(:new).and_return(@current_resource) + @provider.load_current_resource + end + + it "should set the current resources package name to the new resources package name" do + @current_resource.should_receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end + + it "should raise an exception if a source is supplied but not found" do + ::File.stub!(:exists?).and_return(false) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exception::Package) + end + + it "should get the source package version from dpkg-deb if provided" do + @stdout.stub!(:each).and_yield("wget\t1.11.4-1ubuntu1") + @provider.stub!(:popen4).with("dpkg-deb -W #{@new_resource.source}").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:package_name).with("wget") + @new_resource.should_receive(:version).with("1.11.4-1ubuntu1") + @provider.load_current_resource + end + + it "should raise an exception if the source is not set but we are installing" do + @new_resource = mock("Chef::Resource::Package", + :null_object => true, + :name => "wget", + :version => nil, + :package_name => "wget", + :updated => nil, + :source => nil + ) + @provider = Chef::Provider::Package::Dpkg.new(@node, @new_resource) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exception::Package) + + end + + it "should return the current version installed if found by dpkg" do + @stdout.stub!(:each).and_yield("Package: wget"). + and_yield("Status: install ok installed"). + and_yield("Priority: important"). + and_yield("Section: web"). + and_yield("Installed-Size: 1944"). + and_yield("Maintainer: Ubuntu Core developers "). + and_yield("Architecture: amd64"). + and_yield("Version: 1.11.4-1ubuntu1"). + and_yield("Config-Version: 1.11.4-1ubuntu1"). + and_yield("Depends: libc6 (>= 2.8~20080505), libssl0.9.8 (>= 0.9.8f-5)"). + and_yield("Conflicts: wget-ssl") + @provider.stub!(:popen4).with("dpkg -s #{@current_resource.package_name}").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @current_resource.should_receive(:version).with("1.11.4-1ubuntu1") + @provider.load_current_resource + end + + it "should raise an exception if dpkg fails to run" do + @status = mock("Status", :exitstatus => -1) + @provider.stub!(:popen4).and_return(@status) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exception::Package) + end +end + +describe Chef::Provider::Package::Dpkg, "install and upgrade" do + before(:each) do + @node = mock("Chef::Node", :null_object => true) + @new_resource = mock("Chef::Resource::Package", + :null_object => true, + :name => "wget", + :version => nil, + :package_name => "wget", + :updated => nil, + :source => "/tmp/wget_1.11.4-1ubuntu1_amd64.deb" + ) + @provider = Chef::Provider::Package::Dpkg.new(@node, @new_resource) + end + + it "should run dpkg -i with the package source" do + @provider.should_receive(:run_command).with({ + :command => "dpkg -i /tmp/wget_1.11.4-1ubuntu1_amd64.deb", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.install_package("wget", "1.11.4-1ubuntu1") + end + + it "should upgrade by running install_package" do + @provider.should_receive(:install_package).with("wget", "1.11.4-1ubuntu1") + @provider.upgrade_package("wget", "1.11.4-1ubuntu1") + end +end + +describe Chef::Provider::Package::Dpkg, "remove and purge" do + before(:each) do + @node = mock("Chef::Node", :null_object => true) + @new_resource = mock("Chef::Resource::Package", + :null_object => true, + :name => "wget", + :version => nil, + :package_name => "wget", + :updated => nil + ) + @provider = Chef::Provider::Package::Dpkg.new(@node, @new_resource) + end + + it "should run dpkg -r to remove the package" do + @provider.should_receive(:run_command).with({ + :command => "dpkg -r wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.remove_package("wget", "1.11.4-1ubuntu1") + end + + it "should run dpkg -P to purge the package" do + @provider.should_receive(:run_command).with({ + :command => "dpkg -P wget", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + }) + @provider.purge_package("wget", "1.11.4-1ubuntu1") + end +end + diff --git a/chef/spec/unit/provider/package_spec.rb b/chef/spec/unit/provider/package_spec.rb index fbfc8b5bf29..925360e1dd5 100644 --- a/chef/spec/unit/provider/package_spec.rb +++ b/chef/spec/unit/provider/package_spec.rb @@ -126,7 +126,8 @@ :null_object => true, :name => "emacs", :version => nil, - :package_name => "emacs" + :package_name => "emacs", + :to_s => 'package[emacs]' ) @current_resource = mock("Chef::Resource::Package", :null_object => true, @@ -158,6 +159,12 @@ @provider.should_not_receive(:upgrade_package) @provider.action_upgrade end + + it "should print the word 'uninstalled' if there was no original version" do + @current_resource.stub!(:version).and_return(nil) + Chef::Log.should_receive(:info).with("Upgrading #{@new_resource} version from uninstalled to 1.0") + @provider.action_upgrade + end end # Oh ruby, you are so nice. diff --git a/chef/spec/unit/rest_spec.rb b/chef/spec/unit/rest_spec.rb index ba892944fca..525d17e148d 100644 --- a/chef/spec/unit/rest_spec.rb +++ b/chef/spec/unit/rest_spec.rb @@ -140,6 +140,55 @@ def do_run_request(method=:GET, data=false, limit=10, raw=false) do_run_request end + describe "with a client SSL cert" do + before(:each) do + Chef::Config[:ssl_client_cert] = "/etc/chef/client-cert.pem" + Chef::Config[:ssl_client_key] = "/etc/chef/client-cert.key" + File.stub!(:exists?).with("/etc/chef/client-cert.pem").and_return(true) + File.stub!(:exists?).with("/etc/chef/client-cert.key").and_return(true) + File.stub!(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") + File.stub!(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") + OpenSSL::X509::Certificate.stub!(:new).and_return("monkey magic client data") + OpenSSL::PKey::RSA.stub!(:new).and_return("monkey magic key data") + end + + it "should check that the client cert file exists" do + File.should_receive(:exists?).with("/etc/chef/client-cert.pem").and_return(true) + do_run_request + end + + it "should read the cert file" do + File.should_receive(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") + do_run_request + end + + it "should read the cert into OpenSSL" do + OpenSSL::X509::Certificate.should_receive(:new).and_return("monkey magic client data") + do_run_request + end + + it "should set the cert" do + @http_mock.should_receive(:cert=).and_return(true) + do_run_request + end + + it "should read the key file" do + File.should_receive(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") + do_run_request + end + + it "should read the key into OpenSSL" do + OpenSSL::PKey::RSA.should_receive(:new).and_return("monkey magic key data") + do_run_request + end + + it "should set the key" do + @http_mock.should_receive(:key=).and_return(true) + do_run_request + end + + end + it "should set a read timeout based on the rest_timeout config option" do Chef::Config[:rest_timeout] = 10 @http_mock.should_receive(:read_timeout=).with(10).and_return(true) diff --git a/chefserverslice/Rakefile b/chefserverslice/Rakefile index 2f6cd274e66..4f094d0a5f8 100644 --- a/chefserverslice/Rakefile +++ b/chefserverslice/Rakefile @@ -5,7 +5,7 @@ require 'merb-core' require 'merb-core/tasks/merb' GEM_NAME = "chefserverslice" -CHEF_SERVER_VERSION="0.5.3" +CHEF_SERVER_VERSION="0.5.5" AUTHOR = "Opscode" EMAIL = "chef@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" diff --git a/features/data/Rakefile b/features/data/Rakefile new file mode 100644 index 00000000000..fcf46f69b7c --- /dev/null +++ b/features/data/Rakefile @@ -0,0 +1,176 @@ +# +# Rakefile for Chef Server Repository +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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 File.join(File.dirname(__FILE__), 'config', 'rake') + +require 'tempfile' + +if File.directory?(File.join(TOPDIR, ".svn")) + $vcs = :svn +elsif File.directory?(File.join(TOPDIR, ".git")) + $vcs = :git +end + +desc "Update your repository from source control" +task :update do + puts "** Updating your repository" + + case $vcs + when :svn + sh %{svn up} + when :git + pull = false + pull = true if File.join(TOPDIR, ".git", "remotes", "origin") + IO.foreach(File.join(TOPDIR, ".git", "config")) do |line| + pull = true if line =~ /\[remote "origin"\]/ + end + if pull + sh %{git pull} + else + puts "* Skipping git pull, no origin specified" + end + else + puts "* No SCM configured, skipping update" + end +end + +desc "Test your cookbooks for syntax errors" +task :test do + puts "** Testing your cookbooks for syntax errors" + Dir[ File.join(TOPDIR, "cookbooks", "**", "*.rb") ].each do |recipe| + print "Testing recipe #{recipe}: " + sh %{ruby -c #{recipe}} do |ok, res| + if ! ok + raise "Syntax error in #{recipe}" + end + end + end +end + +desc "Install the latest copy of the repository on this Chef Server" +task :install => [ :update, :test ] do + puts "** Installing your cookbooks" + directories = [ + COOKBOOK_PATH, + SITE_COOKBOOK_PATH, + CHEF_CONFIG_PATH + ] + puts "* Creating Directories" + directories.each do |dir| + sh "sudo mkdir -p #{dir}" + sh "sudo chown root #{dir}" + end + puts "* Installing new Cookbooks" + sh "sudo rsync -rlP --delete --exclude '.svn' cookbooks/ #{COOKBOOK_PATH}" + puts "* Installing new Site Cookbooks" + sh "sudo rsync -rlP --delete --exclude '.svn' cookbooks/ #{COOKBOOK_PATH}" + puts "* Installing new Chef Server Config" + sh "sudo cp config/server.rb #{CHEF_SERVER_CONFIG}" + puts "* Installing new Chef Client Config" + sh "sudo cp config/client.rb #{CHEF_CLIENT_CONFIG}" +end + +desc "By default, run rake test" +task :default => [ :test ] + +desc "Create a new cookbook (with COOKBOOK=name)" +task :new_cookbook do + create_cookbook(File.join(TOPDIR, "cookbooks")) +end + +def create_cookbook(dir) + raise "Must provide a COOKBOOK=" unless ENV["COOKBOOK"] + puts "** Creating cookbook #{ENV["COOKBOOK"]}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "attributes")}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "recipes")}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "definitions")}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "libraries")}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "files", "default")}" + sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "templates", "default")}" + unless File.exists?(File.join(dir, ENV["COOKBOOK"], "recipes", "default.rb")) + open(File.join(dir, ENV["COOKBOOK"], "recipes", "default.rb"), "w") do |file| + file.puts <<-EOH +# +# Cookbook Name:: #{ENV["COOKBOOK"]} +# Recipe:: default +# +# Copyright #{Time.now.year}, #{COMPANY_NAME} +# +EOH + case NEW_COOKBOOK_LICENSE + when :apachev2 + file.puts <<-EOH +# 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. +# +EOH + when :none + file.puts <<-EOH +# All rights reserved - Do Not Redistribute +# +EOH + end + end + end +end + +desc "Create a new self-signed SSL certificate for FQDN=foo.example.com" +task :ssl_cert do + $expect_verbose = true + fqdn = ENV["FQDN"] + fqdn =~ /^(.+?)\.(.+)$/ + hostname = $1 + domain = $2 + raise "Must provide FQDN!" unless fqdn && hostname && domain + puts "** Creating self signed SSL Certificate for #{fqdn}" + sh("(cd #{CADIR} && openssl genrsa 2048 > #{fqdn}.key)") + sh("(cd #{CADIR} && chmod 644 #{fqdn}.key)") + puts "* Generating Self Signed Certificate Request" + tf = Tempfile.new("#{fqdn}.ssl-conf") + ssl_config = < #{fqdn}.crt)") + sh("(cd #{CADIR} && openssl x509 -noout -fingerprint -text < #{fqdn}.crt > #{fqdn}.info)") + sh("(cd #{CADIR} && cat #{fqdn}.crt #{fqdn}.key > #{fqdn}.pem)") + sh("(cd #{CADIR} && chmod 644 #{fqdn}.pem)") +end diff --git a/features/data/config/client.rb b/features/data/config/client.rb new file mode 100644 index 00000000000..45471dabaef --- /dev/null +++ b/features/data/config/client.rb @@ -0,0 +1,13 @@ +supportdir = File.expand_path(File.join(File.dirname(__FILE__), "..")) +tmpdir = File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp")) + +log_level :error +log_location STDOUT +file_cache_path File.join(tmpdir, "cache") +ssl_verify_mode :verify_none +registration_url "http://127.0.0.1:4000" +openid_url "http://127.0.0.1:4001" +template_url "http://127.0.0.1:4000" +remotefile_url "http://127.0.0.1:4000" +search_url "http://127.0.0.1:4000" +couchdb_database 'chef_integration' diff --git a/features/data/config/rake.rb b/features/data/config/rake.rb new file mode 100644 index 00000000000..d79d6e9889c --- /dev/null +++ b/features/data/config/rake.rb @@ -0,0 +1,57 @@ +### +# Company and SSL Details +### + +# The company name - used for SSL certificates, and in various other places +COMPANY_NAME = "Opscode" + +# The Country Name to use for SSL Certificates +SSL_COUNTRY_NAME = "US" + +# The State Name to use for SSL Certificates +SSL_STATE_NAME = "Washington" + +# The Locality Name for SSL - typically, the city +SSL_LOCALITY_NAME = "Seattle" + +# What department? +SSL_ORGANIZATIONAL_UNIT_NAME = "Operations" + +# The SSL contact email address +SSL_EMAIL_ADDRESS = "do_not_reply@opscode.com" + +# License for new Cookbooks +# Can be :apachev2 or :none +NEW_COOKBOOK_LICENSE = :apachev2 + +########################## +# Chef Repository Layout # +########################## + +supportdir = File.expand_path(File.join(File.dirname(__FILE__), "..")) +tmpdir = File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp")) + +# Where to find upstream cookbooks +COOKBOOK_PATH = File.join(supportdir, "cookbooks") + +# Where to find site-local modifications to upstream cookbooks +SITE_COOKBOOK_PATH = File.join(supportdir, "site-cookbooks") + +# Chef Config Path +CHEF_CONFIG_PATH = File.join(supportdir, "config") + +# The location of the Chef Server Config file (on the server) +CHEF_SERVER_CONFIG = File.join(CHEF_CONFIG_PATH, "server.rb") + +# The location of the Chef Client Config file (on the client) +CHEF_CLIENT_CONFIG = File.join(CHEF_CONFIG_PATH, "client.rb") + +### +# Useful Extras (which you probably don't need to change) +### + +# The top of the repository checkout +TOPDIR = File.expand_path(File.join(File.dirname(__FILE__), "..")) + +# Where to store certificates generated with ssl_cert +CADIR = File.expand_path(File.join(TOPDIR, "certificates")) diff --git a/features/data/config/server.rb b/features/data/config/server.rb new file mode 100644 index 00000000000..9c7674581d3 --- /dev/null +++ b/features/data/config/server.rb @@ -0,0 +1,20 @@ +supportdir = File.expand_path(File.join(File.dirname(__FILE__), "..")) +tmpdir = File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp")) + +log_level :debug +log_location STDOUT +file_cache_path File.join(tmpdir, "cache") +ssl_verify_mode :verify_none +registration_url "http://127.0.0.1:4000" +openid_url "http://127.0.0.1:4001" +template_url "http://127.0.0.1:4000" +remotefile_url "http://127.0.0.1:4000" +search_url "http://127.0.0.1:4000" +cookbook_path File.join(supportdir, "cookbooks") +openid_store_path File.join(tmpdir, "openid", "store") +openid_cstore_path File.join(tmpdir, "openid", "cstore") +search_index_path File.join(tmpdir, "search_index") +validation_token 'ceelo' +couchdb_database 'chef_integration' + +Chef::Log::Formatter.show_time = true diff --git a/features/data/cookbooks/integration_setup/attributes/integration.rb b/features/data/cookbooks/integration_setup/attributes/integration.rb new file mode 100644 index 00000000000..7ee3a25090e --- /dev/null +++ b/features/data/cookbooks/integration_setup/attributes/integration.rb @@ -0,0 +1,25 @@ +# +# Cookbook Name:: integration_setup +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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 'tmpdir' + +int(Mash.new) +int[:tmpdir] = File.join(Dir.tmpdir, "chef_integration") + +tmpdir int[:tmpdir] \ No newline at end of file diff --git a/features/data/cookbooks/integration_setup/recipes/default.rb b/features/data/cookbooks/integration_setup/recipes/default.rb new file mode 100644 index 00000000000..0ada2aa4856 --- /dev/null +++ b/features/data/cookbooks/integration_setup/recipes/default.rb @@ -0,0 +1,25 @@ +# +# Cookbook Name:: integration_setup +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +directory node[:int][:tmpdir] do + owner "root" + mode 1777 + action :create +end + diff --git a/features/data/cookbooks/manage_files/recipes/create_a_file.rb b/features/data/cookbooks/manage_files/recipes/create_a_file.rb new file mode 100644 index 00000000000..5a327d64b3f --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/create_a_file.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +file "#{node[:tmpdir]}/create_a_file.txt" do + action :create +end \ No newline at end of file diff --git a/features/data/cookbooks/manage_files/recipes/default.rb b/features/data/cookbooks/manage_files/recipes/default.rb new file mode 100644 index 00000000000..8b9f7ab0cf8 --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/default.rb @@ -0,0 +1,19 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + diff --git a/features/data/cookbooks/manage_files/recipes/delete_a_file.rb b/features/data/cookbooks/manage_files/recipes/delete_a_file.rb new file mode 100644 index 00000000000..5d3faf41cd7 --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/delete_a_file.rb @@ -0,0 +1,24 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +include_recipe 'manage_files::create_a_file' + +file "#{node[:tmpdir]}/create_a_file.txt" do + action :delete +end \ No newline at end of file diff --git a/features/data/cookbooks/manage_files/recipes/delete_a_file_that_does_not_already_exist.rb b/features/data/cookbooks/manage_files/recipes/delete_a_file_that_does_not_already_exist.rb new file mode 100644 index 00000000000..f6c2e331473 --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/delete_a_file_that_does_not_already_exist.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +file "#{node[:tmpdir]}/create_a_file.txt" do + action :delete +end \ No newline at end of file diff --git a/features/data/cookbooks/manage_files/recipes/set_the_owner_of_a_created_file.rb b/features/data/cookbooks/manage_files/recipes/set_the_owner_of_a_created_file.rb new file mode 100644 index 00000000000..28a92550e36 --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/set_the_owner_of_a_created_file.rb @@ -0,0 +1,23 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +file "#{node[:tmpdir]}/create_a_file.txt" do + owner 'nobody' + action :create +end \ No newline at end of file diff --git a/features/data/cookbooks/manage_files/recipes/touch_a_file.rb b/features/data/cookbooks/manage_files/recipes/touch_a_file.rb new file mode 100644 index 00000000000..3aff3fa6d62 --- /dev/null +++ b/features/data/cookbooks/manage_files/recipes/touch_a_file.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +file "#{node[:tmpdir]}/touch_test.txt" do + action :touch +end \ No newline at end of file diff --git a/features/data/cookbooks/transfer_remote_files/files/default/transfer_a_file_from_a_cookbook.txt b/features/data/cookbooks/transfer_remote_files/files/default/transfer_a_file_from_a_cookbook.txt new file mode 100644 index 00000000000..9fb38fab625 --- /dev/null +++ b/features/data/cookbooks/transfer_remote_files/files/default/transfer_a_file_from_a_cookbook.txt @@ -0,0 +1 @@ +easy like sunday morning diff --git a/features/data/cookbooks/transfer_remote_files/recipes/default.rb b/features/data/cookbooks/transfer_remote_files/recipes/default.rb new file mode 100644 index 00000000000..8bba761657a --- /dev/null +++ b/features/data/cookbooks/transfer_remote_files/recipes/default.rb @@ -0,0 +1,18 @@ +# +# Cookbook Name:: transfer_remote_files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# diff --git a/features/data/cookbooks/transfer_remote_files/recipes/should_prefer_the_file_for_this_specific_host.rb b/features/data/cookbooks/transfer_remote_files/recipes/should_prefer_the_file_for_this_specific_host.rb new file mode 100644 index 00000000000..98415d1b154 --- /dev/null +++ b/features/data/cookbooks/transfer_remote_files/recipes/should_prefer_the_file_for_this_specific_host.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: transfer_remote_files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +remote_file "#{node[:tmpdir]}/host_specific.txt" do + source "host_specific.txt" +end \ No newline at end of file diff --git a/features/data/cookbooks/transfer_remote_files/recipes/transfer_a_file_from_a_cookbook.rb b/features/data/cookbooks/transfer_remote_files/recipes/transfer_a_file_from_a_cookbook.rb new file mode 100644 index 00000000000..70a650c803a --- /dev/null +++ b/features/data/cookbooks/transfer_remote_files/recipes/transfer_a_file_from_a_cookbook.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: transfer_remote_files +# Recipe:: default +# +# Copyright 2009, Opscode +# +# 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. +# + +remote_file "#{node[:tmpdir]}/transfer_a_file_from_a_cookbook.txt" do + source "transfer_a_file_from_a_cookbook.txt" +end \ No newline at end of file diff --git a/features/manage_files.feature b/features/manage_files.feature new file mode 100644 index 00000000000..0035949e165 --- /dev/null +++ b/features/manage_files.feature @@ -0,0 +1,43 @@ +Feature: Manage Files + In order to save time + As a Developer + I want to manage files declaratively + + Scenario: Create a file + Given a validated node + And it includes the recipe 'manage_files::create_a_file' + When I run the chef-client + Then the run should exit '0' + And a file named 'create_a_file.txt' should exist + + Scenario: Set the owner of a created file + Given a validated node + And it includes the recipe 'manage_files::set_the_owner_of_a_created_file' + When I run the chef-client + Then the run should exit '0' + And the file named 'create_a_file.txt' should be owned by 'nobody' + + Scenario: Delete a file + Given a validated node + And it includes the recipe 'manage_files::delete_a_file' + When I run the chef-client + Then the run should exit '0' + And a file named 'create_a_file.txt' should not exist + + Scenario: Delete a file that already does not exist + Given a validated node + And it includes the recipe 'manage_files::delete_a_file_that_does_not_already_exist' + When I run the chef-client + Then the run should exit '1' + And stdout should have 'Cannot delete file' + + Scenario: Touch a file + Given a validated node + And it includes the recipe 'manage_files::touch_a_file' + And we have an empty file named 'touch_test.txt' + And we have the atime/mtime of 'touch_test.txt' + When I run the chef-client + Then the run should exit '0' + And the atime of 'touch_test.txt' should be different + And the mtime of 'touch_test.txt' should be different + \ No newline at end of file diff --git a/features/steps/couchdb.rb b/features/steps/couchdb.rb new file mode 100644 index 00000000000..e2bfa8d371f --- /dev/null +++ b/features/steps/couchdb.rb @@ -0,0 +1,31 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +Before do + system("mkdir -p #{tmpdir}") + cdb = Chef::CouchDB.new(Chef::Config[:couchdb_url]) + cdb.create_db + Chef::Node.create_design_document + Chef::OpenIDRegistration.create_design_document +end + +After do + r = Chef::REST.new(Chef::Config[:couchdb_url]) + r.delete_rest("#{Chef::Config[:couchdb_database]}/") + system("rm -rf #{tmpdir}") +end \ No newline at end of file diff --git a/features/steps/files.rb b/features/steps/files.rb new file mode 100644 index 00000000000..7decc0cec8c --- /dev/null +++ b/features/steps/files.rb @@ -0,0 +1,65 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +### +# Given +### + +Given /^we have an empty file named '(.+)'$/ do |filename| + filename = File.new(File.join(tmpdir, filename), 'w') + filename.close +end + +Given /^we have the atime\/mtime of '(.+)'$/ do |filename| + @mtime = File.mtime(File.join(tmpdir, filename)) + @atime = File.atime(File.join(tmpdir, filename)) +end + +#### +# Then +#### + +Then /^a file named '(.+)' should exist$/ do |filename| + File.exists?(File.join(tmpdir, filename)).should be(true) +end + +Then /^a file named '(.+)' should not exist$/ do |filename| + File.exists?(File.join(tmpdir, filename)).should be(false) +end + +Then /^the (.)time of '(.+)' should be different$/ do |time_type, filename| + case time_type + when "m" + current_mtime = File.mtime(File.join(tmpdir, filename)) + current_mtime.should_not == @mtime + when "a" + current_atime = File.atime(File.join(tmpdir, filename)) + current_atime.should_not == @atime + end +end + +Then /^a file named '(.+)' should contain '(.+)'$/ do |filename, contents| + file = IO.read(File.join(tmpdir, filename)) + file.should =~ /#{contents}/m +end + +Then /^a file named '(.+)' should be from the '(.+)' specific directory$/ do |filename, specificity| + file = IO.read(File.join(tmpdir, filename)) + file.should == "#{specificity}\n" +end + diff --git a/features/steps/nodes.rb b/features/steps/nodes.rb new file mode 100644 index 00000000000..36cfb76914e --- /dev/null +++ b/features/steps/nodes.rb @@ -0,0 +1,42 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +### +# Given +### +Given /^a validated node$/ do + @client.validation_token = Chef::Config[:validation_token] = 'ceelo' + @client.build_node + @client.node.recipes = "integration_setup" + @client.register + @client.authenticate +end + +Given /^it includes the recipe '(.+)'$/ do |recipe| + @recipe = recipe + @client.node.recipes << recipe + @client.save_node +end + +### +# When +### +When /^the node is converged$/ do + @client.run +end + diff --git a/features/steps/recipe.rb b/features/steps/recipe.rb new file mode 100644 index 00000000000..7c5c96327ca --- /dev/null +++ b/features/steps/recipe.rb @@ -0,0 +1,33 @@ +Given /^the cookbook has a '(.+)' named '(.+)' in the '(.+)' specific directory$/ do |file_type, filename, specificity| + cookbook_name, recipe_name = @recipe.split('::') + type_dir = file_type == 'file' ? 'files' : 'templates' + specific_dir = nil + case specificity + when "host" + specific_dir = "host-#{@client.node[:fqdn]}" + when "platform-version" + specific_dir = "#{@client.node[:platform]}-#{@client.node[:platform_version]}" + when "platform" + specific_dir = @client.node[:platform] + when "default" + specific_dir = "default" + end + new_file_dir = File.expand_path( + File.join( + File.dirname(__FILE__), + "..", + "data", + "cookbooks", + cookbook_name, + type_dir, + specific_dir + ) + ) + @cleanup_dirs << new_file_dir unless new_file_dir =~ /default$/ + system("mkdir -p #{new_file_dir}") + new_file_name = File.join(new_file_dir, filename) + @cleanup_files << new_file_name + new_file = File.open(new_file_name, "w") + new_file.puts(specificity) + new_file.close +end diff --git a/features/steps/run_client.rb b/features/steps/run_client.rb new file mode 100644 index 00000000000..a464d793dd2 --- /dev/null +++ b/features/steps/run_client.rb @@ -0,0 +1,48 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +### +# When +### +When /^I run the chef\-client$/ do + status = Chef::Mixin::Command.popen4( + "chef-client -c #{File.expand_path(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'client.rb'))}", :waitlast => true) do |p, i, o, e| + i.close + @stdout = o.gets(nil) + @stderr = e.gets(nil) + end + @status = status +end + +### +# Then +### +Then /^the run should exit '(.+)'$/ do |exit_code| + begin + @status.exitstatus.should eql(exit_code.to_i) + rescue + puts "--- run stdout: #{@stdout}" + puts @stdout + puts "--- run stderr: #{@stderr}" + raise + end +end + +Then /^stdout should have '(.+)'$/ do |to_match| + @stdout.should match(/#{to_match}/m) +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 00000000000..978ce818bf2 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 Opscode, 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. +# + +%w{chef chef-server}.each do |inc_dir| + $: << File.join(File.dirname(__FILE__), '..', '..', inc_dir, 'lib') +end + +require 'spec/expectations' +require 'chef' +require 'chef/config' +require 'chef/client' +require 'tmpdir' + +Chef::Config.from_file(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'client.rb')) +Ohai::Config[:log_level] = :error + +class ChefWorld + attr_accessor :client, :tmpdir + + def initialize + @client = Chef::Client.new + @tmpdir = File.join(Dir.tmpdir, "chef_integration") + @cleanup_files = Array.new + @cleanup_dirs = Array.new + @recipe = nil + end +end + +World do + ChefWorld.new +end + +After do + @cleanup_files.each do |file| + system("rm #{file}") + end + @cleanup_dirs.each do |dir| + system("rm -rf #{dir}") + end +end + diff --git a/features/transfer_remote_files.feature b/features/transfer_remote_files.feature new file mode 100644 index 00000000000..e75f582cdfc --- /dev/null +++ b/features/transfer_remote_files.feature @@ -0,0 +1,41 @@ +Feature: Transfer Remote Files + In order to easily manage many systems at once + As a Developer + I want to manage the contents of files remotely + + Scenario: Transfer a file from a cookbook + Given a validated node + And it includes the recipe 'transfer_remote_files::transfer_a_file_from_a_cookbook' + When I run the chef-client + Then the run should exit '0' + And a file named 'transfer_a_file_from_a_cookbook.txt' should contain 'easy like sunday morning' + + Scenario: Should prefer the file for this specific host + Given a validated node + And it includes the recipe 'transfer_remote_files::should_prefer_the_file_for_this_specific_host' + And the cookbook has a 'file' named 'host_specific.txt' in the 'host' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'platform-version' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'platform' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'default' specific directory + When I run the chef-client + Then the run should exit '0' + And a file named 'host_specific.txt' should be from the 'host' specific directory + + Scenario: Should prefer the file for the correct platform version + Given a validated node + And it includes the recipe 'transfer_remote_files::should_prefer_the_file_for_this_specific_host' + And the cookbook has a 'file' named 'host_specific.txt' in the 'platform-version' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'platform' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'default' specific directory + When I run the chef-client + Then the run should exit '0' + And a file named 'host_specific.txt' should be from the 'platform-version' specific directory + + Scenario: Should prefer the file for the correct platform + Given a validated node + And it includes the recipe 'transfer_remote_files::should_prefer_the_file_for_this_specific_host' + And the cookbook has a 'file' named 'host_specific.txt' in the 'platform' specific directory + And the cookbook has a 'file' named 'host_specific.txt' in the 'default' specific directory + When I run the chef-client + Then the run should exit '0' + And a file named 'host_specific.txt' should be from the 'platform' specific directory