summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristopher Brown <cb@opscode.com>2009-02-25 08:13:20 -0800
committerChristopher Brown <cb@opscode.com>2009-02-25 08:13:20 -0800
commitf79a7385964f6b60220ef1817120d331ae9d8d80 (patch)
tree62d702e134c1acaa9d0bc3b223705488ebaad29e
parent6f5042c1947415db20edd4453f3c40252eca6cdc (diff)
parent409ffd22488aea6afc75ad18b32e64cc8fffcc78 (diff)
downloadchef-f79a7385964f6b60220ef1817120d331ae9d8d80.tar.gz
merged master 0.5.5
-rw-r--r--.gitignore4
-rw-r--r--CHANGELOG39
-rw-r--r--NOTICE2
-rw-r--r--Rakefile101
-rw-r--r--chef-server/Rakefile2
-rw-r--r--chef-server/chef-server.gemspec58
-rw-r--r--chef-server/lib/views/exceptions/bad_request.html.haml2
-rw-r--r--chef/Rakefile2
-rwxr-xr-xchef/bin/chef-client7
-rw-r--r--chef/chef.gemspec2
-rw-r--r--chef/lib/chef.rb2
-rw-r--r--chef/lib/chef/config.rb2
-rw-r--r--chef/lib/chef/mixin/generate_url.rb2
-rw-r--r--chef/lib/chef/mixin/template.rb49
-rw-r--r--chef/lib/chef/provider/package.rb3
-rw-r--r--chef/lib/chef/provider/package/dpkg.rb110
-rw-r--r--chef/lib/chef/provider/remote_directory.rb4
-rw-r--r--chef/lib/chef/provider/remote_file.rb2
-rw-r--r--chef/lib/chef/resource/remote_directory.rb9
-rw-r--r--chef/lib/chef/rest.rb4
-rw-r--r--chef/spec/unit/mixin/template_spec.rb86
-rw-r--r--chef/spec/unit/provider/package/dpkg_spec.rb177
-rw-r--r--chef/spec/unit/provider/package_spec.rb9
-rw-r--r--chef/spec/unit/rest_spec.rb49
-rw-r--r--chefserverslice/Rakefile2
-rw-r--r--features/data/Rakefile176
-rw-r--r--features/data/config/client.rb13
-rw-r--r--features/data/config/rake.rb57
-rw-r--r--features/data/config/server.rb20
-rw-r--r--features/data/cookbooks/integration_setup/attributes/integration.rb25
-rw-r--r--features/data/cookbooks/integration_setup/recipes/default.rb25
-rw-r--r--features/data/cookbooks/manage_files/recipes/create_a_file.rb22
-rw-r--r--features/data/cookbooks/manage_files/recipes/default.rb19
-rw-r--r--features/data/cookbooks/manage_files/recipes/delete_a_file.rb24
-rw-r--r--features/data/cookbooks/manage_files/recipes/delete_a_file_that_does_not_already_exist.rb22
-rw-r--r--features/data/cookbooks/manage_files/recipes/set_the_owner_of_a_created_file.rb23
-rw-r--r--features/data/cookbooks/manage_files/recipes/touch_a_file.rb22
-rw-r--r--features/data/cookbooks/transfer_remote_files/files/default/transfer_a_file_from_a_cookbook.txt1
-rw-r--r--features/data/cookbooks/transfer_remote_files/recipes/default.rb18
-rw-r--r--features/data/cookbooks/transfer_remote_files/recipes/should_prefer_the_file_for_this_specific_host.rb22
-rw-r--r--features/data/cookbooks/transfer_remote_files/recipes/transfer_a_file_from_a_cookbook.rb22
-rw-r--r--features/manage_files.feature43
-rw-r--r--features/steps/couchdb.rb31
-rw-r--r--features/steps/files.rb65
-rw-r--r--features/steps/nodes.rb42
-rw-r--r--features/steps/recipe.rb33
-rw-r--r--features/steps/run_client.rb48
-rw-r--r--features/support/env.rb56
-rw-r--r--features/transfer_remote_files.feature41
49 files changed, 1557 insertions, 42 deletions
diff --git a/.gitignore b/.gitignore
index 98d5c33e2e..7ade6f67f1 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 8ddecc1880..b75b9a9e68 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 165a89d0d5..74f868edfc 100644
--- a/NOTICE
+++ b/NOTICE
@@ -9,7 +9,7 @@ Contributors and Copyright holders:
* Copyright 2008, Arjuna Christensen <aj@hjksolutions.com>
* Copyright 2008, Bryan McLellan <btm@loftninjas.org>
* Copyright 2008, Ezra Zygmuntowicz <ezra@engineyard.com>
- * Copyright 2008, Sean Cribbs <seancribbs@gmail.com>
+ * Copyright 2009, Sean Cribbs <seancribbs@gmail.com>
* Copyright 2009, Christopher Brown <cb@opscode.com>
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 e1786bb0fe..c6d81b291e 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 64192a945a..c5a05d3b8f 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 0000000000..24728c3075
--- /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<stomp>, [">= 0"])
+ s.add_runtime_dependency(%q<stompserver>, [">= 0"])
+ s.add_runtime_dependency(%q<ferret>, [">= 0"])
+ s.add_runtime_dependency(%q<merb-core>, [">= 0"])
+ s.add_runtime_dependency(%q<merb-haml>, [">= 0"])
+ s.add_runtime_dependency(%q<mongrel>, [">= 0"])
+ s.add_runtime_dependency(%q<haml>, [">= 0"])
+ s.add_runtime_dependency(%q<ruby-openid>, [">= 0"])
+ s.add_runtime_dependency(%q<json>, [">= 0"])
+ s.add_runtime_dependency(%q<syntax>, [">= 0"])
+ else
+ s.add_dependency(%q<stomp>, [">= 0"])
+ s.add_dependency(%q<stompserver>, [">= 0"])
+ s.add_dependency(%q<ferret>, [">= 0"])
+ s.add_dependency(%q<merb-core>, [">= 0"])
+ s.add_dependency(%q<merb-haml>, [">= 0"])
+ s.add_dependency(%q<mongrel>, [">= 0"])
+ s.add_dependency(%q<haml>, [">= 0"])
+ s.add_dependency(%q<ruby-openid>, [">= 0"])
+ s.add_dependency(%q<json>, [">= 0"])
+ s.add_dependency(%q<syntax>, [">= 0"])
+ end
+ else
+ s.add_dependency(%q<stomp>, [">= 0"])
+ s.add_dependency(%q<stompserver>, [">= 0"])
+ s.add_dependency(%q<ferret>, [">= 0"])
+ s.add_dependency(%q<merb-core>, [">= 0"])
+ s.add_dependency(%q<merb-haml>, [">= 0"])
+ s.add_dependency(%q<mongrel>, [">= 0"])
+ s.add_dependency(%q<haml>, [">= 0"])
+ s.add_dependency(%q<ruby-openid>, [">= 0"])
+ s.add_dependency(%q<json>, [">= 0"])
+ s.add_dependency(%q<syntax>, [">= 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 0000000000..c70c26a658
--- /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 9a52dec46a..faa66847a4 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 e217d11a5b..32a3c45fe0 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 430123a498..5273922e20 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 934fdc87aa..1123a0ea4a 100644
--- a/chef/lib/chef.rb
+++ b/chef/lib/chef.rb
@@ -27,7 +27,7 @@ require 'chef/config'
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 b416012044..325156b421 100644
--- a/chef/lib/chef/config.rb
+++ b/chef/lib/chef/config.rb
@@ -53,6 +53,8 @@ class Chef
: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 abb28d6df0..9ebe22b832 100644
--- a/chef/lib/chef/mixin/generate_url.rb
+++ b/chef/lib/chef/mixin/generate_url.rb
@@ -24,7 +24,7 @@ class Chef
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 11329dbaa7..73a6ffbd95 100644
--- a/chef/lib/chef/mixin/template.rb
+++ b/chef/lib/chef/mixin/template.rb
@@ -26,14 +26,59 @@ class Chef
# 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 5b323f2bdd..badc7aaacd 100644
--- a/chef/lib/chef/provider/package.rb
+++ b/chef/lib/chef/provider/package.rb
@@ -71,7 +71,8 @@ class Chef
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 0000000000..0cad2d00e8
--- /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 fb490bbb1d..010e0b29cd 100644
--- a/chef/lib/chef/provider/remote_directory.rb
+++ b/chef/lib/chef/provider/remote_directory.rb
@@ -53,7 +53,7 @@ class Chef
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 @@ class Chef
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 8ceed5f74d..8fd5eb0104 100644
--- a/chef/lib/chef/provider/remote_file.rb
+++ b/chef/lib/chef/provider/remote_file.rb
@@ -91,6 +91,7 @@ class Chef
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 @@ class Chef
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 478cdc6528..1628af3a33 100644
--- a/chef/lib/chef/resource/remote_directory.rb
+++ b/chef/lib/chef/resource/remote_directory.rb
@@ -34,6 +34,7 @@ class Chef
@files_group = nil
@files_mode = 0644
@allowed_actions.push(:create, :delete)
+ @cookbook = nil
end
def source(args=nil)
@@ -76,6 +77,14 @@ class Chef
)
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 3224262589..bac6788e52 100644
--- a/chef/lib/chef/rest.rb
+++ b/chef/lib/chef/rest.rb
@@ -123,6 +123,10 @@ class Chef
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 443ca28e1d..86e0b59232 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 0000000000..390609aca9
--- /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 <ubuntu-devel-discuss@lists.ubuntu.com>").
+ 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 fbfc8b5bf2..925360e1dd 100644
--- a/chef/spec/unit/provider/package_spec.rb
+++ b/chef/spec/unit/provider/package_spec.rb
@@ -126,7 +126,8 @@ describe Chef::Provider::Package, "action_upgrade" do
: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 @@ describe Chef::Provider::Package, "action_upgrade" do
@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 ba892944fc..525d17e148 100644
--- a/chef/spec/unit/rest_spec.rb
+++ b/chef/spec/unit/rest_spec.rb
@@ -140,6 +140,55 @@ describe Chef::REST, "run_request method" do
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 2f6cd274e6..4f094d0a5f 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 0000000000..fcf46f69b7
--- /dev/null
+++ b/features/data/Rakefile
@@ -0,0 +1,176 @@
+#
+# Rakefile for Chef Server Repository
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 = <<EOH
+[ req ]
+distinguished_name = req_distinguished_name
+prompt = no
+
+[ req_distinguished_name ]
+C = #{SSL_COUNTRY_NAME}
+ST = #{SSL_STATE_NAME}
+L = #{SSL_LOCALITY_NAME}
+O = #{COMPANY_NAME}
+OU = #{SSL_ORGANIZATIONAL_UNIT_NAME}
+CN = #{fqdn}
+emailAddress = #{SSL_EMAIL_ADDRESS}
+EOH
+ tf.puts(ssl_config)
+ tf.close
+ sh("(cd #{CADIR} && openssl req -config '#{tf.path}' -new -x509 -nodes -sha1 -days 3650 -key #{fqdn}.key > #{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 0000000000..45471dabae
--- /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 0000000000..d79d6e9889
--- /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 0000000000..9c7674581d
--- /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 0000000000..7ee3a25090
--- /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 0000000000..0ada2aa485
--- /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 0000000000..5a327d64b3
--- /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 0000000000..8b9f7ab0cf
--- /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 0000000000..5d3faf41cd
--- /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 0000000000..f6c2e33147
--- /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 0000000000..28a92550e3
--- /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 0000000000..3aff3fa6d6
--- /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 0000000000..9fb38fab62
--- /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 0000000000..8bba761657
--- /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 0000000000..98415d1b15
--- /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 0000000000..70a650c803
--- /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 0000000000..0035949e16
--- /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 0000000000..e2bfa8d371
--- /dev/null
+++ b/features/steps/couchdb.rb
@@ -0,0 +1,31 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 0000000000..7decc0cec8
--- /dev/null
+++ b/features/steps/files.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 0000000000..36cfb76914
--- /dev/null
+++ b/features/steps/nodes.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 0000000000..7c5c96327c
--- /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 0000000000..a464d793dd
--- /dev/null
+++ b/features/steps/run_client.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 0000000000..978ce818bf
--- /dev/null
+++ b/features/support/env.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# 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 0000000000..e75f582cdf
--- /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