diff options
Diffstat (limited to 'chef/lib')
91 files changed, 2232 insertions, 953 deletions
diff --git a/chef/lib/chef.rb b/chef/lib/chef.rb index 0c494ef594..937e21acc9 100644 --- a/chef/lib/chef.rb +++ b/chef/lib/chef.rb @@ -16,22 +16,25 @@ # limitations under the License. # -$:.unshift(File.dirname(__FILE__)) unless - $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) - -require 'rubygems' require 'extlib' require 'chef/exceptions' require 'chef/log' require 'chef/config' -Dir[File.join(File.dirname(__FILE__), 'chef/mixin/**/*.rb')].sort.each { |lib| require lib } +require 'chef/providers' +require 'chef/resources' + +require 'chef/compile' +require 'chef/daemon' +require 'chef/runner' +require 'chef/webui_user' +require 'chef/openid_registration' class Chef - VERSION = "0.8.11" + VERSION = "0.8.12" end # Adds a Dir.glob to Ruby 1.8.5, for compat -if RUBY_VERSION < "1.8.6" +if RUBY_VERSION < "1.8.6" || RUBY_PLATFORM =~ /mswin|mingw32|windows/ class Dir class << self alias_method :glob_, :glob @@ -41,6 +44,7 @@ if RUBY_VERSION < "1.8.6" pattern.is_a? Array and !pattern.empty? ) or pattern.is_a? String ) + pattern.gsub!(/\\/, "/") if RUBY_PLATFORM =~ /mswin|mingw32|windows/ [pattern].flatten.inject([]) { |r, p| r + glob_(p, flags) } end alias_method :[], :glob diff --git a/chef/lib/chef/application.rb b/chef/lib/chef/application.rb index b992bc1c59..96825a8c01 100644 --- a/chef/lib/chef/application.rb +++ b/chef/lib/chef/application.rb @@ -6,9 +6,9 @@ # 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. @@ -22,10 +22,10 @@ require 'mixlib/cli' class Chef::Application include Mixlib::CLI - - def initialize + + def initialize super - + trap("TERM") do Chef::Application.fatal!("SIGTERM received, stopping", 1) end @@ -33,23 +33,28 @@ class Chef::Application trap("INT") do Chef::Application.fatal!("SIGINT received, stopping", 2) end - - trap("HUP") do - Chef::Log.info("SIGHUP received, reconfiguring") - reconfigure + + unless RUBY_PLATFORM =~ /mswin|mingw32|windows/ + trap("HUP") do + Chef::Log.info("SIGHUP received, reconfiguring") + reconfigure + end end - + at_exit do - # tear down the logger - end + # tear down the logger + end + + # Always switch to a readable directory. Keeps subsequent Dir.chdir() {} + # from failing due to permissions when launched as a less privileged user. end - + # Reconfigure the application. You'll want to override and super this method. def reconfigure configure_chef configure_logging end - + # Get this party started def run reconfigure @@ -60,25 +65,25 @@ class Chef::Application # Parse the configuration file def configure_chef parse_options - + Chef::Config.from_file(config[:config_file]) if !config[:config_file].nil? && File.exists?(config[:config_file]) && File.readable?(config[:config_file]) Chef::Config.merge!(config) end - + # Initialize and configure the logger def configure_logging Chef::Log.init(Chef::Config[:log_location]) Chef::Log.level = Chef::Config[:log_level] end - + # Called prior to starting the application, by the run method def setup_application - raise Chef::Exceptions::Application, "#{self.to_s}: you must override setup_application" + raise Chef::Exceptions::Application, "#{self.to_s}: you must override setup_application" end - + # Actually run the application def run_application - raise Chef::Exceptions::Application, "#{self.to_s}: you must override run_application" + raise Chef::Exceptions::Application, "#{self.to_s}: you must override run_application" end class << self diff --git a/chef/lib/chef/application/client.rb b/chef/lib/chef/application/client.rb index a6bd7a1003..6c265b84f6 100644 --- a/chef/lib/chef/application/client.rb +++ b/chef/lib/chef/application/client.rb @@ -113,12 +113,18 @@ class Chef::Application::Client < Chef::Application :description => "The chef server URL", :proc => nil - option :validation_token, - :short => "-t TOKEN", - :long => "--token TOKEN", - :description => "Set the openid validation token", - :proc => nil - + option :validation_key, + :short => "-K KEY_FILE", + :long => "--validation_key KEY_FILE", + :description => "Set the validation key file location, used for registering new clients", + :proc => nil + + option :client_key, + :short => "-k KEY_FILE", + :long => "--client_key KEY_FILE", + :description => "Set the client key file location", + :proc => nil + option :version, :short => "-v", :long => "--version", @@ -185,7 +191,6 @@ class Chef::Application::Client < Chef::Application @chef_client = Chef::Client.new @chef_client.json_attribs = @chef_client_json - @chef_client.validation_token = Chef::Config[:validation_token] @chef_client.node_name = Chef::Config[:node_name] end diff --git a/chef/lib/chef/application/knife.rb b/chef/lib/chef/application/knife.rb index df60da4040..c5ef8f4317 100644 --- a/chef/lib/chef/application/knife.rb +++ b/chef/lib/chef/application/knife.rb @@ -94,7 +94,13 @@ class Chef::Application::Knife < Chef::Application :short => "-p", :long => "--print-after", :description => "Show the data after a destructive operation" - + + option :format, + :short => "-f FORMAT", + :long => "--format FORMAT", + :description => "Which format to use for output", + :default => "json" + option :version, :short => "-v", :long => "--version", diff --git a/chef/lib/chef/application/solo.rb b/chef/lib/chef/application/solo.rb index 434638d607..8fa2b00592 100644 --- a/chef/lib/chef/application/solo.rb +++ b/chef/lib/chef/application/solo.rb @@ -158,7 +158,7 @@ class Chef::Application::Solo < Chef::Application end if Chef::Config[:recipe_url] - cookbooks_path = Chef::Config[:cookbook_path].detect{|e| e =~ /\/cookbooks\/*$/ } + cookbooks_path = Array(Chef::Config[:cookbook_path]).detect{|e| e =~ /\/cookbooks\/*$/ } recipes_path = File.expand_path(File.join(cookbooks_path, '..')) target_file = File.join(recipes_path, 'recipes.tgz') diff --git a/chef/lib/chef/applications.rb b/chef/lib/chef/applications.rb new file mode 100644 index 0000000000..48fd56acf4 --- /dev/null +++ b/chef/lib/chef/applications.rb @@ -0,0 +1,4 @@ +require 'chef/application/agent' +require 'chef/application/client' +require 'chef/application/knife' +require 'chef/application/solo' diff --git a/chef/lib/chef/cache/checksum.rb b/chef/lib/chef/cache/checksum.rb index 6effb3a303..e026d8875b 100644 --- a/chef/lib/chef/cache/checksum.rb +++ b/chef/lib/chef/cache/checksum.rb @@ -28,8 +28,9 @@ class Chef instance.checksum_for_file(*args) end - def checksum_for_file(file) - key, fstat = filename_to_key(file), File.stat(file) + def checksum_for_file(file, key=nil) + key ||= generate_key(file) + fstat = File.stat(file) lookup_checksum(key, fstat) || generate_checksum(key, file, fstat) end @@ -48,6 +49,10 @@ class Chef checksum end + def generate_key(file, group="chef") + "#{group}-file-#{file.gsub(/(#{File::SEPARATOR}|\.)/, '-')}" + end + private def file_unchanged?(cached, fstat) @@ -59,10 +64,6 @@ class Chef IO.foreach(file) {|line| digest.update(line) } digest.hexdigest end - - def filename_to_key(file) - "chef-file-#{file.gsub(/(#{File::SEPARATOR}|\.)/, '-')}" - end end end diff --git a/chef/lib/chef/certificate.rb b/chef/lib/chef/certificate.rb index 9e57c9ee89..b818b967af 100644 --- a/chef/lib/chef/certificate.rb +++ b/chef/lib/chef/certificate.rb @@ -128,11 +128,11 @@ class Chef return client_cert.public_key, client_keypair end - def gen_validation_key(name=Chef::Config[:validation_client_name], key_file=Chef::Config[:validation_key]) + def gen_validation_key(name=Chef::Config[:validation_client_name], key_file=Chef::Config[:validation_key], admin=false) # Create the validation key api_client = Chef::ApiClient.new api_client.name(name) - api_client.admin(true) + api_client.admin(admin) begin # If both the couch record and file exist, don't do anything. Otherwise, diff --git a/chef/lib/chef/client.rb b/chef/lib/chef/client.rb index 0ffe60e53a..08e2c76db0 100644 --- a/chef/lib/chef/client.rb +++ b/chef/lib/chef/client.rb @@ -37,12 +37,11 @@ class Chef include Chef::Mixin::GenerateURL include Chef::Mixin::Checksum - attr_accessor :node, :registration, :json_attribs, :validation_token, :node_name, :ohai, :rest, :runner, :compile + attr_accessor :node, :registration, :json_attribs, :node_name, :ohai, :rest, :compile # Creates a new Chef::Client. def initialize() @node = nil - @validation_token = nil @registration = nil @json_attribs = nil @node_name = nil @@ -90,7 +89,6 @@ class Chef build_node(@node_name) save_node sync_cookbooks - save_node converge save_node @@ -317,7 +315,7 @@ class Chef Chef::FileCache.list.each do |cache_file| if cache_file =~ /^cookbooks\/(.+?)\// unless cookbook_hash.has_key?($1) - Chef::Log.info("Removing #{cache_file} from the cache; it's cookbook is no longer needed on this client.") + Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.") Chef::FileCache.delete(cache_file) end end @@ -355,6 +353,22 @@ class Chef Chef::Log.debug("Compiling recipes for node #{@node_name}") unless solo Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks") + Chef::Log.warn("Node #{@node_name} has an empty run list.") if @node.run_list.empty? + else + # Check for cookbooks in the path given + # Chef::Config[:cookbook_path] can be a string or an array + # if it's an array, go through it and check each one, raise error at the last one if no files are found + Chef::Log.fatal "BUGBUG: cookbook_path: #{Chef::Config[:cookbook_path]}" + Array(Chef::Config[:cookbook_path]).each_with_index do |cookbook_path, index| + if directory_not_empty?(cookbook_path) + Chef::Log.fatal "BUGBUG: cb path not empty: #{cookbook_path}" + break + else + msg = "No cookbook found in #{Chef::Config[:cookbook_path].inspect}, make sure cookboook_path is set correctly." + Chef::Log.fatal(msg) + raise Chef::Exceptions::CookbookNotFound, msg if is_last_element?(index, Chef::Config[:cookbook_path]) + end + end end @compile = Chef::Compile.new(@node) @compile.go @@ -364,6 +378,16 @@ class Chef @runner.converge true end + + private + + def directory_not_empty?(path) + File.exists?(path) && (Dir.entries(path).size > 2) + end + + def is_last_element?(index, object) + object.kind_of?(Array) ? index == object.size - 1 : true + end end end diff --git a/chef/lib/chef/config.rb b/chef/lib/chef/config.rb index 3d90456486..6e573e7f3f 100644 --- a/chef/lib/chef/config.rb +++ b/chef/lib/chef/config.rb @@ -143,15 +143,14 @@ class Chef search_url "http://localhost:4000" solo false splay nil - ssl_client_cert "" - ssl_client_key "" + ssl_client_cert nil + ssl_client_key nil ssl_verify_mode :verify_none ssl_ca_path nil ssl_ca_file nil template_url "http://localhost:4000" umask 0022 user nil - validation_token nil role_path "/var/chef/roles" role_url "http://localhost:4000" recipe_url nil diff --git a/chef/lib/chef/cookbook_loader.rb b/chef/lib/chef/cookbook_loader.rb index 4a1e053425..c8591501c6 100644 --- a/chef/lib/chef/cookbook_loader.rb +++ b/chef/lib/chef/cookbook_loader.rb @@ -114,7 +114,12 @@ class Chef @cookbook[cookbook].provider_files = cookbook_settings[cookbook][:provider_files].values @metadata[cookbook] = Chef::Cookbook::Metadata.new(@cookbook[cookbook]) cookbook_settings[cookbook][:metadata_files].each do |meta_json| - @metadata[cookbook].from_json(IO.read(meta_json)) + begin + @metadata[cookbook].from_json(IO.read(meta_json)) + rescue JSON::ParserError + puts "Couldn't parse JSON in " + meta_json + raise + end end end end diff --git a/chef/lib/chef/daemon.rb b/chef/lib/chef/daemon.rb index eecdeb9607..1b9db3ba2b 100644 --- a/chef/lib/chef/daemon.rb +++ b/chef/lib/chef/daemon.rb @@ -121,6 +121,8 @@ class Chef # Change process user/group to those specified in Chef::Config # def change_privilege + Dir.chdir("/") + if Chef::Config[:user] and Chef::Config[:group] Chef::Log.info("About to change privilege to #{Chef::Config[:user]}:#{Chef::Config[:group]}") _change_privilege(Chef::Config[:user], Chef::Config[:group]) diff --git a/chef/lib/chef/data_bag_item.rb b/chef/lib/chef/data_bag_item.rb index a18649c989..0d6b1e9696 100644 --- a/chef/lib/chef/data_bag_item.rb +++ b/chef/lib/chef/data_bag_item.rb @@ -22,7 +22,8 @@ require 'chef/config' require 'chef/mixin/params_validate' require 'chef/mixin/from_file' require 'chef/couchdb' -require 'chef/data_bag_item' +require 'chef/index_queue' +require 'chef/data_bag' require 'extlib' require 'json' diff --git a/chef/lib/chef/exceptions.rb b/chef/lib/chef/exceptions.rb index f0fb60f778..d14401d3ad 100644 --- a/chef/lib/chef/exceptions.rb +++ b/chef/lib/chef/exceptions.rb @@ -37,5 +37,10 @@ class Chef class CannotWritePrivateKey < RuntimeError; end class RoleNotFound < RuntimeError; end class ValidationFailed < ArgumentError; end + class InvalidPrivateKey < ArgumentError; end + class ConfigurationError < ArgumentError; end + class RedirectLimitExceeded < RuntimeError; end + class AmbiguousRunlistSpecification < ArgumentError; end + class CookbookNotFound < RuntimeError; end end end diff --git a/chef/lib/chef/file_cache.rb b/chef/lib/chef/file_cache.rb index 4e2446844a..18ecd66512 100644 --- a/chef/lib/chef/file_cache.rb +++ b/chef/lib/chef/file_cache.rb @@ -6,9 +6,9 @@ # 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. @@ -26,7 +26,7 @@ class Chef class << self include Chef::Mixin::ParamsValidate include Chef::Mixin::CreatePath - + # Write a file to the File Cache. # # === Parameters @@ -47,7 +47,7 @@ class Chef :contents => { :kind_of => String }, } ) - + file_path_array = File.split(path) file_name = file_path_array.pop cache_path = create_cache_path(File.join(file_path_array)) @@ -56,7 +56,7 @@ class Chef io.close true end - + # Move a file in to the cache. Useful with the REST raw file output. # # === Parameteres @@ -73,19 +73,19 @@ class Chef :path => { :kind_of => String }, } ) - + file_path_array = File.split(path) file_name = file_path_array.pop if File.exists?(file) && File.writable?(file) FileUtils.mv( - file, + file, File.join(create_cache_path(File.join(file_path_array), true), file_name) ) else raise RuntimeError, "Cannot move #{file} to #{path}!" end end - + # Read a file from the File Cache # # === Parameters @@ -116,7 +116,7 @@ class Chef cache_path end end - + # Delete a file from the File Cache # # === Parameters @@ -140,7 +140,7 @@ class Chef end true end - + # List all the files in the Cache # # === Returns @@ -149,19 +149,19 @@ class Chef keys = Array.new Dir[File.join(Chef::Config[:file_cache_path], '**', '*')].each do |f| if File.file?(f) - path = f.match("^#{Chef::Config[:file_cache_path]}\/(.+)")[1] + path = f.match("^#{Dir[Chef::Config[:file_cache_path]].first}\/(.+)")[1] keys << path end end keys end - + # Whether or not this file exists in the Cache # # === Parameters - # path:: The path to the file you want to check - is relative + # path:: The path to the file you want to check - is relative # to Chef::Config[:file_cache_path] - # + # # === Returns # True:: If the file exists # False:: If it does not @@ -181,25 +181,25 @@ class Chef false end end - + # Create a full path to a given file in the cache. By default, # also creates the path if it does not exist. # # === Parameters # path:: The path to create, relative to Chef::Config[:file_cache_path] # create_if_missing:: True by default - whether to create the path if it does not exist - # + # # === Returns # String:: The fully expanded path def create_cache_path(path, create_if_missing=true) cache_dir = File.expand_path(File.join(Chef::Config[:file_cache_path], path)) if create_if_missing - create_path(cache_dir) + create_path(cache_dir) else cache_dir end end - + end end end diff --git a/chef/lib/chef/index_queue/indexable.rb b/chef/lib/chef/index_queue/indexable.rb index 0654c6b06c..f027497f13 100644 --- a/chef/lib/chef/index_queue/indexable.rb +++ b/chef/lib/chef/index_queue/indexable.rb @@ -44,10 +44,12 @@ class Chef self.class.index_object_type || Mixin::ConvertToClassName.snake_case_basename(self.class.name) end - def with_indexer_metadata(with_metadata={}) + def with_indexer_metadata(indexer_metadata={}) # changing input param symbol keys to strings, as the keys in hash that goes to solr are expected to be strings [cb] - with_metadata.each do |key,value| - with_metadata[key.to_s] = with_metadata.delete(key) + # Ruby 1.9 hates you, cb [dan] + with_metadata = {} + indexer_metadata.each_key do |key| + with_metadata[key.to_s] = indexer_metadata[key] end with_metadata["type"] ||= self.index_object_type diff --git a/chef/lib/chef/knife.rb b/chef/lib/chef/knife.rb index 8aadb0bcd9..beaa7f8326 100644 --- a/chef/lib/chef/knife.rb +++ b/chef/lib/chef/knife.rb @@ -20,6 +20,8 @@ require 'mixlib/cli' require 'chef/mixin/convert_to_class_name' +require 'pp' + class Chef class Knife include Mixlib::CLI @@ -96,11 +98,17 @@ class Chef klass_instance end - def ask_question(q) - print q - a = STDIN.readline - a.chomp! - a + def ask_question(question, opts={}) + question = question + "[#{opts[:default]}] " if opts[:default] + + stdout.print question + a = stdin.readline.strip + + if opts[:default] + a.empty? ? opts[:default] : a + else + a + end end def configure_chef @@ -126,8 +134,24 @@ class Chef puts data end - def json_pretty_print(data) - puts JSON.pretty_generate(data) + def output(data) + case config[:format] + when "json", nil + puts JSON.pretty_generate(data) + when "yaml" + require 'yaml' + puts YAML::dump(data) + when "text" + # If you were looking for some attribute and there is only one match + # just dump the attribute value + if data.length == 1 and config[:attribute] + puts data.values[0] + else + pp data + end + else + raise ArgumentError, "Unknown output format #{config[:format]}" + end end def format_list_for_display(list) @@ -182,7 +206,7 @@ class Chef return true if config[:yes] print "#{question}? (Y/N) " - answer = STDIN.readline + answer = stdin.readline answer.chomp! case answer when "Y", "y" @@ -237,7 +261,7 @@ class Chef Chef::Log.info("Saved #{output}") - json_pretty_print(format_for_display(object)) if config[:print_after] + output(format_for_display(object)) if config[:print_after] end def create_object(object, pretty_name=nil, &block) @@ -253,7 +277,7 @@ class Chef Chef::Log.info("Created (or updated) #{pretty_name}") - json_pretty_print(output) if config[:print_after] + output(output) if config[:print_after] end def delete_object(klass, name, delete_name=nil, &block) @@ -266,7 +290,7 @@ class Chef object.destroy end - json_pretty_print(format_for_display(object)) if config[:print_after] + output(format_for_display(object)) if config[:print_after] obj_name = delete_name ? "#{delete_name}[#{name}]" : object Chef::Log.warn("Deleted #{obj_name}!") @@ -285,7 +309,7 @@ class Chef to_delete = object_list end - json_pretty_print(format_list_for_display(to_delete)) + output(format_list_for_display(to_delete)) confirm("Do you really want to delete the above items") @@ -295,10 +319,18 @@ class Chef else object.destroy end - json_pretty_print(format_for_display(object)) if config[:print_after] + output(format_for_display(object)) if config[:print_after] Chef::Log.warn("Deleted #{fancy_name} #{name}") end end + + def stdout + STDOUT + end + + def stdin + STDIN + end def rest @rest ||= Chef::REST.new(Chef::Config[:chef_server_url]) diff --git a/chef/lib/chef/knife/client_list.rb b/chef/lib/chef/knife/client_list.rb index 24c23124ca..c6f3ca5136 100644 --- a/chef/lib/chef/knife/client_list.rb +++ b/chef/lib/chef/knife/client_list.rb @@ -32,7 +32,7 @@ class Chef :description => "Show corresponding URIs" def run - json_pretty_print(format_list_for_display(Chef::ApiClient.list)) + output(format_list_for_display(Chef::ApiClient.list)) end end end diff --git a/chef/lib/chef/knife/client_show.rb b/chef/lib/chef/knife/client_show.rb index f0efa97f10..81204547b3 100644 --- a/chef/lib/chef/knife/client_show.rb +++ b/chef/lib/chef/knife/client_show.rb @@ -33,7 +33,7 @@ class Chef def run client = Chef::ApiClient.load(@name_args[0]) - json_pretty_print(format_for_display(client)) + output(format_for_display(client)) end end diff --git a/chef/lib/chef/knife/configure.rb b/chef/lib/chef/knife/configure.rb index a362ed4465..58acbeb832 100644 --- a/chef/lib/chef/knife/configure.rb +++ b/chef/lib/chef/knife/configure.rb @@ -6,9 +6,9 @@ # 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. @@ -21,6 +21,8 @@ require 'chef/knife' class Chef class Knife class Configure < Knife + attr_reader :chef_server, :new_client_name, :admin_client_name, :admin_client_key + attr_reader :chef_repo, :new_client_key, :validation_client_name, :validation_key banner "Sub-Command: configure (options)" @@ -37,58 +39,48 @@ class Chef def configure_chef # We are just faking out the system so that you can do this without a key specified - Chef::Config[:node_name] = 'woot' + Chef::Config[:node_name] = 'woot' super Chef::Config[:node_name] = nil end - def run - config[:config_file] ||= ask_question("Where should I put the config file? ") - if File.exists?(config[:config_file]) - confirm("Overwrite #{config[:config_file]}") - end + def run + ask_user_for_config_path Mixlib::Log::Formatter.show_time = false Chef::Log.init(STDOUT) Chef::Log.level(:info) - chef_config_path = File.dirname(config[:config_file]) - FileUtils.mkdir_p(File.dirname(config[:config_file])) + FileUtils.mkdir_p(chef_config_path) - chef_server = config[:chef_server_url] || ask_question("Your chef server URL? ") - opscode_user = config[:node_name] || ask_question("Your client user name? ") - opscode_key = config[:client_key] || File.join(chef_config_path, "#{opscode_user}.pem") - validation_user = config[:validation_client_name] || ask_question("Your validation client user name? ") - validation_key = config[:validation_key] || File.join(chef_config_path, "#{validation_user}.pem") - chef_repo = config[:repository] || ask_question("Path to a chef repository (or leave blank)? ") - + ask_user_for_config - File.open(config[:config_file], "w") do |f| - f.puts <<EOH + ::File.open(config[:config_file], "w") do |f| + f.puts <<-EOH log_level :info log_location STDOUT -node_name '#{opscode_user}' -client_key '#{opscode_key}' -validation_client_name '#{validation_user}' +node_name '#{new_client_name}' +client_key '#{new_client_key}' +validation_client_name '#{validation_client_name}' validation_key '#{validation_key}' -chef_server_url '#{chef_server}' +chef_server_url '#{chef_server}' cache_type 'BasicFile' cache_options( :path => '#{File.join(chef_config_path, "checksums")}' ) EOH - unless chef_repo == "" + unless chef_repo.empty? f.puts "cookbook_path [ '#{chef_repo}/cookbooks', '#{chef_repo}/site-cookbooks' ]" - end + end end if config[:initial] Chef::Log.warn("Creating initial API user...") Chef::Config[:chef_server_url] = chef_server - Chef::Config[:node_name] = 'chef-webui' - Chef::Config[:client_key] = '/etc/chef/webui.pem' + Chef::Config[:node_name] = admin_client_name + Chef::Config[:client_key] = admin_client_key client_create = Chef::Knife::ClientCreate.new - client_create.name_args = [ opscode_user ] + client_create.name_args = [ new_client_name ] client_create.config[:admin] = true - client_create.config[:file] = opscode_key + client_create.config[:file] = new_client_key client_create.config[:yes] = true client_create.config[:no_editor] = true client_create.run @@ -96,7 +88,7 @@ EOH Chef::Log.warn("*****") Chef::Log.warn("") Chef::Log.warn("You must place your client key in:") - Chef::Log.warn(" #{opscode_key}") + Chef::Log.warn(" #{new_client_key}") Chef::Log.warn("Before running commands with Knife!") Chef::Log.warn("") Chef::Log.warn("*****") @@ -111,13 +103,32 @@ EOH Chef::Log.warn("Configuration file written to #{config[:config_file]}") end - end - end -end - - - + def ask_user_for_config_path + config[:config_file] ||= ask_question("Where should I put the config file? ") + if File.exists?(config[:config_file]) + confirm("Overwrite #{config[:config_file]}") + end + end + def ask_user_for_config + @chef_server = config[:chef_server_url] || ask_question("Your chef server URL? ", :default => 'http://localhost:4000') + @new_client_name = config[:node_name] || ask_question("Select a user name for your new client: ", :default => Etc.getlogin) + @admin_client_name = config[:admin_client_name] || ask_question("Your existing admin client user name? ", :default => 'chef-webui') + @admin_client_key = config[:admin_client_key] || ask_question("The location of your existing admin key? ", :default => '/etc/chef/webui.pem') + @validation_client_name = config[:validation_client_name] || ask_question("Your validation client user name? ", :default => 'chef-validator') + @validation_key = config[:validation_key] || ask_question("The location of your validation key? ", :default => '/etc/chef/validation.pem') + @chef_repo = config[:repository] || ask_question("Path to a chef repository (or leave blank)? ") + @new_client_key = config[:client_key] || File.join(chef_config_path, "#{@new_client_name}.pem") + end + def config_file + config[:config_file] + end + def chef_config_path + File.dirname(config_file) + end + end + end +end diff --git a/chef/lib/chef/knife/cookbook_list.rb b/chef/lib/chef/knife/cookbook_list.rb index 1b19bfb7db..7e1599dd62 100644 --- a/chef/lib/chef/knife/cookbook_list.rb +++ b/chef/lib/chef/knife/cookbook_list.rb @@ -31,7 +31,7 @@ class Chef :description => "Show corresponding URIs" def run - json_pretty_print(format_list_for_display(rest.get_rest('cookbooks'))) + output(format_list_for_display(rest.get_rest('cookbooks'))) end end end diff --git a/chef/lib/chef/knife/cookbook_metadata.rb b/chef/lib/chef/knife/cookbook_metadata.rb index b821b5d94b..d8a52693d5 100644 --- a/chef/lib/chef/knife/cookbook_metadata.rb +++ b/chef/lib/chef/knife/cookbook_metadata.rb @@ -7,9 +7,9 @@ # 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. @@ -36,20 +36,20 @@ class Chef :long => "--all", :description => "Generate metadata for all cookbooks, rather than just a single cookbook" - def run + def run if config[:cookbook_path] Chef::Config[:cookbook_path] = config[:cookbook_path] else config[:cookbook_path] = Chef::Config[:cookbook_path] end - if config[:all] + if config[:all] cl = Chef::CookbookLoader.new cl.each do |cookbook| generate_metadata(cookbook.name.to_s) end else - generate_metadata(@name_args[0]) + generate_metadata(@name_args[0]) end end @@ -57,31 +57,26 @@ class Chef Chef::Log.info("Generating metadata for #{cookbook}") config[:cookbook_path].reverse.each do |path| file = File.expand_path(File.join(path, cookbook, 'metadata.rb')) - if File.exists?(file) - Chef::Log.info("Generating from #{file}") - md = Chef::Cookbook::Metadata.new - md.name(cookbook) - md.from_file(file) - json_file = File.join(File.dirname(file), 'metadata.json') - File.open(json_file, "w") do |f| - f.write(JSON.pretty_generate(md)) - end - generated = true - Chef::Log.info("Generated #{json_file}") - else - Chef::Log.debug("No #{file} found; skipping!") - end + generate_metadata_from_file(cookbook, file) end end + def generate_metadata_from_file(cookbook, file) + if File.exists?(file) + Chef::Log.info("Generating from #{file}") + md = Chef::Cookbook::Metadata.new + md.name(cookbook) + md.from_file(file) + json_file = File.join(File.dirname(file), 'metadata.json') + File.open(json_file, "w") do |f| + f.write(JSON.pretty_generate(md)) + end + generated = true + Chef::Log.info("Generated #{json_file}") + else + Chef::Log.debug("No #{file} found; skipping!") + end + end end end end - - - - - - - - diff --git a/chef/lib/chef/knife/cookbook_metadata_from_file.rb b/chef/lib/chef/knife/cookbook_metadata_from_file.rb new file mode 100644 index 0000000000..8da5773e61 --- /dev/null +++ b/chef/lib/chef/knife/cookbook_metadata_from_file.rb @@ -0,0 +1,40 @@ +# +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2010 Matthew Kent +# 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/knife' + +class Chef + class Knife + class CookbookMetadataFromFile < Knife + + banner "Sub-Command: cookbook metadata from FILE (options)" + + def run + file = @name_args[0] + cookbook = File.basename(File.dirname(file)) + + @metadata = Chef::Knife::CookbookMetadata.new + @metadata.generate_metadata_from_file(cookbook, file) + end + + end + end +end diff --git a/chef/lib/chef/knife/cookbook_show.rb b/chef/lib/chef/knife/cookbook_show.rb index eaf23e65d3..ac255e8b13 100644 --- a/chef/lib/chef/knife/cookbook_show.rb +++ b/chef/lib/chef/knife/cookbook_show.rb @@ -52,9 +52,9 @@ class Chef pretty_print(result) when 2 # We are showing a specific part of the cookbook result = rest.get_rest("cookbooks/#{@name_args[0]}") - json_pretty_print(result[@name_args[1]]) + output(result[@name_args[1]]) when 1 # We are showing the whole cookbook data - json_pretty_print(rest.get_rest("cookbooks/#{@name_args[0]}")) + output(rest.get_rest("cookbooks/#{@name_args[0]}")) end end diff --git a/chef/lib/chef/knife/cookbook_site_list.rb b/chef/lib/chef/knife/cookbook_site_list.rb index 505c321f23..19d1eccbb4 100644 --- a/chef/lib/chef/knife/cookbook_site_list.rb +++ b/chef/lib/chef/knife/cookbook_site_list.rb @@ -29,8 +29,8 @@ class Chef :long => "--with-uri", :description => "Show corresponding URIs" - def run - json_pretty_print(format_list_for_display(get_cookbook_list)) + def run + output(format_list_for_display(get_cookbook_list)) end def get_cookbook_list(items=10, start=0, cookbook_collection={}) diff --git a/chef/lib/chef/knife/cookbook_site_search.rb b/chef/lib/chef/knife/cookbook_site_search.rb index 1853b94209..4c4b5cc100 100644 --- a/chef/lib/chef/knife/cookbook_site_search.rb +++ b/chef/lib/chef/knife/cookbook_site_search.rb @@ -23,8 +23,8 @@ class Chef banner "Sub-Command: cookbook site search QUERY (options)" - def run - json_pretty_print(search_cookbook(name_args[0])) + def run + output(search_cookbook(name_args[0])) end def search_cookbook(query, items=10, start=0, cookbook_collection={}) diff --git a/chef/lib/chef/knife/cookbook_site_show.rb b/chef/lib/chef/knife/cookbook_site_show.rb index 2f0bcff897..128ac7bf4c 100644 --- a/chef/lib/chef/knife/cookbook_site_show.rb +++ b/chef/lib/chef/knife/cookbook_site_show.rb @@ -30,7 +30,7 @@ class Chef when 2 cookbook_data = rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].gsub('.', '_')}") end - json_pretty_print(format_for_display(cookbook_data)) + output(format_for_display(cookbook_data)) end def get_cookbook_list(items=10, start=0, cookbook_collection={}) diff --git a/chef/lib/chef/knife/cookbook_test.rb b/chef/lib/chef/knife/cookbook_test.rb new file mode 100644 index 0000000000..380739e68c --- /dev/null +++ b/chef/lib/chef/knife/cookbook_test.rb @@ -0,0 +1,103 @@ +# +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2010 Matthew Kent +# 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/knife' +require 'chef/cache/checksum' + +class Chef + class Knife + class CookbookTest < Knife + + banner "Sub-Command: cookbook test [COOKBOOKS...] (options)" + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| o.split(":") } + + option :all, + :short => "-a", + :long => "--all", + :description => "Test all cookbooks, rather than just a single cookbook" + + def run + if config[:cookbook_path] + Chef::Config[:cookbook_path] = config[:cookbook_path] + else + config[:cookbook_path] = Chef::Config[:cookbook_path] + end + + if config[:all] + cl = Chef::CookbookLoader.new + cl.each do |cookbook| + test_cookbook(cookbook.name.to_s) + end + else + @name_args.each do |cb| + test_cookbook(cb) + end + end + end + + def test_cookbook(cookbook) + Chef::Log.info("Running syntax check on #{cookbook}") + Array(config[:cookbook_path]).reverse.each do |path| + cookbook_dir = File.expand_path(File.join(path, cookbook)) + test_ruby(cookbook_dir) + test_templates(cookbook_dir) + end + end + + def test_ruby(cookbook_dir) + cache = Chef::Cache::Checksum.instance + Dir[File.join(cookbook_dir, '**', '*.rb')].each do |ruby_file| + key = cache.generate_key(ruby_file, "chef-test") + fstat = File.stat(ruby_file) + + if cache.lookup_checksum(key, fstat) + Chef::Log.info("No change in checksum of #{ruby_file}") + else + Chef::Log.info("Testing #{ruby_file} for syntax errors...") + Chef::Mixin::Command.run_command(:command => "ruby -c #{ruby_file}", :output_on_failure => true) + cache.generate_checksum(key, ruby_file, fstat) + end + end + end + + def test_templates(cookbook_dir) + cache = Chef::Cache::Checksum.instance + Dir[File.join(cookbook_dir, '**', '*.erb')].each do |erb_file| + key = cache.generate_key(erb_file, "chef-test") + fstat = File.stat(erb_file) + + if cache.lookup_checksum(key, fstat) + Chef::Log.info("No change in checksum of #{erb_file}") + else + Chef::Log.info("Testing template #{erb_file} for syntax errors...") + Chef::Mixin::Command.run_command(:command => "sh -c 'erubis -x #{erb_file} | ruby -c'", :output_on_failure => true) + cache.generate_checksum(key, erb_file, fstat) + end + end + end + + end + end +end diff --git a/chef/lib/chef/knife/cookbook_upload.rb b/chef/lib/chef/knife/cookbook_upload.rb index 69b61d71bd..a289394ae3 100644 --- a/chef/lib/chef/knife/cookbook_upload.rb +++ b/chef/lib/chef/knife/cookbook_upload.rb @@ -7,9 +7,9 @@ # 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. @@ -19,7 +19,6 @@ require 'chef/knife' require 'chef/streaming_cookbook_uploader' -require 'chef/cache/checksum' class Chef class Knife @@ -38,14 +37,16 @@ class Chef :long => "--all", :description => "Upload all cookbooks, rather than just a single cookbook" - def run + def run + Chef::Log.debug "Uploading cookbooks from #{config[:cookbook_path]}" + if config[:cookbook_path] Chef::Config[:cookbook_path] = config[:cookbook_path] else config[:cookbook_path] = Chef::Config[:cookbook_path] end - if config[:all] + if config[:all] cl = Chef::CookbookLoader.new cl.each do |cookbook| Chef::Log.info("** #{cookbook.name.to_s} **") @@ -58,28 +59,21 @@ class Chef end end end - - def test_ruby(cookbook_dir) - Dir[File.join(cookbook_dir, '**', '*.rb')].each do |ruby_file| - Chef::Log.info("Testing #{ruby_file} for syntax errors...") - Chef::Mixin::Command.run_command(:command => "ruby -c #{ruby_file}") - end - end - - def test_templates(cookbook_dir) - Dir[File.join(cookbook_dir, '**', '*.erb')].each do |erb_file| - Chef::Log.info("Testing template #{erb_file} for syntax errors...") - Chef::Mixin::Command.run_command(:command => "sh -c 'erubis -x #{erb_file} | ruby -c'") - end - end def upload_cookbook(cookbook_name) + # Syntax check all cookbook paths rather than tmp_cookbook_dir as to + # take advantage of the existing cache used/generated by knife cookbook + # test. + check = Chef::Knife::CookbookTest.new + check.config[:cookbook_path] = config[:cookbook_path] + check.name_args = [ cookbook_name ] + check.run if cookbook_name =~ /^#{File::SEPARATOR}/ - child_folders = cookbook_name + child_folders = cookbook_name cookbook_name = File.basename(cookbook_name) else - child_folders = config[:cookbook_path].inject([]) do |r, e| + child_folders = config[:cookbook_path].inject([]) do |r, e| r << File.join(e, cookbook_name) r end @@ -98,26 +92,23 @@ class Chef Chef::Log.debug("Staging at #{tmp_cookbook_dir}") - found_cookbook = false + found_cookbook = false child_folders.each do |file_path| if File.directory?(file_path) - found_cookbook = true + found_cookbook = true Chef::Log.info("Copying from #{file_path} to #{tmp_cookbook_dir}") - FileUtils.cp_r(file_path, tmp_cookbook_dir, :remove_destination => true) + FileUtils.cp_r(file_path, tmp_cookbook_dir, :remove_destination => true, :preserve => true) else Chef::Log.info("Nothing to copy from #{file_path}") end - end + end unless found_cookbook Chef::Log.fatal("Could not find cookbook #{cookbook_name}!") exit 17 end - test_ruby(tmp_cookbook_dir) - test_templates(tmp_cookbook_dir) - # First, generate metadata kcm = Chef::Knife::CookbookMetadata.new kcm.config[:cookbook_path] = [ tmp_cookbook_dir ] @@ -144,32 +135,32 @@ class Chef if cookbook_uploaded Chef::StreamingCookbookUploader.put( - "#{Chef::Config[:chef_server_url]}/cookbooks/#{cookbook_name}/_content", - Chef::Config[:node_name], - Chef::Config[:client_key], + "#{Chef::Config[:chef_server_url]}/cookbooks/#{cookbook_name}/_content", + Chef::Config[:node_name], + Chef::Config[:client_key], { - :file => File.new(tarball_name), + :file => File.new(tarball_name), :name => cookbook_name } ) else Chef::StreamingCookbookUploader.post( - "#{Chef::Config[:chef_server_url]}/cookbooks", - Chef::Config[:node_name], - Chef::Config[:client_key], + "#{Chef::Config[:chef_server_url]}/cookbooks", + Chef::Config[:node_name], + Chef::Config[:client_key], { - :file => File.new(tarball_name), + :file => File.new(tarball_name), :name => cookbook_name } ) end Chef::Log.info("Upload complete!") Chef::Log.debug("Removing local tarball at #{tarball_name}") - FileUtils.rm_rf tarball_name + FileUtils.rm_rf tarball_name Chef::Log.debug("Removing local staging directory at #{tmp_cookbook_dir}") FileUtils.rm_rf tmp_cookbook_dir end - + end end end diff --git a/chef/lib/chef/knife/data_bag_edit.rb b/chef/lib/chef/knife/data_bag_edit.rb index 16ac98dcad..50c53cc7a0 100644 --- a/chef/lib/chef/knife/data_bag_edit.rb +++ b/chef/lib/chef/knife/data_bag_edit.rb @@ -38,7 +38,7 @@ class Chef Chef::Log.info("Saved data_bag_item[#{@name_args[1]}]") - json_pretty_print(format_for_display(object)) if config[:print_after] + output(format_for_display(object)) if config[:print_after] end end end diff --git a/chef/lib/chef/knife/data_bag_list.rb b/chef/lib/chef/knife/data_bag_list.rb index 42a01be1be..30ebbe8a89 100644 --- a/chef/lib/chef/knife/data_bag_list.rb +++ b/chef/lib/chef/knife/data_bag_list.rb @@ -30,8 +30,8 @@ class Chef :long => "--with-uri", :description => "Show corresponding URIs" - def run - json_pretty_print(format_list_for_display(Chef::DataBag.list)) + def run + output(format_list_for_display(Chef::DataBag.list)) end end end diff --git a/chef/lib/chef/knife/data_bag_show.rb b/chef/lib/chef/knife/data_bag_show.rb index 977d2fcfd8..a030e56c85 100644 --- a/chef/lib/chef/knife/data_bag_show.rb +++ b/chef/lib/chef/knife/data_bag_show.rb @@ -32,7 +32,7 @@ class Chef else format_list_for_display(Chef::DataBag.load(@name_args[0])) end - json_pretty_print(display) + output(display) end end end diff --git a/chef/lib/chef/knife/ec2_instance_data.rb b/chef/lib/chef/knife/ec2_instance_data.rb index 82acda9295..00282e7765 100644 --- a/chef/lib/chef/knife/ec2_instance_data.rb +++ b/chef/lib/chef/knife/ec2_instance_data.rb @@ -38,7 +38,7 @@ class Chef "attributes" => { "run_list" => @name_args } } data = edit_data(data) if config[:edit] - json_pretty_print(data) + output(data) end end end diff --git a/chef/lib/chef/knife/index_rebuild.rb b/chef/lib/chef/knife/index_rebuild.rb index 4f99c3c932..b5d982be98 100644 --- a/chef/lib/chef/knife/index_rebuild.rb +++ b/chef/lib/chef/knife/index_rebuild.rb @@ -32,7 +32,7 @@ class Chef def run nag - json_pretty_print rest.post_rest("/search/reindex", {}) + output rest.post_rest("/search/reindex", {}) end def nag @@ -48,4 +48,4 @@ class Chef end end -end
\ No newline at end of file +end diff --git a/chef/lib/chef/knife/node_from_file.rb b/chef/lib/chef/knife/node_from_file.rb index 02e33158a2..b0b5ff713c 100644 --- a/chef/lib/chef/knife/node_from_file.rb +++ b/chef/lib/chef/knife/node_from_file.rb @@ -31,7 +31,7 @@ class Chef updated.save - json_pretty_print(format_for_display(updated)) if config[:print_after] + output(format_for_display(updated)) if config[:print_after] Chef::Log.warn("Updated Node #{updated.name}!") end diff --git a/chef/lib/chef/knife/node_list.rb b/chef/lib/chef/knife/node_list.rb index 5239d2c29d..ad4255a938 100644 --- a/chef/lib/chef/knife/node_list.rb +++ b/chef/lib/chef/knife/node_list.rb @@ -31,8 +31,8 @@ class Chef :long => "--with-uri", :description => "Show corresponding URIs" - def run - json_pretty_print(format_list_for_display(Chef::Node.list)) + def run + output(format_list_for_display(Chef::Node.list)) end end end diff --git a/chef/lib/chef/knife/node_run_list_add.rb b/chef/lib/chef/knife/node_run_list_add.rb index 33f3e81917..5ce61b12a8 100644 --- a/chef/lib/chef/knife/node_run_list_add.rb +++ b/chef/lib/chef/knife/node_run_list_add.rb @@ -41,7 +41,7 @@ class Chef config[:run_list] = true - json_pretty_print(format_for_display(node)) + output(format_for_display(node)) end def add_to_run_list(node, new_value, after=nil) diff --git a/chef/lib/chef/knife/node_run_list_remove.rb b/chef/lib/chef/knife/node_run_list_remove.rb index ab90015128..6bda2c21de 100644 --- a/chef/lib/chef/knife/node_run_list_remove.rb +++ b/chef/lib/chef/knife/node_run_list_remove.rb @@ -36,7 +36,7 @@ class Chef config[:run_list] = true - json_pretty_print(format_for_display(node)) + output(format_for_display(node)) end end diff --git a/chef/lib/chef/knife/node_show.rb b/chef/lib/chef/knife/node_show.rb index 1c7d29a037..e5cede73d1 100644 --- a/chef/lib/chef/knife/node_show.rb +++ b/chef/lib/chef/knife/node_show.rb @@ -38,7 +38,7 @@ class Chef def run node = Chef::Node.load(@name_args[0]) - json_pretty_print(format_for_display(node)) + output(format_for_display(node)) end end end diff --git a/chef/lib/chef/knife/role_from_file.rb b/chef/lib/chef/knife/role_from_file.rb index 0ec9736c1c..b366e7f7f6 100644 --- a/chef/lib/chef/knife/role_from_file.rb +++ b/chef/lib/chef/knife/role_from_file.rb @@ -31,7 +31,7 @@ class Chef updated.save - json_pretty_print(format_for_display(updated)) if config[:print_after] + output(format_for_display(updated)) if config[:print_after] Chef::Log.warn("Updated Role #{updated.name}!") end diff --git a/chef/lib/chef/knife/role_list.rb b/chef/lib/chef/knife/role_list.rb index 6f7e8df55f..a8e127246b 100644 --- a/chef/lib/chef/knife/role_list.rb +++ b/chef/lib/chef/knife/role_list.rb @@ -32,7 +32,7 @@ class Chef :description => "Show corresponding URIs" def run - json_pretty_print(format_list_for_display(Chef::Role.list)) + output(format_list_for_display(Chef::Role.list)) end end end diff --git a/chef/lib/chef/knife/role_show.rb b/chef/lib/chef/knife/role_show.rb index 0ed17209c4..b5e1f0f0a3 100644 --- a/chef/lib/chef/knife/role_show.rb +++ b/chef/lib/chef/knife/role_show.rb @@ -33,7 +33,7 @@ class Chef def run role = Chef::Role.load(@name_args[0]) - json_pretty_print(format_for_display(role)) + output(format_for_display(role)) end end diff --git a/chef/lib/chef/knife/search.rb b/chef/lib/chef/knife/search.rb index cede2f16c9..f9a291c36b 100644 --- a/chef/lib/chef/knife/search.rb +++ b/chef/lib/chef/knife/search.rb @@ -82,7 +82,7 @@ class Chef puts display[:rows].join("\n") end else - json_pretty_print(display) + output(display) end end end diff --git a/chef/lib/chef/knife/terremark_server_create.rb b/chef/lib/chef/knife/terremark_server_create.rb new file mode 100644 index 0000000000..87e933db91 --- /dev/null +++ b/chef/lib/chef/knife/terremark_server_create.rb @@ -0,0 +1,152 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 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 'chef/knife' +require 'json' +require 'tempfile' + +class Chef + class Knife + class TerremarkServerCreate < Knife + + banner "Sub-Command: terremark server create NAME [RUN LIST...] (options)" + + option :terremark_password, + :short => "-K PASSWORD", + :long => "--terremark-password PASSWORD", + :description => "Your terremark password", + :proc => Proc.new { |key| Chef::Config[:knife][:terremark_password] = key } + + option :terremark_username, + :short => "-A USERNAME", + :long => "--terremark-username USERNAME", + :description => "Your terremark username", + :proc => Proc.new { |username| Chef::Config[:knife][:terremark_username] = username } + + option :terremark_service, + :short => "-S SERVICE", + :long => "--terremark-service SERVICE", + :description => "Your terremark service name", + :proc => Proc.new { |service| Chef::Config[:knife][:terremark_service] = service } + + def h + @highline ||= HighLine.new + end + + def run + require 'fog' + require 'highline' + require 'net/ssh/multi' + require 'readline' + require 'net/scp' + + server_name = @name_args[0] + + terremark = Fog::Terremark.new( + :terremark_username => Chef::Config[:knife][:terremark_username], + :terremark_password => Chef::Config[:knife][:terremark_password], + :terremark_service => Chef::Config[:knife][:terremark_service] || :vcloud + ) + + $stdout.sync = true + + puts "Instantiating vApp #{h.color(server_name, :bold)}" + vapp_id = terremark.instantiate_vapp_template(server_name).body['href'].split('/').last + + deploy_task_id = terremark.deploy_vapp(vapp_id).body['href'].split('/').last + print "Waiting for deploy task [#{h.color(deploy_task_id, :bold)}]" + terremark.tasks.get(deploy_task_id).wait_for { print "."; ready? } + print "\n" + + power_on_task_id = terremark.power_on(vapp_id).body['href'].split('/').last + print "Waiting for power on task [#{h.color(power_on_task_id, :bold)}]" + terremark.tasks.get(power_on_task_id).wait_for { print "."; ready? } + print "\n" + + private_ip = terremark.get_vapp(vapp_id).body['IpAddress'] + ssh_internet_service = terremark.create_internet_service(terremark.default_vdc_id, 'SSH', 'TCP', 22).body + ssh_internet_service_id = ssh_internet_service['Id'] + public_ip = ssh_internet_service['PublicIpAddress']['Name'] + public_ip_id = ssh_internet_service['PublicIpAddress']['Id'] + ssh_node_service_id = terremark.add_node_service(ssh_internet_service_id, private_ip, 'SSH', 22).body['Id'] + + puts "\nBootstrapping #{h.color(server_name, :bold)}..." + password = terremark.get_vapp_template(12).body['Description'].scan(/\npassword: (.*)\n/).first.first + + command = <<EOH +bash -c ' +echo nameserver 208.67.222.222 > /etc/resolv.conf +echo nameserver 208.67.220.220 >> /etc/resolv.conf + +if [ ! -f /usr/bin/chef-client ]; then + apt-get update + apt-get install -y ruby ruby1.8-dev build-essential wget libruby-extras libruby1.8-extras + cd /tmp + wget http://rubyforge.org/frs/download.php/69365/rubygems-1.3.6.tgz + tar xvf rubygems-1.3.6.tgz + cd rubygems-1.3.6 + ruby setup.rb + cp /usr/bin/gem1.8 /usr/bin/gem + gem install chef ohai --no-rdoc --no-ri --verbose +fi + +mkdir -p /etc/chef + +( +cat <<'EOP' +#{IO.read(Chef::Config[:validation_key])} +EOP +) > /etc/chef/validation.pem + +( +cat <<'EOP' +log_level :info +log_location STDOUT +chef_server_url "#{Chef::Config[:chef_server_url]}" +validation_client_name "#{Chef::Config[:validation_client_name]}" +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +#{{ "run_list" => @name_args[1..-1] }.to_json} +EOP +) > /etc/chef/first-boot.json + +/usr/bin/chef-client -j /etc/chef/first-boot.json' +EOH + + begin + ssh = Chef::Knife::Ssh.new + ssh.name_args = [ public_ip, "sudo #{command}" ] + ssh.config[:ssh_user] = "vcloud" + ssh.config[:manual] = true + ssh.config[:password] = password + ssh.password = password + ssh.run + rescue Errno::ETIMEDOUT + puts "Timed out on bootstrap, re-trying. Hit CTRL-C to abort." + puts "You probably need to log in to Terremark and powercycle #{h.color(@name_args[0], :bold)}" + retry + end + + end + end + end +end + diff --git a/chef/lib/chef/knife/terremark_server_delete.rb b/chef/lib/chef/knife/terremark_server_delete.rb new file mode 100644 index 0000000000..38f98dec4b --- /dev/null +++ b/chef/lib/chef/knife/terremark_server_delete.rb @@ -0,0 +1,87 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 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 'chef/knife' +require 'json' + +class Chef + class Knife + class TerremarkServerDelete < Knife + + banner "Sub-Command: terremark server delete SERVER (options)" + + def h + @highline ||= HighLine.new + end + + def run + require 'fog' + require 'highline' + + terremark = Fog::Terremark.new( + :terremark_username => Chef::Config[:knife][:terremark_username], + :terremark_password => Chef::Config[:knife][:terremark_password], + :terremark_service => Chef::Config[:knife][:terremark_service] || :vcloud + ) + + $stdout.sync = true + + vapp_id = terremark.servers.detect {|server| server.name == @name_args[0]}.id + confirm("Do you really want to delete server ID #{vapp_id} named #{@name_args[0]}") + + puts "Cleaning up internet services..." + private_ip = terremark.servers.get(vapp_id).ip_address + internet_services = terremark.get_internet_services(terremark.default_vdc_id).body['InternetServices'] + public_ip_usage = {} + internet_services.each do |internet_service| + public_ip_address = internet_service['PublicIpAddress']['Name'] + public_ip_usage[public_ip_address] ||= [] + public_ip_usage[public_ip_address] << internet_service['Id'] + end + internet_services.each do |internet_service| + node_services = terremark.get_node_services(internet_service['Id']).body['NodeServices'] + node_services.delete_if do |node_service| + if node_service['IpAddress'] == private_ip + terremark.delete_node_service(node_service['Id']) + end + end + if node_services.empty? + terremark.delete_internet_service(internet_service['Id']) + public_ip_usage.each_value {|internet_services| internet_services.delete(internet_service['Id'])} + if public_ip_usage[internet_service['PublicIpAddress']['Name']].empty? + terremark.delete_public_ip(internet_service['PublicIpAddress']['Id']) + end + end + end + + power_off_task_id = terremark.power_off(vapp_id).body['href'].split('/').last + print "Waiting for power off task [#{h.color(power_off_task_id, :bold)}]" + terremark.tasks.get(power_off_task_id).wait_for { print '.'; ready? } + print "\n" + + print "Deleting vApp #{h.color(vapp_id, :bold)}" + delete_vapp_task_id = terremark.delete_vapp(vapp_id).headers['Location'].split('/').last + terremark.tasks.get(delete_vapp_task_id).wait_for { print '.'; ready? } + print "\n" + + Chef::Log.warn("Deleted server #{@name_args[0]}") + end + end + end +end + diff --git a/chef/lib/chef/knife/terremark_server_list.rb b/chef/lib/chef/knife/terremark_server_list.rb new file mode 100644 index 0000000000..3aba858570 --- /dev/null +++ b/chef/lib/chef/knife/terremark_server_list.rb @@ -0,0 +1,77 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 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 'chef/knife' +require 'json' +require 'tempfile' + +class Chef + class Knife + class TerremarkServerList < Knife + + banner "Sub-Command: terremark server list (options)" + + option :terremark_password, + :short => "-K PASSWORD", + :long => "--terremark-password PASSWORD", + :description => "Your terremark password", + :proc => Proc.new { |key| Chef::Config[:knife][:terremark_password] = key } + + option :terremark_username, + :short => "-A USERNAME", + :long => "--terremark-username USERNAME", + :description => "Your terremark username", + :proc => Proc.new { |username| Chef::Config[:knife][:terremark_username] = username } + + option :terremark_service, + :short => "-S SERVICE", + :long => "--terremark-service SERVICE", + :description => "Your terremark service name", + :proc => Proc.new { |service| Chef::Config[:knife][:terremark_service] = service } + + def h + @highline ||= HighLine.new + end + + def run + require 'fog' + require 'highline' + + server_name = @name_args[0] + + terremark = Fog::Terremark.new( + :terremark_username => Chef::Config[:knife][:terremark_username], + :terremark_password => Chef::Config[:knife][:terremark_password], + :terremark_service => Chef::Config[:knife][:terremark_service] || :vcloud + ) + + $stdout.sync = true + + server_list = [ h.color('ID', :bold), h.color('Name', :bold) ] + terremark.servers.all.each do |server| + server_list << server.id.to_s + server_list << server.name + end + puts h.list(server_list, :columns_across, 2) + + end + end + end +end + + diff --git a/chef/lib/chef/mixin/command.rb b/chef/lib/chef/mixin/command.rb index eec0966189..f703674c03 100644 --- a/chef/lib/chef/mixin/command.rb +++ b/chef/lib/chef/mixin/command.rb @@ -97,6 +97,7 @@ class Chef # timeout<String>: How many seconds to wait for the command to execute before timing out # returns<String>: The single exit value command is expected to return, otherwise causes an exception # ignore_failure<Boolean>: Whether to raise an exception on failure, or just return the status + # output_on_failure<Boolean>: Return output in raised exception regardless of Log.level # # user<String>: The UID or user name of the user to execute the command as # group<String>: The GID or group name of the group to execute the command as @@ -108,6 +109,7 @@ class Chef command_output = "" args[:ignore_failure] ||= false + args[:output_on_failure] ||= false if args.has_key?(:creates) if File.exists?(args[:creates]) @@ -165,18 +167,18 @@ class Chef module_function :output_of_command - def handle_command_failures(status, command_output, args={}) - unless args[:ignore_failure] - args[:returns] ||= 0 - if status.exitstatus != args[:returns] + def handle_command_failures(status, command_output, opts={}) + unless opts[:ignore_failure] + opts[:returns] ||= 0 + unless Array(opts[:returns]).include?(status.exitstatus) # if the log level is not debug, through output of command when we fail output = "" - if Chef::Log.level == :debug - output << "\n---- Begin output of #{args[:command]} ----\n" - output << "#{command_output}" - output << "---- End output of #{args[:command]} ----\n" + if Chef::Log.level == :debug || opts[:output_on_failure] + output << "\n---- Begin output of #{opts[:command]} ----\n" + output << command_output.to_s + output << "\n---- End output of #{opts[:command]} ----\n" end - raise Chef::Exceptions::Exec, "#{args[:command]} returned #{status.exitstatus}, expected #{args[:returns]}#{output}" + raise Chef::Exceptions::Exec, "#{opts[:command]} returned #{status.exitstatus}, expected #{opts[:returns]}#{output}" end end end diff --git a/chef/lib/chef/mixin/recipe_definition_dsl_core.rb b/chef/lib/chef/mixin/recipe_definition_dsl_core.rb index 1f27d1bd79..c8afd07574 100644 --- a/chef/lib/chef/mixin/recipe_definition_dsl_core.rb +++ b/chef/lib/chef/mixin/recipe_definition_dsl_core.rb @@ -17,11 +17,14 @@ # limitations under the License. # -require 'chef/recipe' require 'chef/resource' require 'chef/mixin/convert_to_class_name' require 'chef/mixin/language' +# UGH. this is a circular require that will cause an uninitialized constant +# error, but this file really does depend on Chef::Recipe. oh well. +# require 'chef/recipe' + class Chef module Mixin module RecipeDefinitionDSLCore diff --git a/chef/lib/chef/mixin/template.rb b/chef/lib/chef/mixin/template.rb index 0309b5994c..df6c2839df 100644 --- a/chef/lib/chef/mixin/template.rb +++ b/chef/lib/chef/mixin/template.rb @@ -42,15 +42,16 @@ class Chef 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 + Tempfile.open("chef-rendered-template") do |tempfile| + tempfile.print(output) + tempfile.close + yield tempfile + end end class TemplateError < RuntimeError attr_reader :original_exception, :context - SOURCE_CONTEXT_WINDOW = 2 unless defined? SOURCE_CONTEXT_WINDOW + SOURCE_CONTEXT_WINDOW = 2 def initialize(original_exception, template, context) @original_exception, @template, @context = original_exception, template, context diff --git a/chef/lib/chef/mixin/xml_escape.rb b/chef/lib/chef/mixin/xml_escape.rb index c9ef7e76f9..7ab40470b4 100644 --- a/chef/lib/chef/mixin/xml_escape.rb +++ b/chef/lib/chef/mixin/xml_escape.rb @@ -80,18 +80,18 @@ class Chef 156 => 339, # latin small ligature oe 158 => 382, # latin small letter z with caron 159 => 376 # latin capital letter y with diaeresis - } unless defined?(CP1252) + } # http://www.w3.org/TR/REC-xml/#dt-chardata PREDEFINED = { 38 => '&', # ampersand 60 => '<', # left angle bracket 62 => '>' # right angle bracket - } unless defined?(PREDEFINED) + } # http://www.w3.org/TR/REC-xml/#charsets VALID = [[0x9, 0xA, 0xD], (0x20..0xD7FF), - (0xE000..0xFFFD), (0x10000..0x10FFFF)] unless defined?(VALID) + (0xE000..0xFFFD), (0x10000..0x10FFFF)] def xml_escape(unescaped_str) begin diff --git a/chef/lib/chef/mixins.rb b/chef/lib/chef/mixins.rb new file mode 100644 index 0000000000..5c9b8f4320 --- /dev/null +++ b/chef/lib/chef/mixins.rb @@ -0,0 +1,16 @@ +require 'chef/mixin/check_helper' +require 'chef/mixin/checksum' +require 'chef/mixin/command' +require 'chef/mixin/convert_to_class_name' +require 'chef/mixin/create_path' +require 'chef/mixin/deep_merge' +require 'chef/mixin/find_preferred_file' +require 'chef/mixin/from_file' +require 'chef/mixin/generate_url' +require 'chef/mixin/language' +require 'chef/mixin/language_include_attribute' +require 'chef/mixin/language_include_recipe' +require 'chef/mixin/params_validate' +require 'chef/mixin/recipe_definition_dsl_core' +require 'chef/mixin/template' +require 'chef/mixin/xml_escape'
\ No newline at end of file diff --git a/chef/lib/chef/node.rb b/chef/lib/chef/node.rb index f79dd4fc87..9bdf7228e6 100644 --- a/chef/lib/chef/node.rb +++ b/chef/lib/chef/node.rb @@ -7,9 +7,9 @@ # 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. @@ -22,6 +22,7 @@ require 'chef/mixin/check_helper' require 'chef/mixin/params_validate' require 'chef/mixin/from_file' require 'chef/mixin/language_include_attribute' +require 'chef/mixin/deep_merge' require 'chef/couchdb' require 'chef/rest' require 'chef/run_list' @@ -36,20 +37,20 @@ class Chef attr_accessor :recipe_list, :couchdb, :couchdb_rev, :run_state, :run_list, :override_attrs, :default_attrs, :normal_attrs, :automatic_attrs, :cookbook_loader attr_reader :node attr_reader :couchdb_id - + include Chef::Mixin::CheckHelper include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate include Chef::Mixin::LanguageIncludeAttribute include Chef::IndexQueue::Indexable - + DESIGN_DOCUMENT = { "version" => 9, "language" => "javascript", "views" => { "all" => { "map" => <<-EOJS - function(doc) { + function(doc) { if (doc.chef_type == "node") { emit(doc.name, doc); } @@ -58,7 +59,7 @@ class Chef }, "all_id" => { "map" => <<-EOJS - function(doc) { + function(doc) { if (doc.chef_type == "node") { emit(doc.name, doc.name); } @@ -84,7 +85,7 @@ class Chef to_emit["ohai_time"] = doc["attributes"]["ohai_time"]; } else { to_emit["ohai_time"] = "Undefined"; - } + } if (doc["attributes"]["uptime"]) { to_emit["uptime"] = doc["attributes"]["uptime"]; } else { @@ -125,7 +126,7 @@ class Chef } }, } - + # Create a new Chef::Node object. def initialize(couchdb=nil) @name = nil @@ -154,36 +155,36 @@ class Chef end def chef_server_rest - Chef::REST.new(Chef::Config[:chef_server_url]) + Chef::REST.new(Chef::Config[:chef_server_url]) end def self.chef_server_rest - Chef::REST.new(Chef::Config[:chef_server_url]) + Chef::REST.new(Chef::Config[:chef_server_url]) end - # Find a recipe for this Chef::Node by fqdn. Will search first for + # Find a recipe for this Chef::Node by fqdn. Will search first for # Chef::Config["node_path"]/fqdn.rb, then hostname.rb, then default.rb. - # + # # Returns a new Chef::Node object. # - # Raises an ArgumentError if it cannot find the node. + # Raises an ArgumentError if it cannot find the node. def find_file(fqdn) host_parts = fqdn.split(".") hostname = host_parts[0] - + [fqdn, hostname, "default"].each { |fname| - node_file = File.join(Chef::Config[:node_path], "#{fname.to_s}.rb") + node_file = File.join(Chef::Config[:node_path], "#{fname.to_s}.rb") return self.from_file(node_file) if File.exists?(node_file) } - - raise ArgumentError, "Cannot find a node matching #{fqdn}, not even with default.rb!" + + raise ArgumentError, "Cannot find a node matching #{fqdn}, not even with default.rb!" end - + # Set the name of this Node, or return the current name. def name(arg=nil) if arg != nil validate( - {:name => arg }, + {:name => arg }, {:name => { :kind_of => String, :cannot_be => :blank} }) @@ -194,7 +195,7 @@ class Chef end def attribute - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attribute end def attribute=(value) @@ -203,14 +204,14 @@ class Chef # Return an attribute of this node. Returns nil if the attribute is not found. def [](attrib) - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs)[attrib] + attribute[attrib] end - + # Set an attribute of this node def []=(attrib, value) - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs)[attrib] = value + attribute[attrib] = value end - + def store(attrib, value) self[attrib] = value end @@ -218,7 +219,7 @@ class Chef # Set a normal attribute of this node, but auto-vivifiy any Mashes that # might be missing def normal - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :normal attrs.auto_vivifiy_on_read = true attrs @@ -229,7 +230,7 @@ class Chef # Set a normal attribute of this node, auto-vivifiying any mashes that are # missing, but if the final value already exists, don't set it def normal_unless - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :normal attrs.auto_vivifiy_on_read = true attrs.set_unless_value_present = true @@ -240,7 +241,7 @@ class Chef # Set a default of this node, but auto-vivifiy any Mashes that might # be missing def default - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :default attrs.auto_vivifiy_on_read = true attrs @@ -249,7 +250,7 @@ class Chef # Set a default attribute of this node, auto-vivifiying any mashes that are # missing, but if the final value already exists, don't set it def default_unless - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :default attrs.auto_vivifiy_on_read = true attrs.set_unless_value_present = true @@ -259,7 +260,7 @@ class Chef # Set an override attribute of this node, but auto-vivifiy any Mashes that # might be missing def override - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :override attrs.auto_vivifiy_on_read = true attrs @@ -268,7 +269,7 @@ class Chef # Set an override attribute of this node, auto-vivifiying any mashes that # are missing, but if the final value already exists, don't set it def override_unless - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :override attrs.auto_vivifiy_on_read = true attrs.set_unless_value_present = true @@ -281,44 +282,44 @@ class Chef # Only works on the top level. Preferred way is to use the normal [] style # lookup and call attribute?() def attribute?(attrib) - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs).attribute?(attrib) + attribute.attribute?(attrib) end - - # Yield each key of the top level to the block. + + # Yield each key of the top level to the block. def each(&block) - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs).each(&block) + attribute.each(&block) end - + # Iterates over each attribute, passing the attribute and value to the block. def each_attribute(&block) - Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs).each_attribute(&block) + attribute.each_attribute(&block) end # Set an attribute based on the missing method. If you pass an argument, we'll use that # to set the attribute values. Otherwise, we'll wind up just returning the attributes # value. def method_missing(symbol, *args) - attrs = Chef::Node::Attribute.new(@normal_attrs, @default_attrs, @override_attrs, @automatic_attrs) + attrs = attribute attrs.set_type = :normal attrs.send(symbol, *args) end - + # Returns true if this Node expects a given recipe, false if not. def recipe?(recipe_name) @run_list.include?(recipe_name) || @run_state[:seen_recipes].include?(recipe_name) end - + # Returns true if this Node expects a given role, false if not. def role?(role_name) @run_list.include?("role[#{role_name}]") end # Returns an Array of roles and recipes, in the order they will be applied. - # If you call it with arguments, they will become the new list of roles and recipes. + # If you call it with arguments, they will become the new list of roles and recipes. def run_list(*args) args.length > 0 ? @run_list.reset!(args) : @run_list end - + def recipes(*args) Chef::Log.warn "Chef::Node#recipes method is deprecated. Please use Chef::Node#run_list" run_list(*args) @@ -328,22 +329,22 @@ class Chef def run_list?(item) @run_list.detect { |r| r == item } ? true : false end - + def consume_attributes(attrs) attrs ||= {} Chef::Log.debug("Adding JSON Attributes") - attrs.each do |key, value| - if ["recipes", "run_list"].include?(key) - run_list(value) - else - Chef::Log.debug("JSON Attribute: #{key} - #{value.inspect}") - store(key, value) + if new_run_list = attrs.delete("recipes") || attrs.delete("run_list") + if attrs.key?("recipes") || attrs.key?("run_list") + raise Chef::Exceptions::AmbiguousRunlistSpecification, "please set the node's run list using the 'run_list' attribute only." end + Chef::Log.info("Replacing the run_list with #{new_run_list.inspect} from JSON") + run_list(new_run_list) end + Chef::Mixin::DeepMerge.merge(@attribute, attrs) + self[:tags] = Array.new unless attribute?(:tags) - end - + # Transform the node to a Hash def to_hash index_hash = Hash.new @@ -357,8 +358,8 @@ class Chef index_hash["run_list"] = @run_list.run_list if @run_list.run_list.length > 0 index_hash end - - # Serialize this object as a hash + + # Serialize this object as a hash def to_json(*a) result = { "name" => @name, @@ -373,11 +374,12 @@ class Chef result["_rev"] = @couchdb_rev if @couchdb_rev result.to_json(*a) end - + # Create a Chef::Node from JSON def self.json_create(o) node = new node.name(o["name"]) + if o.has_key?("attributes") node.normal_attrs = o["attributes"] end @@ -396,7 +398,7 @@ class Chef node.index_id = node.couchdb_id node end - + # List all the Chef::Node objects in the CouchDB. If inflate is set to true, you will get # the full list of all Nodes, fully inflated. def self.cdb_list(inflate=false, couchdb=nil) @@ -416,10 +418,10 @@ class Chef Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("nodes") end end - + # Load a node by name from CouchDB def self.cdb_load(name, couchdb=nil) - (couchdb || Chef::CouchDB.new).load("node", name) + (couchdb || Chef::CouchDB.new).load("node", name) end def self.exists?(nodename, couchdb) @@ -429,12 +431,12 @@ class Chef nil end end - + # Load a node by name def self.load(name) Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("nodes/#{name}") end - + # Remove this node from the CouchDB def cdb_destroy @couchdb.delete("node", @name, @couchdb_rev) @@ -444,7 +446,7 @@ class Chef def destroy chef_server_rest.delete_rest("nodes/#{@name}") end - + # Save this node to the CouchDB def cdb_save @couchdb_rev = @couchdb.store("node", @name, self)["rev"] @@ -460,18 +462,18 @@ class Chef end self end - + # Create the node via the REST API def create chef_server_rest.post_rest("nodes", self) self - end + end # Set up our CouchDB design document def self.create_design_document(couchdb=nil) (couchdb || Chef::CouchDB.new).create_design_document("nodes", DESIGN_DOCUMENT) end - + # As a string def to_s "node[#{@name}]" diff --git a/chef/lib/chef/openid_registration.rb b/chef/lib/chef/openid_registration.rb index fcc67dd08c..82bee0798e 100644 --- a/chef/lib/chef/openid_registration.rb +++ b/chef/lib/chef/openid_registration.rb @@ -21,7 +21,6 @@ require 'chef/mixin/params_validate' require 'chef/couchdb' require 'chef/index_queue' require 'digest/sha1' -require 'rubygems' require 'json' class Chef diff --git a/chef/lib/chef/platform.rb b/chef/lib/chef/platform.rb index 6d81f7a9e0..58ebc4d0fb 100644 --- a/chef/lib/chef/platform.rb +++ b/chef/lib/chef/platform.rb @@ -19,128 +19,162 @@ require 'chef/config' require 'chef/log' require 'chef/mixin/params_validate' -require 'chef/platform' -require 'chef/resource' -Dir[File.join(File.dirname(__FILE__), 'provider/**/*.rb')].sort.each { |lib| require lib } + +# Actually, this file depends on nearly every provider in chef, but actually +# requiring them causes circular requires resulting in uninitialized constant +# errors. +require 'chef/provider' +require 'chef/provider/log' +require 'chef/provider/user' +require 'chef/provider/group' +require 'chef/provider/mount' +require 'chef/provider/service' +require 'chef/provider/package' + class Chef class Platform - @platforms = { - :mac_os_x => { - :default => { - :package => Chef::Provider::Package::Macports, - :user => Chef::Provider::User::Dscl, - :group => Chef::Provider::Group::Dscl - } - }, - :freebsd => { - :default => { - :group => Chef::Provider::Group::Pw, - :package => Chef::Provider::Package::Freebsd, - :service => Chef::Provider::Service::Freebsd, - :user => Chef::Provider::User::Pw, - :cron => Chef::Provider::Cron - } - }, - :ubuntu => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - :cron => Chef::Provider::Cron, - :mdadm => Chef::Provider::Mdadm - } - }, - :debian => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - :cron => Chef::Provider::Cron, - :mdadm => Chef::Provider::Mdadm - } - }, - :centos => { - :default => { - :service => Chef::Provider::Service::Redhat, - :cron => Chef::Provider::Cron, - :package => Chef::Provider::Package::Yum, - :mdadm => Chef::Provider::Mdadm - } - }, - :fedora => { - :default => { - :service => Chef::Provider::Service::Redhat, - :cron => Chef::Provider::Cron, - :package => Chef::Provider::Package::Yum, - :mdadm => Chef::Provider::Mdadm - } - }, - :suse => { - :default => { - :service => Chef::Provider::Service::Redhat, - :cron => Chef::Provider::Cron, - :package => Chef::Provider::Package::Zypper - } - }, - :redhat => { - :default => { - :service => Chef::Provider::Service::Redhat, - :cron => Chef::Provider::Cron, - :package => Chef::Provider::Package::Yum, - :mdadm => Chef::Provider::Mdadm - } - }, - :gentoo => { - :default => { - :package => Chef::Provider::Package::Portage, - :service => Chef::Provider::Service::Gentoo, - :cron => Chef::Provider::Cron, - :mdadm => Chef::Provider::Mdadm - } - }, - :arch => { - :default => { - :package => Chef::Provider::Package::Pacman, - :service => Chef::Provider::Service::Arch, - :cron => Chef::Provider::Cron, - :mdadm => Chef::Provider::Mdadm - } - }, - :solaris => {}, - :default => { - :file => Chef::Provider::File, - :directory => Chef::Provider::Directory, - :link => Chef::Provider::Link, - :template => Chef::Provider::Template, - :remote_file => Chef::Provider::RemoteFile, - :remote_directory => Chef::Provider::RemoteDirectory, - :execute => Chef::Provider::Execute, - :mount => Chef::Provider::Mount::Mount, - :script => Chef::Provider::Script, - :service => Chef::Provider::Service::Init, - :perl => Chef::Provider::Script, - :python => Chef::Provider::Script, - :ruby => Chef::Provider::Script, - :bash => Chef::Provider::Script, - :csh => Chef::Provider::Script, - :user => Chef::Provider::User::Useradd, - :group => Chef::Provider::Group::Gpasswd, - :http_request => Chef::Provider::HttpRequest, - :route => Chef::Provider::Route, - :ifconfig => Chef::Provider::Ifconfig, - :ruby_block => Chef::Provider::RubyBlock, - :erl_call => Chef::Provider::ErlCall, - :log => Chef::Provider::Log::ChefLog - } - } - class << self - attr_accessor :platforms + attr_writer :platforms + + def platforms + @platforms ||= { + :mac_os_x => { + :default => { + :package => Chef::Provider::Package::Macports, + :user => Chef::Provider::User::Dscl, + :group => Chef::Provider::Group::Dscl + } + }, + :freebsd => { + :default => { + :group => Chef::Provider::Group::Pw, + :package => Chef::Provider::Package::Freebsd, + :service => Chef::Provider::Service::Freebsd, + :user => Chef::Provider::User::Pw, + :cron => Chef::Provider::Cron + } + }, + :ubuntu => { + :default => { + :package => Chef::Provider::Package::Apt, + :service => Chef::Provider::Service::Debian, + :cron => Chef::Provider::Cron, + :mdadm => Chef::Provider::Mdadm + } + }, + :debian => { + :default => { + :package => Chef::Provider::Package::Apt, + :service => Chef::Provider::Service::Debian, + :cron => Chef::Provider::Cron, + :mdadm => Chef::Provider::Mdadm + } + }, + :centos => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm + } + }, + :scientific => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm + } + }, + :fedora => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm + } + }, + :suse => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Zypper + } + }, + :redhat => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm + } + }, + :gentoo => { + :default => { + :package => Chef::Provider::Package::Portage, + :service => Chef::Provider::Service::Gentoo, + :cron => Chef::Provider::Cron, + :mdadm => Chef::Provider::Mdadm + } + }, + :arch => { + :default => { + :package => Chef::Provider::Package::Pacman, + :service => Chef::Provider::Service::Arch, + :cron => Chef::Provider::Cron, + :mdadm => Chef::Provider::Mdadm + } + }, + :mswin => { + :default => { + :service => Chef::Provider::Service::Windows + } + }, + :mingw32 => { + :default => { + :service => Chef::Provider::Service::Windows + } + }, + :windows => { + :default => { + :service => Chef::Provider::Service::Windows + } + }, + :solaris => {}, + :default => { + :file => Chef::Provider::File, + :directory => Chef::Provider::Directory, + :link => Chef::Provider::Link, + :template => Chef::Provider::Template, + :remote_file => Chef::Provider::RemoteFile, + :remote_directory => Chef::Provider::RemoteDirectory, + :execute => Chef::Provider::Execute, + :mount => Chef::Provider::Mount::Mount, + :script => Chef::Provider::Script, + :service => Chef::Provider::Service::Init, + :perl => Chef::Provider::Script, + :python => Chef::Provider::Script, + :ruby => Chef::Provider::Script, + :bash => Chef::Provider::Script, + :csh => Chef::Provider::Script, + :user => Chef::Provider::User::Useradd, + :group => Chef::Provider::Group::Gpasswd, + :http_request => Chef::Provider::HttpRequest, + :route => Chef::Provider::Route, + :ifconfig => Chef::Provider::Ifconfig, + :ruby_block => Chef::Provider::RubyBlock, + :erl_call => Chef::Provider::ErlCall, + :log => Chef::Provider::Log::ChefLog + } + } + end include Chef::Mixin::ParamsValidate def find(name, version) - provider_map = @platforms[:default].clone + provider_map = platforms[:default].clone name_sym = name if name.kind_of?(String) @@ -149,15 +183,15 @@ class Chef name_sym = name.to_sym end - if @platforms.has_key?(name_sym) - if @platforms[name_sym].has_key?(version) + if platforms.has_key?(name_sym) + if platforms[name_sym].has_key?(version) Chef::Log.debug("Platform #{name.to_s} version #{version} found") - if @platforms[name_sym].has_key?(:default) - provider_map.merge!(@platforms[name_sym][:default]) + if platforms[name_sym].has_key?(:default) + provider_map.merge!(platforms[name_sym][:default]) end - provider_map.merge!(@platforms[name_sym][version]) - elsif @platforms[name_sym].has_key?(:default) - provider_map.merge!(@platforms[name_sym][:default]) + provider_map.merge!(platforms[name_sym][version]) + elsif platforms[name_sym].has_key?(:default) + provider_map.merge!(platforms[name_sym][:default]) end else Chef::Log.debug("Platform #{name} not found, using all defaults. (Unsupported platform?)") @@ -221,30 +255,30 @@ class Chef ) if args.has_key?(:platform) if args.has_key?(:version) - if @platforms.has_key?(args[:platform]) - if @platforms[args[:platform]].has_key?(args[:version]) - @platforms[args[:platform]][args[:version]][args[:resource].to_sym] = args[:provider] + if platforms.has_key?(args[:platform]) + if platforms[args[:platform]].has_key?(args[:version]) + platforms[args[:platform]][args[:version]][args[:resource].to_sym] = args[:provider] else - @platforms[args[:platform]][args[:version]] = { + platforms[args[:platform]][args[:version]] = { args[:resource].to_sym => args[:provider] } end else - @platforms[args[:platform]] = { + platforms[args[:platform]] = { args[:version] => { args[:resource].to_sym => args[:provider] } } end else - if @platforms.has_key?(args[:platform]) - if @platforms[args[:platform]].has_key?(:default) - @platforms[args[:platform]][:default][args[:resource].to_sym] = args[:provider] + if platforms.has_key?(args[:platform]) + if platforms[args[:platform]].has_key?(:default) + platforms[args[:platform]][:default][args[:resource].to_sym] = args[:provider] else - @platforms[args[:platform]] = { :default => { args[:resource].to_sym => args[:provider] } } + platforms[args[:platform]] = { :default => { args[:resource].to_sym => args[:provider] } } end else - @platforms[args[:platform]] = { + platforms[args[:platform]] = { :default => { args[:resource].to_sym => args[:provider] } @@ -252,10 +286,10 @@ class Chef end end else - if @platforms.has_key?(:default) - @platforms[:default][args[:resource].to_sym] = args[:provider] + if platforms.has_key?(:default) + platforms[:default][args[:resource].to_sym] = args[:provider] else - @platforms[:default] = { + platforms[:default] = { args[:resource].to_sym => args[:provider] } end @@ -264,8 +298,8 @@ class Chef def find_provider(platform, version, resource_type) pmap = Chef::Platform.find(platform, version) - provider_klass = explicit_provider(platform, version, resource_type) || - platform_provider(platform, version, resource_type) || + provider_klass = explicit_provider(platform, version, resource_type) || + platform_provider(platform, version, resource_type) || resource_matching_provider(platform, version, resource_type) raise ArgumentError, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil? @@ -288,8 +322,8 @@ class Chef def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) begin - Chef::Provider.const_get(resource_type.class.to_s.split('::').last) - rescue NameError + Chef::Provider.const_get(resource_type.class.to_s.split('::').last) + rescue NameError nil end else diff --git a/chef/lib/chef/provider.rb b/chef/lib/chef/provider.rb index 2191aabc94..bd7c50a065 100644 --- a/chef/lib/chef/provider.rb +++ b/chef/lib/chef/provider.rb @@ -49,10 +49,10 @@ class Chef protected - def recipe_eval(*args, &block) + def recipe_eval(&block) provider_collection, @collection = @collection, Chef::ResourceCollection.new - instance_eval(*args, &block) + instance_eval(&block) Chef::Runner.new(@node, @collection).converge @collection = provider_collection diff --git a/chef/lib/chef/provider/cron.rb b/chef/lib/chef/provider/cron.rb index 25467ffbf6..df3f9e66fe 100644 --- a/chef/lib/chef/provider/cron.rb +++ b/chef/lib/chef/provider/cron.rb @@ -6,9 +6,9 @@ # 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. @@ -35,19 +35,19 @@ class Chef attr_accessor :cron_exists, :cron_empty def load_current_resource - crontab = String.new + crontab_lines = [] @current_resource = Chef::Resource::Cron.new(@new_resource.name) @current_resource.user(@new_resource.user) status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr| - stdout.each { |line| crontab << line } + stdout.each_line { |line| crontab_lines << line } end if status.exitstatus > 1 raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}" elsif status.exitstatus == 0 cron_found = false - crontab.each do |line| - case line - when /^# Chef Name: #{@new_resource.name}/ + crontab_lines.each do |line| + case line.chomp + when "# Chef Name: #{@new_resource.name}" Chef::Log.debug("Found cron '#{@new_resource.name}'") cron_found = true @cron_exists = true @@ -66,11 +66,11 @@ class Chef next when CRON_PATTERN if cron_found - @current_resource.minute($1) - @current_resource.hour($2) + @current_resource.minute($1) + @current_resource.hour($2) @current_resource.day($3) - @current_resource.month($4) - @current_resource.weekday($5) + @current_resource.month($4) + @current_resource.weekday($5) @current_resource.command($6) cron_found=false end @@ -84,7 +84,7 @@ class Chef Chef::Log.debug("Cron empty for '#{@new_resource.user}'") @cron_empty = true end - + @current_resource end @@ -112,8 +112,8 @@ class Chef end status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr| stdout.each_line do |line| - case line - when /^# Chef Name: #{@new_resource.name}\n/ + case line.chomp + when "# Chef Name: #{@new_resource.name}" cron_found = true next when CRON_PATTERN @@ -125,12 +125,12 @@ class Chef else next if cron_found end - crontab << line + crontab << line end end status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr| - crontab.each { |line| stdin.puts "#{line}" } + crontab.each_line { |line| stdin.puts "#{line}" } end Chef::Log.info("Updated cron '#{@new_resource.name}'") @new_resource.updated = true @@ -140,11 +140,11 @@ class Chef stdout.each { |line| crontab << line } end end - + crontab << newcron status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr| - crontab.each { |line| stdin.puts "#{line}" } + crontab.each_line { |line| stdin.puts "#{line}" } end Chef::Log.info("Added cron '#{@new_resource.name}'") @new_resource.updated = true @@ -157,8 +157,8 @@ class Chef cron_found = false status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr| stdout.each_line do |line| - case line - when /^# Chef Name: #{@new_resource.name}\n/ + case line.chomp + when "# Chef Name: #{@new_resource.name}" cron_found = true next when CRON_PATTERN @@ -169,12 +169,12 @@ class Chef else next if cron_found end - crontab << line + crontab << line end end status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr| - crontab.each { |line| stdin.puts "#{line}" } + crontab.each_line { |line| stdin.puts "#{line}" } end Chef::Log.debug("Deleted cron '#{@new_resource.name}'") @new_resource.updated = true diff --git a/chef/lib/chef/provider/deploy/revision.rb b/chef/lib/chef/provider/deploy/revision.rb index aea4901f6b..00a30515ba 100644 --- a/chef/lib/chef/provider/deploy/revision.rb +++ b/chef/lib/chef/provider/deploy/revision.rb @@ -50,11 +50,15 @@ class Chef cache end + def sorted_releases_from_filesystem + Dir.glob(new_resource.deploy_to + "/releases/*").sort_by { |d| ::File.ctime(d) } + end + def load_cache begin JSON.parse(Chef::FileCache.load("revision-deploys/#{new_resource.name}")) rescue Chef::Exceptions::FileNotFound - save_cache([]) + sorted_releases_from_filesystem end end diff --git a/chef/lib/chef/provider/erl_call.rb b/chef/lib/chef/provider/erl_call.rb index 39b2dcfab6..7d29e24081 100644 --- a/chef/lib/chef/provider/erl_call.rb +++ b/chef/lib/chef/provider/erl_call.rb @@ -59,11 +59,11 @@ class Chef Chef::Log.debug("Running erl_call[#{@new_resource.name}]") Chef::Log.debug("erl_call[#{@new_resource.name}] command: #{command}") Chef::Log.debug("erl_call[#{@new_resource.name}] code: #{@new_resource.code}") - @new_resource.code.each { |line| stdin.puts "#{line.chomp!}" } + @new_resource.code.each_line { |line| stdin.puts "#{line.chomp!}" } stdin.close Chef::Log.info("Ran erl_call[#{@new_resource.name}] successfully") Chef::Log.debug("erl_call[#{@new_resource.name}] output: ") - stdout.each { |line| Chef::Log.debug("#{line}")} + stdout.each_line { |line| Chef::Log.debug("#{line}")} end end diff --git a/chef/lib/chef/provider/file.rb b/chef/lib/chef/provider/file.rb index 0bb2c4529b..969c7c9703 100644 --- a/chef/lib/chef/provider/file.rb +++ b/chef/lib/chef/provider/file.rb @@ -46,6 +46,7 @@ class Chef def load_current_resource @current_resource = Chef::Resource::File.new(@new_resource.name) + @new_resource.path.gsub!(/\\/, "/") # for Windows @current_resource.path(@new_resource.path) if ::File.exist?(@current_resource.path) && ::File.readable?(@current_resource.path) cstats = ::File.stat(@current_resource.path) diff --git a/chef/lib/chef/provider/group/dscl.rb b/chef/lib/chef/provider/group/dscl.rb index 905a1deaaf..4462aa28a6 100644 --- a/chef/lib/chef/provider/group/dscl.rb +++ b/chef/lib/chef/provider/group/dscl.rb @@ -75,11 +75,13 @@ class Chef def set_members unless @new_resource.append Chef::Log.debug("#{@new_resource}: removing group members #{@current_resource.members.join(' ')}") unless @current_resource.members.empty? - safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembers") # clear guid list - safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembership") # clear user list + safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembers ''") # clear guid list + safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembership ''") # clear user list + end + unless @new_resource.members.empty? + Chef::Log.debug("#{@new_resource}: setting group members #{@new_resource.members.join(', ')}") + safe_dscl("append /Groups/#{@new_resource.group_name} GroupMembership #{@new_resource.members.join(' ')}") end - Chef::Log.debug("#{@new_resource}: setting group members #{@new_resource.members.join(', ')}") unless @new_resource.members.empty? - safe_dscl("append /Groups/#{@new_resource.group_name} GroupMembership #{@new_resource.members.join(' ')}") end def load_current_resource diff --git a/chef/lib/chef/provider/mdadm.rb b/chef/lib/chef/provider/mdadm.rb index 6a22c289b4..6df655a186 100644 --- a/chef/lib/chef/provider/mdadm.rb +++ b/chef/lib/chef/provider/mdadm.rb @@ -26,10 +26,6 @@ class Chef include Chef::Mixin::Command - def initialize(node, new_resource) - super(node, new_resource) - end - def load_current_resource @current_resource = Chef::Resource::Mdadm.new(@new_resource.name) @current_resource.raid_device(@new_resource.raid_device) diff --git a/chef/lib/chef/provider/mount/mount.rb b/chef/lib/chef/provider/mount/mount.rb index 1669ddebb5..18bc8441bc 100644 --- a/chef/lib/chef/provider/mount/mount.rb +++ b/chef/lib/chef/provider/mount/mount.rb @@ -64,12 +64,16 @@ class Chef # Check to see if there is a entry in /etc/fstab. Last entry for a volume wins. enabled = false - ::File.read("/etc/fstab").each do |line| + ::File.foreach("/etc/fstab") do |line| case line when /^[#\s]/ next - when /^#{device_fstab_regex}\s+#{@new_resource.mount_point}/ + when /^#{device_fstab_regex}\s+#{@new_resource.mount_point}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ enabled = true + @current_resource.fstype($1) + @current_resource.options($2) + @current_resource.dump($3.to_i) + @current_resource.pass($4.to_i) Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/fstab") when /^[\/\w]+\s+#{@new_resource.mount_point}/ enabled = false @@ -119,13 +123,21 @@ class Chef end def enable_fs - unless @current_resource.enabled - ::File.open("/etc/fstab", "a") do |fstab| - fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? "defaults" : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}") - Chef::Log.info("Enabled #{@new_resource.mount_point}") + if @current_resource.enabled + if @current_resource.fstype == @new_resource.fstype and + @current_resource.options == @new_resource.options and + @current_resource.dump == @new_resource.dump and + @current_resource.pass == @new_resource.pass + Chef::Log.debug("#{@new_resource.mount_point} is already enabled.") + return end - else - Chef::Log.debug("#{@new_resource.mount_point} is already enabled.") + # The current options don't match what we have, so + # disable, then enable. + disable_fs + end + ::File.open("/etc/fstab", "a") do |fstab| + fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? "defaults" : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}") + Chef::Log.info("Enabled #{@new_resource.mount_point}") end end diff --git a/chef/lib/chef/provider/package.rb b/chef/lib/chef/provider/package.rb index bf6dd4cf47..6f0ecbbd9d 100644 --- a/chef/lib/chef/provider/package.rb +++ b/chef/lib/chef/provider/package.rb @@ -120,7 +120,7 @@ class Chef raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :purge" end - def preseed_package(name, version, preseed) + def preseed_package(name, version) raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support pre-seeding package install/upgrade instructions - don't ask it to!" end diff --git a/chef/lib/chef/provider/package/freebsd.rb b/chef/lib/chef/provider/package/freebsd.rb index bfe67c8b73..18dd38107c 100644 --- a/chef/lib/chef/provider/package/freebsd.rb +++ b/chef/lib/chef/provider/package/freebsd.rb @@ -7,9 +7,9 @@ # 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. @@ -24,8 +24,13 @@ require 'chef/resource/package' class Chef class Provider class Package - class Freebsd < Chef::Provider::Package - + class Freebsd < Chef::Provider::Package + + def initialize(*args) + super + @current_resource = Chef::Resource::Package.new(@new_resource.name) + end + def current_installed_version command = "pkg_info -E \"#{package_name}*\"" status = popen4(command) do |pid, stdin, stdout, stderr| @@ -41,7 +46,7 @@ class Chef end nil end - + def port_path case @new_resource.package_name # When the package name starts with a '/' treat it as the full path to the ports directory @@ -61,10 +66,10 @@ class Chef end end end - raise Chef::Exception::Package, "Could not find port with the name #{@new_resource.package_name}" - end + raise Chef::Exceptions::Package, "Could not find port with the name #{@new_resource.package_name}" + end end - + def ports_makefile_variable_value(variable) command = "cd #{port_path}; make -V #{variable}" status = popen4(command) do |pid, stdin, stdout, stderr| @@ -75,34 +80,33 @@ class Chef end nil end - + def ports_candidate_version ports_makefile_variable_value("PORTVERSION") end - + def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - + @current_resource.version(current_installed_version) Chef::Log.debug("Current version is #{@current_resource.version}") if @current_resource.version - + @candidate_version = ports_candidate_version Chef::Log.debug("Ports candidate version is #{@candidate_version}") if @candidate_version - + @current_resource end - + def latest_link_name ports_makefile_variable_value("LATEST_LINK") end - + # The name of the package (without the version number) as understood by pkg_add and pkg_info def package_name if ports_makefile_variable_value("PKGNAME") =~ /^(.+)-[^-]+$/ $1 else - raise Chef::Exception::Package, "Unexpected form for PKGNAME variable in #{port_path}/Makefile" + raise Chef::Exceptions::Package, "Unexpected form for PKGNAME variable in #{port_path}/Makefile" end end @@ -134,7 +138,7 @@ class Chef end end end - + def remove_package(name, version) # a version is mandatory if version diff --git a/chef/lib/chef/provider/package/rubygems.rb b/chef/lib/chef/provider/package/rubygems.rb index c6392d91a3..d9c80f2e33 100644 --- a/chef/lib/chef/provider/package/rubygems.rb +++ b/chef/lib/chef/provider/package/rubygems.rb @@ -27,9 +27,8 @@ class Chef def gem_list_parse(line) installed_versions = Array.new - if line.match("^#{@new_resource.package_name} \\((.+?)\\)$") - installed_versions = $1.split(/, /) - installed_versions + if md = line.match(/^#{@new_resource.package_name} \((.+?)(?: [^\)\.]+)?\)$/) + md.captures.first.split(/, /) else nil end @@ -47,9 +46,8 @@ class Chef # First, we need to look up whether we have the local gem installed or not status = popen4("#{gem_binary_path} list --local #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - installed_versions = gem_list_parse(line) - next unless installed_versions + stdout.each_line do |line| + next unless installed_versions = gem_list_parse(line) # If the version we are asking for is installed, make that our current # version. Otherwise, go ahead and use the highest one, which # happens to come first in the array. @@ -75,9 +73,8 @@ class Chef return @candidate_version if @candidate_version status = popen4("#{gem_binary_path} list --remote #{@new_resource.package_name}#{' --source=' + @new_resource.source if @new_resource.source}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - installed_versions = gem_list_parse(line) - next unless installed_versions + stdout.each_line do |line| + next unless installed_versions = gem_list_parse(line) Chef::Log.debug("candidate_version: remote rubygem(s) available: #{installed_versions.inspect}") unless installed_versions.empty? @@ -97,7 +94,7 @@ class Chef def install_package(name, version) src = nil if @new_resource.source - src = " --source=#{@new_resource.source} --source=http://gems.rubyforge.org" + src = " --source=#{@new_resource.source} --source=http://rubygems.org" end run_command_with_systems_locale( :command => "#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}" diff --git a/chef/lib/chef/provider/remote_directory.rb b/chef/lib/chef/provider/remote_directory.rb index b5b65367fa..2816e54d51 100644 --- a/chef/lib/chef/provider/remote_directory.rb +++ b/chef/lib/chef/provider/remote_directory.rb @@ -26,6 +26,7 @@ require 'chef/platform' require 'uri' require 'tempfile' require 'net/https' +require 'set' class Chef class Provider @@ -47,8 +48,22 @@ class Chef Chef::Log.debug("Doing a remote recursive directory transfer for #{@new_resource}") end + existing_files = Set.new Dir[::File.join(@new_resource.path, '**', '*')] + files_to_transfer.each do |remote_file_source| fetch_remote_file(remote_file_source) + existing_files.delete(::File.join(@new_resource.path, remote_file_source)) + end + if @new_resource.purge + existing_files.sort { |a,b| b <=> a }.each do |f| + if ::File.directory?(f) + Chef::Log.debug("Removing directory #{f}") + Dir::rmdir(f) + else + Chef::Log.debug("Deleting file #{f}") + ::File.delete(f) + end + end end end diff --git a/chef/lib/chef/provider/remote_file.rb b/chef/lib/chef/provider/remote_file.rb index 247549ed95..c8904c2941 100644 --- a/chef/lib/chef/provider/remote_file.rb +++ b/chef/lib/chef/provider/remote_file.rb @@ -45,42 +45,20 @@ class Chef def do_remote_file(source, path) retval = true - if(@new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{@new_resource.checksum}/) - Chef::Log.debug("File #{@new_resource} checksum matches, not updating") + if current_resource_matches_target_checksum? + Chef::Log.debug("File #{@new_resource} checksum matches target checksum (#{@new_resource.checksum}), not updating") else begin - # The remote filehandle - raw_file = get_from_uri(source) || - get_from_server(source, @current_resource.checksum) || - get_from_local_cookbook(source) - - # If the file exists - Chef::Log.debug "#{@new_resource}: Checking for file existence of #{@new_resource.path}" - if ::File.exists?(@new_resource.path) - # And it matches the checksum of the raw file - @new_resource.checksum(self.checksum(raw_file.path)) - Chef::Log.debug "#{@new_resource}: File exists at #{@new_resource.path}" - Chef::Log.debug "#{@new_resource}: Target checksum: #{@current_resource.checksum}" - Chef::Log.debug "#{@new_resource}: Source checksum: #{@new_resource.checksum}" - if @new_resource.checksum != @current_resource.checksum - # Updating target file, let's perform a backup! - Chef::Log.debug "#{@new_resource}: checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}" - Chef::Log.info "#{@new_resource}: Updating #{@new_resource.path}" - backup @new_resource.path + source_file(source, @current_resource.checksum) do |raw_file| + if matches_current_checksum?(raw_file) + Chef::Log.debug "#{@new_resource}: Target and Source checksums are the same, taking no action" + else + backup_new_resource + Chef::Log.debug "copying remote file from origin #{raw_file.path} to destination #{@new_resource.path}" FileUtils.cp raw_file.path, @new_resource.path @new_resource.updated = true - else - Chef::Log.debug "#{@new_resource}: Target and Source checksums are the same, taking no action" end - else - # We're creating a new file - Chef::Log.info "#{@new_resource}: Creating #{@new_resource.path}" - FileUtils.cp raw_file.path, @new_resource.path - @new_resource.updated = true end - - # We're done with the file, so make sure to close it if it was open. - raw_file.close unless raw_file.closed? rescue Net::HTTPRetriableError => e if e.response.kind_of?(Net::HTTPNotModified) Chef::Log.debug("File #{path} is unchanged") @@ -89,38 +67,78 @@ class Chef raise e end end + + Chef::Log.debug "#{@new_resource} completed" + retval end - + enforce_ownership_and_permissions + + retval + end + + def enforce_ownership_and_permissions set_owner if @new_resource.owner set_group if @new_resource.group set_mode if @new_resource.mode - retval end - def get_from_uri(source) - begin - uri = URI.parse(source) - if uri.absolute - r = Chef::REST.new(source, nil, nil) - Chef::Log.debug("Downloading from absolute URI: #{source}") - r.get_rest(source, true).open - end - rescue URI::InvalidURIError - nil + def current_resource_matches_target_checksum? + @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{@new_resource.checksum}/ + end + + def matches_current_checksum?(candidate_file) + Chef::Log.debug "#{@new_resource}: Checking for file existence of #{@new_resource.path}" + if ::File.exists?(@new_resource.path) + Chef::Log.debug "#{@new_resource}: File exists at #{@new_resource.path}" + @new_resource.checksum(checksum(candidate_file.path)) + Chef::Log.debug "#{@new_resource}: Target checksum: #{@current_resource.checksum}" + Chef::Log.debug "#{@new_resource}: Source checksum: #{@new_resource.checksum}" + @new_resource.checksum == @current_resource.checksum + else + Chef::Log.info "#{@new_resource}: Creating #{@new_resource.path}" + false + end + end + + def backup_new_resource + if ::File.exists?(@new_resource.path) + Chef::Log.debug "#{@new_resource}: checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}" + Chef::Log.info "#{@new_resource}: Updating #{@new_resource.path}" + backup @new_resource.path end end - 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 + def source_file(source, current_checksum, &block) + if absolute_uri?(source) + fetch_from_uri(source, &block) + elsif !Chef::Config[:solo] + fetch_from_chef_server(source, current_checksum, &block) + else + fetch_from_local_cookbook(source, &block) end end - def get_from_local_cookbook(source) + private + + def absolute_uri?(source) + URI.parse(source).absolute? + rescue URI::InvalidURIError + false + end + + def fetch_from_uri(source) + Chef::Log.debug("Downloading from absolute URI: #{source}") + Chef::REST.new(source, nil, nil).fetch(source) { |tmp_file| yield tmp_file } + end + + def fetch_from_chef_server(source, current_checksum) + url = generate_url(source, "files", :checksum => current_checksum) + Chef::Log.debug("Downloading #{@new_resource} from server: #{url}") + Chef::REST.new(Chef::Config[:remotefile_url]).fetch(url) { |tmp_file| yield tmp_file } + end + + def fetch_from_local_cookbook(source) if Chef::Config[:solo] cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name filename = find_preferred_file( @@ -132,7 +150,12 @@ class Chef @node[:platform_version] ) Chef::Log.debug("Using local file for remote_file:#{filename}") - ::File.open(filename) + begin + file = ::File.open(filename) + yield file + ensure + file.close + end end end diff --git a/chef/lib/chef/provider/script.rb b/chef/lib/chef/provider/script.rb index 6fd4a367a0..8a7bbb69f5 100644 --- a/chef/lib/chef/provider/script.rb +++ b/chef/lib/chef/provider/script.rb @@ -6,9 +6,9 @@ # 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. @@ -22,21 +22,23 @@ require 'chef/provider/execute' class Chef class Provider class Script < Chef::Provider::Execute - - def action_run + + def action_run tf = Tempfile.new("chef-script") tf.puts(@new_resource.code) tf.close - + fr = Chef::Resource::File.new(tf.path, nil, @node) fr.owner(@new_resource.user) fr.group(@new_resource.group) fr.run_action(:create) - - @new_resource.command("#{@new_resource.interpreter} #{tf.path}") + + @new_resource.command("#{@new_resource.interpreter} #{tf.path}") super + ensure + tf && tf.close! end - + end end end
\ No newline at end of file diff --git a/chef/lib/chef/provider/service/windows.rb b/chef/lib/chef/provider/service/windows.rb new file mode 100644 index 0000000000..69920c08eb --- /dev/null +++ b/chef/lib/chef/provider/service/windows.rb @@ -0,0 +1,129 @@ +# +# Author:: Nuo Yan <nuo@opscode.com> +# Copyright:: Copyright (c) 2010 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 'chef/provider/service/init' + +class Chef::Provider::Service::Windows < Chef::Provider::Service::Init + + def initialize(node, new_resource, collection=nil, definitions=nil, cookbook_loader=nil) + super(node, new_resource, collection, definitions, cookbook_loader) + @init_command = "sc" + end + + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + status = IO.popen("#{@init_command} query #{@new_resource.service_name}").entries + raise Chef::Exceptions::Exec, "Service #{@new_resource.service_name} does not exist.\n#{status.join}\n" if status[0].include?("FAILED 1060") + + begin + started = status[3].include?("4") + @current_resource.running started + + start_type = IO.popen("#{@init_command} qc #{@new_resource.service_name}").entries[4] + @current_resource.enabled(start_type.include?('2') || start_type.include?('3') ? true : false) + + Chef::Log.debug "#{@new_resource}: running: #{@current_resource.running}" + rescue StandardError + raise Chef::Exceptions::Exec + rescue Chef::Exceptions::Exec + Chef::Log.debug "Failed to determine the current status of the service, assuming it is not running" + @current_resource.running false + nil + end + @current_resource + end + + def start_service + begin + result = if @new_resource.start_command + Chef::Log.debug "starting service using the given start_command" + IO.popen(@new_resource.start_command).readlines + else + IO.popen("#{@init_command} start #{@new_resource.service_name}").readlines + end + Chef::Log.debug result.join + result[3].include?('4') || result.include?('2') ? true : false + rescue + Chef::Log.debug "Failed to start service #{@new_resource.service_name}" + false + end + end + + def stop_service + begin + Chef::Log.debug "stopping service using the given stop_command" + result = if @new_resource.stop_command + IO.popen(@new_resource.stop_command).readlines + else + IO.popen("#{@init_command} stop #{@new_resource.service_name}").readlines + end + Chef::Log.debug result.join + result[3].include?('1') + rescue + Chef::Log.debug "Failed to stop service #{@new_resource.service_name}" + false + end + end + + def restart_service + begin + if @new_resource.restart_command + Chef::Log.debug "restarting service using the given restart_command" + result = IO.popen(@new_resource.restart_command).readlines + Chef::Log.debug result.join + else + Chef::Log.debug IO.popen("#{@init_command} stop #{@new_resource.service_name}").readlines.join + sleep 1 + result = IO.popen("#{@init_command} start #{@new_resource.service_name}").readlines + Chef::Log.debug result.join + end + result[3].include?('4') || result.include?('2') + rescue + Chef::Log.debug "Failed to restart service #{@new_resource.service_name}" + false + end + end + + def enable_service() + begin + Chef::Log.debug result = IO.popen("#{@init_command} config #{@new_resource.service_name} start= #{determine_startup_type}").readlines.join + result.include?('SUCCESS') + rescue + Chef::Log.debug "Failed to enable service #{@new_resource.service_name}" + false + end + end + + def disable_service() + begin + Chef::Log.debug result = IO.popen("#{@init_command} config #{@new_resource.service_name} start= disabled").readlines.join + result.include?('SUCCESS') + rescue + Chef::Log.debug "Failed to disable service #{@new_resource.service_name}" + false + end + end + + private + + def determine_startup_type + {:automatic => 'auto', :mannual => 'demand'}[@new_resource.startup_type] + end + +end
\ No newline at end of file diff --git a/chef/lib/chef/provider/subversion.rb b/chef/lib/chef/provider/subversion.rb index 826dd9b35d..c58805e4d3 100644 --- a/chef/lib/chef/provider/subversion.rb +++ b/chef/lib/chef/provider/subversion.rb @@ -86,7 +86,7 @@ class Chef if @new_resource.revision =~ /^\d+$/ @new_resource.revision else - command = scm(:info, @new_resource.repository, authentication, "-r#{@new_resource.revision}") + command = scm(:info, @new_resource.repository, @new_resource.svn_info_args, authentication, "-r#{@new_resource.revision}") status, svn_info, error_message = output_of_command(command, run_options) handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}") extract_revision_info(svn_info) diff --git a/chef/lib/chef/provider/template.rb b/chef/lib/chef/provider/template.rb index 89a51718a8..47a5ed6ca7 100644 --- a/chef/lib/chef/provider/template.rb +++ b/chef/lib/chef/provider/template.rb @@ -6,9 +6,9 @@ # 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. @@ -27,63 +27,64 @@ require 'tempfile' class Chef class Provider - + class Template < Chef::Provider::File - + include Chef::Mixin::Checksum include Chef::Mixin::Template include Chef::Mixin::FindPreferredFile - + def action_create raw_template_file = nil - + Chef::Log.debug("looking for template #{@new_resource.source} in cookbook #{cookbook_name.inspect}") - + cache_file_name = "cookbooks/#{cookbook_name}/templates/default/#{@new_resource.source}" template_cache_name = "#{cookbook_name}_#{@new_resource.source}" - + if @new_resource.local cache_file_name = @new_resource.source elsif Chef::Config[:solo] cache_file_name = solo_cache_file_name else raw_template_file = fetch_template_via_rest(cache_file_name, template_cache_name) - end - + end + if template_updated? Chef::Log.debug("Updating template for #{@new_resource} in the cache") Chef::FileCache.move_to(raw_template_file.path, cache_file_name) end - template_file = render_with_context(cache_file_name) + render_with_context(cache_file_name) do |template_file| - update = false - - if ::File.exists?(@new_resource.path) - @new_resource.checksum(self.checksum(template_file.path)) - if @new_resource.checksum != @current_resource.checksum - Chef::Log.debug("#{@new_resource} changed from #{@current_resource.checksum} to #{@new_resource.checksum}") - Chef::Log.info("Updating #{@new_resource} at #{@new_resource.path}") + update = false + + if ::File.exists?(@new_resource.path) + @new_resource.checksum(checksum(template_file.path)) + if @new_resource.checksum != @current_resource.checksum + Chef::Log.debug("#{@new_resource} changed from #{@current_resource.checksum} to #{@new_resource.checksum}") + Chef::Log.info("Updating #{@new_resource} at #{@new_resource.path}") + update = true + end + else + Chef::Log.info("Creating #{@new_resource} at #{@new_resource.path}") update = true end - else - Chef::Log.info("Creating #{@new_resource} at #{@new_resource.path}") - update = true - end - - if update - backup - FileUtils.cp(template_file.path, @new_resource.path) - @new_resource.updated = true - else - Chef::Log.debug("#{@new_resource} is unchanged") + + if update + backup + FileUtils.mv(template_file.path, @new_resource.path) + @new_resource.updated = true + else + Chef::Log.debug("#{@new_resource} is unchanged") + end end - + set_owner if @new_resource.owner != nil set_group if @new_resource.group != nil set_mode if @new_resource.mode != nil end - + def action_create_if_missing if ::File.exists?(@new_resource.path) Chef::Log.debug("Template #{@new_resource} exists, taking no action.") @@ -91,32 +92,32 @@ class Chef action_create end end - + private - + def template_updated @template_updated = true end - + def template_not_updated @template_updated = false end - + def template_updated? @template_updated end - + def cookbook_name @cookbook_name = (@new_resource.cookbook || @new_resource.cookbook_name) end - - def render_with_context(cache_file_name) + + def render_with_context(cache_file_name, &block) context = {} context.merge!(@new_resource.variables) context[:node] = @node - render_template(Chef::FileCache.load(cache_file_name), context) + render_template(Chef::FileCache.load(cache_file_name), context, &block) end - + def solo_cache_file_name filename = find_preferred_file( cookbook_name, @@ -129,32 +130,32 @@ class Chef Chef::Log.debug("Using local file for template:#{filename}") Pathname.new(filename).relative_path_from(Pathname.new(Chef::Config[:file_cache_path])).to_s end - + def fetch_template_via_rest(cache_file_name, template_cache_name) if @node.run_state[:template_cache].has_key?(template_cache_name) Chef::Log.debug("I have already fetched the template for #{@new_resource} once this run, not checking again.") template_not_updated return false end - + r = Chef::REST.new(Chef::Config[:template_url]) - + current_checksum = nil - + if Chef::FileCache.has_key?(cache_file_name) current_checksum = self.checksum(Chef::FileCache.load(cache_file_name, false)) else Chef::Log.debug("Template #{@new_resource} is not in the template cache") end - + template_url = generate_url( - @new_resource.source, + @new_resource.source, "templates", { :checksum => current_checksum } ) - + begin raw_template_file = r.get_rest(template_url, true) template_updated @@ -165,13 +166,13 @@ class Chef raise e end end - + # We have checked the cache for this template this run @node.run_state[:template_cache][template_cache_name] = true - + raw_template_file end - + end end end diff --git a/chef/lib/chef/providers.rb b/chef/lib/chef/providers.rb new file mode 100644 index 0000000000..02168b2dcd --- /dev/null +++ b/chef/lib/chef/providers.rb @@ -0,0 +1,80 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 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 'chef/provider/breakpoint' +require 'chef/provider/cron' +require 'chef/provider/deploy' +require 'chef/provider/directory' +require 'chef/provider/erl_call' +require 'chef/provider/execute' +require 'chef/provider/file' +require 'chef/provider/git' +require 'chef/provider/group' +require 'chef/provider/http_request' +require 'chef/provider/ifconfig' +require 'chef/provider/link' +require 'chef/provider/log' +require 'chef/provider/mdadm' +require 'chef/provider/mount' +require 'chef/provider/package' +require 'chef/provider/remote_directory' +require 'chef/provider/remote_file' +require 'chef/provider/route' +require 'chef/provider/ruby_block' +require 'chef/provider/script' +require 'chef/provider/service' +require 'chef/provider/subversion' +require 'chef/provider/template' +require 'chef/provider/user' + +require 'chef/provider/package/apt' +require 'chef/provider/package/dpkg' +require 'chef/provider/package/easy_install' +require 'chef/provider/package/freebsd' +require 'chef/provider/package/macports' +require 'chef/provider/package/pacman' +require 'chef/provider/package/portage' +require 'chef/provider/package/rpm' +require 'chef/provider/package/rubygems' +require 'chef/provider/package/yum' +require 'chef/provider/package/zypper' + +require 'chef/provider/service/arch' +require 'chef/provider/service/debian' +require 'chef/provider/service/freebsd' +require 'chef/provider/service/gentoo' +require 'chef/provider/service/init' +require 'chef/provider/service/redhat' +require 'chef/provider/service/simple' +require 'chef/provider/service/upstart' +require 'chef/provider/service/windows' + +require 'chef/provider/user/dscl' +require 'chef/provider/user/pw' +require 'chef/provider/user/useradd' + +require 'chef/provider/group/dscl' +require 'chef/provider/group/gpasswd' +require 'chef/provider/group/groupadd' +require 'chef/provider/group/pw' +require 'chef/provider/group/usermod' + +require 'chef/provider/mount/mount' + +require 'chef/provider/deploy/revision' +require 'chef/provider/deploy/timestamped' diff --git a/chef/lib/chef/recipe.rb b/chef/lib/chef/recipe.rb index d9a2362889..da6d0c0a05 100644 --- a/chef/lib/chef/recipe.rb +++ b/chef/lib/chef/recipe.rb @@ -17,15 +17,13 @@ # limitations under the License. # -require 'chef/resource' -Dir[File.join(File.dirname(__FILE__), 'resource/**/*.rb')].sort.each { |lib| require lib } + +require 'chef/mixin/recipe_definition_dsl_core' require 'chef/mixin/from_file' require 'chef/mixin/language' require 'chef/mixin/language_include_recipe' -require 'chef/mixin/recipe_definition_dsl_core' require 'chef/resource_collection' require 'chef/cookbook_loader' -require 'chef/rest' class Chef class Recipe diff --git a/chef/lib/chef/resource.rb b/chef/lib/chef/resource.rb index 89f4513576..fc95abd595 100644 --- a/chef/lib/chef/resource.rb +++ b/chef/lib/chef/resource.rb @@ -80,7 +80,7 @@ class Chef prior_resource = @collection.lookup(self.to_s) Chef::Log.debug("Setting #{self.to_s} to the state of the prior #{self.to_s}") prior_resource.instance_variables.each do |iv| - unless iv == "@source_line" || iv == "@action" + unless iv.to_sym == :@source_line || iv.to_sym == :@action self.instance_variable_set(iv, prior_resource.instance_variable_get(iv)) end end @@ -162,10 +162,18 @@ class Chef if args.size > 1 notifies_helper(*args) else - resources_array = *args - resources_array.each do |resource| - resource.each do |key, value| - notifies_helper(value[0], key, value[1]) + # This syntax is so weird. surely people will just give us one hash? + notifications = args.flatten + notifications.each do |resources_notifications| + begin + resources_notifications.each do |resource, notification| + Chef::Log.error "resource KV: `#{resource.inspect}' => `#{notification.inspect}'" + notifies_helper(notification[0], resource, notification[1]) + end + rescue NoMethodError + Chef::Log.fatal("encountered NME processing resource #{resources_notifications.inspect}") + Chef::Log.fatal("incoming args: #{args.inspect}") + raise end end end @@ -193,7 +201,11 @@ class Chef end def is(*args) - return *args + if args.size == 1 + args.first + else + return *args + end end def to_s @@ -218,7 +230,9 @@ class Chef def to_hash instance_vars = Hash.new self.instance_variables.each do |iv| - instance_vars[iv.sub(/^@/,'').to_sym] = self.instance_variable_get(iv) unless iv == "@collection" + iv = iv.to_s + next if iv == "@collection" + instance_vars[iv.sub(/^@/,'').to_sym] = self.instance_variable_get(iv) end instance_vars end diff --git a/chef/lib/chef/resource/cron.rb b/chef/lib/chef/resource/cron.rb index 2da76b62f8..206bf2c540 100644 --- a/chef/lib/chef/resource/cron.rb +++ b/chef/lib/chef/resource/cron.rb @@ -47,7 +47,7 @@ class Chef converted_arg = arg end begin - if Integer(arg) > 59 then raise RangeError end + if integerize(arg) > 59 then raise RangeError end rescue ArgumentError end set_or_return( @@ -64,7 +64,7 @@ class Chef converted_arg = arg end begin - if Integer(arg) > 23 then raise RangeError end + if integerize(arg) > 23 then raise RangeError end rescue ArgumentError end set_or_return( @@ -81,7 +81,7 @@ class Chef converted_arg = arg end begin - if Integer(arg) > 31 then raise RangeError end + if integerize(arg) > 31 then raise RangeError end rescue ArgumentError end set_or_return( @@ -98,7 +98,7 @@ class Chef converted_arg = arg end begin - if Integer(arg) > 12 then raise RangeError end + if integerize(arg) > 12 then raise RangeError end rescue ArgumentError end set_or_return( @@ -115,7 +115,7 @@ class Chef converted_arg = arg end begin - if Integer(arg) > 7 then raise RangeError end + if integerize(arg) > 7 then raise RangeError end rescue ArgumentError end set_or_return( @@ -172,6 +172,15 @@ class Chef :kind_of => String ) end + + private + + # On Ruby 1.8, Kernel#Integer will happily do this for you. On 1.9, no. + def integerize(integerish) + Integer(integerish) + rescue TypeError + 0 + end end end end diff --git a/chef/lib/chef/resource/deploy.rb b/chef/lib/chef/resource/deploy.rb index 327f9425c8..a513b16411 100644 --- a/chef/lib/chef/resource/deploy.rb +++ b/chef/lib/chef/resource/deploy.rb @@ -6,9 +6,9 @@ # 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. @@ -38,20 +38,20 @@ require "chef/resource/scm" class Chef class Resource - + # Deploy: Deploy apps from a source control repository. # # Callbacks: # Callbacks can be a block or a string. If given a block, the code # is evaluated as an embedded recipe, and run at the specified # point in the deploy process. If given a string, the string is taken as - # a path to a callback file/recipe. Paths are evaluated relative to the + # a path to a callback file/recipe. Paths are evaluated relative to the # release directory. Callback files can contain chef code (resources, etc.) # class Deploy < Chef::Resource - + provider_base Chef::Provider::Deploy - + def initialize(name, collection=nil, node=nil) super(name, collection, node) @resource_name = :deploy @@ -74,26 +74,26 @@ class Chef @provider = Chef::Provider::Deploy::Timestamped @allowed_actions.push(:force_deploy, :deploy, :rollback) end - + # where the checked out/cloned code goes def destination @destination ||= shared_path + "/#{@repository_cache}" end - + # where shared stuff goes, i.e., logs, tmp, etc. goes here def shared_path @shared_path ||= @deploy_to + "/shared" end - + # where the deployed version of your code goes def current_path @current_path ||= @deploy_to + "/current" end - + def depth @shallow_clone ? "5" : nil end - + # note: deploy_to is your application "meta-root." def deploy_to(arg=nil) set_or_return( @@ -102,7 +102,7 @@ class Chef :kind_of => [ String ] ) end - + def repo(arg=nil) set_or_return( :repo, @@ -111,7 +111,7 @@ class Chef ) end alias :repository :repo - + def remote(arg=nil) set_or_return( :remote, @@ -119,7 +119,7 @@ class Chef :kind_of => [ String ] ) end - + def role(arg=nil) set_or_return( :role, @@ -127,7 +127,7 @@ class Chef :kind_of => [ String ] ) end - + def restart_command(arg=nil, &block) arg ||= block set_or_return( @@ -137,7 +137,7 @@ class Chef ) end alias :restart :restart_command - + def migrate(arg=nil) set_or_return( :migrate, @@ -145,7 +145,7 @@ class Chef :kind_of => [ TrueClass, FalseClass ] ) end - + def migration_command(arg=nil) set_or_return( :migration_command, @@ -153,7 +153,7 @@ class Chef :kind_of => [ String ] ) end - + def user(arg=nil) set_or_return( :user, @@ -161,15 +161,15 @@ class Chef :kind_of => [ String ] ) end - + def group(arg=nil) set_or_return( :group, arg, :kind_of => [ String ] ) - end - + end + def enable_submodules(arg=nil) set_or_return( :enable_submodules, @@ -177,7 +177,7 @@ class Chef :kind_of => [ TrueClass, FalseClass ] ) end - + def shallow_clone(arg=nil) set_or_return( :shallow_clone, @@ -193,7 +193,7 @@ class Chef :kind_of => [ String ] ) end - + def copy_exclude(arg=nil) set_or_return( :copy_exclude, @@ -201,7 +201,7 @@ class Chef :kind_of => [ String ] ) end - + def revision(arg=nil) set_or_return( :revision, @@ -210,7 +210,7 @@ class Chef ) end alias :branch :revision - + def git_ssh_wrapper(arg=nil) set_or_return( :git_ssh_wrapper, @@ -219,7 +219,7 @@ class Chef ) end alias :ssh_wrapper :git_ssh_wrapper - + def svn_username(arg=nil) set_or_return( :svn_username, @@ -227,7 +227,7 @@ class Chef :kind_of => [ String ] ) end - + def svn_password(arg=nil) set_or_return( :svn_password, @@ -235,7 +235,7 @@ class Chef :kind_of => [ String ] ) end - + def svn_arguments(arg=nil) set_or_return( :svn_arguments, @@ -243,7 +243,14 @@ class Chef :kind_of => [ String ] ) end - + + def svn_info_args(arg=nil) + set_or_return( + :svn_arguments, + arg, + :kind_of => [ String ]) + end + def scm_provider(arg=nil) klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) lookup_provider_constant(arg) @@ -256,7 +263,7 @@ class Chef :kind_of => [ Class ] ) end - + def svn_force_export(arg=nil) set_or_return( :svn_force_export, @@ -264,7 +271,7 @@ class Chef :kind_of => [ TrueClass, FalseClass ] ) end - + def environment(arg=nil) if arg.is_a?(String) Chef::Log.info "Setting RAILS_ENV, RACK_ENV, and MERB_ENV to `#{arg}'" @@ -277,8 +284,8 @@ class Chef :kind_of => [ Hash ] ) end - - # An array of paths, relative to your app's root, to be purged from a + + # An array of paths, relative to your app's root, to be purged from a # SCM clone/checkout before symlinking. Use this to get rid of files and # directories you want to be shared between releases. # Default: ["log", "tmp/pids", "public/system"] @@ -289,12 +296,12 @@ class Chef :kind_of => Array ) end - + # An array of paths, relative to your app's root, where you expect dirs to # exist before symlinking. This runs after #purge_before_symlink, so you # can use this to recreate dirs that you had previously purged. - # For example, if you plan to use a shared directory for pids, and you - # want it to be located in $APP_ROOT/tmp/pids, you could purge tmp, + # For example, if you plan to use a shared directory for pids, and you + # want it to be located in $APP_ROOT/tmp/pids, you could purge tmp, # then specify tmp here so that the tmp directory will exist when you # symlink the pids directory in to the current release. # Default: ["tmp", "public", "config"] @@ -305,10 +312,10 @@ class Chef :kind_of => Array ) end - + # A Hash of shared/dir/path => release/dir/path. This attribute determines # which files and dirs in the shared directory get symlinked to the current - # release directory, and where they go. If you have a directory + # release directory, and where they go. If you have a directory # $shared/pids that you would like to symlink as $current_release/tmp/pids # you specify it as "pids" => "tmp/pids" # Default {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"} @@ -319,8 +326,8 @@ class Chef :kind_of => Hash ) end - - # A Hash of shared/dir/path => release/dir/path. This attribute determines + + # A Hash of shared/dir/path => release/dir/path. This attribute determines # which files in the shared directory get symlinked to the current release # directory and where they go. Unlike map_shared_files, these are symlinked # *before* any migration is run. @@ -334,31 +341,31 @@ class Chef :kind_of => Hash ) end - + # Callback fires before migration is run. def before_migrate(arg=nil, &block) arg ||= block set_or_return(:before_migrate, arg, :kind_of => [Proc, String]) end - + # Callback fires before symlinking def before_symlink(arg=nil, &block) arg ||= block set_or_return(:before_symlink, arg, :kind_of => [Proc, String]) end - + # Callback fires before restart def before_restart(arg=nil, &block) arg ||= block set_or_return(:before_restart, arg, :kind_of => [Proc, String]) end - + # Callback fires after restart def after_restart(arg=nil, &block) arg ||= block set_or_return(:after_restart, arg, :kind_of => [Proc, String]) end - + end end end diff --git a/chef/lib/chef/resource/execute.rb b/chef/lib/chef/resource/execute.rb index 3f2ba07547..46b7287d6a 100644 --- a/chef/lib/chef/resource/execute.rb +++ b/chef/lib/chef/resource/execute.rb @@ -100,7 +100,7 @@ class Chef set_or_return( :returns, arg, - :kind_of => [ Integer ] + :kind_of => [ Integer, Array ] ) end diff --git a/chef/lib/chef/resource/mount.rb b/chef/lib/chef/resource/mount.rb index affedf333c..56dcfedee9 100644 --- a/chef/lib/chef/resource/mount.rb +++ b/chef/lib/chef/resource/mount.rb @@ -28,7 +28,7 @@ class Chef @mount_point = name @device = nil @device_type = :device - @fstype = nil + @fstype = "auto" @options = ["defaults"] @dump = 0 @pass = 2 diff --git a/chef/lib/chef/resource/remote_directory.rb b/chef/lib/chef/resource/remote_directory.rb index 9742df5540..1920c086de 100644 --- a/chef/lib/chef/resource/remote_directory.rb +++ b/chef/lib/chef/resource/remote_directory.rb @@ -6,9 +6,9 @@ # 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. @@ -21,7 +21,7 @@ require 'chef/resource/directory' class Chef class Resource class RemoteDirectory < Chef::Resource::Directory - + def initialize(name, collection=nil, node=nil) super(name, collection, node) @resource_name = :remote_directory @@ -30,6 +30,7 @@ class Chef @delete = false @action = :create @recursive = true + @purge = false @files_backup = 5 @files_owner = nil @files_group = nil @@ -37,7 +38,7 @@ class Chef @allowed_actions.push(:create, :delete) @cookbook = nil end - + def source(args=nil) set_or_return( :source, @@ -45,7 +46,7 @@ class Chef :kind_of => String ) end - + def files_backup(arg=nil) set_or_return( :files_backup, @@ -53,7 +54,15 @@ class Chef :kind_of => [ Integer, FalseClass ] ) end - + + def purge(arg=nil) + set_or_return( + :purge, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + def files_group(arg=nil) set_or_return( :files_group, @@ -61,7 +70,7 @@ class Chef :regex => [ /^([a-z]|[A-Z]|[0-9]|_|-)+$/, /^\d+$/ ] ) end - + def files_mode(arg=nil) set_or_return( :files_mode, @@ -69,7 +78,7 @@ class Chef :regex => /^\d{3,4}$/ ) end - + def files_owner(arg=nil) set_or_return( :files_owner, @@ -77,7 +86,7 @@ class Chef :regex => [ /^([a-z]|[A-Z]|[0-9]|_|-)+$/, /^\d+$/ ] ) end - + def cookbook(args=nil) set_or_return( :cookbook, @@ -85,7 +94,7 @@ class Chef :kind_of => String ) end - + end end end diff --git a/chef/lib/chef/resource/scm.rb b/chef/lib/chef/resource/scm.rb index d444d7f6d6..215fb59d06 100644 --- a/chef/lib/chef/resource/scm.rb +++ b/chef/lib/chef/resource/scm.rb @@ -6,9 +6,9 @@ # 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. @@ -22,7 +22,7 @@ require 'chef/resource' class Chef class Resource class Scm < Chef::Resource - + def initialize(name, collection=nil, node=nil) super(name, collection, node) @destination = name @@ -34,7 +34,7 @@ class Chef @depth = nil @allowed_actions.push(:checkout, :export, :sync, :diff, :log) end - + def destination(arg=nil) set_or_return( :destination, @@ -42,7 +42,7 @@ class Chef :kind_of => String ) end - + def repository(arg=nil) set_or_return( :repository, @@ -50,7 +50,7 @@ class Chef :kind_of => String ) end - + def revision(arg=nil) set_or_return( :revision, @@ -58,7 +58,7 @@ class Chef :kind_of => String ) end - + def user(arg=nil) set_or_return( :user, @@ -66,7 +66,7 @@ class Chef :kind_of => [String, Integer] ) end - + def group(arg=nil) set_or_return( :group, @@ -74,7 +74,7 @@ class Chef :kind_of => [String, Integer] ) end - + def svn_username(arg=nil) set_or_return( :svn_username, @@ -82,7 +82,7 @@ class Chef :kind_of => String ) end - + def svn_password(arg=nil) set_or_return( :svn_password, @@ -90,7 +90,7 @@ class Chef :kind_of => String ) end - + def svn_arguments(arg=nil) set_or_return( :svn_arguments, @@ -98,7 +98,14 @@ class Chef :kind_of => String ) end - + + def svn_info_args(arg=nil) + set_or_return( + :svn_arguments, + arg, + :kind_of => String) + end + # Capistrano and git-deploy use ``shallow clone'' def depth(arg=nil) set_or_return( @@ -107,7 +114,7 @@ class Chef :kind_of => Integer ) end - + def enable_submodules(arg=nil) set_or_return( :enable_submodules, @@ -115,7 +122,7 @@ class Chef :kind_of => [TrueClass, FalseClass] ) end - + def remote(arg=nil) set_or_return( :remote, @@ -123,7 +130,7 @@ class Chef :kind_of => String ) end - + def ssh_wrapper(arg=nil) set_or_return( :ssh_wrapper, @@ -131,7 +138,7 @@ class Chef :kind_of => String ) end - + end end end
\ No newline at end of file diff --git a/chef/lib/chef/resource/service.rb b/chef/lib/chef/resource/service.rb index 1ca5ba6e71..f32d3c062b 100644 --- a/chef/lib/chef/resource/service.rb +++ b/chef/lib/chef/resource/service.rb @@ -35,6 +35,7 @@ class Chef @restart_command = nil @reload_command = nil @action = "nothing" + @startup_type = :automatic @supports = { :restart => false, :reload => false, :status => false } @allowed_actions.push(:enable, :disable, :start, :stop, :restart, :reload) end @@ -127,7 +128,15 @@ class Chef @supports end end - + + # This attribute applies for Windows only. + def startup_type(arg=nil) + set_or_return( + :startup_type, + arg, + :equal_to => [:automatic, :mannual] + ) + end end end diff --git a/chef/lib/chef/resources.rb b/chef/lib/chef/resources.rb new file mode 100644 index 0000000000..8577c1b843 --- /dev/null +++ b/chef/lib/chef/resources.rb @@ -0,0 +1,60 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 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 'chef/resource/apt_package' +require 'chef/resource/bash' +require 'chef/resource/breakpoint' +require 'chef/resource/cron' +require 'chef/resource/csh' +require 'chef/resource/deploy' +require 'chef/resource/deploy_revision' +require 'chef/resource/directory' +require 'chef/resource/dpkg_package' +require 'chef/resource/easy_install_package' +require 'chef/resource/erl_call' +require 'chef/resource/execute' +require 'chef/resource/file' +require 'chef/resource/freebsd_package' +require 'chef/resource/gem_package' +require 'chef/resource/git' +require 'chef/resource/group' +require 'chef/resource/http_request' +require 'chef/resource/ifconfig' +require 'chef/resource/link' +require 'chef/resource/log' +require 'chef/resource/macports_package' +require 'chef/resource/mdadm' +require 'chef/resource/mount' +require 'chef/resource/package' +require 'chef/resource/pacman_package' +require 'chef/resource/perl' +require 'chef/resource/portage_package' +require 'chef/resource/python' +require 'chef/resource/remote_directory' +require 'chef/resource/remote_file' +require 'chef/resource/route' +require 'chef/resource/ruby' +require 'chef/resource/ruby_block' +require 'chef/resource/scm' +require 'chef/resource/script' +require 'chef/resource/service' +require 'chef/resource/subversion' +require 'chef/resource/template' +require 'chef/resource/timestamped_deploy' +require 'chef/resource/user' +require 'chef/resource/yum_package' diff --git a/chef/lib/chef/rest.rb b/chef/lib/chef/rest.rb index 9e214a1ddb..d8723ae71a 100644 --- a/chef/lib/chef/rest.rb +++ b/chef/lib/chef/rest.rb @@ -10,9 +10,9 @@ # 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. @@ -20,67 +20,58 @@ # limitations under the License. # -require 'chef/mixin/params_validate' require 'net/https' require 'uri' require 'json' require 'tempfile' -require 'singleton' -require 'mixlib/authentication/signedheaderauth' require 'chef/api_client' - -include Mixlib::Authentication::SignedHeaderAuth +require 'chef/rest/auth_credentials' +require 'chef/rest/rest_request' class Chef class REST + attr_reader :auth_credentials + attr_accessor :url, :cookies, :sign_on_redirect, :redirect_limit - class CookieJar < Hash - include Singleton - end - - attr_accessor :url, :cookies, :client_name, :signing_key, :signing_key_filename, :sign_on_redirect, :sign_request - def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={}) @url = url @cookies = CookieJar.instance - @client_name = client_name @default_headers = options[:headers] || {} - if signing_key_filename - @signing_key_filename = signing_key_filename - @signing_key = load_signing_key(signing_key_filename) - @sign_request = true - else - @signing_key = nil - @sign_request = false - end - @sign_on_redirect = true + @auth_credentials = AuthCredentials.new(client_name, signing_key_filename) + @sign_on_redirect, @sign_request = true, true + @redirects_followed = 0 + @redirect_limit = 10 end - def load_signing_key(key) - begin - IO.read(key) - rescue StandardError=>se - Chef::Log.error "Failed to read the private key #{key}: #{se.inspect}, #{se.backtrace}" - raise Chef::Exceptions::PrivateKeyMissing, "I cannot read #{key}, which you told me to use to sign requests!" - end + def signing_key_filename + @auth_credentials.key_file + end + + def client_name + @auth_credentials.client_name + end + + def signing_key + @auth_credentials.raw_key end - - # Register the client - def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key]) - raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?" if (File.exists?(destination) && !File.writable?(destination)) + # Register the client + def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key]) + if (File.exists?(destination) && !File.writable?(destination)) + raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?" + end nc = Chef::ApiClient.new nc.name(name) catch(:done) do - retries = Chef::Config[:client_registration_retries] || 5 + retries = config[:client_registration_retries] || 5 0.upto(retries) do |n| begin response = nc.save(true, true) Chef::Log.debug("Registration response: #{response.inspect}") raise Chef::Exceptions::CannotWritePrivateKey, "The response from the server did not include a private key!" unless response.has_key?("private_key") # Write out the private key - file = File.open(destination, File::WRONLY|File::EXCL|File::CREAT, 0600) + file = ::File.open(destination, File::WRONLY|File::EXCL|File::CREAT, 0600) file.print(response["private_key"]) file.close throw :done @@ -100,27 +91,40 @@ class Chef # # === Parameters # path:: The path to GET - # raw:: Whether you want the raw body returned, or JSON inflated. Defaults + # raw:: Whether you want the raw body returned, or JSON inflated. Defaults # to JSON inflated. def get_rest(path, raw=false, headers={}) - run_request(:GET, create_url(path), headers, false, 10, raw) - end - + if raw + streaming_request(create_url(path), headers) + else + api_request(:GET, create_url(path), headers) + end + end + # Send an HTTP DELETE request to the path - def delete_rest(path, headers={}) - run_request(:DELETE, create_url(path), headers) - end - - # Send an HTTP POST request to the path + def delete_rest(path, headers={}) + api_request(:DELETE, create_url(path), headers) + end + + # Send an HTTP POST request to the path def post_rest(path, json, headers={}) - run_request(:POST, create_url(path), headers, json) - end - + api_request(:POST, create_url(path), headers, json) + end + # Send an HTTP PUT request to the path def put_rest(path, json, headers={}) - run_request(:PUT, create_url(path), headers, json) + api_request(:PUT, create_url(path), headers, json) end - + + # Streams a download to a tempfile, then yields the tempfile to a block. + # After the download, the tempfile will be closed and unlinked. + # If you rename the tempfile, it will not be deleted. + # Beware that if the server streams infinite content, this method will + # stream it until you run out of disk space. + def fetch(path, headers={}) + streaming_request(create_url(path), headers) {|tmp_file| yield tmp_file } + end + def create_url(path) if path =~ /^(http|https):\/\// URI.parse(path) @@ -128,150 +132,39 @@ class Chef URI.parse("#{@url}/#{path}") end end - - def sign_request(http_method, path, private_key, user_id, body = "", host="localhost") - #body = "" if body == false - timestamp = Time.now.utc.iso8601 - sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object( - :http_method=>http_method, - :path => path, - :body=>body, - :user_id=>user_id, - :timestamp=>timestamp) - signed = sign_obj.sign(private_key).merge({:host => host}) - signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo} + + def sign_requests? + auth_credentials.sign_requests? && @sign_request end - + # Actually run an HTTP request. First argument is the HTTP method, # which should be one of :GET, :PUT, :POST or :DELETE. Next is the # URL, then an object to include in the body (which will be converted with - # .to_json) and finally, the limit of HTTP Redirects to follow (10). + # .to_json). The limit argument is unused, it is present for backwards + # compatibility. Configure the redirect limit with #redirect_limit= + # instead. # # Typically, you won't use this method -- instead, you'll use one of # the helper methods (get_rest, post_rest, etc.) # # Will return the body of the response on success. - def run_request(method, url, headers={}, data=false, limit=10, raw=false) - - http_retry_delay = Chef::Config[:http_retry_delay] - http_retry_count = Chef::Config[:http_retry_count] - - raise ArgumentError, 'HTTP redirect too deep' if limit == 0 - - http = Net::HTTP.new(url.host, url.port) - if url.scheme == "https" - http.use_ssl = true - if Chef::Config[:ssl_verify_mode] == :verify_none - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - elsif Chef::Config[:ssl_verify_mode] == :verify_peer - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - end - if Chef::Config[:ssl_ca_path] and File.exists?(Chef::Config[:ssl_ca_path]) - http.ca_path = Chef::Config[:ssl_ca_path] - elsif Chef::Config[:ssl_ca_file] and File.exists?(Chef::Config[:ssl_ca_file]) - http.ca_file = Chef::Config[:ssl_ca_file] - end - if Chef::Config[:ssl_client_cert] && 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 = @default_headers.merge(headers) - - unless raw - headers = headers.merge({ - 'Accept' => "application/json", - }) - end + def run_request(method, url, headers={}, data=false, limit=nil, raw=false) + json_body = data ? data.to_json : nil + headers = build_headers(method, url, headers, json_body, raw) - headers['X-Chef-Version'] = ::Chef::VERSION - - if @cookies.has_key?("#{url.host}:#{url.port}") - headers['Cookie'] = @cookies["#{url.host}:#{url.port}"] - end - - json_body = data ? data.to_json : nil - - if @sign_request - raise ArgumentError, "Cannot sign the request without a client name, check that :node_name is assigned" if @client_name.nil? - Chef::Log.debug("Signing the request as #{@client_name}") - if json_body - headers.merge!(sign_request(method, url.path, OpenSSL::PKey::RSA.new(@signing_key), @client_name, json_body, "#{url.host}:#{url.port}")) - else - headers.merge!(sign_request(method, url.path, OpenSSL::PKey::RSA.new(@signing_key), @client_name, "", "#{url.host}:#{url.port}")) - end - end - - req = nil - case method - when :GET - req_path = "#{url.path}" - req_path << "?#{url.query}" if url.query - req = Net::HTTP::Get.new(req_path, headers) - when :POST - headers["Content-Type"] = 'application/json' if data - req_path = "#{url.path}" - req_path << "?#{url.query}" if url.query - req = Net::HTTP::Post.new(req_path, headers) - req.body = json_body if json_body - when :PUT - headers["Content-Type"] = 'application/json' if data - req_path = "#{url.path}" - req_path << "?#{url.query}" if url.query - req = Net::HTTP::Put.new(req_path, headers) - req.body = json_body if json_body - when :DELETE - req_path = "#{url.path}" - req_path << "?#{url.query}" if url.query - req = Net::HTTP::Delete.new(req_path, headers) - else - raise ArgumentError, "You must provide :GET, :PUT, :POST or :DELETE as the method" - end - - Chef::Log.debug("Sending HTTP Request via #{req.method} to #{url.host}:#{url.port}#{req.path}") - - # Optionally handle HTTP Basic Authentication - req.basic_auth(url.user, url.password) if url.user - - res = nil tf = nil - http_attempts = 0 - begin - http_attempts += 1 - - res = http.request(req) do |response| + retriable_rest_request(method, url, json_body, headers) do |rest_request| + + res = rest_request.call do |response| if raw - tf = Tempfile.new("chef-rest") - # Stolen from http://www.ruby-forum.com/topic/166423 - # Kudos to _why! - size, total = 0, response.header['Content-Length'].to_i - response.read_body do |chunk| - tf.write(chunk) - size += chunk.size - if size == 0 - Chef::Log.debug("#{req.path} done (0 length file)") - elsif total == 0 - Chef::Log.debug("#{req.path} (zero content length)") - else - Chef::Log.debug("#{req.path}" + " %d%% done (%d of %d)" % [(size * 100) / total, size, total]) - end - end - tf.close - tf + tf = stream_to_tempfile(url, response) else response.read_body end - response end - + if res.kind_of?(Net::HTTPSuccess) - if res['set-cookie'] - @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] - end if res['content-type'] =~ /json/ response_body = res.body.chomp JSON.parse(response_body) @@ -283,37 +176,114 @@ class Chef end end elsif res.kind_of?(Net::HTTPFound) or res.kind_of?(Net::HTTPMovedPermanently) - if res['set-cookie'] - @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] - end - @sign_request = false if @sign_on_redirect == false - run_request(:GET, create_url(res['location']), {}, false, limit - 1, raw) + follow_redirect {run_request(:GET, create_url(res['location']), {}, false, nil, raw)} else if res['content-type'] =~ /json/ exception = JSON.parse(res.body) - Chef::Log.debug("HTTP Request Returned #{res.code} #{res.message}: #{exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"]}") + msg = "HTTP Request Returned #{res.code} #{res.message}: " + msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s) + Chef::Log.warn(msg) end res.error! end - + end + end + + # Similar to #run_request but only supports JSON APIs. File Download not supported. + def api_request(method, url, headers={}, data=false) + json_body = data ? data.to_json : nil + headers = build_headers(method, url, headers, json_body) + + retriable_rest_request(method, url, json_body, headers) do |rest_request| + response = rest_request.call {|r| r.read_body} + + if response.kind_of?(Net::HTTPSuccess) + if response['content-type'] =~ /json/ + JSON.parse(response.body.chomp) + else + Chef::Log.warn("Expected JSON response, but got content-type '#{response['content-type']}'") + response.body + end + elsif redirect_location = redirected_to(response) + follow_redirect {api_request(:GET, create_url(redirect_location))} + else + if response['content-type'] =~ /json/ + exception = JSON.parse(response.body) + msg = "HTTP Request Returned #{response.code} #{response.message}: " + msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s) + Chef::Log.warn(msg) + end + response.error! + end + end + end + + # similar to #run_request but only supports streaming downloads. + # Only supports GET, doesn't speak JSON + # Streams the response body to a tempfile. If a block is given, it's + # passed to the tempfile, which means that the tempfile will automatically + # be unlinked after the block is executed. + # If no block is given, the tempfile is returned, which means it's up to + # you to unlink the tempfile when you're done with it. + def streaming_request(url, headers, &block) + headers = build_headers(:GET, url, headers, nil, true) + retriable_rest_request(:GET, url, nil, headers) do |rest_request| + tempfile = nil + response = rest_request.call do |r| + if block_given? && r.kind_of?(Net::HTTPSuccess) + begin + tempfile = stream_to_tempfile(url, r, &block) + yield tempfile + ensure + tempfile.close! + end + else + tempfile = stream_to_tempfile(url, r) + end + end + if response.kind_of?(Net::HTTPSuccess) + tempfile + elsif redirect_location = redirected_to(response) + # TODO: test tempfile unlinked when following redirects. + tempfile && tempfile.close! + follow_redirect {streaming_request(create_url(redirect_location), {}, &block)} + else + tempfile && tempfile.close! + response.error! + end + end + end + + def retriable_rest_request(method, url, req_body, headers) + rest_request = Chef::REST::RESTRequest.new(method, url, req_body, headers) + + Chef::Log.debug("Sending HTTP Request via #{method} to #{url.host}:#{url.port}#{rest_request.path}") + + http_attempts = 0 + + begin + http_attempts += 1 + + res = yield rest_request + rescue Errno::ECONNREFUSED if http_retry_count - http_attempts + 1 > 0 - Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{req.path}, retry #{http_attempts}/#{http_retry_count}") + Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end - raise Errno::ECONNREFUSED, "Connection refused connecting to #{url.host}:#{url.port} for #{req.path}, giving up" + raise Errno::ECONNREFUSED, "Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up" rescue Timeout::Error if http_retry_count - http_attempts + 1 > 0 - Chef::Log.error("Timeout connecting to #{url.host}:#{url.port} for #{req.path}, retry #{http_attempts}/#{http_retry_count}") + Chef::Log.error("Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end - raise Timeout::Error, "Timeout connecting to #{url.host}:#{url.port} for #{req.path}, giving up" + raise Timeout::Error, "Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up" rescue Net::HTTPServerException if res.kind_of?(Net::HTTPForbidden) if http_retry_count - http_attempts + 1 > 0 - Chef::Log.error("Received 403 Forbidden against #{url.host}:#{url.port} for #{req.path}, retry #{http_attempts}/#{http_retry_count}") + Chef::Log.error("Received 403 Forbidden against #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end @@ -321,6 +291,81 @@ class Chef raise end end - + + def authentication_headers(method, url, json_body=nil) + request_params = {:http_method => method, :path => url.path, :body => json_body, :host => "#{url.host}:#{url.port}"} + request_params[:body] ||= "" + auth_credentials.signature_headers(request_params) + end + + def http_retry_delay + config[:http_retry_delay] + end + + def http_retry_count + config[:http_retry_count] + end + + def config + Chef::Config + end + + def follow_redirect + raise Chef::Exceptions::RedirectLimitExceeded if @redirects_followed >= redirect_limit + @redirects_followed += 1 + Chef::Log.debug("Following redirect #{@redirects_followed}/#{redirect_limit}") + if @sign_on_redirect + yield + else + @sign_request = false + yield + end + ensure + @redirects_followed = 0 + @sign_request = true + end + + private + + def redirected_to(response) + if response.kind_of?(Net::HTTPFound) || response.kind_of?(Net::HTTPMovedPermanently) + response['location'] + else + nil + end + end + + def build_headers(method, url, headers={}, json_body=false, raw=false) + headers = @default_headers.merge(headers) + headers['Accept'] = "application/json" unless raw + headers["Content-Type"] = 'application/json' if json_body + headers.merge!(authentication_headers(method, url, json_body)) if sign_requests? + headers + end + + def stream_to_tempfile(url, response) + tf = Tempfile.open("chef-rest") + Chef::Log.debug("Streaming download from #{url.to_s} to tempfile #{tf.path}") + # Stolen from http://www.ruby-forum.com/topic/166423 + # Kudos to _why! + size, total = 0, response.header['Content-Length'].to_i + response.read_body do |chunk| + tf.write(chunk) + size += chunk.size + if size == 0 + Chef::Log.debug("#{url.path} done (0 length file)") + elsif total == 0 + Chef::Log.debug("#{url.path} (zero content length or no Content-Length header)") + else + Chef::Log.debug("#{url.path}" + " %d%% done (%d of %d)" % [(size * 100) / total, size, total]) + end + end + tf.close + tf + rescue Exception + tf.close! + raise + end + end end diff --git a/chef/lib/chef/rest/auth_credentials.rb b/chef/lib/chef/rest/auth_credentials.rb new file mode 100644 index 0000000000..9e2aaf8a24 --- /dev/null +++ b/chef/lib/chef/rest/auth_credentials.rb @@ -0,0 +1,78 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Thom May (<thom@clearairturbulence.org>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 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 'chef/exceptions' +require 'mixlib/authentication/signedheaderauth' + +class Chef + class REST + class AuthCredentials + attr_reader :key_file, :client_name, :key, :raw_key + + def initialize(client_name=nil, key_file=nil) + @client_name, @key_file = client_name, key_file + load_signing_key if sign_requests? + end + + def sign_requests? + !!key_file + end + + def signature_headers(request_params={}) + raise ArgumentError, "Cannot sign the request without a client name, check that :node_name is assigned" if client_name.nil? + Chef::Log.debug("Signing the request as #{client_name}") + + # params_in = {:http_method => :GET, :path => "/clients", :body => "", :host => "localhost"} + request_params = request_params.dup + request_params[:timestamp] = Time.now.utc.iso8601 + request_params[:user_id] = client_name + host = request_params.delete(:host) || "localhost" + + sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(request_params) + signed = sign_obj.sign(key).merge({:host => host}) + signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo} + end + + private + + def load_signing_key + begin + @raw_key = IO.read(key_file) + rescue SystemCallError, IOError => e + Chef::Log.fatal "Failed to read the private key #{key_file}: #{e.inspect}, #{e.backtrace}" + raise Chef::Exceptions::PrivateKeyMissing, "I cannot read #{key_file}, which you told me to use to sign requests!" + end + assert_valid_key_format!(@raw_key) + @key = OpenSSL::PKey::RSA.new(@raw_key) + end + + def assert_valid_key_format!(raw_key) + unless (raw_key =~ /\A-----BEGIN RSA PRIVATE KEY-----$/) && (raw_key =~ /^-----END RSA PRIVATE KEY-----\Z/) + msg = "The file #{key_file} does not contain a correctly formatted private key.\n" + msg << "The key file should begin with '-----BEGIN RSA PRIVATE KEY-----' and end with '-----END RSA PRIVATE KEY-----'" + raise Chef::Exceptions::InvalidPrivateKey, msg + end + end + + end + end +end diff --git a/chef/lib/chef/application/server.rb b/chef/lib/chef/rest/cookie_jar.rb index a42f61dc1e..e3137708a2 100644 --- a/chef/lib/chef/application/server.rb +++ b/chef/lib/chef/rest/cookie_jar.rb @@ -1,19 +1,31 @@ # -# Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Thom May (<thom@clearairturbulence.org>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 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 'singleton' -require 'chef/application' - +class Chef + class REST + class CookieJar < Hash + include Singleton + end + end +end diff --git a/chef/lib/chef/rest/rest_request.rb b/chef/lib/chef/rest/rest_request.rb new file mode 100644 index 0000000000..66e2a11233 --- /dev/null +++ b/chef/lib/chef/rest/rest_request.rb @@ -0,0 +1,151 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Thom May (<thom@clearairturbulence.org>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 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 'uri' +require 'net/http' +require 'chef/rest/cookie_jar' + +class Chef + class REST + class RESTRequest + attr_reader :method, :url, :headers, :http_client, :http_request + + def initialize(method, url, req_body, base_headers={}) + @method, @url = method, url + @request_body = nil + @cookies = CookieJar.instance + configure_http_client + build_headers(base_headers) + configure_http_request(req_body) + end + + def host + @url.host + end + + def port + @url.port + end + + def query + @url.query + end + + def path + @url.path.empty? ? "/" : @url.path + end + + def call + http_client.request(http_request) do |response| + store_cookie(response) + yield response if block_given? + response + end + end + + def config + Chef::Config + end + + private + + def store_cookie(response) + if response['set-cookie'] + @cookies["#{host}:#{port}"] = response['set-cookie'] + end + end + + def build_headers(headers) + @headers = headers.dup + # TODO: need to set accept somewhere else + # headers.merge!('Accept' => "application/json") unless raw + @headers['X-Chef-Version'] = ::Chef::VERSION + + if @cookies.has_key?("#{host}:#{port}") + @headers['Cookie'] = @cookies["#{host}:#{port}"] + end + end + + def configure_http_client + @http_client = Net::HTTP.new(host, port) + if url.scheme == "https" + @http_client.use_ssl = true + if config[:ssl_verify_mode] == :verify_none + @http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE + elsif config[:ssl_verify_mode] == :verify_peer + @http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + if config[:ssl_ca_path] + unless ::File.exist?(config[:ssl_ca_path]) + raise Chef::Exceptions::ConfigurationError, "The configured ssl_ca_path #{config[:ssl_ca_path]} does not exist" + end + @http_client.ca_path = config[:ssl_ca_path] + elsif config[:ssl_ca_file] + unless ::File.exist?(config[:ssl_ca_file]) + raise Chef::Exceptions::ConfigurationError, "The configured ssl_ca_file #{config[:ssl_ca_file]} does not exist" + end + @http_client.ca_file = config[:ssl_ca_file] + end + if (config[:ssl_client_cert] || config[:ssl_client_key]) + unless (config[:ssl_client_cert] && config[:ssl_client_key]) + raise Chef::Exceptions::ConfigurationError, "You must configure ssl_client_cert and ssl_client_key together" + end + unless ::File.exists?(config[:ssl_client_cert]) + raise Chef::Exceptions::ConfigurationError, "The configured ssl_client_cert #{config[:ssl_client_cert]} does not exist" + end + unless ::File.exists?(config[:ssl_client_key]) + raise Chef::Exceptions::ConfigurationError, "The configured ssl_client_key #{config[:ssl_client_key]} does not exist" + end + @http_client.cert = OpenSSL::X509::Certificate.new(::File.read(config[:ssl_client_cert])) + @http_client.key = OpenSSL::PKey::RSA.new(::File.read(config[:ssl_client_key])) + end + end + + @http_client.read_timeout = config[:rest_timeout] + end + + + def configure_http_request(request_body=nil) + req_path = "#{path}" + req_path << "?#{query}" if query + + @http_request = case method.to_s.downcase + when "get" + Net::HTTP::Get.new(req_path, headers) + when "post" + Net::HTTP::Post.new(req_path, headers) + when "put" + Net::HTTP::Put.new(req_path, headers) + when "delete" + Net::HTTP::Delete.new(req_path, headers) + else + raise ArgumentError, "You must provide :GET, :PUT, :POST or :DELETE as the method" + end + + @http_request.body = request_body if (request_body && @http_request.request_body_permitted?) + # Optionally handle HTTP Basic Authentication + @http_request.basic_auth(url.user, url.password) if url.user + end + + end + end +end
\ No newline at end of file diff --git a/chef/lib/chef/role.rb b/chef/lib/chef/role.rb index 64ae7609e9..75707477f3 100644 --- a/chef/lib/chef/role.rb +++ b/chef/lib/chef/role.rb @@ -8,9 +8,9 @@ # 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. @@ -28,19 +28,19 @@ require 'extlib' require 'json' class Chef - class Role - + class Role + include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate include Chef::IndexQueue::Indexable - + DESIGN_DOCUMENT = { "version" => 6, "language" => "javascript", "views" => { "all" => { "map" => <<-EOJS - function(doc) { + function(doc) { if (doc.chef_type == "role") { emit(doc.name, doc); } @@ -49,7 +49,7 @@ class Chef }, "all_id" => { "map" => <<-EOJS - function(doc) { + function(doc) { if (doc.chef_type == "role") { emit(doc.name, doc.name); } @@ -61,14 +61,14 @@ class Chef attr_accessor :couchdb_rev, :couchdb attr_reader :couchdb_id - + # Create a new Chef::Role object. def initialize(couchdb=nil) - @name = '' - @description = '' + @name = '' + @description = '' @default_attributes = Mash.new @override_attributes = Mash.new - @run_list = Chef::RunList.new + @run_list = Chef::RunList.new @couchdb_rev = nil @couchdb_id = nil @couchdb = couchdb || Chef::CouchDB.new @@ -87,7 +87,7 @@ class Chef Chef::REST.new(Chef::Config[:chef_server_url]) end - def name(arg=nil) + def name(arg=nil) set_or_return( :name, arg, @@ -95,7 +95,7 @@ class Chef ) end - def description(arg=nil) + def description(arg=nil) set_or_return( :description, arg, @@ -113,7 +113,7 @@ class Chef # Chef::Log.warn "Chef::Role#recipes method is deprecated. Please use Chef::Role#run_list" # run_list(*args) # end - + def default_attributes(arg=nil) set_or_return( :default_attributes, @@ -144,11 +144,11 @@ class Chef result end - # Serialize this object as a hash + # Serialize this object as a hash def to_json(*a) to_hash.to_json(*a) end - + # Create a Chef::Role from JSON def self.json_create(o) role = new @@ -164,15 +164,15 @@ class Chef role.couchdb_rev = o["_rev"] if o.has_key?("_rev") role.index_id = role.couchdb_id role.couchdb_id = o["_id"] if o.has_key?("_id") - role + role end - + # List all the Chef::Role objects in the CouchDB. If inflate is set to true, you will get # the full list of all Roles, fully inflated. def self.cdb_list(inflate=false, couchdb=nil) rs = (couchdb || Chef::CouchDB.new).list("roles", inflate) lookup = (inflate ? "value" : "key") - rs["rows"].collect { |r| r[lookup] } + rs["rows"].collect { |r| r[lookup] } end # Get the list of all roles from the API. @@ -187,12 +187,12 @@ class Chef chef_server_rest.get_rest("roles") end end - + # Load a role by name from CouchDB def self.cdb_load(name, couchdb=nil) (couchdb || Chef::CouchDB.new).load("role", name) end - + # Load a role by name from the API def self.load(name) chef_server_rest.get_rest("roles/#{name}") @@ -205,44 +205,36 @@ class Chef nil end end - + # Remove this role from the CouchDB def cdb_destroy couchdb.delete("role", @name, couchdb_rev) - if Chef::Config[:couchdb_version] == 0.9 - rs = couchdb.get_view("nodes", "by_run_list", :startkey => "role[#{@name}]", :endkey => "role[#{@name}]", :include_docs => true) - rs["rows"].each do |row| - node = row["doc"] - node.run_list.remove("role[#{@name}]") - node.cdb_save - end - else - Chef::Node.cdb_list.each do |node| - n = Chef::Node.cdb_load(node) - n.run_list.remove("role[#{@name}]") - n.cdb_save - end + rs = couchdb.get_view("nodes", "by_run_list", :startkey => "role[#{@name}]", :endkey => "role[#{@name}]", :include_docs => true) + rs["rows"].each do |row| + node = row["doc"] + node.run_list.remove("role[#{@name}]") + node.cdb_save end end - + # Remove this role via the REST API def destroy chef_server_rest.delete_rest("roles/#{@name}") - + Chef::Node.list.each do |node| n = Chef::Node.load(node[0]) n.run_list.remove("role[#{@name}]") n.save end - + end - + # Save this role to the CouchDB def cdb_save self.couchdb_rev = couchdb.store("role", @name, self)["rev"] end - + # Save this role via the REST API def save begin @@ -253,18 +245,18 @@ class Chef end self end - + # Create the role via the REST API def create chef_server_rest.post_rest("roles", self) self - end - + end + # Set up our CouchDB design document def self.create_design_document(couchdb=nil) (couchdb || Chef::CouchDB.new).create_design_document("roles", DESIGN_DOCUMENT) end - + # As a string def to_s "role[#{@name}]" @@ -289,14 +281,14 @@ class Chef # Sync all the json roles with couchdb from disk def self.sync_from_disk_to_couchdb Dir[File.join(Chef::Config[:role_path], "*.json")].each do |role_file| - short_name = File.basename(role_file, ".json") + short_name = File.basename(role_file, ".json") Chef::Log.warn("Loading #{short_name}") r = Chef::Role.from_disk(short_name, "json") begin couch_role = Chef::Role.cdb_load(short_name) r.couchdb_rev = couch_role.couchdb_rev Chef::Log.debug("Replacing role #{short_name} with data from #{role_file}") - rescue Chef::Exceptions::CouchDBNotFound + rescue Chef::Exceptions::CouchDBNotFound Chef::Log.debug("Creating role #{short_name} with data from #{role_file}") end r.cdb_save diff --git a/chef/lib/chef/streaming_cookbook_uploader.rb b/chef/lib/chef/streaming_cookbook_uploader.rb index f7e8b23ff9..1e1bb8f4dc 100644 --- a/chef/lib/chef/streaming_cookbook_uploader.rb +++ b/chef/lib/chef/streaming_cookbook_uploader.rb @@ -1,9 +1,15 @@ +# inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html +# On Apr 6, 2010, at 3:00 PM, Stanislav Vitvitskiy wrote: +# +# It's free to use / modify / distribute. No need to mention anything. Just copy/paste and use. +# +# Regards, +# Stan + require 'net/http' require 'mixlib/authentication/signedheaderauth' require 'openssl' -# inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html -# TODO: confirm that code is public domain class Chef class StreamingCookbookUploader diff --git a/chef/lib/chef/tasks/chef_repo.rake b/chef/lib/chef/tasks/chef_repo.rake index 58b1319d33..b22d84b25f 100644 --- a/chef/lib/chef/tasks/chef_repo.rake +++ b/chef/lib/chef/tasks/chef_repo.rake @@ -72,8 +72,8 @@ task :install => [ :update, :roles, :upload_cookbooks ] do end end -desc "By default, run rake test" -task :default => [ :test ] +desc "By default, run rake test_cookbooks" +task :default => [ :test_cookbooks ] desc "Create a new cookbook (with COOKBOOK=name, optional CB_PREFIX=site-)" task :new_cookbook do @@ -213,7 +213,7 @@ EOH end rule(%r{\b(?:site-)?cookbooks/[^/]+/metadata\.json\Z} => [ proc { |task_name| task_name.sub(/\.[^.]+$/, '.rb') } ]) do |t| - system("knife cookbook metadata #{t.source}") + system("knife cookbook metadata from file #{t.source}") end desc "Build cookbook metadata.json from metadata.rb" @@ -228,7 +228,7 @@ task :roles => FileList[File.join(TOPDIR, 'roles', '**', '*.rb')].pathmap('%X.j desc "Update a specific role" task :role, :role_name do |t, args| - system("knife role from file #{args.cookbook}") + system("knife role from file #{File.join(TOPDIR, 'roles', args.role_name)}.rb") end desc "Upload all cookbooks" @@ -243,3 +243,13 @@ task :upload_cookbook, :cookbook do |t, args| system("knife cookbook upload #{args.cookbook}") end +desc "Test all cookbooks" +task :test_cookbooks do + system("knife cookbook test --all") +end + +desc "Test a single cookbook" +task :test_cookbook, :cookbook do |t, args| + system("knife cookbook test #{args.cookbook}") +end + diff --git a/chef/lib/chef/util/file_edit.rb b/chef/lib/chef/util/file_edit.rb index 2ecaa98642..83b1568f08 100644 --- a/chef/lib/chef/util/file_edit.rb +++ b/chef/lib/chef/util/file_edit.rb @@ -15,7 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'ftools' require 'fileutils' require 'tempfile' diff --git a/chef/lib/chef/webui_user.rb b/chef/lib/chef/webui_user.rb index 240e119dc5..fbfe1724d1 100644 --- a/chef/lib/chef/webui_user.rb +++ b/chef/lib/chef/webui_user.rb @@ -22,7 +22,6 @@ require 'chef/mixin/params_validate' require 'chef/couchdb' require 'chef/index_queue' require 'digest/sha1' -require 'rubygems' require 'json' |