diff options
author | sawanoboly <sawanoboriyu@higanworks.com> | 2014-09-12 13:01:48 +0900 |
---|---|---|
committer | sawanoboly <sawanoboriyu@higanworks.com> | 2014-09-12 13:01:48 +0900 |
commit | 1ab9ae58068da346bf2262cb0b8acc14a72d9f2b (patch) | |
tree | 4bb2f07e095d0653a0fe67374c843e130c704d1b /lib | |
parent | 5c94009b6053e7ae71d528485f3560e2bf8a7851 (diff) | |
parent | 34d956c6b96087e6ca4bfbc9080037ded481709d (diff) | |
download | chef-1ab9ae58068da346bf2262cb0b8acc14a72d9f2b.tar.gz |
Merge remote-tracking branch 'upstream/master' into prevew_archive_before_site_share
Diffstat (limited to 'lib')
128 files changed, 2971 insertions, 1309 deletions
diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 5b404a3a50..0430d4acfa 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -46,6 +46,7 @@ class Chef::Application configure_chef configure_logging configure_proxy_environment_variables + configure_encoding end # Get this party started @@ -73,7 +74,6 @@ class Chef::Application end end - # Parse configuration (options and config file) def configure_chef parse_options @@ -82,10 +82,11 @@ class Chef::Application # Parse the config file def load_config_file - config_fetcher = Chef::ConfigFetcher.new(config[:config_file], Chef::Config.config_file_jail) + config_fetcher = Chef::ConfigFetcher.new(config[:config_file]) if config[:config_file].nil? Chef::Log.warn("No config file found or specified on command line, using command line options.") elsif config_fetcher.config_missing? + pp config_missing: true Chef::Log.warn("*****************************************") Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.") Chef::Log.warn("*****************************************") @@ -175,6 +176,11 @@ class Chef::Application configure_no_proxy end + # Sets the default external encoding to UTF-8 (users can change this, but they shouldn't) + def configure_encoding + Encoding.default_external = Chef::Config[:ruby_encoding] + 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" @@ -219,30 +225,39 @@ class Chef::Application # Set ENV['http_proxy'] def configure_http_proxy if http_proxy = Chef::Config[:http_proxy] - env['http_proxy'] = configure_proxy("http", http_proxy, - Chef::Config[:http_proxy_user], Chef::Config[:http_proxy_pass]) + http_proxy_string = configure_proxy("http", http_proxy, + Chef::Config[:http_proxy_user], Chef::Config[:http_proxy_pass]) + env['http_proxy'] = http_proxy_string unless env['http_proxy'] + env['HTTP_PROXY'] = http_proxy_string unless env['HTTP_PROXY'] end end # Set ENV['https_proxy'] def configure_https_proxy if https_proxy = Chef::Config[:https_proxy] - env['https_proxy'] = configure_proxy("https", https_proxy, - Chef::Config[:https_proxy_user], Chef::Config[:https_proxy_pass]) + https_proxy_string = configure_proxy("https", https_proxy, + Chef::Config[:https_proxy_user], Chef::Config[:https_proxy_pass]) + env['https_proxy'] = https_proxy_string unless env['https_proxy'] + env['HTTPS_PROXY'] = https_proxy_string unless env['HTTPS_PROXY'] end end # Set ENV['ftp_proxy'] def configure_ftp_proxy if ftp_proxy = Chef::Config[:ftp_proxy] - env['ftp_proxy'] = configure_proxy("ftp", ftp_proxy, + ftp_proxy_string = configure_proxy("ftp", ftp_proxy, Chef::Config[:ftp_proxy_user], Chef::Config[:ftp_proxy_pass]) + env['ftp_proxy'] = ftp_proxy_string unless env['ftp_proxy'] + env['FTP_PROXY'] = ftp_proxy_string unless env['FTP_PROXY'] end end # Set ENV['no_proxy'] def configure_no_proxy - env['no_proxy'] = Chef::Config[:no_proxy] if Chef::Config[:no_proxy] + if Chef::Config[:no_proxy] + env['no_proxy'] = Chef::Config[:no_proxy] unless env['no_proxy'] + env['NO_PROXY'] = Chef::Config[:no_proxy] unless env['NO_PROXY'] + end end # Builds a proxy uri. Examples: @@ -256,7 +271,7 @@ class Chef::Application # pass = password def configure_proxy(scheme, path, user, pass) begin - path = "#{scheme}://#{path}" unless path.start_with?(scheme) + path = "#{scheme}://#{path}" unless path.include?('://') # URI.split returns the following parts: # [scheme, userinfo, host, port, registry, path, opaque, query, fragment] parts = URI.split(URI.encode(path)) diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb index ab35b35389..ea9154c6f2 100644 --- a/lib/chef/application/apply.rb +++ b/lib/chef/application/apply.rb @@ -134,6 +134,10 @@ class Chef::Application::Apply < Chef::Application @recipe_text = STDIN.read temp_recipe_file else + if !ARGV[0] + puts opt_parser + Chef::Application.exit! "No recipe file provided", 1 + end @recipe_filename = ARGV[0] @recipe_text,@recipe_fh = read_recipe_file @recipe_filename end diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index c581bb0da0..6c06ad656d 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -24,6 +24,7 @@ require 'chef/daemon' require 'chef/log' require 'chef/config_fetcher' require 'chef/handler/error_report' +require 'chef/workstation_config_loader' class Chef::Application::Client < Chef::Application @@ -219,9 +220,10 @@ class Chef::Application::Client < Chef::Application :long => "--chef-zero-port PORT", :description => "Port (or port range) to start chef-zero on. Port ranges like 1000,1010 or 8889-9999 will try all given ports until one works." - option :config_file_jail, - :long => "--config-file-jail PATH", - :description => "Directory under which config files are allowed to be loaded (no client.rb or knife.rb outside this path will be loaded)." + option :disable_config, + :long => "--disable-config", + :description => "Refuse to load a config file and use defaults. This is for development and not a stable API", + :boolean => true option :run_lock_timeout, :long => "--run-lock-timeout SECONDS", @@ -273,11 +275,9 @@ class Chef::Application::Client < Chef::Application end def load_config_file - Chef::Config.config_file_jail = config[:config_file_jail] if config[:config_file_jail] - if !config.has_key?(:config_file) + if !config.has_key?(:config_file) && !config[:disable_config] if config[:local_mode] - require 'chef/knife' - config[:config_file] = Chef::Knife.locate_config_file + config[:config_file] = Chef::WorkstationConfigLoader.new(nil, Chef::Log).config_location else config[:config_file] = Chef::Config.platform_specific_path("/etc/chef/client.rb") end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index 5e54535014..f0e578d5ef 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -185,24 +185,22 @@ class Chef::Application::Solo < Chef::Application Chef::Config[:interval] ||= 1800 end - if Chef::Config[:json_attribs] - config_fetcher = Chef::ConfigFetcher.new(Chef::Config[:json_attribs]) - @chef_client_json = config_fetcher.fetch_json - end - if Chef::Config[:recipe_url] cookbooks_path = Array(Chef::Config[:cookbook_path]).detect{|e| e =~ /\/cookbooks\/*$/ } recipes_path = File.expand_path(File.join(cookbooks_path, '..')) Chef::Log.debug "Creating path #{recipes_path} to extract recipes into" - FileUtils.mkdir_p recipes_path - path = File.join(recipes_path, 'recipes.tgz') - File.open(path, 'wb') do |f| - open(Chef::Config[:recipe_url]) do |r| - f.write(r.read) - end - end - Chef::Mixin::Command.run_command(:command => "tar zxvf #{path} -C #{recipes_path}") + FileUtils.mkdir_p(recipes_path) + tarball_path = File.join(recipes_path, 'recipes.tgz') + fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) + Chef::Mixin::Command.run_command(:command => "tar zxvf #{tarball_path} -C #{recipes_path}") + end + + # json_attribs shuld be fetched after recipe_url tarball is unpacked. + # Otherwise it may fail if points to local file from tarball. + if Chef::Config[:json_attribs] + config_fetcher = Chef::ConfigFetcher.new(Chef::Config[:json_attribs]) + @chef_client_json = config_fetcher.fetch_json end end @@ -246,4 +244,14 @@ class Chef::Application::Solo < Chef::Application end end + private + + def fetch_recipe_tarball(url, path) + Chef::Log.debug("Download recipes tarball from #{url} to #{path}") + File.open(path, 'wb') do |f| + open(url) do |r| + f.write(r.read) + end + end + end end diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index b2435d8201..484ab07390 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -27,7 +27,61 @@ require 'fileutils' class Chef module ChefFS + # + # Translation layer between chef-zero's DataStore (a place where it expects + # files to be stored) and ChefFS (the user's repository directory layout). + # + # chef-zero expects the data store to store files *its* way--for example, it + # expects get("nodes/blah") to return the JSON text for the blah node, and + # it expects get("cookbooks/blah/1.0.0") to return the JSON definition of + # the blah cookbook version 1.0.0. + # + # The repository is defined the way the *user* wants their layout. These + # two things are very similar in layout (for example, nodes are stored under + # the nodes/ directory and their filename is the name of the node). + # + # However, there are a few differences that make this more than just a raw + # file store: + # + # 1. Cookbooks are stored much differently. + # - chef-zero places JSON text with the checksums for the cookbook at + # /cookbooks/NAME/VERSION, and expects the JSON to contain URLs to the + # actual files, which are stored elsewhere. + # - The repository contains an actual directory with just the cookbook + # files and a metadata.rb containing a version #. There is no JSON to + # be found. + # - Further, if versioned_cookbooks is false, that directory is named + # /cookbooks/NAME and only one version exists. If versioned_cookbooks + # is true, the directory is named /cookbooks/NAME-VERSION. + # - Therefore, ChefFSDataStore calculates the cookbook JSON by looking at + # the files in the cookbook and checksumming them, and reading metadata.rb + # for the version and dependency information. + # - ChefFSDataStore also modifies the cookbook file URLs so that they point + # to /file_store/repo/<filename> (the path to the actual file under the + # repository root). For example, /file_store/repo/apache2/metadata.rb or + # /file_store/repo/cookbooks/apache2/recipes/default.rb). + # + # 2. Sandboxes don't exist in the repository. + # - ChefFSDataStore lets cookbooks be uploaded into a temporary memory + # storage, and when the cookbook is committed, copies the files onto the + # disk in the correct place (/cookbooks/apache2/recipes/default.rb). + # 3. Data bags: + # - The Chef server expects data bags in /data/BAG/ITEM + # - The repository stores data bags in /data_bags/BAG/ITEM + # + # 4. JSON filenames are generally NAME.json in the repository (e.g. /nodes/foo.json). + # class ChefFSDataStore + # + # Create a new ChefFSDataStore + # + # ==== Arguments + # + # [chef_fs] + # A +ChefFS::FileSystem+ object representing the repository root. + # Generally will be a +ChefFS::FileSystem::ChefRepositoryFileSystemRoot+ + # object, created from +ChefFS::Config.local_fs+. + # def initialize(chef_fs) @chef_fs = chef_fs @memory_store = ChefZero::DataStore::MemoryStore.new @@ -103,7 +157,7 @@ class Chef value.each do |file| if file.is_a?(Hash) && file.has_key?('checksum') relative = ['file_store', 'repo', 'cookbooks'] - if Chef::Config.versioned_cookbooks + if chef_fs.versioned_cookbooks relative << "#{path[1]}-#{path[2]}" else relative << path[1] @@ -190,7 +244,7 @@ class Chef elsif path[0] == 'cookbooks' && path.length == 1 with_entry(path) do |entry| begin - if Chef::Config.versioned_cookbooks + if chef_fs.versioned_cookbooks # /cookbooks/name-version -> /cookbooks/name entry.children.map { |child| split_name_version(child.name)[0] }.uniq else @@ -203,7 +257,7 @@ class Chef end elsif path[0] == 'cookbooks' && path.length == 2 - if Chef::Config.versioned_cookbooks + if chef_fs.versioned_cookbooks result = with_entry([ 'cookbooks' ]) do |entry| # list /cookbooks/name = filter /cookbooks/name-version down to name entry.children.map { |child| split_name_version(child.name) }. @@ -261,7 +315,7 @@ class Chef end def write_cookbook(path, data, *options) - if Chef::Config.versioned_cookbooks + if chef_fs.versioned_cookbooks cookbook_path = File.join('cookbooks', "#{path[1]}-#{path[2]}") else cookbook_path = File.join('cookbooks', path[1]) @@ -318,7 +372,7 @@ class Chef elsif path[0] == 'cookbooks' if path.length == 2 raise ChefZero::DataStore::DataNotFoundError.new(path) - elsif Chef::Config.versioned_cookbooks + elsif chef_fs.versioned_cookbooks if path.length >= 3 # cookbooks/name/version -> cookbooks/name-version path = [ path[0], "#{path[1]}-#{path[2]}" ] + path[3..-1] @@ -351,7 +405,7 @@ class Chef end elsif path[0] == 'cookbooks' - if Chef::Config.versioned_cookbooks + if chef_fs.versioned_cookbooks # cookbooks/name-version/... -> cookbooks/name/version/... if path.length >= 2 name, version = split_name_version(path[1]) diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index e08b976961..fcad6c919f 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -22,17 +22,87 @@ require 'chef/chef_fs/path_utils' class Chef module ChefFS # - # Helpers to take Chef::Config and create chef_fs and local_fs from it + # Helpers to take Chef::Config and create chef_fs and local_fs (ChefFS + # objects representing the server and local repository, respectively). # class Config - def initialize(chef_config = Chef::Config, cwd = Dir.pwd, options = {}) + # + # Create a new Config object which can produce a chef_fs and local_fs. + # + # ==== Arguments + # + # [chef_config] + # A hash that looks suspiciously like +Chef::Config+. These hash keys + # include: + # + # :chef_repo_path:: + # The root where all local chef object data is stored. Mirrors + # +Chef::Config.chef_repo_path+ + # :cookbook_path, node_path, ...:: + # Paths to cookbooks/, nodes/, data_bags/, etc. Mirrors + # +Chef::Config.cookbook_path+, etc. Defaults to + # +<chef_repo_path>/cookbooks+, etc. + # :repo_mode:: + # The directory format on disk. 'everything', 'hosted_everything' and + # 'static'. Default: autodetected based on whether the URL has + # "/organizations/NAME." + # :versioned_cookbooks:: + # If true, the repository contains cookbooks with versions in their + # name (apache2-1.0.0). If false, the repository just has one version + # of each cookbook and the directory has the cookbook name (apache2). + # Default: +false+ + # :chef_server_url:: + # The URL to the Chef server, e.g. https://api.opscode.com/organizations/foo. + # Used as the server for the remote chef_fs, and to "guess" repo_mode + # if not specified. + # :node_name:: The username to authenticate to the Chef server with. + # :client_key:: The private key for the user for authentication + # :environment:: The environment in which you are presently working + # :repo_mode:: + # The repository mode, :hosted_everything, :everything or :static. + # This determines the set of subdirectories the Chef server will offer + # up. + # :versioned_cookbooks:: Whether or not to include versions in cookbook names + # + # [cwd] + # The current working directory to base relative Chef paths from. + # Defaults to +Dir.pwd+. + # + # [options] + # A hash of other, not-suspiciously-like-chef-config options: + # :cookbook_version:: + # When downloading cookbooks, download this cookbook version instead + # of the latest. + # + # [ui] + # The object to print output to, with "output", "warn" and "error" + # (looks a little like a Chef::Knife::UI object, obtainable from + # Chef::Knife.ui). + # + # ==== Example + # + # require 'chef/chef_fs/config' + # config = Chef::ChefFS::Config.new + # config.chef_fs.child('cookbooks').children.each do |cookbook| + # puts "Cookbook on server: #{cookbook.name}" + # end + # config.local_fs.child('cookbooks').children.each do |cookbook| + # puts "Local cookbook: #{cookbook.name}" + # end + # + def initialize(chef_config = Chef::Config, cwd = Dir.pwd, options = {}, ui = nil) @chef_config = chef_config @cwd = cwd @cookbook_version = options[:cookbook_version] + if @chef_config[:repo_mode] == 'everything' && is_hosted? && !ui.nil? + ui.warn %Q{You have repo_mode set to 'everything', but your chef_server_url + looks like it might be a hosted setup. If this is the case please use + hosted_everything or allow repo_mode to default} + end # Default to getting *everything* from the server. if !@chef_config[:repo_mode] - if @chef_config[:chef_server_url] =~ /\/+organizations\/.+/ + if is_hosted? @chef_config[:repo_mode] = 'hosted_everything' else @chef_config[:repo_mode] = 'everything' @@ -44,6 +114,10 @@ class Chef attr_reader :cwd attr_reader :cookbook_version + def is_hosted? + @chef_config[:chef_server_url] =~ /\/+organizations\/.+/ + end + def chef_fs @chef_fs ||= create_chef_fs end @@ -59,7 +133,7 @@ class Chef def create_local_fs require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' - Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths) + Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths, Array(chef_config[:chef_repo_path]).flatten, @chef_config) end # Returns the given real path's location relative to the server root. diff --git a/lib/chef/chef_fs/data_handler/acl_data_handler.rb b/lib/chef/chef_fs/data_handler/acl_data_handler.rb index 5ce4e335f4..8def8a543d 100644 --- a/lib/chef/chef_fs/data_handler/acl_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/acl_data_handler.rb @@ -4,9 +4,9 @@ class Chef module ChefFS module DataHandler class AclDataHandler < DataHandlerBase - def normalize(node, entry) + def normalize(acl, entry) # Normalize the order of the keys for easier reading - result = normalize_hash(node, { + result = normalize_hash(acl, { 'create' => {}, 'read' => {}, 'update' => {}, diff --git a/lib/chef/chef_fs/data_handler/client_data_handler.rb b/lib/chef/chef_fs/data_handler/client_data_handler.rb index 4b6b8f5c79..d81f35e861 100644 --- a/lib/chef/chef_fs/data_handler/client_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/client_data_handler.rb @@ -22,7 +22,7 @@ class Chef result end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/data_handler/container_data_handler.rb b/lib/chef/chef_fs/data_handler/container_data_handler.rb index 8b108bcf73..980453cbab 100644 --- a/lib/chef/chef_fs/data_handler/container_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/container_data_handler.rb @@ -11,7 +11,7 @@ class Chef }) end - def preserve_key(key) + def preserve_key?(key) return key == 'containername' end diff --git a/lib/chef/chef_fs/data_handler/cookbook_data_handler.rb b/lib/chef/chef_fs/data_handler/cookbook_data_handler.rb index d2e2a3ef6c..56b7e0b765 100644 --- a/lib/chef/chef_fs/data_handler/cookbook_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/cookbook_data_handler.rb @@ -23,7 +23,7 @@ class Chef }) end - def preserve_key(key) + def preserve_key?(key) return key == 'cookbook_name' || key == 'version' end diff --git a/lib/chef/chef_fs/data_handler/data_bag_item_data_handler.rb b/lib/chef/chef_fs/data_handler/data_bag_item_data_handler.rb index 240a42756d..1306922081 100644 --- a/lib/chef/chef_fs/data_handler/data_bag_item_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/data_bag_item_data_handler.rb @@ -34,7 +34,7 @@ class Chef normalize_for_post(data_bag_item, entry) end - def preserve_key(key) + def preserve_key?(key) return key == 'id' end diff --git a/lib/chef/chef_fs/data_handler/data_handler_base.rb b/lib/chef/chef_fs/data_handler/data_handler_base.rb index a9bbc0bf1b..a3dc92405c 100644 --- a/lib/chef/chef_fs/data_handler/data_handler_base.rb +++ b/lib/chef/chef_fs/data_handler/data_handler_base.rb @@ -1,17 +1,32 @@ class Chef module ChefFS module DataHandler + # + # The base class for all *DataHandlers. + # + # DataHandlers' job is to know the innards of Chef objects and manipulate + # JSON for them, adding defaults and formatting them. + # class DataHandlerBase + # + # Remove all default values from a Chef object's JSON so that the only + # thing you see are the values that have been explicitly set. + # Achieves this by calling normalize({}, entry) to get the list of + # defaults, and subtracting anything that is the same. + # def minimize(object, entry) default_object = default(entry) object.each_pair do |key, value| - if default_object[key] == value && !preserve_key(key) + if default_object[key] == value && !preserve_key?(key) object.delete(key) end end object end + # + # Takes a name like blah.json and removes the .json from it. + # def remove_dot_json(name) if name.length < 5 || name[-5,5] != ".json" raise "Invalid name #{path}: must end in .json" @@ -19,14 +34,34 @@ class Chef name[0,name.length-5] end - def preserve_key(key) + # + # Return true if minimize() should preserve a key even if it is the same + # as the default. Often used for ids and names. + # + def preserve_key?(key) false end + # + # Get the default value for an entry. Calls normalize({}, entry). + # def default(entry) normalize({}, entry) end + # + # Utility function to help subclasses do normalize(). Pass in a hash + # and a list of keys with defaults, and normalize will: + # + # 1. Fill in the defaults + # 2. Put the actual values in the order of the defaults + # 3. Move any other values to the end + # + # == Example + # + # normalize_hash({x: 100, c: 2, a: 1}, { a: 10, b: 20, c: 30}) + # -> { a: 1, b: 20, c: 2, x: 100} + # def normalize_hash(object, defaults) # Make a normalized result in the specified order for diffing result = {} @@ -39,14 +74,25 @@ class Chef result end + # Specialized function to normalize an object before POSTing it, since + # some object types want slightly different values on POST. + # If not overridden, this just calls normalize() def normalize_for_post(object, entry) normalize(object, entry) end + # Specialized function to normalize an object before PUTing it, since + # some object types want slightly different values on PUT. + # If not overridden, this just calls normalize(). def normalize_for_put(object, entry) normalize(object, entry) end + # + # normalize a run list (an array of run list items). + # Leaves recipe[name] and role[name] alone, and translates + # name to recipe[name]. Then calls uniq on the result. + # def normalize_run_list(run_list) run_list.map{|item| case item.to_s @@ -60,22 +106,46 @@ class Chef }.uniq end + # + # Bring in an instance of this object from Ruby. (Like roles/x.rb) + # def from_ruby(ruby) chef_class.from_file(ruby).to_hash end + # + # Turn a JSON hash into a bona fide Chef object (like Chef::Node). + # def chef_object(object) chef_class.json_create(object) end + # + # Write out the Ruby file for this instance. (Like roles/x.rb) + # def to_ruby(object) raise NotImplementedError end + # + # Get the class for instances of this type. Must be overridden. + # def chef_class raise NotImplementedError end + # + # Helper to write out a Ruby file for a JSON hash. Writes out only + # the keys specified in "keys"; anything else must be emitted by the + # caller. + # + # == Example + # + # to_ruby_keys({"name" => "foo", "environment" => "desert", "foo": "bar"}, [ "name", "environment" ]) + # -> + # 'name "foo" + # environment "desert"' + # def to_ruby_keys(object, keys) result = '' keys.each do |key| @@ -115,6 +185,10 @@ class Chef result end + # + # Verify that the JSON hash for this type has a key that matches its name. + # Calls the on_error block with the error, if there is one. + # def verify_integrity(object, entry, &on_error) base_name = remove_dot_json(entry.name) if object['name'] != base_name diff --git a/lib/chef/chef_fs/data_handler/environment_data_handler.rb b/lib/chef/chef_fs/data_handler/environment_data_handler.rb index 9da10ebfa5..5105f2ac49 100644 --- a/lib/chef/chef_fs/data_handler/environment_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/environment_data_handler.rb @@ -17,7 +17,7 @@ class Chef }) end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/data_handler/group_data_handler.rb b/lib/chef/chef_fs/data_handler/group_data_handler.rb index 619822fe70..4d1b10f321 100644 --- a/lib/chef/chef_fs/data_handler/group_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/group_data_handler.rb @@ -36,7 +36,7 @@ class Chef result end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/data_handler/node_data_handler.rb b/lib/chef/chef_fs/data_handler/node_data_handler.rb index f2c97c734f..04faa527f0 100644 --- a/lib/chef/chef_fs/data_handler/node_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/node_data_handler.rb @@ -21,7 +21,7 @@ class Chef result end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/data_handler/organization_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_data_handler.rb new file mode 100644 index 0000000000..da911c08f0 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_data_handler.rb @@ -0,0 +1,30 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationDataHandler < DataHandlerBase + def normalize(organization, entry) + result = normalize_hash(organization, { + 'name' => entry.org, + 'full_name' => entry.org, + 'org_type' => 'Business', + 'clientname' => "#{entry.org}-validator", + 'billing_plan' => 'platform-free', + }) + result + end + + def preserve_key?(key) + return key == 'name' + end + + def verify_integrity(object, entry, &on_error) + if entry.org != object['name'] + on_error.call("Name must be '#{entry.org}' (is '#{object['name']}')") + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb new file mode 100644 index 0000000000..db56ecc504 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb @@ -0,0 +1,17 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationInvitesDataHandler < DataHandlerBase + def normalize(invites, entry) + invites.map { |invite| invite.is_a?(Hash) ? invite['username'] : invite }.sort.uniq + end + + def minimize(invites, entry) + invites + end + end + end + end +end diff --git a/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb new file mode 100644 index 0000000000..afa331775c --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb @@ -0,0 +1,17 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationMembersDataHandler < DataHandlerBase + def normalize(members, entry) + members.map { |member| member.is_a?(Hash) ? member['user']['username'] : member }.sort.uniq + end + + def minimize(members, entry) + members + end + end + end + end +end diff --git a/lib/chef/chef_fs/data_handler/role_data_handler.rb b/lib/chef/chef_fs/data_handler/role_data_handler.rb index bc1c076280..21c3013e9f 100644 --- a/lib/chef/chef_fs/data_handler/role_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/role_data_handler.rb @@ -23,7 +23,7 @@ class Chef result end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/data_handler/user_data_handler.rb b/lib/chef/chef_fs/data_handler/user_data_handler.rb index 66a780690a..2b50ce38d8 100644 --- a/lib/chef/chef_fs/data_handler/user_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/user_data_handler.rb @@ -7,6 +7,7 @@ class Chef def normalize(user, entry) normalize_hash(user, { 'name' => remove_dot_json(entry.name), + 'username' => remove_dot_json(entry.name), 'admin' => false, 'json_class' => 'Chef::WebUIUser', 'chef_type' => 'webui_user', @@ -16,7 +17,7 @@ class Chef }) end - def preserve_key(key) + def preserve_key?(key) return key == 'name' end diff --git a/lib/chef/chef_fs/file_system.rb b/lib/chef/chef_fs/file_system.rb index 4d15d7af33..730fa0e5cc 100644 --- a/lib/chef/chef_fs/file_system.rb +++ b/lib/chef/chef_fs/file_system.rb @@ -273,7 +273,6 @@ class Chef # case we shouldn't waste time trying PUT if we know the file doesn't # exist. # Will need to decide how that works with checksums, though. - error = false begin dest_path = format_path.call(dest_entry) if ui diff --git a/lib/chef/chef_fs/file_system/acl_entry.rb b/lib/chef/chef_fs/file_system/acl_entry.rb index 1bd03a6095..b2545af5ae 100644 --- a/lib/chef/chef_fs/file_system/acl_entry.rb +++ b/lib/chef/chef_fs/file_system/acl_entry.rb @@ -32,7 +32,7 @@ class Chef end def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self, e), "ACLs cannot be deleted." + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self), "ACLs cannot be deleted." end def write(file_contents) diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb index b151db6973..20a3f4e2be 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb @@ -35,7 +35,7 @@ class Chef loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore) # We need the canonical cookbook name if we are using versioned cookbooks, but we don't # want to spend a lot of time adding code to the main Chef libraries - if Chef::Config[:versioned_cookbooks] + if root.versioned_cookbooks _canonical_name = canonical_cookbook_name(File.basename(file_path)) fail "When versioned_cookbooks mode is on, cookbook #{file_path} must match format <cookbook_name>-x.y.z" unless _canonical_name diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb index 6ccdc2cf5f..9acfe4b936 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -31,8 +31,12 @@ class Chef @data_handler = data_handler end + def write_pretty_json=(value) + @write_pretty_json = value + end + def write_pretty_json - root.write_pretty_json + @write_pretty_json.nil? ? root.write_pretty_json : @write_pretty_json end def data_handler diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb index d615e0f415..ac272d4c1a 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -33,32 +33,71 @@ require 'chef/chef_fs/data_handler/container_data_handler' class Chef module ChefFS module FileSystem + # + # Represents the root of a local Chef repository, with directories for + # nodes, cookbooks, roles, etc. under it. + # class ChefRepositoryFileSystemRootDir < BaseFSDir - def initialize(child_paths) + # + # Create a new Chef Repository File System root. + # + # == Parameters + # [child_paths] + # A hash of child paths, e.g.: + # "nodes" => [ '/var/nodes', '/home/jkeiser/nodes' ], + # "roles" => [ '/var/roles' ], + # ... + # [root_paths] + # An array of paths representing the top level, where + # +org.json+, +members.json+, and +invites.json+ will be stored. + # [chef_config] - a hash of options that looks suspiciously like the ones + # stored in Chef::Config, containing at least these keys: + # :versioned_cookbooks:: whether to include versions in cookbook names + def initialize(child_paths, root_paths=[], chef_config=Chef::Config) super("", nil) @child_paths = child_paths + @root_paths = root_paths + @versioned_cookbooks = chef_config[:versioned_cookbooks] end attr_accessor :write_pretty_json + attr_reader :root_paths attr_reader :child_paths + attr_reader :versioned_cookbooks + + CHILDREN = %w(invitations.json members.json org.json) def children - @children ||= child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } + @children ||= begin + result = child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } + result += root_dir.children.select { |c| CHILDREN.include?(c.name) } if root_dir + result.sort_by { |c| c.name } + end end def can_have_child?(name, is_dir) - child_paths.has_key?(name) && is_dir + if is_dir + child_paths.has_key?(name) + elsif root_dir + CHILDREN.include?(name) + else + false + end end def create_child(name, file_contents = nil) - child_paths[name].each do |path| - begin - Dir.mkdir(path) - rescue Errno::EEXIST + if file_contents + child = root_dir.create_child(name, file_contents) + else + child_paths[name].each do |path| + begin + Dir.mkdir(path) + rescue Errno::EEXIST + end end + child = make_child_entry(name) end - child = make_child_entry(name) @children = nil child end @@ -67,17 +106,17 @@ class Chef nil end - # Used to print out the filesystem + # Used to print out a human-readable file system description def fs_description - repo_path = File.dirname(child_paths['cookbooks'][0]) - result = "repository at #{repo_path}\n" - if Chef::Config[:versioned_cookbooks] + repo_paths = root_paths || [ File.dirname(child_paths['cookbooks'][0]) ] + result = "repository at #{repo_paths.join(', ')}\n" + if versioned_cookbooks result << " Multiple versions per cookbook\n" else result << " One version per cookbook\n" end child_paths.each_pair do |name, paths| - if paths.any? { |path| File.dirname(path) != repo_path } + if paths.any? { |path| !repo_paths.include?(File.dirname(path)) } result << " #{name} at #{paths.join(', ')}\n" end end @@ -86,6 +125,27 @@ class Chef private + # + # A FileSystemEntry representing the root path where invites.json, + # members.json and org.json may be found. + # + def root_dir + existing_paths = root_paths.select { |path| File.exists?(path) } + if existing_paths.size > 0 + MultiplexedDir.new(existing_paths.map do |path| + dir = ChefRepositoryFileSystemEntry.new(name, parent, path) + dir.write_pretty_json = !!write_pretty_json + dir + end) + end + end + + # + # Create a child entry of the appropriate type: + # cookbooks, data_bags, acls, etc. All will be multiplexed (i.e. if + # you have multiple paths for cookbooks, the multiplexed dir will grab + # cookbooks from all of them when you list or grab them). + # def make_child_entry(name) paths = child_paths[name].select do |path| File.exists?(path) diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb index 0083ee4cfa..370308ee0a 100644 --- a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb @@ -23,6 +23,9 @@ require 'chef/chef_fs/file_system/rest_list_dir' require 'chef/chef_fs/file_system/cookbooks_dir' require 'chef/chef_fs/file_system/data_bags_dir' require 'chef/chef_fs/file_system/nodes_dir' +require 'chef/chef_fs/file_system/org_entry' +require 'chef/chef_fs/file_system/organization_invites_entry' +require 'chef/chef_fs/file_system/organization_members_entry' require 'chef/chef_fs/file_system/environments_dir' require 'chef/chef_fs/data_handler/client_data_handler' require 'chef/chef_fs/data_handler/role_data_handler' @@ -33,7 +36,35 @@ require 'chef/chef_fs/data_handler/container_data_handler' class Chef module ChefFS module FileSystem + # + # Represents the root of a Chef server (or organization), under which + # nodes, roles, cookbooks, etc. can be found. + # class ChefServerRootDir < BaseFSDir + # + # Create a new Chef server root. + # + # == Parameters + # + # [root_name] + # A friendly name for the root, for printing--like "remote" or "chef_central". + # [chef_config] + # A hash with options that look suspiciously like Chef::Config, including the + # following keys: + # :chef_server_url:: The URL to the Chef server or top of the organization + # :node_name:: The username to authenticate to the Chef server with + # :client_key:: The private key for the user for authentication + # :environment:: The environment in which you are presently working + # :repo_mode:: + # The repository mode, :hosted_everything, :everything or :static. + # This determines the set of subdirectories the Chef server will + # offer up. + # :versioned_cookbooks:: whether or not to include versions in cookbook names + # [options] + # Other options: + # :cookbook_version:: when cookbooks are retrieved, grab this version for them. + # :freeze:: freeze cookbooks on upload + # def initialize(root_name, chef_config, options = {}) super("", nil) @chef_server_url = chef_config[:chef_server_url] @@ -41,6 +72,7 @@ class Chef @chef_private_key = chef_config[:client_key] @environment = chef_config[:environment] @repo_mode = chef_config[:repo_mode] + @versioned_cookbooks = chef_config[:versioned_cookbooks] @root_name = root_name @cookbook_version = options[:cookbook_version] # Used in knife diff and download for server cookbook version end @@ -51,6 +83,7 @@ class Chef attr_reader :environment attr_reader :repo_mode attr_reader :cookbook_version + attr_reader :versioned_cookbooks def fs_description "Chef server at #{chef_server_url} (user #{chef_username}), repo_mode = #{repo_mode}" @@ -81,10 +114,13 @@ class Chef end def org - @org ||= if URI.parse(chef_server_url).path =~ /^\/+organizations\/+([^\/]+)$/ - $1 - else - nil + @org ||= begin + path = Pathname.new(URI.parse(chef_server_url).path).cleanpath + if File.dirname(path) == '/organizations' + File.basename(path) + else + nil + end end end @@ -102,7 +138,10 @@ class Chef RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), RestListDir.new("containers", self, nil, Chef::ChefFS::DataHandler::ContainerDataHandler.new), RestListDir.new("groups", self, nil, Chef::ChefFS::DataHandler::GroupDataHandler.new), - NodesDir.new(self) + NodesDir.new(self), + OrgEntry.new("org.json", self), + OrganizationMembersEntry.new("members.json", self), + OrganizationInvitesEntry.new("invitations.json", self) ] elsif repo_mode != 'static' result += [ diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb index d7411e1c74..03652dc376 100644 --- a/lib/chef/chef_fs/file_system/cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -32,7 +32,7 @@ class Chef @exists = options[:exists] # If the name is apache2-1.0.0 and versioned_cookbooks is on, we know # the actual cookbook_name and version. - if Chef::Config[:versioned_cookbooks] + if root.versioned_cookbooks if name =~ VALID_VERSIONED_COOKBOOK_NAME @cookbook_name = $1 @version = $2 diff --git a/lib/chef/chef_fs/file_system/cookbook_file.rb b/lib/chef/chef_fs/file_system/cookbook_file.rb index 7868322590..16203b727c 100644 --- a/lib/chef/chef_fs/file_system/cookbook_file.rb +++ b/lib/chef/chef_fs/file_system/cookbook_file.rb @@ -18,7 +18,7 @@ require 'chef/chef_fs/file_system/base_fs_object' require 'chef/http/simple' -require 'digest/md5' +require 'openssl' class Chef module ChefFS @@ -74,7 +74,7 @@ class Chef private def calc_checksum(value) - Digest::MD5.hexdigest(value) + OpenSSL::Digest::MD5.hexdigest(value) end end end diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb index d4857cdabd..27bedd3827 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -51,7 +51,7 @@ class Chef def children @children ||= begin - if Chef::Config[:versioned_cookbooks] + if root.versioned_cookbooks result = [] root.get_json("#{api_path}/?num_versions=all").each_pair do |cookbook_name, cookbooks| cookbooks['versions'].each do |cookbook_version| @@ -71,7 +71,7 @@ class Chef end def upload_cookbook_from(other, options = {}) - Chef::Config[:versioned_cookbooks] ? upload_versioned_cookbook(other, options) : upload_unversioned_cookbook(other, options) + root.versioned_cookbooks ? upload_versioned_cookbook(other, options) : upload_unversioned_cookbook(other, options) rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e), "Timeout writing: #{e}" rescue Net::HTTPServerException => e @@ -155,7 +155,7 @@ class Chef def can_have_child?(name, is_dir) return false if !is_dir - return false if Chef::Config[:versioned_cookbooks] && name !~ Chef::ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME + return false if root.versioned_cookbooks && name !~ Chef::ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME return true end end diff --git a/lib/chef/chef_fs/file_system/org_entry.rb b/lib/chef/chef_fs/file_system/org_entry.rb new file mode 100644 index 0000000000..852956e1e5 --- /dev/null +++ b/lib/chef/chef_fs/file_system/org_entry.rb @@ -0,0 +1,34 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/org.json + # Represents the actual data at /organizations/NAME (the full name, etc.) + class OrgEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationDataHandler.new + end + + # /organizations/foo/org.json -> GET /organizations/foo + def api_path + parent.api_path + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/organization_invites_entry.rb new file mode 100644 index 0000000000..cb26326050 --- /dev/null +++ b/lib/chef/chef_fs/file_system/organization_invites_entry.rb @@ -0,0 +1,58 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_invites_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/invitations.json + # read data from: + # - GET /organizations/NAME/association_requests + # write data to: + # - remove from list: DELETE /organizations/NAME/association_requests/id + # - add to list: POST /organizations/NAME/association_requests + class OrganizationInvitesEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationInvitesDataHandler.new + end + + # /organizations/foo/invites.json -> /organizations/foo/association_requests + def api_path + File.join(parent.api_path, 'association_requests') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_invites = minimize_value(JSON.parse(contents, :create_additions => false)) + actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } + invites = actual_invites.keys + (desired_invites - invites).each do |invite| + begin + rest.post(api_path, { 'user' => invite }) + rescue Net::HTTPServerException => e + if e.response.code == '409' + Chef::Log.warn("Could not invite #{invite} to organization #{org}: #{api_error_text(e.response)}") + else + raise + end + end + end + (invites - desired_invites).each do |invite| + rest.delete(File.join(api_path, actual_invites[invite])) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/organization_members_entry.rb b/lib/chef/chef_fs/file_system/organization_members_entry.rb new file mode 100644 index 0000000000..eb524d5ea2 --- /dev/null +++ b/lib/chef/chef_fs/file_system/organization_members_entry.rb @@ -0,0 +1,57 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_members_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/members.json + # reads data from: + # - GET /organizations/NAME/users + # writes data to: + # - remove from list: DELETE /organizations/NAME/users/name + # - add to list: POST /organizations/NAME/users/name + class OrganizationMembersEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationMembersDataHandler.new + end + + # /organizations/foo/members.json -> /organizations/foo/users + def api_path + File.join(parent.api_path, 'users') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_members = minimize_value(JSON.parse(contents, :create_additions => false)) + members = minimize_value(_read_json) + (desired_members - members).each do |member| + begin + rest.post(File.join(api_path, member), {}) + rescue Net::HTTPServerException => e + if e.response.code == '404' + raise "Chef server at #{api_path} does not allow you to directly add members. Please either upgrade your Chef server or move the users you want into invitations.json instead of members.json." + else + raise + end + end + end + (members - desired_members).each do |member| + rest.delete(File.join(api_path, member)) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb index 67252a6f2f..ac47ff4f25 100644 --- a/lib/chef/chef_fs/file_system/rest_list_entry.rb +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -80,13 +80,13 @@ class Chef end def read - Chef::JSONCompat.to_json_pretty(_read_hash) + Chef::JSONCompat.to_json_pretty(minimize_value(_read_json)) end - def _read_hash + def _read_json begin # Minimize the value (get rid of defaults) so the results don't look terrible - minimize_value(root.get_json(api_path)) + root.get_json(api_path) rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e), "Timeout reading: #{e}" rescue Net::HTTPServerException => e @@ -119,7 +119,7 @@ class Chef # Grab this value begin - value = _read_hash + value = _read_json rescue Chef::ChefFS::FileSystem::NotFoundError return [ false, :none, other_value_json ] end @@ -169,7 +169,16 @@ class Chef end end end + + def api_error_text(response) + begin + JSON.parse(response.body)['error'].join("\n") + rescue + response.body + end + end end + end end end diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb index 652c728550..86872dab71 100644 --- a/lib/chef/chef_fs/knife.rb +++ b/lib/chef/chef_fs/knife.rb @@ -68,7 +68,7 @@ class Chef end end - @chef_fs_config = Chef::ChefFS::Config.new(Chef::Config, Dir.pwd, config) + @chef_fs_config = Chef::ChefFS::Config.new(Chef::Config, Dir.pwd, config, ui) Chef::ChefFS::Parallelizer.threads = (Chef::Config[:concurrency] || 10) - 1 end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 69e191bdf8..2de3ca3e64 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -25,7 +25,6 @@ require 'chef/log' require 'chef/rest' require 'chef/api_client' require 'chef/api_client/registration' -require 'chef/platform/query_helpers' require 'chef/node' require 'chef/role' require 'chef/file_cache' diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 9d465a3cea..1963a95aab 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -23,6 +23,7 @@ require 'chef/log' require 'chef/exceptions' require 'mixlib/config' require 'chef/util/selinux' +require 'chef/util/path_helper' require 'pathname' class Chef @@ -30,6 +31,8 @@ class Chef extend Mixlib::Config + PathHelper = Chef::Util::PathHelper + # Evaluates the given string as config. # # +filename+ is used for context in stacktraces, but doesn't need to be the name of an actual file. @@ -58,37 +61,13 @@ class Chef configuration.inspect end - def self.on_windows? - RUBY_PLATFORM =~ /mswin|mingw|windows/ - end - - BACKSLASH = '\\'.freeze - - def self.platform_path_separator - if on_windows? - File::ALT_SEPARATOR || BACKSLASH - else - File::SEPARATOR - end - end - - def self.path_join(*args) - args = args.flatten - args.inject do |joined_path, component| - unless joined_path[-1,1] == platform_path_separator - joined_path += platform_path_separator - end - joined_path += component - end - end - def self.platform_specific_path(path) - if on_windows? - # turns /etc/chef/client.rb into C:/chef/client.rb - system_drive = env['SYSTEMDRIVE'] ? env['SYSTEMDRIVE'] : "" - path = File.join(system_drive, path.split('/')[2..-1]) - # ensure all forward slashes are backslashes - path.gsub!(File::SEPARATOR, (File::ALT_SEPARATOR || '\\')) + path = PathHelper.cleanpath(path) + if Chef::Platform.windows? + # turns \etc\chef\client.rb and \var\chef\client.rb into C:/chef/client.rb + if env['SYSTEMDRIVE'] && path[0] == '\\' && path.split('\\')[2] == 'chef' + path = PathHelper.join(env['SYSTEMDRIVE'], path.split('\\', 3)[2]) + end end path end @@ -101,17 +80,13 @@ class Chef configurable(:config_file) default(:config_dir) do - if local_mode - path_join(user_home, ".chef#{platform_path_separator}") + if config_file + PathHelper.dirname(config_file) else - config_file && ::File.dirname(config_file) + PathHelper.join(user_home, ".chef", "") end end - # No config file (client.rb / knife.rb / etc.) will be loaded outside this path. - # Major use case is tests, where we don't want to load the user's config files. - configurable(:config_file_jail) - default :formatters, [] # Override the config dispatch to set the value of multiple server options simultaneously @@ -119,7 +94,7 @@ class Chef # === Parameters # url<String>:: String to be set for all of the chef-server-api URL's # - configurable(:chef_server_url).writes_value { |url| url.strip } + configurable(:chef_server_url).writes_value { |url| url.to_s.strip } # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel. # So while this is basically identical to what method_missing would do, we pull @@ -150,7 +125,7 @@ class Chef # In local mode, we auto-discover the repo root by looking for a path with "cookbooks" under it. # This allows us to run config-free. path = cwd - until File.directory?(path_join(path, "cookbooks")) + until File.directory?(PathHelper.join(path, "cookbooks")) new_path = File.expand_path('..', path) if new_path == path Chef::Log.warn("No cookbooks directory found at or above current directory. Assuming #{Dir.pwd}.") @@ -164,9 +139,9 @@ class Chef def self.derive_path_from_chef_repo_path(child_path) if chef_repo_path.kind_of?(String) - path_join(chef_repo_path, child_path) + PathHelper.join(chef_repo_path, child_path) else - chef_repo_path.map { |path| path_join(path, child_path)} + chef_repo_path.map { |path| PathHelper.join(path, child_path)} end end @@ -238,7 +213,7 @@ class Chef # this is under the user's home directory. default(:cache_path) do if local_mode - "#{config_dir}local-mode-cache" + PathHelper.join(config_dir, 'local-mode-cache') else primary_cache_root = platform_specific_path("/var") primary_cache_path = platform_specific_path("/var/chef") @@ -247,8 +222,7 @@ class Chef # Otherwise, we'll create .chef under the user's home directory and use that as # the cache path. unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root) - secondary_cache_path = File.join(user_home, '.chef') - secondary_cache_path.gsub!(File::SEPARATOR, platform_path_separator) # Safety, mainly for Windows... + secondary_cache_path = PathHelper.join(user_home, '.chef') Chef::Log.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}") secondary_cache_path else @@ -263,20 +237,20 @@ class Chef end # Where cookbook files are stored on the server (by content checksum) - default(:checksum_path) { path_join(cache_path, "checksums") } + default(:checksum_path) { PathHelper.join(cache_path, "checksums") } # Where chef's cache files should be stored - default(:file_cache_path) { path_join(cache_path, "cache") } + default(:file_cache_path) { PathHelper.join(cache_path, "cache") } # Where backups of chef-managed files should go - default(:file_backup_path) { path_join(cache_path, "backup") } + default(:file_backup_path) { PathHelper.join(cache_path, "backup") } # The chef-client (or solo) lockfile. # # If your `file_cache_path` resides on a NFS (or non-flock()-supporting # fs), it's recommended to set this to something like # '/tmp/chef-client-running.pid' - default(:lockfile) { path_join(file_cache_path, "chef-client-running.pid") } + default(:lockfile) { PathHelper.join(file_cache_path, "chef-client-running.pid") } ## Daemonization Settings ## # What user should Chef run as? @@ -372,7 +346,7 @@ class Chef # Path to the default CA bundle files. default :ssl_ca_path, nil default(:ssl_ca_file) do - if on_windows? and embedded_path = embedded_dir + if Chef::Platform.windows? and embedded_path = embedded_dir cacert_path = File.join(embedded_path, "ssl/certs/cacert.pem") cacert_path if File.exist?(cacert_path) else @@ -384,7 +358,7 @@ class Chef # certificates in this directory will be added to whatever CA bundle ruby # is using. Use this to add self-signed certs for your Chef Server or local # HTTP file servers. - default(:trusted_certs_dir) { config_dir && path_join(config_dir, "trusted_certs") } + default(:trusted_certs_dir) { PathHelper.join(config_dir, "trusted_certs") } # Where should chef-solo download recipes from? default :recipe_url, nil @@ -417,10 +391,6 @@ class Chef # This secret is used to decrypt encrypted data bag items. default(:encrypted_data_bag_secret) do - # We have to check for the existence of the default file before setting it - # since +Chef::Config[:encrypted_data_bag_secret]+ is read by older - # bootstrap templates to determine if the local secret should be uploaded to - # node being bootstrapped. This should be removed in Chef 12. if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret")) platform_specific_path("/etc/chef/encrypted_data_bag_secret") else @@ -456,15 +426,15 @@ class Chef default :validation_client_name, "chef-validator" # When creating a new client via the validation_client account, Chef 11 - # servers allow the client to generate a key pair locally and sent the + # servers allow the client to generate a key pair locally and send the # public key to the server. This is more secure and helps offload work from # the server, enhancing scalability. If enabled and the remote server # implements only the Chef 10 API, client registration will not work # properly. # - # The default value is `false` (Server generates client keys). Set to - # `true` to enable client-side key generation. - default(:local_key_generation) { false } + # The default value is `true`. Set to `false` to disable client-side key + # generation (server generates client keys). + default(:local_key_generation) { true } # Zypper package provider gpg checks. Set to true to enable package # gpg signature checking. This will be default in the @@ -492,7 +462,7 @@ class Chef default(:syntax_check_cache_path) { cache_options[:path] } # Deprecated: - default(:cache_options) { { :path => path_join(file_cache_path, "checksums") } } + default(:cache_options) { { :path => PathHelper.join(file_cache_path, "checksums") } } # Set to false to silence Chef 11 deprecation warnings: default :chef11_deprecation_warnings, true @@ -505,6 +475,9 @@ class Chef default :ssh_gateway, nil default :bootstrap_version, nil default :bootstrap_proxy, nil + default :bootstrap_template, "chef-full" + default :secret, nil + default :secret_file, nil default :identity_file, nil default :host_key_verify, nil default :forward_agent, nil @@ -537,7 +510,7 @@ class Chef # Those lists of regular expressions define what chef considers a # valid user and group name - if on_windows? + if Chef::Platform.windows? set_defaults_for_windows else set_defaults_for_nix @@ -550,7 +523,7 @@ class Chef end def self.windows_home_path - windows_home_path = env['SYSTEMDRIVE'] + env['HOMEPATH'] if env['SYSTEMDRIVE'] && env['HOMEPATH'] + env['SYSTEMDRIVE'] + env['HOMEPATH'] if env['SYSTEMDRIVE'] && env['HOMEPATH'] end # returns a platform specific path to the user home dir if set, otherwise default to current directory. @@ -614,6 +587,51 @@ class Chef default :normal_attribute_whitelist, nil default :override_attribute_whitelist, nil + # Chef requires an English-language UTF-8 locale to function properly. We attempt + # to use the 'locale -a' command and search through a list of preferences until we + # find one that we can use. On Ubuntu systems we should find 'C.UTF-8' and be + # able to use that even if there is no English locale on the server, but Mac, Solaris, + # AIX, etc do not have that locale. We then try to find an English locale and fall + # back to 'C' if we do not. The choice of fallback is pick-your-poison. If we try + # to do the work to return a non-US UTF-8 locale then we fail inside of providers when + # things like 'svn info' return Japanese and we can't parse them. OTOH, if we pick 'C' then + # we will blow up on UTF-8 characters. Between the warn we throw and the Encoding + # exception that ruby will throw it is more obvious what is broken if we drop UTF-8 by + # default rather than drop English. + # + # If there is no 'locale -a' then we return 'en_US.UTF-8' since that is the most commonly + # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. + default :internal_locale do + begin + locales = `locale -a`.split + case + when locales.include?('C.UTF-8') + 'C.UTF-8' + when locales.include?('en_US.UTF-8') + 'en_US.UTF-8' + when locales.include?('en.UTF-8') + 'en.UTF-8' + when guesses = locales.select { |l| l =~ /^en_.*UTF-8$'/ } + guesses.first + else + Chef::Log.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." + 'C' + end + rescue + Chef::Log.warn "No usable locale -a command found, assuming you have en_US.UTF-8 installed." + 'en_US.UTF-8' + end + end + + # Force UTF-8 Encoding, for when we fire up in the 'C' locale or other strange locales (e.g. + # japanese windows encodings). If we do not do this, then knife upload will fail when a cookbook's + # README.md has UTF-8 characters that do not encode in whatever surrounding encoding we have been + # passed. Effectively, the Chef Ecosystem is globally UTF-8 by default. Anyone who wants to be + # able to upload Shift_JIS or ISO-8859-1 files needs to mark *those* files explicitly with + # magic tags to make ruby correctly identify the encoding being used. Changing this default will + # break Chef community cookbooks and is very highly discouraged. + default :ruby_encoding, Encoding::UTF_8 + # If installed via an omnibus installer, this gives the path to the # "embedded" directory which contains all of the software packaged with # omnibus. This is used to locate the cacert.pem file on windows. diff --git a/lib/chef/config_fetcher.rb b/lib/chef/config_fetcher.rb index 1d0693eaa2..a8aad0740d 100644 --- a/lib/chef/config_fetcher.rb +++ b/lib/chef/config_fetcher.rb @@ -7,11 +7,9 @@ class Chef class ConfigFetcher attr_reader :config_location - attr_reader :config_file_jail - def initialize(config_location, config_file_jail=nil) + def initialize(config_location) @config_location = config_location - @config_file_jail = config_file_jail end def fetch_json @@ -48,24 +46,11 @@ class Chef def config_missing? return false if remote_config? - # Check if the config file exists, and check if it is underneath the config file jail - begin - real_config_file = Pathname.new(config_location).realpath.to_s - rescue Errno::ENOENT - return true - end - - # If realpath succeeded, the file exists - return false if !config_file_jail - - begin - real_jail = Pathname.new(config_file_jail).realpath.to_s - rescue Errno::ENOENT - Chef::Log.warn("Config file jail #{config_file_jail} does not exist: will not load any config file.") - return true - end - - !Chef::ChefFS::PathUtils.descendant_of?(real_config_file, real_jail) + # Check if the config file exists + Pathname.new(config_location).realpath.to_s + false + rescue Errno::ENOENT + return true end def http diff --git a/lib/chef/cookbook/cookbook_version_loader.rb b/lib/chef/cookbook/cookbook_version_loader.rb index fac8c80993..47258c4d4e 100644 --- a/lib/chef/cookbook/cookbook_version_loader.rb +++ b/lib/chef/cookbook/cookbook_version_loader.rb @@ -18,21 +18,29 @@ class Chef UPLOADED_COOKBOOK_VERSION_FILE = ".uploaded-cookbook-version.json".freeze - attr_reader :cookbook_name attr_reader :cookbook_settings attr_reader :cookbook_paths attr_reader :metadata_filenames attr_reader :frozen attr_reader :uploaded_cookbook_version_file + attr_reader :cookbook_path + + # The cookbook's name as inferred from its directory. + attr_reader :inferred_cookbook_name + + attr_reader :metadata_error + def initialize(path, chefignore=nil) @cookbook_path = File.expand_path( path ) # cookbook_path from which this was loaded # We keep a list of all cookbook paths that have been merged in - @cookbook_paths = [ @cookbook_path ] - @cookbook_name = File.basename( path ) + @cookbook_paths = [ cookbook_path ] + + @inferred_cookbook_name = File.basename( path ) @chefignore = chefignore - @metadata = Hash.new + @metadata = nil @relative_path = /#{Regexp.escape(@cookbook_path)}\/(.+)$/ + @metadata_loaded = false @cookbook_settings = { :attribute_filenames => {}, :definition_filenames => {}, @@ -46,9 +54,29 @@ class Chef } @metadata_filenames = [] + @metadata_error = nil + end + + # Load the cookbook. Raises an error if the cookbook_path given to the + # constructor doesn't point to a valid cookbook. + def load! + file_paths_map = load + + if empty? + raise Exceptions::CookbookNotFoundInRepo, "The directory #{cookbook_path} does not contain a cookbook" + end + file_paths_map end - def load_cookbooks + # Load the cookbook. Does not raise an error if given a non-cookbook + # directory as the cookbook_path. This behavior is provided for + # compatibility, it is recommended to use #load! instead. + def load + metadata # force lazy evaluation to occur + + # re-raise any exception that occurred when reading the metadata + raise_metadata_error! + load_as(:attribute_filenames, 'attributes', '*.rb') load_as(:definition_filenames, 'definitions', '*.rb') load_as(:recipe_filenames, 'recipes', '*.rb') @@ -61,31 +89,37 @@ class Chef remove_ignored_files - if File.exists?(File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)) - @uploaded_cookbook_version_file = File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE) + if empty? + Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping." + end + @cookbook_settings + end + + alias :load_cookbooks :load + + def metadata_filenames + return @metadata_filenames unless @metadata_filenames.empty? + if File.exists?(File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)) + @uploaded_cookbook_version_file = File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE) end - if File.exists?(File.join(@cookbook_path, "metadata.rb")) - @metadata_filenames << File.join(@cookbook_path, "metadata.rb") - elsif File.exists?(File.join(@cookbook_path, "metadata.json")) - @metadata_filenames << File.join(@cookbook_path, "metadata.json") + if File.exists?(File.join(cookbook_path, "metadata.rb")) + @metadata_filenames << File.join(cookbook_path, "metadata.rb") + elsif File.exists?(File.join(cookbook_path, "metadata.json")) + @metadata_filenames << File.join(cookbook_path, "metadata.json") elsif @uploaded_cookbook_version_file @metadata_filenames << @uploaded_cookbook_version_file end # Set frozen based on .uploaded-cookbook-version.json set_frozen - - if empty? - Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping." - end - @cookbook_settings + @metadata_filenames end def cookbook_version return nil if empty? - Chef::CookbookVersion.new(@cookbook_name.to_sym, *@cookbook_paths).tap do |c| + Chef::CookbookVersion.new(cookbook_name, *cookbook_paths).tap do |c| c.attribute_filenames = cookbook_settings[:attribute_filenames].values c.definition_filenames = cookbook_settings[:definition_filenames].values c.recipe_filenames = cookbook_settings[:recipe_filenames].values @@ -95,16 +129,32 @@ class Chef c.resource_filenames = cookbook_settings[:resource_filenames].values c.provider_filenames = cookbook_settings[:provider_filenames].values c.root_filenames = cookbook_settings[:root_filenames].values - c.metadata_filenames = @metadata_filenames - c.metadata = metadata(c) + c.metadata_filenames = metadata_filenames + c.metadata = metadata + c.freeze_version if @frozen end end + def cookbook_name + # The `name` attribute is now required in metadata, so + # inferred_cookbook_name generally should not be used. Per CHEF-2923, + # we have to not raise errors in cookbook metadata immediately, so that + # users can still `knife cookbook upload some-cookbook` when an + # unrelated cookbook has an error in its metadata. This situation + # could prevent us from reading the `name` attribute from the metadata + # entirely, but the name is used as a hash key in CookbookLoader, so we + # fall back to the inferred name here. + (metadata.name || @inferred_cookbook_name).to_sym + end + # Generates the Cookbook::Metadata object - def metadata(cookbook_version) - @metadata = Chef::Cookbook::Metadata.new(cookbook_version) - @metadata_filenames.each do |metadata_file| + def metadata + return @metadata unless @metadata.nil? + + @metadata = Chef::Cookbook::Metadata.new + + metadata_filenames.each do |metadata_file| case metadata_file when /\.rb$/ apply_ruby_metadata(metadata_file) @@ -116,51 +166,75 @@ class Chef raise RuntimeError, "Invalid metadata file: #{metadata_file} for cookbook: #{cookbook_version}" end end + + @metadata + + # Rescue errors so that users can upload cookbooks via `knife cookbook + # upload` even if some cookbooks in their chef-repo have errors in + # their metadata. We only rescue StandardError because you have to be + # doing something *really* terrible to raise an exception that inherits + # directly from Exception in your metadata.rb file. + rescue StandardError => e + @metadata_error = e @metadata end + def raise_metadata_error! + raise @metadata_error unless @metadata_error.nil? + # Metadata won't be valid if the cookbook is empty. If the cookbook is + # actually empty, a metadata error here would be misleading, so don't + # raise it (if called by #load!, a different error is raised). + if !empty? && !metadata.valid? + message = "Cookbook loaded at path(s) [#{@cookbook_paths.join(', ')}] has invalid metadata: #{metadata.errors.join('; ')}" + raise Exceptions::MetadataNotValid, message + end + false + end + def empty? - @cookbook_settings.values.all? { |files_hash| files_hash.empty? } && @metadata_filenames.size == 0 + cookbook_settings.values.all? { |files_hash| files_hash.empty? } && metadata_filenames.size == 0 end def merge!(other_cookbook_loader) other_cookbook_settings = other_cookbook_loader.cookbook_settings - @cookbook_settings.each do |file_type, file_list| + cookbook_settings.each do |file_type, file_list| file_list.merge!(other_cookbook_settings[file_type]) end - @metadata_filenames.concat(other_cookbook_loader.metadata_filenames) + metadata_filenames.concat(other_cookbook_loader.metadata_filenames) @cookbook_paths += other_cookbook_loader.cookbook_paths @frozen = true if other_cookbook_loader.frozen + @metadata = nil # reset metadata so it gets reloaded and all metadata files applied. + self end def chefignore - @chefignore ||= Chefignore.new(File.basename(@cookbook_path)) + @chefignore ||= Chefignore.new(File.basename(cookbook_path)) end def load_root_files - Dir.glob(File.join(@cookbook_path, '*'), File::FNM_DOTMATCH).each do |file| + Dir.glob(File.join(cookbook_path, '*'), File::FNM_DOTMATCH).each do |file| next if File.directory?(file) next if File.basename(file) == UPLOADED_COOKBOOK_VERSION_FILE - @cookbook_settings[:root_filenames][file[@relative_path, 1]] = file + cookbook_settings[:root_filenames][file[@relative_path, 1]] = file end end def load_recursively_as(category, category_dir, glob) - file_spec = File.join(@cookbook_path, category_dir, '**', glob) + file_spec = File.join(cookbook_path, category_dir, '**', glob) Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file| next if File.directory?(file) - @cookbook_settings[category][file[@relative_path, 1]] = file + cookbook_settings[category][file[@relative_path, 1]] = file end end def load_as(category, *path_glob) - Dir[File.join(@cookbook_path, *path_glob)].each do |file| - @cookbook_settings[category][file[@relative_path, 1]] = file + Dir[File.join(cookbook_path, *path_glob)].each do |file| + cookbook_settings[category][file[@relative_path, 1]] = file end end def remove_ignored_files - @cookbook_settings.each_value do |file_list| + cookbook_settings.each_value do |file_list| file_list.reject! do |relative_path, full_path| chefignore.ignored?(relative_path) end @@ -171,7 +245,7 @@ class Chef begin @metadata.from_file(file) rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Error evaluating metadata.rb for #@cookbook_name in " + file) + Chef::Log.error("Error evaluating metadata.rb for #@inferred_cookbook_name in " + file) raise end end @@ -180,17 +254,26 @@ class Chef begin @metadata.from_json(IO.read(file)) rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file) + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file) raise end end def apply_json_cookbook_version_metadata(file) begin - data = Chef::JSONCompat.from_json(IO.read(file), :create_additions => false) + data = Chef::JSONCompat.parse(IO.read(file)) @metadata.from_hash(data['metadata']) + # the JSON cookbok metadata file is only used by chef-zero. + # The Chef Server API currently does not enforce that the metadata + # have a `name` field, but that will cause an error when attempting + # to load the cookbook. To keep compatibility, we fake it by setting + # the metadata name from the cookbook version object's name. + # + # This behavior can be removed if/when Chef Server enforces that the + # metadata contains a name key. + @metadata.name(data['cookbook_name']) unless data['metadata'].key?('name') rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file) + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file) raise end end @@ -198,10 +281,10 @@ class Chef def set_frozen if uploaded_cookbook_version_file begin - data = Chef::JSONCompat.from_json(IO.read(uploaded_cookbook_version_file), :create_additions => false) + data = Chef::JSONCompat.parse(IO.read(uploaded_cookbook_version_file)) @frozen = data['frozen?'] rescue Chef::Exceptions::JSON::ParseError - Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in #{uploaded_cookbook_version_file}") + Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in #{uploaded_cookbook_version_file}") raise end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 8d3f8b84aa..3964354d50 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -18,6 +18,7 @@ # limitations under the License. # +require 'chef/exceptions' require 'chef/mash' require 'chef/mixin/from_file' require 'chef/mixin/params_validate' @@ -67,18 +68,17 @@ class Chef include Chef::Mixin::ParamsValidate include Chef::Mixin::FromFile - attr_reader :cookbook, - :platforms, - :dependencies, - :recommendations, - :suggestions, - :conflicting, - :providing, - :replacing, - :attributes, - :groupings, - :recipes, - :version + attr_reader :platforms + attr_reader :dependencies + attr_reader :recommendations + attr_reader :suggestions + attr_reader :conflicting + attr_reader :providing + attr_reader :replacing + attr_reader :attributes + attr_reader :groupings + attr_reader :recipes + attr_reader :version # Builds a new Chef::Cookbook::Metadata object. # @@ -90,14 +90,16 @@ class Chef # # === Returns # metadata<Chef::Cookbook::Metadata> - def initialize(cookbook=nil, maintainer='YOUR_COMPANY_NAME', maintainer_email='YOUR_EMAIL', license='none') - @cookbook = cookbook - @name = cookbook ? cookbook.name : "" - @long_description = "" - self.maintainer(maintainer) - self.maintainer_email(maintainer_email) - self.license(license) - self.description('A fabulous new cookbook') + def initialize + @name = nil + + @description = '' + @long_description = '' + @license = 'All rights reserved' + + @maintainer = nil + @maintainer_email = nil + @platforms = Mash.new @dependencies = Mash.new @recommendations = Mash.new @@ -108,15 +110,9 @@ class Chef @attributes = Mash.new @groupings = Mash.new @recipes = Mash.new - @version = Version.new "0.0.0" - if cookbook - @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e| - e = self.name.to_s if e =~ /::default$/ - r[e] ||= "" - self.provides e - r - end - end + @version = Version.new("0.0.0") + + @errors = [] end def ==(other) @@ -125,6 +121,32 @@ class Chef end end + # Whether this metadata is valid. In order to be valid, all required + # fields must be set. Chef's validation implementation checks the content + # of a given field when setting (and raises an error if the content does + # not meet the criteria), so the content of the fields is not considered + # when checking validity. + # + # === Returns + # valid<Boolean>:: Whether this metadata object is valid + def valid? + run_validation + @errors.empty? + end + + # A list of validation errors for this metadata object. See #valid? for + # comments about the validation criteria. + # + # If there are any validation errors, one or more error strings will be + # returned. Otherwise an empty array is returned. + # + # === Returns + # error messages<Array>:: Whether this metadata object is valid + def errors + run_validation + @errors + end + # Sets the cookbooks maintainer, or returns it. # # === Parameters @@ -365,6 +387,32 @@ class Chef @recipes[name] = description end + # Sets the cookbook's recipes to the list of recipes in the given + # +cookbook+. Any recipe that already has a description (if set by the + # #recipe method) will not be updated. + # + # === Parameters + # cookbook<CookbookVersion>:: CookbookVersion object representing the cookbook + # description<String>:: The description of the recipe + # + # === Returns + # recipe_unqualified_names<Array>:: An array of the recipe names given by the cookbook + def recipes_from_cookbook_version(cookbook) + cookbook.fully_qualified_recipe_names.map do |recipe_name| + unqualified_name = + if recipe_name =~ /::default$/ + self.name.to_s + else + recipe_name + end + + @recipes[unqualified_name] ||= "" + provides(unqualified_name) + + unqualified_name + end + end + # Adds an attribute )hat a user needs to configure for this cookbook. Takes # a name (with the / notation for a nested attribute), followed by any of # these options @@ -480,9 +528,9 @@ class Chef def self.validate_json(json_str) o = Chef::JSONCompat.from_json(json_str) metadata = new() - VERSION_CONSTRAINTS.each do |method_name, hash_key| - if constraints = o[hash_key] - constraints.each do |cb_name, constraints| + VERSION_CONSTRAINTS.each do |dependency_type, hash_key| + if dependency_group = o[hash_key] + dependency_group.each do |cb_name, constraints| metadata.send(method_name, cb_name, *Array(constraints)) end end @@ -497,6 +545,12 @@ class Chef private + def run_validation + if name.nil? + @errors = ["The `name' attribute is required in cookbook metadata"] + end + end + def new_args_format(caller_name, dep_name, version_constraints) if version_constraints.empty? ">= 0.0.0" diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb index 27cf978acb..d569cdd008 100644 --- a/lib/chef/cookbook_loader.rb +++ b/lib/chef/cookbook_loader.rb @@ -50,6 +50,9 @@ class Chef repo_path = File.expand_path(repo_path) end + @preloaded_cookbooks = false + @loaders_by_name = {} + # Used to track which cookbooks appear in multiple places in the cookbook repos # and are merged in to a single cookbook by file shadowing. This behavior is # deprecated, so users of this class may issue warnings to the user by checking @@ -64,25 +67,25 @@ class Chef end def load_cookbooks - @repo_paths.each do |repo_path| - Dir[File.join(repo_path, "*")].each do |cookbook_path| - load_cookbook(File.basename(cookbook_path), [repo_path]) - end + preload_cookbooks + @loaders_by_name.each do |cookbook_name, _loaders| + load_cookbook(cookbook_name) end @cookbooks_by_name end - def load_cookbook(cookbook_name, repo_paths=nil) - repo_paths ||= @repo_paths - repo_paths.each do |repo_path| - @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path) - cookbook_path = File.join(repo_path, cookbook_name.to_s) - next unless File.directory?(cookbook_path) and Dir[File.join(repo_path, "*")].include?(cookbook_path) - loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path]) - loader.load_cookbooks + def load_cookbook(cookbook_name) + preload_cookbooks + + return nil unless @loaders_by_name.key?(cookbook_name.to_s) + + cookbook_loaders_for(cookbook_name).each do |loader| + loader.load + next if loader.empty? - cookbook_name = loader.cookbook_name - @cookbooks_paths[cookbook_name] << cookbook_path # for deprecation warnings + + @cookbooks_paths[cookbook_name] << loader.cookbook_path # for deprecation warnings + if @loaded_cookbooks.key?(cookbook_name) @merged_cookbooks << cookbook_name # for deprecation warnings @loaded_cookbooks[cookbook_name].merge!(loader) @@ -130,5 +133,50 @@ class Chef end alias :cookbooks :values + private + + def preload_cookbooks + return false if @preloaded_cookbooks + + all_directories_in_repo_paths.each do |cookbook_path| + preload_cookbook(cookbook_path) + end + @preloaded_cookbooks = true + true + end + + def preload_cookbook(cookbook_path) + repo_path = File.dirname(cookbook_path) + @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path) + loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path]) + add_cookbook_loader(loader) + end + + def all_directories_in_repo_paths + @all_directories_in_repo_paths ||= + all_files_in_repo_paths.select { |path| File.directory?(path) } + end + + def all_files_in_repo_paths + @all_files_in_repo_paths ||= + begin + @repo_paths.inject([]) do |all_children, repo_path| + all_children += Dir[File.join(repo_path, "*")] + end + end + end + + def add_cookbook_loader(loader) + cookbook_name = loader.cookbook_name + + @loaders_by_name[cookbook_name.to_s] ||= [] + @loaders_by_name[cookbook_name.to_s] << loader + loader + end + + def cookbook_loaders_for(cookbook_name) + @loaders_by_name[cookbook_name.to_s] + end + end end diff --git a/lib/chef/cookbook_site_streaming_uploader.rb b/lib/chef/cookbook_site_streaming_uploader.rb index 92193fee33..c444c8251b 100644 --- a/lib/chef/cookbook_site_streaming_uploader.rb +++ b/lib/chef/cookbook_site_streaming_uploader.rb @@ -143,7 +143,7 @@ class Chef http = Net::HTTP.new(url.host, url.port) if url.scheme == "https" http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.verify_mode = verify_mode end res = http.request(req) #res = http.start {|http_proc| http_proc.request(req) } @@ -165,6 +165,17 @@ class Chef res end + private + + def verify_mode + verify_mode = Chef::Config[:ssl_verify_mode] + if verify_mode == :verify_none + OpenSSL::SSL::VERIFY_NONE + elsif verify_mode == :verify_peer + OpenSSL::SSL::VERIFY_PEER + end + end + end class StreamPart diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 4482f778c1..76e6d152b2 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -51,10 +51,14 @@ class Chef attr_accessor :provider_filenames attr_accessor :root_filenames attr_accessor :name - attr_accessor :metadata attr_accessor :metadata_filenames attr_accessor :status + # A Chef::Cookbook::Metadata object. It has a setter that fixes up the + # metadata to add descriptions of the recipes contained in this + # CookbookVersion. + attr_reader :metadata + # attribute_filenames also has a setter that has non-default # functionality. attr_reader :attribute_filenames @@ -195,6 +199,12 @@ class Chef attribute_filenames end + def metadata=(metadata) + @metadata = metadata + @metadata.recipes_from_cookbook_version(self) + @metadata + end + ## BACKCOMPAT/DEPRECATED - Remove these and fix breakage before release [DAN - 5/20/2010]## alias :attribute_files :attribute_filenames alias :attribute_files= :attribute_filenames= diff --git a/lib/chef/digester.rb b/lib/chef/digester.rb index 669ff8b8a5..0805bccee3 100644 --- a/lib/chef/digester.rb +++ b/lib/chef/digester.rb @@ -18,14 +18,11 @@ # limitations under the License. # -require 'digest' +require 'openssl' class Chef class Digester - - def self.instance - @instance ||= new - end + include Singleton def self.checksum_for_file(*args) instance.checksum_for_file(*args) @@ -40,7 +37,7 @@ class Chef end def generate_checksum(file) - checksum_file(file, Digest::SHA256.new) + checksum_file(file, OpenSSL::Digest::SHA256.new) end def self.generate_md5_checksum_for_file(*args) @@ -48,11 +45,11 @@ class Chef end def generate_md5_checksum_for_file(file) - checksum_file(file, Digest::MD5.new) + checksum_file(file, OpenSSL::Digest::MD5.new) end def generate_md5_checksum(io) - checksum_io(io, Digest::MD5.new) + checksum_io(io, OpenSSL::Digest::MD5.new) end private @@ -70,4 +67,3 @@ class Chef end end - diff --git a/lib/chef/dsl/data_query.rb b/lib/chef/dsl/data_query.rb index 65e7b185a7..3dafbca6bf 100644 --- a/lib/chef/dsl/data_query.rb +++ b/lib/chef/dsl/data_query.rb @@ -53,14 +53,60 @@ class Chef raise end - def data_bag_item(bag, item) + def data_bag_item(bag, item, secret=nil) DataBag.validate_name!(bag.to_s) DataBagItem.validate_id!(item) - DataBagItem.load(bag, item) + + item = DataBagItem.load(bag, item) + if encrypted?(item.raw_data) + Log.debug("Data bag item looks encrypted: #{bag.inspect} #{item.inspect}") + + # Try to load the data bag item secret, if secret is not provided. + # Chef::EncryptedDataBagItem.load_secret may throw a variety of errors. + begin + secret ||= EncryptedDataBagItem.load_secret + item = EncryptedDataBagItem.new(item.raw_data, secret) + rescue Exception + Log.error("Failed to load secret for encrypted data bag item: #{bag.inspect} #{item.inspect}") + raise + end + end + + item rescue Exception Log.error("Failed to load data bag item: #{bag.inspect} #{item.inspect}") raise end + + private + + # Tries to autodetect if the item's raw hash appears to be encrypted. + def encrypted?(raw_data) + data = raw_data.reject { |k, _| k == "id" } # Remove the "id" key. + # Assume hashes containing only the "id" key are not encrypted. + # Otherwise, remove the keys that don't appear to be encrypted and compare + # the result with the hash. If some entry has been removed, then some entry + # doesn't appear to be encrypted and we assume the entire hash is not encrypted. + data.empty? ? false : data.reject { |_, v| !looks_like_encrypted?(v) } == data + end + + # Checks if data looks like it has been encrypted by + # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor. Returns + # true only when there is an exact match between the VersionXEncryptor + # keys and the hash's keys. + def looks_like_encrypted?(data) + return false unless data.is_a?(Hash) && data.has_key?("version") + case data["version"] + when 1 + Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor.encryptor_keys.sort == data.keys.sort + when 2 + Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor.encryptor_keys.sort == data.keys.sort + when 3 + Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.encryptor_keys.sort == data.keys.sort + else + false # version means something else... assume not encrypted. + end + end end end end @@ -68,4 +114,3 @@ end # **DEPRECATED** # This used to be part of chef/mixin/language. Load the file to activate the deprecation code. require 'chef/mixin/language' - diff --git a/lib/chef/dsl/platform_introspection.rb b/lib/chef/dsl/platform_introspection.rb index 33aa451f30..2a52010a70 100644 --- a/lib/chef/dsl/platform_introspection.rb +++ b/lib/chef/dsl/platform_introspection.rb @@ -50,8 +50,12 @@ class Chef def value_for_node(node) platform, version = node[:platform].to_s, node[:platform_version].to_s + # Check if we match a version constraint via Chef::VersionConstraint and Chef::Version::Platform + matched_value = match_versions(node) if @values.key?(platform) && @values[platform].key?(version) @values[platform][version] + elsif matched_value + matched_value elsif @values.key?(platform) && @values[platform].key?("default") @values[platform]["default"] elsif @values.key?("default") @@ -63,6 +67,44 @@ class Chef private + def match_versions(node) + begin + platform, version = node[:platform].to_s, node[:platform_version].to_s + return nil unless @values.key?(platform) + node_version = Chef::Version::Platform.new(version) + key_matches = [] + keys = @values[platform].keys + keys.each do |k| + begin + if Chef::VersionConstraint.new(k).include?(node_version) + key_matches << k + end + rescue Chef::Exceptions::InvalidVersionConstraint => e + Chef::Log.debug "Caught InvalidVersionConstraint. This means that a key in value_for_platform cannot be interpreted as a Chef::VersionConstraint." + Chef::Log.debug(e) + end + end + return @values[platform][version] if key_matches.include?(version) + case key_matches.length + when 0 + return nil + when 1 + return @values[platform][key_matches.first] + else + raise "Multiple matches detected for #{platform} with values #{@values}. The matches are: #{key_matches}" + end + rescue Chef::Exceptions::InvalidCookbookVersion => e + # Lets not break because someone passes a weird string like 'default' :) + Chef::Log.debug(e) + Chef::Log.debug "InvalidCookbookVersion exceptions are common and expected here: the generic constraint matcher attempted to match something which is not a constraint. Moving on to next version or constraint" + return nil + rescue Chef::Exceptions::InvalidPlatformVersion => e + Chef::Log.debug "Caught InvalidPlatformVersion, this means that Chef::Version::Platform does not know how to turn #{node_version} into an x.y.z format" + Chef::Log.debug(e) + return nil + end + end + def set(platforms, value) if platforms.to_s == 'default' @values["default"] = value diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index 23cfbd558c..3282320b8c 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -85,6 +85,20 @@ class Chef resource = build_resource(type, name, created_at, &resource_attrs_block) + # Some resources (freebsd_package) can be invoked with multiple names + # (package || freebsd_package). + # https://github.com/opscode/chef/issues/1773 + # For these resources we want to make sure + # their key in resource collection is same as the name they are declared + # as. Since this might be a breaking change for resources that define + # customer to_s methods, we are working around the issue by letting + # resources know of their created_as_type until this issue is fixed in + # Chef 12: + # https://github.com/opscode/chef/issues/1817 + if resource.respond_to?(:created_as_type=) + resource.created_as_type = type + end + run_context.resource_collection.insert(resource) resource end diff --git a/lib/chef/encrypted_data_bag_item.rb b/lib/chef/encrypted_data_bag_item.rb index f722b5dc38..120eb2a4ae 100644 --- a/lib/chef/encrypted_data_bag_item.rb +++ b/lib/chef/encrypted_data_bag_item.rb @@ -128,7 +128,7 @@ class Chef::EncryptedDataBagItem def self.load_secret(path=nil) path ||= Chef::Config[:encrypted_data_bag_secret] if !path - raise ArgumentError, "No secret specified to load_secret and no secret found at #{Chef::Config.platform_specific_path('/etc/chef/encrypted_data_bag_secret')}" + raise ArgumentError, "No secret specified and no secret found at #{Chef::Config.platform_specific_path('/etc/chef/encrypted_data_bag_secret')}" end secret = case path when /^\w+:\/\// diff --git a/lib/chef/encrypted_data_bag_item/decryptor.rb b/lib/chef/encrypted_data_bag_item/decryptor.rb index 97a166b932..86b99cc284 100644 --- a/lib/chef/encrypted_data_bag_item/decryptor.rb +++ b/lib/chef/encrypted_data_bag_item/decryptor.rb @@ -152,7 +152,7 @@ class Chef::EncryptedDataBagItem d = OpenSSL::Cipher.new(algorithm) d.decrypt # We must set key before iv: https://bugs.ruby-lang.org/issues/8221 - d.key = Digest::SHA256.digest(key) + d.key = OpenSSL::Digest::SHA256.digest(key) d.iv = iv d end diff --git a/lib/chef/encrypted_data_bag_item/encryptor.rb b/lib/chef/encrypted_data_bag_item/encryptor.rb index 673b52a3c3..034413c1bd 100644 --- a/lib/chef/encrypted_data_bag_item/encryptor.rb +++ b/lib/chef/encrypted_data_bag_item/encryptor.rb @@ -102,7 +102,7 @@ class Chef::EncryptedDataBagItem encryptor = OpenSSL::Cipher.new(algorithm) encryptor.encrypt # We must set key before iv: https://bugs.ruby-lang.org/issues/8221 - encryptor.key = Digest::SHA256.digest(key) + encryptor.key = OpenSSL::Digest::SHA256.digest(key) @iv ||= encryptor.random_iv encryptor.iv = @iv encryptor @@ -125,6 +125,10 @@ class Chef::EncryptedDataBagItem def serialized_data FFI_Yajl::Encoder.encode(:json_wrapper => plaintext_data) end + + def self.encryptor_keys + %w( encrypted_data iv version cipher ) + end end class Version2Encryptor < Version1Encryptor @@ -149,6 +153,10 @@ class Chef::EncryptedDataBagItem Base64.encode64(raw_hmac) end end + + def self.encryptor_keys + super + %w( hmac ) + end end class Version3Encryptor < Version1Encryptor @@ -207,6 +215,10 @@ class Chef::EncryptedDataBagItem end end + def self.encryptor_keys + super + %w( auth_tag ) + end + end end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index bd6d887884..23e223f204 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -83,6 +83,7 @@ class Chef class RequestedUIDUnavailable < RuntimeError; end class InvalidHomeDirectory < ArgumentError; end class DsclCommandFailed < RuntimeError; end + class PlistUtilCommandFailed < RuntimeError; end class UserIDNotFound < ArgumentError; end class GroupIDNotFound < ArgumentError; end class ConflictingMembersInGroup < ArgumentError; end @@ -132,6 +133,8 @@ class Chef # Version constraints are not allowed in chef-solo class IllegalVersionConstraint < NotImplementedError; end + class MetadataNotValid < StandardError; end + # File operation attempted but no permissions to perform it class InsufficientPermissions < RuntimeError; end @@ -344,5 +347,7 @@ class Chef class EncodeError < RuntimeError; end class ParseError < RuntimeError; end end + + class InvalidSearchQuery < ArgumentError; end end end diff --git a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb index cfee8a2151..aa5eb8485d 100644 --- a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb @@ -56,7 +56,7 @@ class Chef # * could be no read on the node error_description.section("Authorization Error",<<-E) This client is not authorized to read some of the information required to -access its coobooks (HTTP 403). +access its cookbooks (HTTP 403). To access its cookbooks, a client needs to be able to read its environment and all of the cookbooks in its expanded run list. diff --git a/lib/chef/http/basic_client.rb b/lib/chef/http/basic_client.rb index b9a82499ed..f0f5151dbd 100644 --- a/lib/chef/http/basic_client.rb +++ b/lib/chef/http/basic_client.rb @@ -71,6 +71,20 @@ class Chef end Chef::Log.debug("---- End HTTP Status/Header Data ----") + # For non-400's, log the request and response bodies + if !response.code || !response.code.start_with?('2') + if response.body + Chef::Log.debug("---- HTTP Response Body ----") + Chef::Log.debug(response.body) + Chef::Log.debug("---- End HTTP Response Body -----") + end + if req_body + Chef::Log.debug("---- HTTP Request Body ----") + Chef::Log.debug(req_body) + Chef::Log.debug("---- End HTTP Request Body ----") + end + end + yield response if block_given? # http_client.request may not have the return signature we want, so # force the issue: diff --git a/lib/chef/http/json_output.rb b/lib/chef/http/json_output.rb index ae164c6aed..069eb6a87f 100644 --- a/lib/chef/http/json_output.rb +++ b/lib/chef/http/json_output.rb @@ -26,6 +26,9 @@ class Chef # Middleware that takes an HTTP response, parses it as JSON if possible. class JSONOutput + attr_accessor :raw_output + attr_accessor :inflate_json_class + def initialize(opts={}) @raw_output = opts[:raw_output] @inflate_json_class = opts[:inflate_json_class] @@ -44,13 +47,15 @@ class Chef # needed to keep conditional get stuff working correctly. return [http_response, rest_request, return_value] if return_value == false if http_response['content-type'] =~ /json/ - if @raw_output + if http_response.body.nil? + return_value = nil + elsif raw_output return_value = http_response.body.to_s else - if @inflate_json_class + if inflate_json_class return_value = Chef::JSONCompat.from_json(http_response.body.chomp) else - return_value = Chef::JSONCompat.from_json(http_response.body.chomp, :create_additions => false) + return_value = Chef::JSONCompat.parse(http_response.body.chomp) end end [http_response, rest_request, return_value] diff --git a/lib/chef/json_compat.rb b/lib/chef/json_compat.rb index ddccfe5fcb..e92d5c36ae 100644 --- a/lib/chef/json_compat.rb +++ b/lib/chef/json_compat.rb @@ -101,7 +101,7 @@ class Chef def to_json(obj, opts = nil) begin FFI_Yajl::Encoder.encode(obj, opts) - rescue FFI_Yajl::EndodeError => e + rescue FFI_Yajl::EncodeError => e raise Chef::Exceptions::JSON::EncodeError, e.message end end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 038ab61715..6421384f01 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -20,7 +20,7 @@ require 'forwardable' require 'chef/version' require 'mixlib/cli' -require 'chef/config_fetcher' +require 'chef/workstation_config_loader' require 'chef/mixin/convert_to_class_name' require 'chef/mixin/path_sanity' require 'chef/knife/core/subcommand_loader' @@ -159,6 +159,27 @@ class Chef end end + # Shared with subclasses + @@chef_config_dir = nil + + def self.load_config(explicit_config_file) + config_loader = WorkstationConfigLoader.new(explicit_config_file, Chef::Log) + Chef::Log.debug("Using configuration from #{config_loader.config_location}") + config_loader.load + + ui.warn("No knife configuration file found") if config_loader.no_config_found? + @@chef_config_dir = config_loader.chef_config_dir + + config_loader + rescue Exceptions::ConfigurationError => e + ui.error(ui.color("CONFIGURATION ERROR:", :red) + e.message) + exit 1 + end + + def self.chef_config_dir + @@chef_config_dir + end + # Run knife for the given +args+ (ARGV), adding +options+ to the list of # CLI options that the subcommand knows how to handle. # ===Arguments @@ -166,6 +187,16 @@ class Chef # options::: A Mixlib::CLI option parser hash. These +options+ are how # subcommands know about global knife CLI options def self.run(args, options={}) + # Fallback debug logging. Normally the logger isn't configured until we + # read the config, but this means any logging that happens before the + # config file is read may be lost. If the KNIFE_DEBUG variable is set, we + # setup the logger for debug logging to stderr immediately to catch info + # from early in the setup process. + if ENV['KNIFE_DEBUG'] + Chef::Log.init($stderr) + Chef::Log.level(:debug) + end + load_commands subcommand_class = subcommand_class_from(args) subcommand_class.options = options.merge!(subcommand_class.options) @@ -239,40 +270,12 @@ class Chef exit 10 end - def self.working_directory - a = if Chef::Platform.windows? - ENV['CD'] - else - ENV['PWD'] - end || Dir.pwd - - a - end - def self.reset_config_path! @@chef_config_dir = nil end reset_config_path! - - # search upward from current_dir until .chef directory is found - def self.chef_config_dir - if @@chef_config_dir.nil? # share this with subclasses - @@chef_config_dir = false - full_path = working_directory.split(File::SEPARATOR) - (full_path.length - 1).downto(0) do |i| - candidate_directory = File.join(full_path[0..i] + [".chef" ]) - if File.exist?(candidate_directory) && File.directory?(candidate_directory) - @@chef_config_dir = candidate_directory - break - end - end - end - @@chef_config_dir - end - - public # Create a new instance of the current class configured for the given @@ -322,39 +325,6 @@ class Chef config_file_settings end - def self.config_fetcher(candidate_config) - Chef::ConfigFetcher.new(candidate_config, Chef::Config.config_file_jail) - end - - def self.locate_config_file - candidate_configs = [] - - # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine) - if ENV['KNIFE_HOME'] - candidate_configs << File.join(ENV['KNIFE_HOME'], 'knife.rb') - end - # Look for $PWD/knife.rb - if Dir.pwd - candidate_configs << File.join(Dir.pwd, 'knife.rb') - end - # Look for $UPWARD/.chef/knife.rb - if chef_config_dir - candidate_configs << File.join(chef_config_dir, 'knife.rb') - end - # Look for $HOME/.chef/knife.rb - if ENV['HOME'] - candidate_configs << File.join(ENV['HOME'], '.chef', 'knife.rb') - end - - candidate_configs.each do | candidate_config | - fetcher = config_fetcher(candidate_config) - if !fetcher.config_missing? - return candidate_config - end - end - return nil - end - # Apply Config in this order: # defaults from mixlib-cli # settings from config file, via Chef::Config[:knife] @@ -386,6 +356,8 @@ class Chef Chef::Config[:log_level] = :debug end + Chef::Config[:log_level] = :debug if ENV['KNIFE_DEBUG'] + Chef::Config[:node_name] = config[:node_name] if config[:node_name] Chef::Config[:client_key] = config[:client_key] if config[:client_key] Chef::Config[:chef_server_url] = config[:chef_server_url] if config[:chef_server_url] @@ -416,70 +388,13 @@ class Chef end def configure_chef - if !config[:config_file] - located_config_file = self.class.locate_config_file - config[:config_file] = located_config_file if located_config_file - end - - # Don't try to load a knife.rb if it wasn't specified. - if config[:config_file] - Chef::Config.config_file = config[:config_file] - fetcher = Chef::ConfigFetcher.new(config[:config_file], Chef::Config.config_file_jail) - if fetcher.config_missing? - ui.error("Specified config file #{config[:config_file]} does not exist#{Chef::Config.config_file_jail ? " or is not under config file jail #{Chef::Config.config_file_jail}" : ""}!") - exit 1 - end - Chef::Log.debug("Using configuration from #{config[:config_file]}") - read_config(fetcher.read_config, config[:config_file]) - else - # ...but do log a message if no config was found. - Chef::Config[:color] = config[:color] - ui.warn("No knife configuration file found") - end + config_loader = self.class.load_config(config[:config_file]) + config[:config_file] = config_loader.config_location merge_configs apply_computed_config end - def read_config(config_content, config_file_path) - Chef::Config.from_string(config_content, config_file_path) - rescue SyntaxError => e - ui.error "You have invalid ruby syntax in your config file #{config_file_path}" - ui.info(ui.color(e.message, :red)) - if file_line = e.message[/#{Regexp.escape(config_file_path)}:[\d]+/] - line = file_line[/:([\d]+)$/, 1].to_i - highlight_config_error(config_file_path, line) - end - exit 1 - rescue Exception => e - ui.error "You have an error in your config file #{config_file_path}" - ui.info "#{e.class.name}: #{e.message}" - filtered_trace = e.backtrace.grep(/#{Regexp.escape(config_file_path)}/) - filtered_trace.each {|line| ui.msg(" " + ui.color(line, :red))} - if !filtered_trace.empty? - line_nr = filtered_trace.first[/#{Regexp.escape(config_file_path)}:([\d]+)/, 1] - highlight_config_error(config_file_path, line_nr.to_i) - end - - exit 1 - end - - def highlight_config_error(file, line) - config_file_lines = [] - IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"} - if line == 1 - lines = config_file_lines[0..3] - lines[0] = ui.color(lines[0], :red) - else - lines = config_file_lines[Range.new(line - 2, line)] - lines[1] = ui.color(lines[1], :red) - end - ui.msg "" - ui.msg ui.color(" # #{file}", :white) - lines.each {|l| ui.msg(l)} - ui.msg "" - end - def show_usage stdout.puts("USAGE: " + self.opt_parser.to_s) end diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index d3d45bad4b..36a0fc1e47 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -94,11 +94,20 @@ class Chef :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode", :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np } + # DEPR: Remove this option in Chef 13 option :distro, :short => "-d DISTRO", :long => "--distro DISTRO", - :description => "Bootstrap a distro using a template", - :default => "chef-full" + :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.", + :proc => Proc.new { |v| + Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use -t / --bootstrap-template option instead.") + v + } + + option :bootstrap_template, + :short => "-t TEMPLATE", + :long => "--bootstrap-template TEMPLATE", + :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates." option :use_sudo, :long => "--sudo", @@ -110,10 +119,14 @@ class Chef :description => "Execute the bootstrap via sudo with password", :boolean => false + # DEPR: Remove this option in Chef 13 option :template_file, :long => "--template-file TEMPLATE", - :description => "Full path to location of template to use", - :default => false + :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.", + :proc => Proc.new { |v| + Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use -t / --bootstrap-template option instead.") + v + } option :run_list, :short => "-r RUN_LIST", @@ -141,7 +154,8 @@ class Chef :proc => Proc.new { |h| Chef::Config[:knife][:hints] ||= Hash.new name, path = h.split("=") - Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new } + Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new + } option :secret, :short => "-s SECRET", @@ -174,53 +188,75 @@ class Chef :description => "Add options to curl when install chef-client", :proc => Proc.new { |co| Chef::Config[:knife][:bootstrap_curl_options] = co } - def find_template(template=nil) - # Are we bootstrapping using an already shipped template? - if config[:template_file] - bootstrap_files = config[:template_file] - else - bootstrap_files = [] - bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{config[:distro]}.erb") - bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{config[:distro]}.erb") if Knife.chef_config_dir - bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{config[:distro]}.erb") if ENV['HOME'] - bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{config[:distro]}.erb")) - bootstrap_files.flatten! + option :node_ssl_verify_mode, + :long => "--node-ssl-verify-mode [peer|none]", + :description => "Whether or not to verify the SSL cert for all HTTPS requests.", + :proc => Proc.new { |v| + valid_values = ["none", "peer"] + unless valid_values.include?(v) + raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}" + end + } + + option :node_verify_api_cert, + :long => "--[no-]node-verify-api-cert", + :description => "Verify the SSL cert for HTTPS requests to the Chef server API.", + :boolean => true + + def bootstrap_template + # For some reason knife.merge_configs doesn't pick up the default values from + # Chef::Config[:knife][:bootstrap_template] unless Chef::Config[:knife][:bootstrap_template] + # is forced to pick up the values before calling merge_configs. + # We therefore have Chef::Config[:knife][:bootstrap_template] to pick up the defaults + # if no option is specified. + config[:bootstrap_template] || config[:distro] || config[:template_file] || Chef::Config[:knife][:bootstrap_template] + end + + def find_template + template = bootstrap_template + + # Use the template directly if it's a path to an actual file + if File.exists?(template) + Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}") + return template + end - template = Array(bootstrap_files).find do |bootstrap_template| + # Otherwise search the template directories until we find the right one + bootstrap_files = [] + bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{template}.erb") + bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir + bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{template}.erb") if ENV['HOME'] + bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb")) + bootstrap_files.flatten! + + template_file = Array(bootstrap_files).find do |bootstrap_template| Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}") File.exists?(bootstrap_template) end - unless template - ui.info("Can not find bootstrap definition for #{config[:distro]}") + unless template_file + ui.info("Can not find bootstrap definition for #{template}") raise Errno::ENOENT end - Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}") + Chef::Log.debug("Found bootstrap template in #{File.dirname(template_file)}") - template + template_file end - def render_template(template=nil) + def render_template + template_file = find_template + template = IO.read(template_file).chomp context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config) Erubis::Eruby.new(template).evaluate(context) end - def read_template - IO.read(@template_file).chomp - end - def run validate_name_args! - warn_chef_config_secret_key - @template_file = find_template(config[:bootstrap_template]) @node_name = Array(@name_args).first - # back compat--templates may use this setting: - config[:server_name] = @node_name $stdout.sync = true - ui.info("Connecting to #{ui.color(@node_name, :bold)}") begin @@ -272,7 +308,7 @@ class Chef end def ssh_command - command = render_template(read_template) + command = render_template if config[:use_sudo] command = config[:use_sudo_password] ? "echo '#{config[:ssh_password]}' | sudo -S #{command}" : "sudo #{command}" @@ -281,28 +317,6 @@ class Chef command end - def warn_chef_config_secret_key - unless Chef::Config[:encrypted_data_bag_secret].nil? - ui.warn "* " * 40 - ui.warn(<<-WARNING) -Specifying the encrypted data bag secret key using an 'encrypted_data_bag_secret' -entry in 'knife.rb' is deprecated. Please see CHEF-4011 for more details. You -can supress this warning and still distribute the secret key to all bootstrapped -machines by adding the following to your 'knife.rb' file: - - knife[:secret_file] = "/path/to/your/secret" - -If you would like to selectively distribute a secret key during bootstrap -please use the '--secret' or '--secret-file' options of this command instead. - -#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this -behavior will be removed and any 'encrypted_data_bag_secret' entries in -'knife.rb' will be ignored completely. -WARNING - ui.warn "* " * 40 - end - end - end end end diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/archlinux-gems.erb index ab2aa7a7f1..bb84340c05 100644 --- a/lib/chef/knife/bootstrap/archlinux-gems.erb +++ b/lib/chef/knife/bootstrap/archlinux-gems.erb @@ -6,7 +6,7 @@ if [ ! -f /usr/bin/chef-client ]; then pacman -S --noconfirm ruby ntp base-devel ntpdate -u pool.ntp.org gem install ohai --no-user-install --no-document --verbose - gem install chef --no-user-install --no-document --verbose <%= bootstrap_version_string %> + gem install chef --no-user-install --no-document --verbose <%= Chef::VERSION %> fi mkdir -p /etc/chef diff --git a/lib/chef/knife/bootstrap/centos5-gems.erb b/lib/chef/knife/bootstrap/centos5-gems.erb deleted file mode 100644 index 6aacc47179..0000000000 --- a/lib/chef/knife/bootstrap/centos5-gems.erb +++ /dev/null @@ -1,62 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -if [ ! -f /usr/bin/chef-client ]; then - tmp_dir=$(mktemp -d) || exit 1 - pushd "$tmp_dir" - - yum install -y wget - - wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm - rpm -Uvh epel-release-5-4.noarch.rpm - wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://rpm.aegisco.com/aegisco/rhel/aegisco-rhel.rpm - rpm -Uvh aegisco-rhel.rpm - - yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf make - - wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz -O - | tar zxf - - (cd rubygems-1.6.2 && ruby setup.rb --no-format-executable) - - popd - rm -r "$tmp_dir" -fi - -gem update --system -gem update -gem install ohai --no-rdoc --no-ri --verbose -gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> - -mkdir -p /etc/chef - -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb index c953a7e433..a4e85b9d67 100644 --- a/lib/chef/knife/bootstrap/chef-full.erb +++ b/lib/chef/knife/bootstrap/chef-full.erb @@ -23,7 +23,6 @@ exists() { <%= knife_config[:bootstrap_install_command] %> <% else %> install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://www.opscode.com/chef/install.sh" %>" - version_string="-v <%= chef_version %>" if ! exists /usr/bin/chef-client; then echo "Installing Chef Client..." if exists wget; then diff --git a/lib/chef/knife/bootstrap/fedora13-gems.erb b/lib/chef/knife/bootstrap/fedora13-gems.erb deleted file mode 100644 index 0aabc31085..0000000000 --- a/lib/chef/knife/bootstrap/fedora13-gems.erb +++ /dev/null @@ -1,44 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf rubygems make - -gem update --system -gem update -gem install ohai --no-rdoc --no-ri --verbose -gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> - -mkdir -p /etc/chef - -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb b/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb deleted file mode 100644 index 4549b94d2b..0000000000 --- a/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb +++ /dev/null @@ -1,53 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -if [ ! -f /usr/bin/chef-client ]; then - apt-get install -y wget - echo "chef chef/chef_server_url string <%= @chef_config[:chef_server_url] %>" | debconf-set-selections - [ -f /etc/apt/sources.list.d/opscode.list ] || echo "deb http://apt.opscode.com <%= chef_version.to_f == 0.10 ? "lucid-0.10" : "lucid" %> main" > /etc/apt/sources.list.d/opscode.list - wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>-O- http://apt.opscode.com/packages@opscode.com.gpg.key | apt-key add - -fi -apt-get update -apt-get install -y chef - -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> -EOP -<% end -%> -<% end -%> - -<% unless @chef_config[:validation_client_name] == "chef-validator" -%> -[ `grep -qx "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" /etc/chef/client.rb` ] || echo "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" >> /etc/chef/client.rb -<% end -%> - -<% if @config[:chef_node_name] %> -[ `grep -qx "node_name \"<%= @config[:chef_node_name] %>\"" /etc/chef/client.rb` ] || echo "node_name \"<%= @config[:chef_node_name] %>\"" >> /etc/chef/client.rb -<% end -%> - -<% if knife_config[:bootstrap_proxy] %> -echo 'http_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb -echo 'https_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb -<% end -%> - -cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb deleted file mode 100644 index 62ff7c857e..0000000000 --- a/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb +++ /dev/null @@ -1,48 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -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 - wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz -O - | tar zxf - - (cd rubygems-1.6.2 && ruby setup.rb --no-format-executable) -fi - -gem update --no-rdoc --no-ri -gem install ohai --no-rdoc --no-ri --verbose -gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> - -mkdir -p /etc/chef - -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb deleted file mode 100644 index 8e9c6583d0..0000000000 --- a/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb +++ /dev/null @@ -1,46 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -if [ ! -f /usr/bin/chef-client ]; then - aptitude update - aptitude install -y ruby ruby1.8-dev build-essential wget libruby1.8 rubygems -fi - -gem update --no-rdoc --no-ri -gem install ohai --no-rdoc --no-ri --verbose -gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> - -mkdir -p /etc/chef - -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/cookbook_site_download.rb b/lib/chef/knife/cookbook_site_download.rb index 645b1728e6..de6e21d0d2 100644 --- a/lib/chef/knife/cookbook_site_download.rb +++ b/lib/chef/knife/cookbook_site_download.rb @@ -58,7 +58,7 @@ class Chef private def cookbooks_api_url - 'http://cookbooks.opscode.com/api/v1/cookbooks' + 'https://supermarket.getchef.com/api/v1/cookbooks' end def current_cookbook_data diff --git a/lib/chef/knife/cookbook_site_list.rb b/lib/chef/knife/cookbook_site_list.rb index fe83b71388..6fcf7e6064 100644 --- a/lib/chef/knife/cookbook_site_list.rb +++ b/lib/chef/knife/cookbook_site_list.rb @@ -41,7 +41,7 @@ class Chef end def get_cookbook_list(items=10, start=0, cookbook_collection={}) - cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}" + cookbooks_url = "https://supermarket.getchef.com/api/v1/cookbooks?items=#{items}&start=#{start}" cr = noauth_rest.get_rest(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_search.rb b/lib/chef/knife/cookbook_site_search.rb index b636276cba..ec4d196ee3 100644 --- a/lib/chef/knife/cookbook_site_search.rb +++ b/lib/chef/knife/cookbook_site_search.rb @@ -29,7 +29,7 @@ class Chef end def search_cookbook(query, items=10, start=0, cookbook_collection={}) - cookbooks_url = "http://cookbooks.opscode.com/api/v1/search?q=#{query}&items=#{items}&start=#{start}" + cookbooks_url = "https://supermarket.getchef.com/api/v1/search?q=#{query}&items=#{items}&start=#{start}" cr = noauth_rest.get_rest(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb index 71bbb00abc..e5c56f654d 100644 --- a/lib/chef/knife/cookbook_site_share.rb +++ b/lib/chef/knife/cookbook_site_share.rb @@ -99,7 +99,7 @@ class Chef end def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) - uri = "http://cookbooks.opscode.com/api/v1/cookbooks" + uri = "https://supermarket.getchef.com/api/v1/cookbooks" category_string = Chef::JSONCompat.to_json({ 'category'=>cookbook_category }) diff --git a/lib/chef/knife/cookbook_site_show.rb b/lib/chef/knife/cookbook_site_show.rb index d15098e915..c520c00621 100644 --- a/lib/chef/knife/cookbook_site_show.rb +++ b/lib/chef/knife/cookbook_site_show.rb @@ -31,14 +31,14 @@ class Chef def get_cookbook_data case @name_args.length when 1 - noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}") + noauth_rest.get_rest("https://supermarket.getchef.com/api/v1/cookbooks/#{@name_args[0]}") when 2 - noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].gsub('.', '_')}") + noauth_rest.get_rest("https://supermarket.getchef.com/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].gsub('.', '_')}") end end def get_cookbook_list(items=10, start=0, cookbook_collection={}) - cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}" + cookbooks_url = "https://supermarket.getchef.com/api/v1/cookbooks?items=#{items}&start=#{start}" cr = noauth_rest.get_rest(cookbooks_url) cr["items"].each do |cookbook| cookbook_collection[cookbook["cookbook_name"]] = cookbook diff --git a/lib/chef/knife/cookbook_site_unshare.rb b/lib/chef/knife/cookbook_site_unshare.rb index a2828549a0..f095885f15 100644 --- a/lib/chef/knife/cookbook_site_unshare.rb +++ b/lib/chef/knife/cookbook_site_unshare.rb @@ -41,7 +41,7 @@ class Chef confirm "Do you really want to unshare the cookbook #{@cookbook_name}" begin - rest.delete_rest "http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}" + rest.delete_rest "https://supermarket.getchef.com/api/v1/cookbooks/#{@name_args[0]}" rescue Net::HTTPServerException => e raise e unless e.message =~ /Forbidden/ ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it." diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 742ef226a3..9fa6dcc46f 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -34,14 +34,6 @@ class Chef @chef_config = chef_config end - def bootstrap_version_string - if @config[:prerelease] - "--prerelease" - else - "--version #{chef_version}" - end - end - def bootstrap_environment @chef_config[:environment] || '_default' end @@ -52,10 +44,12 @@ class Chef def encrypted_data_bag_secret knife_config[:secret] || begin - if knife_config[:secret_file] && File.exist?(knife_config[:secret_file]) - IO.read(File.expand_path(knife_config[:secret_file])) - elsif @chef_config[:encrypted_data_bag_secret] && File.exist?(@chef_config[:encrypted_data_bag_secret]) - IO.read(File.expand_path(@chef_config[:encrypted_data_bag_secret])) + secret_file_path = knife_config[:secret_file] + expanded_secret_file_path = File.expand_path(secret_file_path.to_s) + if secret_file_path && File.exist?(expanded_secret_file_path) + IO.read(expanded_secret_file_path) + else + nil end end end @@ -72,6 +66,36 @@ CONFIG client_rb << "# Using default node name (fqdn)\n" end + # We configure :verify_api_cert only when it's overridden on the CLI + # or when specified in the knife config. + if !@config[:node_verify_api_cert].nil? || knife_config.has_key?(:verify_api_cert) + value = @config[:node_verify_api_cert].nil? ? knife_config[:verify_api_cert] : @config[:node_verify_api_cert] + client_rb << %Q{verify_api_cert #{value}\n} + end + + # We configure :ssl_verify_mode only when it's overridden on the CLI + # or when specified in the knife config. + if @config[:node_ssl_verify_mode] || knife_config.has_key?(:ssl_verify_mode) + value = case @config[:node_ssl_verify_mode] + when "peer" + :verify_peer + when "none" + :verify_none + when nil + knife_config[:ssl_verify_mode] + else + nil + end + + if value + client_rb << %Q{ssl_verify_mode :#{value}\n} + end + end + + if @config[:ssl_verify_mode] + client_rb << %Q{ssl_verify_mode :#{knife_config[:ssl_verify_mode]}\n} + end + if knife_config[:bootstrap_proxy] client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n} client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n} @@ -93,7 +117,7 @@ CONFIG client_path = @chef_config[:chef_client_path] || 'chef-client' s = "#{client_path} -j /etc/chef/first-boot.json" s << ' -l debug' if @config[:verbosity] and @config[:verbosity] >= 2 - s << " -E #{bootstrap_environment}" if chef_version.to_f != 0.9 # only use the -E option on Chef 0.10+ + s << " -E #{bootstrap_environment}" s end @@ -102,29 +126,26 @@ CONFIG end # - # This function is used by older bootstrap templates other than chef-full - # and potentially by custom templates as well hence it's logic needs to be - # preserved for backwards compatibility reasons until we hit Chef 12. - def chef_version - knife_config[:bootstrap_version] || Chef::VERSION - end - - # # chef version string to fetch the latest current version from omnitruck # If user is on X.Y.Z bootstrap will use the latest X release # X here can be 10 or 11 def latest_current_chef_version_string - chef_version_string = if knife_config[:bootstrap_version] - knife_config[:bootstrap_version] + installer_version_string = nil + if @config[:prerelease] + installer_version_string = "-p" else - Chef::VERSION.split(".").first - end + chef_version_string = if knife_config[:bootstrap_version] + knife_config[:bootstrap_version] + else + Chef::VERSION.split(".").first + end - installer_version_string = ["-v", chef_version_string] + installer_version_string = ["-v", chef_version_string] - # If bootstrapping a pre-release version add -p to the installer string - if chef_version_string.split(".").length > 3 - installer_version_string << "-p" + # If bootstrapping a pre-release version add -p to the installer string + if chef_version_string.split(".").length > 3 + installer_version_string << "-p" + end end installer_version_string.join(" ") diff --git a/lib/chef/knife/core/node_editor.rb b/lib/chef/knife/core/node_editor.rb index 073492197c..fe14e18d9d 100644 --- a/lib/chef/knife/core/node_editor.rb +++ b/lib/chef/knife/core/node_editor.rb @@ -42,8 +42,8 @@ class Chef end def updated? - pristine_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(node), :create_additions => false) - updated_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@updated_node), :create_additions => false) + pristine_copy = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(node)) + updated_copy = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(@updated_node)) unless pristine_copy == updated_copy updated_properties = %w{name normal chef_environment run_list default override automatic}.reject do |key| pristine_copy[key] == updated_copy[key] @@ -107,4 +107,3 @@ class Chef end end end - diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb index c4d7d73b00..0007480ea2 100644 --- a/lib/chef/knife/core/ui.rb +++ b/lib/chef/knife/core/ui.rb @@ -195,8 +195,8 @@ class Chef # We wouldn't have to do these shenanigans if all the editable objects # implemented to_hash, or if to_json against a hash returned a string # with stable key order. - object_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(object), :create_additions => false) - output_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(output), :create_additions => false) + object_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(object)) + output_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(output)) if object_parsed_again != output_parsed_again output.save self.msg("Saved #{output}") diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb index bc020c0445..34d12168b6 100644 --- a/lib/chef/knife/search.rb +++ b/lib/chef/knife/search.rb @@ -71,6 +71,11 @@ class Chef :long => "--query QUERY", :description => "The search query; useful to protect queries starting with -" + option :filter_result, + :short => "-f FILTER", + :long => "--filter-result FILTER", + :description => "Only bring back specific attributes of the matching objects; for example: \"ServerName=name, Kernel=kernel.version\"" + def run read_cli_args fuzzify_query @@ -79,7 +84,6 @@ class Chef ui.use_presenter Knife::Core::NodePresenter end - q = Chef::Search::Query.new escaped_query = URI.escape(@query, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) @@ -87,14 +91,26 @@ class Chef result_items = [] result_count = 0 - rows = config[:rows] - start = config[:start] + search_args = Hash.new + search_args[:sort] = config[:sort] + search_args[:start] = config[:start] + search_args[:rows] = config[:rows] + if config[:filter_result] + search_args[:filter_result] = create_result_filter(config[:filter_result]) + elsif (not ui.config[:attribute].nil?) && (not ui.config[:attribute].empty?) + search_args[:filter_result] = create_result_filter_from_attributes(ui.config[:attribute]) + end + begin - q.search(@type, escaped_query, config[:sort], start, rows) do |item| - formatted_item = format_for_display(item) - # if formatted_item.respond_to?(:has_key?) && !formatted_item.has_key?('id') - # formatted_item['id'] = item.has_key?('id') ? item['id'] : item.name - # end + q.search(@type, escaped_query, search_args) do |item| + formatted_item = Hash.new + if item.is_a?(Hash) + # doing a little magic here to set the correct name + formatted_item[item["data"]["__display_name"]] = item["data"] + formatted_item[item["data"]["__display_name"]].delete("__display_name") + else + formatted_item = format_for_display(item) + end result_items << formatted_item result_count += 1 end @@ -149,10 +165,38 @@ class Chef end end + # This method turns a set of key value pairs in a string into the appropriate data structure that the + # chef-server search api is expecting. + # expected input is in the form of: + # -f "return_var1=path.to.attribute, return_var2=shorter.path" + # + # a more concrete example might be: + # -f "env=chef_environment, ruby_platform=languages.ruby.platform" + # + # The end result is a hash where the key is a symbol in the hash (the return variable) + # and the path is an array with the path elements as strings (in order) + # See lib/chef/search/query.rb for more examples of this. + def create_result_filter(filter_string) + final_filter = Hash.new + filter_string.gsub!(" ", "") + filters = filter_string.split(",") + filters.each do |f| + return_id, attr_path = f.split("=") + final_filter[return_id.to_sym] = attr_path.split(".") + end + return final_filter + end + + def create_result_filter_from_attributes(filter_array) + final_filter = Hash.new + filter_array.each do |f| + final_filter[f] = f.split(".") + end + # adding magic filter so we can actually pull the name as before + final_filter["__display_name"] = [ "name" ] + return final_filter + end + end end end - - - - diff --git a/lib/chef/knife/serve.rb b/lib/chef/knife/serve.rb index 15994590cd..870177e0be 100644 --- a/lib/chef/knife/serve.rb +++ b/lib/chef/knife/serve.rb @@ -33,7 +33,7 @@ class Chef def run server = Chef::LocalMode.chef_zero_server begin - output "Serving files from:\n#{server.options[:data_store].chef_fs.fs_description}" + output "Serving files from:\n#{Chef::LocalMode.chef_fs.fs_description}" server.stop server.start(stdout) # to print header ensure diff --git a/lib/chef/local_mode.rb b/lib/chef/local_mode.rb index ad1968a6a6..e66acb6b66 100644 --- a/lib/chef/local_mode.rb +++ b/lib/chef/local_mode.rb @@ -52,9 +52,10 @@ class Chef require 'chef/chef_fs/chef_fs_data_store' require 'chef/chef_fs/config' - chef_fs = Chef::ChefFS::Config.new.local_fs - chef_fs.write_pretty_json = true - data_store = Chef::ChefFS::ChefFSDataStore.new(chef_fs) + @chef_fs = Chef::ChefFS::Config.new.local_fs + @chef_fs.write_pretty_json = true + data_store = Chef::ChefFS::ChefFSDataStore.new(@chef_fs) + data_store = ChefZero::DataStore::V1ToV2Adapter.new(data_store, 'chef') server_options = {} server_options[:data_store] = data_store server_options[:log_level] = Chef::Log.level @@ -62,7 +63,7 @@ class Chef server_options[:port] = parse_port(Chef::Config.chef_zero.port) @chef_zero_server = ChefZero::Server.new(server_options) @chef_zero_server.start_background - Chef::Log.info("Started chef-zero at #{@chef_zero_server.url} with #{chef_fs.fs_description}") + Chef::Log.info("Started chef-zero at #{@chef_zero_server.url} with #{@chef_fs.fs_description}") Chef::Config.chef_server_url = @chef_zero_server.url end end @@ -72,6 +73,11 @@ class Chef @chef_zero_server end + # Return the chef_fs object for the current chef-zero server. + def self.chef_fs + @chef_fs + end + # If chef_zero_server is non-nil, stop it and remove references to it. def self.destroy_server_connectivity if @chef_zero_server diff --git a/lib/chef/mixin/deep_merge.rb b/lib/chef/mixin/deep_merge.rb index a8a4737758..5e3327a526 100644 --- a/lib/chef/mixin/deep_merge.rb +++ b/lib/chef/mixin/deep_merge.rb @@ -29,7 +29,6 @@ class Chef class InvalidSubtractiveMerge < ArgumentError; end - OLD_KNOCKOUT_PREFIX = "!merge:".freeze # Regex to match the "knockout prefix" that was used to indicate @@ -86,8 +85,12 @@ class Chef when Hash if dest.kind_of?(Hash) source.each do |src_key, src_value| - if dest[src_key] - dest[src_key] = deep_merge!(src_value, dest[src_key]) + if dest.has_key? src_key + if dest[src_key].nil? + dest[src_key] = nil + else + dest[src_key] = deep_merge!(src_value, dest[src_key]) + end else # dest[src_key] doesn't exist so we take whatever source has raise_if_knockout_used!(src_value) dest[src_key] = src_value diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb index 3f80290f90..82772b584a 100644 --- a/lib/chef/mixin/shell_out.rb +++ b/lib/chef/mixin/shell_out.rb @@ -20,7 +20,6 @@ require 'chef/shell_out' require 'mixlib/shellout' -require 'chef/config' class Chef module Mixin @@ -31,32 +30,39 @@ class Chef # Generally speaking, 'extend Chef::Mixin::ShellOut' in your recipes and include 'Chef::Mixin::ShellOut' in your LWRPs # You can also call Mixlib::Shellout.new directly, but you lose all of the above functionality + # we use 'en_US.UTF-8' by default because we parse localized strings in English as an API and + # generally must support UTF-8 unicode. def shell_out(*command_args) - cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args)) - cmd.live_stream ||= io_for_live_stream - cmd.run_command - cmd + args = command_args.dup + if args.last.is_a?(Hash) + options = args.pop.dup + env_key = options.has_key?(:env) ? :env : :environment + options[env_key] ||= {} + options[env_key] = options[env_key].dup + options[env_key]['LC_ALL'] ||= Chef::Config[:internal_locale] unless options[env_key].has_key?('LC_ALL') + args << options + else + args << { :environment => { 'LC_ALL' => Chef::Config[:internal_locale] } } + end + + shell_out_command(*args) end + # call shell_out (using en_US.UTF-8) and raise errors def shell_out!(*command_args) - cmd= shell_out(*command_args) + cmd = shell_out(*command_args) cmd.error! cmd end - # environment['LC_ALL'] should be nil or what the user specified def shell_out_with_systems_locale(*command_args) - args = command_args.dup - if args.last.is_a?(Hash) - options = args.last - env_key = options.has_key?(:env) ? :env : :environment - options[env_key] ||= {} - options[env_key]['LC_ALL'] ||= nil - else - args << { :environment => { 'LC_ALL' => nil } } - end + shell_out_command(*command_args) + end - shell_out(*args) + def shell_out_with_systems_locale!(*command_args) + cmd = shell_out_with_systems_locale(*command_args) + cmd.error! + cmd end DEPRECATED_OPTIONS = @@ -83,6 +89,13 @@ class Chef private + def shell_out_command(*command_args) + cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args)) + cmd.live_stream ||= io_for_live_stream + cmd.run_command + cmd + end + def deprecate_option(old_option, new_option) Chef::Log.logger.warn "DEPRECATION: Chef::Mixin::ShellOut option :#{old_option} is deprecated. Use :#{new_option}" end @@ -97,3 +110,6 @@ class Chef end end end + +# Break circular dep +require 'chef/config' diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index edcd596341..ff118c1d3d 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' +require 'chef/platform/query_helpers' require 'win32/api' if Chef::Platform.windows? require 'chef/win32/api/process' if Chef::Platform.windows? require 'chef/win32/api/error' if Chef::Platform.windows? diff --git a/lib/chef/null_logger.rb b/lib/chef/null_logger.rb new file mode 100644 index 0000000000..5195cc5ce2 --- /dev/null +++ b/lib/chef/null_logger.rb @@ -0,0 +1,72 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + + # Null logger implementation that just ignores everything. This is used by + # classes that are intended to be reused outside of Chef, but need to offer + # logging functionality when used by other Chef code. + # + # It does not define the full interface provided by Logger, just enough to be + # a reasonable duck type. In particular, methods setting the log level, log + # device, etc., are not implemented because any code calling those methods + # probably expected a real logger and not this "fake" one. + class NullLogger + + def fatal(message, &block) + end + + def error(message, &block) + end + + def warn(message, &block) + end + + def info(message, &block) + end + + def debug(message, &block) + end + + def add(severity, message=nil, progname=nil) + end + + def <<(message) + end + + def fatal? + false + end + + def error? + false + end + + def warn? + false + end + + def info? + false + end + + def debug? + false + end + + end +end diff --git a/lib/chef/platform.rb b/lib/chef/platform.rb index 8f494731ab..841aa1b46c 100644 --- a/lib/chef/platform.rb +++ b/lib/chef/platform.rb @@ -16,8 +16,9 @@ # limitations under the License. # -require 'chef/platform/provider_mapping' +# Order of these headers is important: query helpers is needed by many things require 'chef/platform/query_helpers' +require 'chef/platform/provider_mapping' class Chef class Platform diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index d61298e182..7f79c38a6a 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/config' require 'chef/log' +require 'chef/exceptions' require 'chef/mixin/params_validate' require 'chef/version_constraint/platform' diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index db7629dbcb..bdfe826944 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -19,14 +19,12 @@ require 'chef/mixin/from_file' require 'chef/mixin/convert_to_class_name' -require 'chef/dsl/recipe' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' class Chef class Provider - include Chef::DSL::Recipe include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 1be15f9f5f..c3be9746df 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -118,6 +118,12 @@ class Chef when ENV_PATTERN crontab << line unless cron_found next + when SPECIAL_PATTERN + if cron_found + cron_found = false + crontab << newcron + next + end when CRON_PATTERN if cron_found cron_found = false @@ -163,6 +169,11 @@ class Chef next when ENV_PATTERN next if cron_found + when SPECIAL_PATTERN + if cron_found + cron_found = false + next + end when CRON_PATTERN if cron_found cron_found = false diff --git a/lib/chef/provider/deploy/revision.rb b/lib/chef/provider/deploy/revision.rb index 89710088d1..ed65742154 100644 --- a/lib/chef/provider/deploy/revision.rb +++ b/lib/chef/provider/deploy/revision.rb @@ -90,7 +90,7 @@ class Chef def load_cache begin - Chef::JSONCompat.from_json(Chef::FileCache.load("revision-deploys/#{new_resource.name}")) + Chef::JSONCompat.parse(Chef::FileCache.load("revision-deploys/#{new_resource.name}")) rescue Chef::Exceptions::FileNotFound sorted_releases_from_filesystem end diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb index 525249a726..aa58fdc4a1 100644 --- a/lib/chef/provider/git.rb +++ b/lib/chef/provider/git.rb @@ -283,6 +283,7 @@ class Chef env['GIT_SSH'] = @new_resource.ssh_wrapper if @new_resource.ssh_wrapper run_opts[:log_tag] = @new_resource.to_s run_opts[:timeout] = @new_resource.timeout if @new_resource.timeout + env.merge!(@new_resource.environment) if @new_resource.environment run_opts[:environment] = env unless env.empty? run_opts diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index 3ec6f6f668..c39c20da67 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -39,7 +39,14 @@ class Chef def create_group command = "pw groupadd" command << set_options - member_options = set_members_options + + # pw group[add|mod] -M is used to set the full membership list on a + # new or existing group. Because pw groupadd does not support the -m + # and -d options used by manage_group, we treat group creation as a + # special case and use -M. + Chef::Log.debug("#{@new_resource} setting group members: #{@new_resource.members.join(',')}") + member_options = [" -M #{@new_resource.members.join(',')}"] + if member_options.empty? run_command(:command => command) else diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 31f88e5406..ac52100b56 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -19,6 +19,7 @@ require 'chef/log' require 'chef/mixin/command' require 'chef/provider' +require 'chef/resource/file' require 'chef/exceptions' require 'erb' @@ -109,11 +110,11 @@ class Chef :command => command ) Chef::Log.info("#{@new_resource} added") - # Write out the config files - generate_config end end end + # Write out the config files + generate_config end def action_enable @@ -140,12 +141,12 @@ class Chef run_command( :command => command ) - delete_config Chef::Log.info("#{@new_resource} deleted") end else Chef::Log.debug("#{@new_resource} does not exist - nothing to do") end + delete_config end def action_disable @@ -168,27 +169,25 @@ class Chef ! @config_template.nil? and ! @config_path.nil? end + def resource_for_config(path) + Chef::Resource::File.new(path, run_context) + end + def generate_config return unless can_generate_config? b = binding template = ::ERB.new(@config_template) - converge_by ("generate configuration file : #{@config_path}") do - network_file = ::File.new(@config_path, "w") - network_file.puts(template.result(b)) - network_file.close - end - Chef::Log.info("#{@new_resource} created configuration file") + config = resource_for_config(@config_path) + config.content(template.result(b)) + config.run_action(:create) + @new_resource.updated_by_last_action(true) if config.updated? end def delete_config return unless can_generate_config? - require 'fileutils' - if ::File.exist?(@config_path) - converge_by ("delete the #{@config_path}") do - FileUtils.rm_f(@config_path, :verbose => false) - end - end - Chef::Log.info("#{@new_resource} deleted configuration file") + config = resource_for_config(@config_path) + config.run_action(:delete) + @new_resource.updated_by_last_action(true) if config.updated? end private diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 207b0cde6c..2d4a5aadef 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -127,7 +127,7 @@ class Chef end def remount_command - return "mount -o remount #{@new_resource.mount_point}" + return "mount -o remount,#{@new_resource.options.join(',')} #{@new_resource.mount_point}" end def remount_fs diff --git a/lib/chef/provider/mount/solaris.rb b/lib/chef/provider/mount/solaris.rb index 462fa32b71..cf04150322 100644 --- a/lib/chef/provider/mount/solaris.rb +++ b/lib/chef/provider/mount/solaris.rb @@ -1,4 +1,4 @@ -# +# Encoding: utf-8 # Author:: Hugo Fichter # Author:: Lamont Granquist (<lamont@getchef.com>) # Author:: Joshua Timberman (<joshua@getchef.com>) @@ -25,14 +25,16 @@ require 'forwardable' class Chef class Provider class Mount + # Mount Solaris File systems class Solaris < Chef::Provider::Mount extend Forwardable - VFSTAB = "/etc/vfstab".freeze + VFSTAB = '/etc/vfstab'.freeze def_delegator :@new_resource, :device, :device def_delegator :@new_resource, :device_type, :device_type def_delegator :@new_resource, :dump, :dump + def_delegator :@new_resource, :fsck_device, :fsck_device def_delegator :@new_resource, :fstype, :fstype def_delegator :@new_resource, :mount_point, :mount_point def_delegator :@new_resource, :options, :options @@ -42,6 +44,7 @@ class Chef @current_resource = Chef::Resource::Mount.new(new_resource.name) current_resource.mount_point(mount_point) current_resource.device(device) + current_resource.fsck_device(fsck_device) current_resource.device_type(device_type) update_current_resource_state end @@ -53,6 +56,14 @@ class Chef a.whyrun("Assuming device #{device} would have been created") end + unless fsck_device == '-' + requirements.assert(:mount, :remount) do |a| + a.assertion { ::File.exist?(fsck_device) } + a.failure_message(Chef::Exceptions::Mount, "Device #{fsck_device} does not exist") + a.whyrun("Assuming device #{fsck_device} would have been created") + end + end + requirements.assert(:mount, :remount) do |a| a.assertion { ::File.exist?(mount_point) } a.failure_message(Chef::Exceptions::Mount, "Mount point #{mount_point} does not exist") @@ -62,7 +73,7 @@ class Chef def mount_fs actual_options = options || [] - actual_options.delete("noauto") + actual_options.delete('noauto') command = "mount -F #{fstype}" command << " -o #{actual_options.join(',')}" unless actual_options.empty? command << " #{device} #{mount_point}" @@ -74,59 +85,28 @@ class Chef end def remount_fs - # FIXME: what about options like "-o remount,logging" to enable logging on a UFS device? - shell_out!("mount -o remount #{mount_point}") + # FIXME: Should remount always do the remount or only if the options change? + actual_options = options || [] + actual_options.delete('noauto') + mount_options = actual_options.empty? ? '' : ",#{actual_options.join(',')}" + shell_out!("mount -o remount#{mount_options} #{mount_point}") end def enable_fs - if !mount_options_unchanged? + unless mount_options_unchanged? # we are enabling because our options have changed, so disable first then re-enable. # XXX: this should be refactored to be the responsibility of the caller disable_fs if current_resource.enabled end - auto = options.nil? || ! options.include?("noauto") - actual_options = unless options.nil? - options.delete("noauto") - options - end - - autostr = auto ? 'yes' : 'no' - passstr = pass == 0 ? "-" : pass - optstr = (actual_options.nil? || actual_options.empty?) ? "-" : actual_options.join(',') - - etc_tempfile do |f| - f.write(IO.read(VFSTAB).chomp) - f.puts("\n#{device}\t-\t#{mount_point}\t#{fstype}\t#{passstr}\t#{autostr}\t#{optstr}") - f.close - # move, preserving modes of destination file - mover = Chef::FileContentManagement::Deploy.strategy(true) - mover.deploy(f.path, VFSTAB) - end - + vfstab_write(merge_vfstab_entry) end def disable_fs - contents = [] - - found = false - ::File.readlines(VFSTAB).reverse_each do |line| - if !found && line =~ /^#{device_regex}\s+\S+\s+#{Regexp.escape(mount_point)}/ - found = true - Chef::Log.debug("#{new_resource} is removed from vfstab") - next - end - contents << line - end + contents, found = delete_vfstab_entry if found - etc_tempfile do |f| - f.write(contents.reverse.join('')) - f.close - # move, preserving modes of destination file - mover = Chef::FileContentManagement::Deploy.strategy(true) - mover.deploy(f.path, VFSTAB) - end + vfstab_write(contents.reverse) else # this is likely some kind of internal error, since we should only call disable_fs when there # the filesystem we want to disable is enabled. @@ -135,19 +115,24 @@ class Chef end def etc_tempfile - yield Tempfile.open("vfstab", "/etc") + yield Tempfile.open('vfstab', '/etc') end def mount_options_unchanged? - current_resource.fstype == fstype and - current_resource.options == options and - current_resource.dump == dump and - current_resource.pass == pass + new_options = options_remove_noauto(options) + current_options = options_remove_noauto(current_resource.nil? ? nil : current_resource.options) + + current_resource.fsck_device == fsck_device && + current_resource.fstype == fstype && + current_options == new_options && + current_resource.dump == dump && + current_resource.pass == pass && + current_resource.options.include?('noauto') == !mount_at_boot? end def update_current_resource_state current_resource.mounted(mounted?) - ( enabled, fstype, options, pass ) = read_vfstab_status + (enabled, fstype, options, pass) = read_vfstab_status current_resource.enabled(enabled) current_resource.fstype(fstype) current_resource.options(options) @@ -158,17 +143,18 @@ class Chef read_vfstab_status[0] end + # Check for the device in mounttab. + # <device> on <mountpoint> type <fstype> <options> on <date> + # /dev/dsk/c1t0d0s0 on / type ufs read/write/setuid/devices/intr/largefiles/logging/xattr/onerror=panic/dev=700040 on Tue May 1 11:33:55 2012 def mounted? mounted = false - shell_out!("mount -v").stdout.each_line do |line| - # <device> on <mountpoint> type <fstype> <options> on <date> - # /dev/dsk/c1t0d0s0 on / type ufs read/write/setuid/devices/intr/largefiles/logging/xattr/onerror=panic/dev=700040 on Tue May 1 11:33:55 2012 + shell_out!('mount -v').stdout.each_line do |line| case line when /^#{device_regex}\s+on\s+#{Regexp.escape(mount_point)}\s+/ Chef::Log.debug("Special device #{device} is mounted as #{mount_point}") mounted = true when /^([\/\w]+)\son\s#{Regexp.escape(mount_point)}\s+/ - Chef::Log.debug("Special device #{$1} is mounted as #{mount_point}") + Chef::Log.debug("Special device #{Regexp.last_match[1]} is mounted as #{mount_point}") mounted = false end end @@ -178,7 +164,7 @@ class Chef private def read_vfstab_status - # Check to see if there is a entry in /etc/vfstab. Last entry for a volume wins. + # Check to see if there is an entry in /etc/vfstab. Last entry for a volume wins. enabled = false fstype = options = pass = nil ::File.foreach(VFSTAB) do |line| @@ -190,18 +176,18 @@ class Chef # to mount to fsck point type pass at boot options when /^#{device_regex}\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ enabled = true - fstype = $1 - options = $4 + fstype = Regexp.last_match[1] + options = Regexp.last_match[4] # Store the 'mount at boot' column from vfstab as the 'noauto' option # in current_resource.options (linux style) - if $3 == "yes" + if Regexp.last_match[3] == 'no' if options.nil? || options.empty? - options = "noauto" + options = 'noauto' else - options += ",noauto" + options += ',noauto' end end - pass = ( $2 == "-" ) ? 0 : $2.to_i + pass = (Regexp.last_match[2] == '-') ? 0 : Regexp.last_match[2].to_i Chef::Log.debug("Found mount #{device} to #{mount_point} in #{VFSTAB}") next when /^[-\/\w]+\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+/ @@ -210,21 +196,73 @@ class Chef Chef::Log.debug("Found conflicting mount point #{mount_point} in #{VFSTAB}") end end - [ enabled, fstype, options, pass ] + [enabled, fstype, options, pass] end def device_should_exist? - !%w{tmpfs nfs ctfs proc mntfs objfs sharefs fd smbfs}.include?(fstype) + !%w(tmpfs nfs ctfs proc mntfs objfs sharefs fd smbfs vxfs).include?(fstype) + end + + def mount_at_boot? + options.nil? || !options.include?('noauto') + end + + def vfstab_write(contents) + etc_tempfile do |f| + f.write(contents.join('')) + f.close + # move, preserving modes of destination file + mover = Chef::FileContentManagement::Deploy.strategy(true) + mover.deploy(f.path, VFSTAB) + end + end + + def vfstab_entry + actual_options = unless options.nil? + tempops = options.dup + tempops.delete('noauto') + tempops + end + autostr = mount_at_boot? ? 'yes' : 'no' + passstr = pass == 0 ? '-' : pass + optstr = (actual_options.nil? || actual_options.empty?) ? '-' : actual_options.join(',') + "\n#{device}\t#{fsck_device}\t#{mount_point}\t#{fstype}\t#{passstr}\t#{autostr}\t#{optstr}\n" + end + + def delete_vfstab_entry + contents = [] + found = false + ::File.readlines(VFSTAB).reverse_each do |line| + if !found && line =~ /^#{device_regex}\s+\S+\s+#{Regexp.escape(mount_point)}/ + found = true + Chef::Log.debug("#{new_resource} is removed from vfstab") + next + end + contents << line + end + [contents, found] + end + + def merge_vfstab_entry + contents = ::File.readlines(VFSTAB) + contents[-1].chomp! + contents << vfstab_entry + end + + def options_remove_noauto(temp_options) + new_options = [] + new_options += temp_options.nil? ? [] : temp_options + new_options.delete('noauto') + new_options end def device_regex if ::File.symlink?(device) - "(?:#{Regexp.escape(device)}|#{Regexp.escape(::File.expand_path(::File.readlink(device),::File.dirname(device)))})" + "(?:#{Regexp.escape(device)}|#{Regexp.escape(::File.expand_path(::File.readlink(device), ::File.dirname(device)))})" else Regexp.escape(device) end end - end end end diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index 9fb87d6ea0..da3e6d1684 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -112,14 +112,10 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") if @new_resource.options.nil? - run_command_with_systems_locale( - :command => "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" - ) + shell_out!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else - run_command_with_systems_locale( - :command => "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" - ) + shell_out!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end @@ -128,14 +124,10 @@ class Chef def remove_package(name, version) if @new_resource.options.nil? - run_command_with_systems_locale( - :command => "installp -u #{name}" - ) + shell_out!( "installp -u #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - run_command_with_systems_locale( - :command => "installp -u #{expand_options(@new_resource.options)} #{name}" - ) + shell_out!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index fed3e34124..4090507303 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -39,47 +39,39 @@ class Chef end end - def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.name) - check_package_state(@new_resource.package_name) - @current_resource + def get_current_version + shell_out("pkg info #{@new_resource.package_name}").stdout.each_line do |line| + return $1.split[0] if line =~ /^\s+Version: (.*)/ + end + return nil end - def check_package_state(package) - Chef::Log.debug("Checking package status for #{package}") - installed = false - depends = false - - shell_out!("pkg info -r #{package}").stdout.each_line do |line| - case line - when /^\s+State: Installed/ - installed = true - when /^\s+Version: (.*)/ - @candidate_version = $1.split[0] - if installed - @current_resource.version($1) - else - @current_resource.version(nil) - end - end + def get_candidate_version + shell_out!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| + return $1.split[0] if line =~ /Version: (.*)/ end + return nil + end - return installed + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + Chef::Log.debug("Checking package status for #{@new_resource.name}") + @current_resource.version(get_current_version) + @candidate_version = get_candidate_version + @current_resource end def install_package(name, version) package_name = "#{name}@#{version}" normal_command = "pkg#{expand_options(@new_resource.options)} install -q #{package_name}" - if @new_resource.respond_to?(:accept_license) and @new_resource.accept_license - command = normal_command.gsub('-q', '-q --accept') - else - command = normal_command - end - begin - run_command_with_systems_locale(:command => command) - rescue - end + command = + if @new_resource.respond_to?(:accept_license) and @new_resource.accept_license + normal_command.gsub('-q', '-q --accept') + else + normal_command + end + shell_out(command) end def upgrade_package(name, version) @@ -88,9 +80,7 @@ class Chef def remove_package(name, version) package_name = "#{name}@#{version}" - run_command_with_systems_locale( - :command => "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" - ) + shell_out!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) end end end diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index 6ef303ee4f..05247e6d31 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -45,27 +45,21 @@ class Chef unless @current_resource.version == version command = "port#{expand_options(@new_resource.options)} install #{name}" command << " @#{version}" if version and !version.empty? - run_command_with_systems_locale( - :command => command - ) + shell_out!(command) end end def purge_package(name, version) command = "port#{expand_options(@new_resource.options)} uninstall #{name}" command << " @#{version}" if version and !version.empty? - run_command_with_systems_locale( - :command => command - ) + shell_out!(command) end def remove_package(name, version) command = "port#{expand_options(@new_resource.options)} deactivate #{name}" command << " @#{version}" if version and !version.empty? - run_command_with_systems_locale( - :command => command - ) + shell_out!(command) end def upgrade_package(name, version) @@ -78,9 +72,7 @@ class Chef # that hasn't been installed. install_package(name, version) elsif current_version != version - run_command_with_systems_locale( - :command => "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" - ) + shell_out!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) end end diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index 2e8bb7850b..1014ebcaa5 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -86,9 +86,7 @@ class Chef end def install_package(name, version) - run_command_with_systems_locale( - :command => "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" - ) + shell_out!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def upgrade_package(name, version) @@ -96,9 +94,7 @@ class Chef end def remove_package(name, version) - run_command_with_systems_locale( - :command => "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" - ) + shell_out!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index 6a3587558a..7e0eebd0d9 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -110,9 +110,7 @@ class Chef pkg = "~#{name}-#{$1}" end - run_command_with_systems_locale( - :command => "emerge -g --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" - ) + shell_out!( "emerge -g --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" ) end def upgrade_package(name, version) @@ -126,9 +124,7 @@ class Chef pkg = "#{@new_resource.package_name}" end - run_command_with_systems_locale( - :command => "emerge --unmerge --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" - ) + shell_out!( "emerge --unmerge --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index bbb561bd15..c0a6444252 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -90,13 +90,9 @@ class Chef def install_package(name, version) unless @current_resource.version - run_command_with_systems_locale( - :command => "rpm #{@new_resource.options} -i #{@new_resource.source}" - ) + shell_out!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) else - run_command_with_systems_locale( - :command => "rpm #{@new_resource.options} -U #{@new_resource.source}" - ) + shell_out!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) end end @@ -104,13 +100,9 @@ class Chef def remove_package(name, version) if version - run_command_with_systems_locale( - :command => "rpm #{@new_resource.options} -e #{name}-#{version}" - ) + shell_out!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) else - run_command_with_systems_locale( - :command => "rpm #{@new_resource.options} -e #{name}" - ) + shell_out!( "rpm #{@new_resource.options} -e #{name}" ) end end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index 0f45b61e18..19f844b66a 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -112,9 +112,7 @@ class Chef else command = "pkgadd -n -d #{@new_resource.source} all" end - run_command_with_systems_locale( - :command => command - ) + shell_out!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else if ::File.directory?(@new_resource.source) # CHEF-4469 @@ -122,23 +120,17 @@ class Chef else command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" end - run_command_with_systems_locale( - :command => command - ) + shell_out!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end def remove_package(name, version) if @new_resource.options.nil? - run_command_with_systems_locale( - :command => "pkgrm -n #{name}" - ) + shell_out!( "pkgrm -n #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - run_command_with_systems_locale( - :command => "pkgrm -n#{expand_options(@new_resource.options)} #{name}" - ) + shell_out!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index d15f22ae16..e77319c254 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -19,6 +19,7 @@ require 'chef/config' require 'chef/provider/package' require 'chef/mixin/command' +require 'chef/mixin/shell_out' require 'chef/resource/package' require 'singleton' require 'chef/mixin/get_source_from_package' @@ -645,6 +646,7 @@ class Chef # Cache for our installed and available packages, pulled in from yum-dump.py class YumCache include Chef::Mixin::Command + include Chef::Mixin::ShellOut include Singleton def initialize diff --git a/lib/chef/provider/remote_file/cache_control_data.rb b/lib/chef/provider/remote_file/cache_control_data.rb index 75b2a5535a..f9b729362c 100644 --- a/lib/chef/provider/remote_file/cache_control_data.rb +++ b/lib/chef/provider/remote_file/cache_control_data.rb @@ -139,7 +139,7 @@ class Chef end def load_data - Chef::JSONCompat.from_json(load_json_data) + Chef::JSONCompat.parse(load_json_data) rescue Chef::Exceptions::FileNotFound, Chef::Exceptions::JSON::ParseError false end diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 06fe7fc480..1ebef90349 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -130,15 +130,15 @@ class Chef def enable_service if @new_resource.priority.is_a? Integer - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") - run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults #{@new_resource.priority} #{100 - @new_resource.priority}") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") + shell_out!("/usr/sbin/update-rc.d #{@new_resource.service_name} defaults #{@new_resource.priority} #{100 - @new_resource.priority}") elsif @new_resource.priority.is_a? Hash # we call the same command regardless of we're enabling or disabling # users passing a Hash are responsible for setting their own start priorities set_priority else # No priority, go with update-rc.d defaults - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") - run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") + shell_out!("/usr/sbin/update-rc.d #{@new_resource.service_name} defaults") end end @@ -146,16 +146,16 @@ class Chef def disable_service if @new_resource.priority.is_a? Integer # Stop processes in reverse order of start using '100 - start_priority' - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop #{100 - @new_resource.priority} 2 3 4 5 .") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop #{100 - @new_resource.priority} 2 3 4 5 .") elsif @new_resource.priority.is_a? Hash # we call the same command regardless of we're enabling or disabling # users passing a Hash are responsible for setting their own stop priorities set_priority else # no priority, using '100 - 20 (update-rc.d default)' to stop in reverse order of start - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop 80 2 3 4 5 .") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop 80 2 3 4 5 .") end end @@ -166,8 +166,8 @@ class Chef priority = o[1] args += "#{action} #{priority} #{level} . " end - run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") - run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} #{args}") + shell_out!("/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove") + shell_out!("/usr/sbin/update-rc.d #{@new_resource.service_name} #{args}") end end end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 800cd4ec13..08d58232e1 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -25,89 +25,76 @@ class Chef class Service class Freebsd < Chef::Provider::Service::Init - def load_current_resource - @current_resource = Chef::Resource::Service.new(@new_resource.name) - @current_resource.service_name(@new_resource.service_name) - @rcd_script_found = true + attr_reader :enabled_state_found + + def initialize(new_resource, run_context) + super @enabled_state_found = false - # Determine if we're talking about /etc/rc.d or /usr/local/etc/rc.d - if ::File.exists?("/etc/rc.d/#{current_resource.service_name}") - @init_command = "/etc/rc.d/#{current_resource.service_name}" - elsif ::File.exists?("/usr/local/etc/rc.d/#{current_resource.service_name}") - @init_command = "/usr/local/etc/rc.d/#{current_resource.service_name}" - else - @rcd_script_found = false - return - end - Chef::Log.debug("#{@current_resource} found at #{@init_command}") - determine_current_status! - # Default to disabled if the service doesn't currently exist - # at all - var_name = service_enable_variable_name - if ::File.exists?("/etc/rc.conf") && var_name - read_rc_conf.each do |line| - case line - when /#{Regexp.escape(var_name)}="(\w+)"/ - @enabled_state_found = true - if $1 =~ /[Yy][Ee][Ss]/ - @current_resource.enabled true - elsif $1 =~ /[Nn][Oo][Nn]?[Oo]?[Nn]?[Ee]?/ - @current_resource.enabled false - end - end - end - end - unless @current_resource.enabled - Chef::Log.debug("#{@new_resource.name} enable/disable state unknown") - @current_resource.enabled false + @init_command = nil + if ::File.exist?("/etc/rc.d/#{new_resource.service_name}") + @init_command = "/etc/rc.d/#{new_resource.service_name}" + elsif ::File.exist?("/usr/local/etc/rc.d/#{new_resource.service_name}") + @init_command = "/usr/local/etc/rc.d/#{new_resource.service_name}" end + end + + def load_current_resource + @current_resource = Chef::Resource::Service.new(new_resource.name) + current_resource.service_name(new_resource.service_name) + + return current_resource unless init_command - @current_resource + Chef::Log.debug("#{current_resource} found at #{init_command}") + + determine_current_status! # see Chef::Provider::Service::Simple + + determine_enabled_status! + current_resource end def define_resource_requirements shared_resource_requirements + requirements.assert(:start, :enable, :reload, :restart) do |a| - a.assertion { @rcd_script_found } - a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the rc.d script" + a.assertion { init_command } + a.failure_message Chef::Exceptions::Service, "#{new_resource}: unable to locate the rc.d script" end requirements.assert(:all_actions) do |a| - a.assertion { @enabled_state_found } + a.assertion { enabled_state_found } # for consistentcy with original behavior, this will not fail in non-whyrun mode; # rather it will silently set enabled state=>false a.whyrun "Unable to determine enabled/disabled state, assuming this will be correct for an actual run. Assuming disabled." end requirements.assert(:start, :enable, :reload, :restart) do |a| - a.assertion { @rcd_script_found && service_enable_variable_name != nil } - a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{@init_command} and rcvar" + a.assertion { init_command && service_enable_variable_name != nil } + a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{init_command} and rcvar" # No recovery in whyrun mode - the init file is present but not correct. end end def start_service - if @new_resource.start_command + if new_resource.start_command super else - shell_out!("#{@init_command} faststart") + shell_out_with_systems_locale!("#{init_command} faststart") end end def stop_service - if @new_resource.stop_command + if new_resource.stop_command super else - shell_out!("#{@init_command} faststop") + shell_out_with_systems_locale!("#{init_command} faststop") end end def restart_service - if @new_resource.restart_command - + if new_resource.restart_command super - elsif @new_resource.supports[:restart] - shell_out!("#{@init_command} fastrestart") + elsif new_resource.supports[:restart] + shell_out_with_systems_locale!("#{init_command} fastrestart") else stop_service sleep 1 @@ -115,6 +102,16 @@ class Chef end end + def enable_service + set_service_enable("YES") unless current_resource.enabled + end + + def disable_service + set_service_enable("NO") if current_resource.enabled + end + + private + def read_rc_conf ::File.open("/etc/rc.conf", 'r') { |file| file.readlines } end @@ -127,46 +124,65 @@ class Chef # The variable name used in /etc/rc.conf for enabling this service def service_enable_variable_name - # Look for name="foo" in the shell script @init_command. Use this for determining the variable name in /etc/rc.conf - # corresponding to this service - # For example: to enable the service mysql-server with the init command /usr/local/etc/rc.d/mysql-server, you need - # to set mysql_enable="YES" in /etc/rc.conf$ - if @rcd_script_found - ::File.open(@init_command) do |rcscript| - rcscript.each_line do |line| - if line =~ /^name="?(\w+)"?/ - return $1 + "_enable" + @service_enable_variable_name ||= + begin + # Look for name="foo" in the shell script @init_command. Use this for determining the variable name in /etc/rc.conf + # corresponding to this service + # For example: to enable the service mysql-server with the init command /usr/local/etc/rc.d/mysql-server, you need + # to set mysql_enable="YES" in /etc/rc.conf$ + if init_command + ::File.open(init_command) do |rcscript| + rcscript.each_line do |line| + if line =~ /^name="?(\w+)"?/ + return $1 + "_enable" + end + end + end + # some scripts support multiple instances through symlinks such as openvpn. + # We should get the service name from rcvar. + Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") + sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + else + # for why-run mode when the rcd_script is not there yet + new_resource.service_name + end + end + end + + def determine_enabled_status! + var_name = service_enable_variable_name + if ::File.exist?("/etc/rc.conf") && var_name + read_rc_conf.each do |line| + case line + when /^#{Regexp.escape(var_name)}="(\w+)"/ + enabled_state_found! + if $1 =~ /^yes$/i + current_resource.enabled true + elsif $1 =~ /^(no|none)$/i + current_resource.enabled false end end end - # some scripts support multiple instances through symlinks such as openvpn. - # We should get the service name from rcvar. - Chef::Log.debug("name=\"service\" not found at #{@init_command}. falling back to rcvar") - sn = shell_out!("#{@init_command} rcvar").stdout[/(\w+_enable)=/, 1] - return sn end - # Fallback allows us to keep running in whyrun mode when - # the script does not exist. - @new_resource.service_name + + if current_resource.enabled.nil? + Chef::Log.debug("#{new_resource.name} enable/disable state unknown") + current_resource.enabled false + end end def set_service_enable(value) lines = read_rc_conf # Remove line that set the old value - lines.delete_if { |line| line =~ /#{Regexp.escape(service_enable_variable_name)}/ } + lines.delete_if { |line| line =~ /^#{Regexp.escape(service_enable_variable_name)}=/ } # And append the line that sets the new value at the end lines << "#{service_enable_variable_name}=\"#{value}\"" write_rc_conf(lines) end - def enable_service() - set_service_enable("YES") unless @current_resource.enabled - end - - def disable_service() - set_service_enable("NO") if @current_resource.enabled + def enabled_state_found! + @enabled_state_found = true end - end end end diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb index 1559c7893f..a68abfebc9 100644 --- a/lib/chef/provider/service/gentoo.rb +++ b/lib/chef/provider/service/gentoo.rb @@ -58,10 +58,10 @@ class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init end def enable_service() - run_command(:command => "/sbin/rc-update add #{@new_resource.service_name} default") + shell_out!("/sbin/rc-update add #{@new_resource.service_name} default") end def disable_service() - run_command(:command => "/sbin/rc-update del #{@new_resource.service_name} default") + shell_out!("/sbin/rc-update del #{@new_resource.service_name} default") end end diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index cf5da852c3..5d8bb5bb38 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -24,6 +24,8 @@ class Chef class Service class Init < Chef::Provider::Service::Simple + attr_accessor :init_command + def initialize(new_resource, run_context) super @init_command = "/etc/init.d/#{@new_resource.service_name}" @@ -48,7 +50,7 @@ class Chef if @new_resource.start_command super else - shell_out!("#{default_init_command} start") + shell_out_with_systems_locale!("#{default_init_command} start") end end @@ -56,7 +58,7 @@ class Chef if @new_resource.stop_command super else - shell_out!("#{default_init_command} stop") + shell_out_with_systems_locale!("#{default_init_command} stop") end end @@ -64,7 +66,7 @@ class Chef if @new_resource.restart_command super elsif @new_resource.supports[:restart] - shell_out!("#{default_init_command} restart") + shell_out_with_systems_locale!("#{default_init_command} restart") else stop_service sleep 1 @@ -76,7 +78,7 @@ class Chef if @new_resource.reload_command super elsif @new_resource.supports[:reload] - shell_out!("#{default_init_command} reload") + shell_out_with_systems_locale!("#{default_init_command} reload") end end end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 35767ee7b9..f4c85dd9d3 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -37,12 +37,12 @@ class Chef end def enable_service() - run_command(:command => "/sbin/insserv -r -f #{@new_resource.service_name}") - run_command(:command => "/sbin/insserv -d -f #{@new_resource.service_name}") + shell_out!("/sbin/insserv -r -f #{@new_resource.service_name}") + shell_out!("/sbin/insserv -d -f #{@new_resource.service_name}") end def disable_service() - run_command(:command => "/sbin/insserv -r -f #{@new_resource.service_name}") + shell_out!("/sbin/insserv -r -f #{@new_resource.service_name}") end end end diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 36930ee4ac..cf5e554559 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -83,7 +83,7 @@ class Chef if @new_resource.start_command super else - shell_out!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + shell_out_with_systems_locale!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid) end end end @@ -95,7 +95,7 @@ class Chef if @new_resource.stop_command super else - shell_out!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + shell_out_with_systems_locale!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid) end end end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb index f03b8a18a1..bd51d15f84 100644 --- a/lib/chef/provider/service/simple.rb +++ b/lib/chef/provider/service/simple.rb @@ -25,6 +25,8 @@ class Chef class Service class Simple < Chef::Provider::Service + attr_reader :status_load_success + def load_current_resource @current_resource = Chef::Resource::Service.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @@ -83,16 +85,16 @@ class Chef end def start_service - shell_out!(@new_resource.start_command) + shell_out_with_systems_locale!(@new_resource.start_command) end def stop_service - shell_out!(@new_resource.stop_command) + shell_out_with_systems_locale!(@new_resource.stop_command) end def restart_service if @new_resource.restart_command - shell_out!(@new_resource.restart_command) + shell_out_with_systems_locale!(@new_resource.restart_command) else stop_service sleep 1 @@ -101,7 +103,7 @@ class Chef end def reload_service - shell_out!(@new_resource.reload_command) + shell_out_with_systems_locale!(@new_resource.reload_command) end protected diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb index 0c47a3462b..f0584dcf6d 100644 --- a/lib/chef/provider/service/solaris.rb +++ b/lib/chef/provider/service/solaris.rb @@ -56,7 +56,7 @@ class Chef alias_method :start_service, :enable_service def reload_service - shell_out!("#{default_init_command} refresh #{@new_resource.service_name}") + shell_out_with_systems_locale!("#{default_init_command} refresh #{@new_resource.service_name}") end def restart_service diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb index 6231603d03..31feee65d4 100644 --- a/lib/chef/provider/service/systemd.rb +++ b/lib/chef/provider/service/systemd.rb @@ -28,7 +28,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple if @new_resource.status_command Chef::Log.debug("#{@new_resource} you have specified a status command, running..") - unless shell_out_with_systems_locale(@new_resource.status_command).error? + unless shell_out(@new_resource.status_command).error? @current_resource.running(true) else @status_check_success = false @@ -61,7 +61,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple if @new_resource.start_command super else - shell_out_with_systems_locale("/bin/systemctl start #{@new_resource.service_name}") + shell_out_with_systems_locale!("/bin/systemctl start #{@new_resource.service_name}") end end end @@ -73,7 +73,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple if @new_resource.stop_command super else - shell_out_with_systems_locale("/bin/systemctl stop #{@new_resource.service_name}") + shell_out_with_systems_locale!("/bin/systemctl stop #{@new_resource.service_name}") end end end @@ -82,7 +82,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple if @new_resource.restart_command super else - shell_out_with_systems_locale("/bin/systemctl restart #{@new_resource.service_name}") + shell_out_with_systems_locale!("/bin/systemctl restart #{@new_resource.service_name}") end end @@ -91,7 +91,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple super else if @current_resource.running - shell_out_with_systems_locale("/bin/systemctl reload #{@new_resource.service_name}") + shell_out_with_systems_locale!("/bin/systemctl reload #{@new_resource.service_name}") else start_service end @@ -99,18 +99,18 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple end def enable_service - shell_out_with_systems_locale("/bin/systemctl enable #{@new_resource.service_name}") + shell_out!("/bin/systemctl enable #{@new_resource.service_name}") end def disable_service - shell_out_with_systems_locale("/bin/systemctl disable #{@new_resource.service_name}") + shell_out!("/bin/systemctl disable #{@new_resource.service_name}") end def is_active? - shell_out_with_systems_locale("/bin/systemctl is-active #{@new_resource.service_name} --quiet").exitstatus == 0 + shell_out("/bin/systemctl is-active #{@new_resource.service_name} --quiet").exitstatus == 0 end def is_enabled? - shell_out_with_systems_locale("/bin/systemctl is-enabled #{@new_resource.service_name} --quiet").exitstatus == 0 + shell_out("/bin/systemctl is-enabled #{@new_resource.service_name} --quiet").exitstatus == 0 end end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index c81a8a50dc..670bf9e5f8 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -97,10 +97,10 @@ class Chef Chef::Log.debug("#{@new_resource} you have specified a status command, running..") begin - if run_command_with_systems_locale(:command => @new_resource.status_command) == 0 + if shell_out!(@new_resource.status_command) == 0 @current_resource.running true end - rescue Chef::Exceptions::Exec + rescue @command_success = false @current_resource.running false nil @@ -153,7 +153,7 @@ class Chef if @new_resource.start_command super else - run_command_with_systems_locale(:command => "/sbin/start #{@job}") + shell_out_with_systems_locale!("/sbin/start #{@job}") end end end @@ -167,7 +167,7 @@ class Chef if @new_resource.stop_command super else - run_command_with_systems_locale(:command => "/sbin/stop #{@job}") + shell_out_with_systems_locale!("/sbin/stop #{@job}") end end end @@ -179,7 +179,7 @@ class Chef # Older versions of upstart would fail on restart if the service was currently stopped, check for that. LP:430883 else if @current_resource.running - run_command_with_systems_locale(:command => "/sbin/restart #{@job}") + shell_out_with_systems_locale!("/sbin/restart #{@job}") else start_service end @@ -191,7 +191,7 @@ class Chef super else # upstart >= 0.6.3-4 supports reload (HUP) - run_command_with_systems_locale(:command => "/sbin/reload #{@job}") + shell_out_with_systems_locale!("/sbin/reload #{@job}") end end diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index 2d478fa9fe..d31aad4c9d 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -27,6 +27,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service #Win32::Service.get_start_type AUTO_START = 'auto start' + MANUAL = 'demand start' DISABLED = 'disabled' #Win32::Service.get_current_state @@ -45,11 +46,16 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end def load_current_resource - @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource = Chef::Resource::WindowsService.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @current_resource.running(current_state == RUNNING) Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" - @current_resource.enabled(start_type == AUTO_START) + case current_start_type + when AUTO_START + @current_resource.enabled(true) + when DISABLED + @current_resource.enabled(false) + end Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}" @current_resource end @@ -125,15 +131,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service def enable_service if Win32::Service.exists?(@new_resource.service_name) - if start_type == AUTO_START - Chef::Log.debug "#{@new_resource} already enabled - nothing to do" - else - Win32::Service.configure( - :service_name => @new_resource.service_name, - :start_type => Win32::Service::AUTO_START - ) - @new_resource.updated_by_last_action(true) - end + set_startup_type(:automatic) else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end @@ -141,26 +139,76 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service def disable_service if Win32::Service.exists?(@new_resource.service_name) - if start_type == AUTO_START - Win32::Service.configure( - :service_name => @new_resource.service_name, - :start_type => Win32::Service::DISABLED - ) - @new_resource.updated_by_last_action(true) - else - Chef::Log.debug "#{@new_resource} already disabled - nothing to do" - end + set_startup_type(:disabled) else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end + def action_enable + if current_start_type != AUTO_START + converge_by("enable service #{@new_resource}") do + enable_service + Chef::Log.info("#{@new_resource} enabled") + end + else + Chef::Log.debug("#{@new_resource} already enabled - nothing to do") + end + load_new_resource_state + @new_resource.enabled(true) + end + + def action_disable + if current_start_type != DISABLED + converge_by("disable service #{@new_resource}") do + disable_service + Chef::Log.info("#{@new_resource} disabled") + end + else + Chef::Log.debug("#{@new_resource} already disabled - nothing to do") + end + load_new_resource_state + @new_resource.enabled(false) + end + + def action_configure_startup + case @new_resource.startup_type + when :automatic + if current_start_type != AUTO_START + converge_by("set service #{@new_resource} startup type to automatic") do + set_startup_type(:automatic) + end + else + Chef::Log.debug("#{@new_resource} startup_type already automatic - nothing to do") + end + when :manual + if current_start_type != MANUAL + converge_by("set service #{@new_resource} startup type to manual") do + set_startup_type(:manual) + end + else + Chef::Log.debug("#{@new_resource} startup_type already manual - nothing to do") + end + when :disabled + if current_start_type != DISABLED + converge_by("set service #{@new_resource} startup type to disabled") do + set_startup_type(:disabled) + end + else + Chef::Log.debug("#{@new_resource} startup_type already disabled - nothing to do") + end + end + + # Avoid changing enabled from true/false for now + @new_resource.enabled(nil) + end + private def current_state Win32::Service.status(@new_resource.service_name).current_state end - def start_type + def current_start_type Win32::Service.config_info(@new_resource.service_name).start_type end @@ -188,4 +236,22 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service worker.join end end + + # Takes Win32::Service start_types + def set_startup_type(type) + # Set-Service Startup Type => Win32::Service Constant + allowed_types = { :automatic => Win32::Service::AUTO_START, + :manual => Win32::Service::DEMAND_START, + :disabled => Win32::Service::DISABLED } + unless allowed_types.keys.include?(type) + raise Chef::Exceptions::ConfigurationError, "#{@new_resource.name}: Startup type '#{type}' is not supported" + end + + Chef::Log.debug "#{@new_resource.name} setting start_type to #{type}" + Win32::Service.configure( + :service_name => @new_resource.service_name, + :start_type => allowed_types[type] + ) + @new_resource.updated_by_last_action(true) + end end diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb index 81ed639c53..6cf31c8ec8 100644 --- a/lib/chef/provider/subversion.rb +++ b/lib/chef/provider/subversion.rb @@ -60,7 +60,7 @@ class Chef def action_checkout if target_dir_non_existent_or_empty? converge_by("perform checkout of #{@new_resource.repository} into #{@new_resource.destination}") do - run_command(run_options(:command => checkout_command)) + shell_out!(run_options(command: checkout_command)) end else Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do" @@ -77,7 +77,7 @@ class Chef def action_force_export converge_by("export #{@new_resource.repository} into #{@new_resource.destination}") do - run_command(run_options(:command => export_command)) + shell_out!(run_options(command: export_command)) end end @@ -88,7 +88,7 @@ class Chef Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{revision_int}" unless current_revision_matches_target_revision? converge_by("sync #{@new_resource.destination} from #{@new_resource.repository}") do - run_command(run_options(:command => sync_command)) + shell_out!(run_options(command: sync_command)) Chef::Log.info "#{@new_resource} updated to revision: #{revision_int}" end end @@ -100,14 +100,14 @@ class Chef def sync_command c = scm :update, @new_resource.svn_arguments, verbose, authentication, "-r#{revision_int}", @new_resource.destination Chef::Log.debug "#{@new_resource} updated working copy #{@new_resource.destination} to revision #{@new_resource.revision}" - c + c end def checkout_command c = scm :checkout, @new_resource.svn_arguments, verbose, authentication, "-r#{revision_int}", @new_resource.repository, @new_resource.destination Chef::Log.info "#{@new_resource} checked out #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}" - c + c end def export_command @@ -116,7 +116,7 @@ class Chef "-r#{revision_int}" << @new_resource.repository << @new_resource.destination c = scm :export, *args Chef::Log.info "#{@new_resource} exported #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}" - c + c end # If the specified revision isn't an integer ("HEAD" for example), look diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb index 96b5db24ba..a24a047596 100644 --- a/lib/chef/provider/user/dscl.rb +++ b/lib/chef/provider/user/dscl.rb @@ -16,40 +16,213 @@ # limitations under the License. # +require 'mixlib/shellout' require 'chef/provider/user' require 'openssl' +require 'plist' class Chef class Provider class User + # + # The most tricky bit of this provider is the way it deals with user passwords. + # Mac OS X has different password shadow calculations based on the version. + # < 10.7 => password shadow calculation format SALTED-SHA1 + # => stored in: /var/db/shadow/hash/#{guid} + # => shadow binary length 68 bytes + # => First 4 bytes salt / Next 64 bytes shadow value + # = 10.7 => password shadow calculation format SALTED-SHA512 + # => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist + # => shadow binary length 68 bytes + # => First 4 bytes salt / Next 64 bytes shadow value + # > 10.7 => password shadow calculation format SALTED-SHA512-PBKDF2 + # => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist + # => shadow binary length 128 bytes + # => Salt / Iterations are stored seperately in the same file + # + # This provider only supports Mac OSX versions 10.7 and above class Dscl < Chef::Provider::User - NFS_HOME_DIRECTORY = %r{^NFSHomeDirectory: (.*)$} - AUTHENTICATION_AUTHORITY = %r{^AuthenticationAuthority: (.*)$} + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { mac_osx_version_less_than_10_7? == false } + a.failure_message(Chef::Exceptions::User, "Chef::Provider::User::Dscl only supports Mac OS X versions 10.7 and above.") + end + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/dscl") } + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{@new_resource}!") + end + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/plutil") } + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{@new_resource}!") + end + + requirements.assert(:create, :modify, :manage) do |a| + a.assertion do + if @new_resource.password && mac_osx_version_greater_than_10_7? + # SALTED-SHA512 password shadow hashes are not supported on 10.8 and above. + !salted_sha512?(@new_resource.password) + else + true + end + end + a.failure_message(Chef::Exceptions::User, "SALTED-SHA512 passwords are not supported on Mac 10.8 and above. \ +If you want to set the user password using shadow info make sure you specify a SALTED-SHA512-PBKDF2 shadow hash \ +in 'password', with the associated 'salt' and 'iterations'.") + end + + requirements.assert(:create, :modify, :manage) do |a| + a.assertion do + if @new_resource.password && mac_osx_version_greater_than_10_7? && salted_sha512_pbkdf2?(@new_resource.password) + # salt and iterations should be specified when + # SALTED-SHA512-PBKDF2 password shadow hash is given + !@new_resource.salt.nil? && !@new_resource.iterations.nil? + else + true + end + end + a.failure_message(Chef::Exceptions::User, "SALTED-SHA512-PBKDF2 shadow hash is given without associated \ +'salt' and 'iterations'. Please specify 'salt' and 'iterations' in order to set the user password using shadow hash.") + end + + requirements.assert(:create, :modify, :manage) do |a| + a.assertion do + if @new_resource.password && !mac_osx_version_greater_than_10_7? + # On 10.7 SALTED-SHA512-PBKDF2 is not supported + !salted_sha512_pbkdf2?(@new_resource.password) + else + true + end + end + a.failure_message(Chef::Exceptions::User, "SALTED-SHA512-PBKDF2 shadow hashes are not supported on \ +Mac OS X version 10.7. Please specify a SALTED-SHA512 shadow hash in 'password' attribute to set the \ +user password using shadow hash.") + end - def dscl(*args) - shell_out("dscl . -#{args.join(' ')}") end - def safe_dscl(*args) - result = dscl(*args) - return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 ) - raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0 - raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: / - return result.stdout + def load_current_resource + @current_resource = Chef::Resource::User.new(@new_resource.username) + @current_resource.username(@new_resource.username) + + @user_info = read_user_info + if @user_info + @current_resource.uid(dscl_get(@user_info, :uid)) + @current_resource.gid(dscl_get(@user_info, :gid)) + @current_resource.home(dscl_get(@user_info, :home)) + @current_resource.shell(dscl_get(@user_info, :shell)) + @current_resource.comment(dscl_get(@user_info, :comment)) + @authentication_authority = dscl_get(@user_info, :auth_authority) + + if @new_resource.password && dscl_get(@user_info, :password) == "********" + # A password is set. Let's get the password information from shadow file + shadow_hash_binary = dscl_get(@user_info, :shadow_hash) + + # Calling shell_out directly since we want to give an input stream + shadow_hash_xml = convert_binary_plist_to_xml(shadow_hash_binary.string) + shadow_hash = Plist::parse_xml(shadow_hash_xml) + + if shadow_hash["SALTED-SHA512"] + # Convert the shadow value from Base64 encoding to hex before consuming them + @password_shadow_conversion_algorithm = "SALTED-SHA512" + @current_resource.password(shadow_hash["SALTED-SHA512"].string.unpack('H*').first) + elsif shadow_hash["SALTED-SHA512-PBKDF2"] + @password_shadow_conversion_algorithm = "SALTED-SHA512-PBKDF2" + # Convert the entropy from Base64 encoding to hex before consuming them + @current_resource.password(shadow_hash["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first) + @current_resource.iterations(shadow_hash["SALTED-SHA512-PBKDF2"]["iterations"]) + # Convert the salt from Base64 encoding to hex before consuming them + @current_resource.salt(shadow_hash["SALTED-SHA512-PBKDF2"]["salt"].string.unpack('H*').first) + else + raise(Chef::Exceptions::User,"Unknown shadow_hash format: #{shadow_hash.keys.join(' ')}") + end + end + + convert_group_name if @new_resource.gid + else + @user_exists = false + Chef::Log.debug("#{@new_resource} user does not exist") + end + + @current_resource + end + + # + # Provider Actions + # + + def create_user + dscl_create_user + # set_password modifies the plist file of the user directly. So update + # the password first before making any modifications to the user. + set_password + dscl_create_comment + dscl_set_uid + dscl_set_gid + dscl_set_home + dscl_set_shell + end + + def manage_user + # set_password modifies the plist file of the user directly. So update + # the password first before making any modifications to the user. + set_password if diverged_password? + dscl_create_user if diverged?(:username) + dscl_create_comment if diverged?(:comment) + dscl_set_uid if diverged?(:uid) + dscl_set_gid if diverged?(:gid) + dscl_set_home if diverged?(:home) + dscl_set_shell if diverged?(:shell) + end + + # + # Action Helpers + # + + # + # Create a user using dscl + # + def dscl_create_user + run_dscl("create /Users/#{@new_resource.username}") + end + + # + # Saves the specified Chef user `comment` into RealName attribute + # of Mac user. + # + def dscl_create_comment + run_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") end - # This is handled in providers/group.rb by Etc.getgrnam() - # def user_exists?(user) - # users = safe_dscl("list /Users") - # !! ( users =~ Regexp.new("\n#{user}\n") ) - # end + # + # Sets the user id for the user using dscl. + # If a `uid` is not specified, it finds the next available one starting + # from 200 if `system` is set, 500 otherwise. + # + def dscl_set_uid + @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '') + + if uid_used?(@new_resource.uid) + raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use") + end + + run_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + end - # get a free UID greater than 200 + # + # Find the next available uid on the system. starting with 200 if `system` is set, + # 500 otherwise. + # def get_free_uid(search_limit=1000) - uid = nil; next_uid_guess = 200 - users_uids = safe_dscl("list /Users uid") - while(next_uid_guess < search_limit + 200) + uid = nil + base_uid = @new_resource.system ? 200 : 500 + next_uid_guess = base_uid + users_uids = run_dscl("list /Users uid") + while(next_uid_guess < search_limit + base_uid) if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n") next_uid_guess += 1 else @@ -60,22 +233,41 @@ class Chef return uid || raise("uid not found. Exhausted. Searched #{search_limit} times") end + # + # Returns true if uid is in use by a different account, false otherwise. + # def uid_used?(uid) return false unless uid - users_uids = safe_dscl("list /Users uid") + users_uids = run_dscl("list /Users uid") !! ( users_uids =~ Regexp.new("#{Regexp.escape(uid.to_s)}\n") ) end - def set_uid - @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '') - if uid_used?(@new_resource.uid) - raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use") + # + # Sets the group id for the user using dscl. Fails if a group doesn't + # exist on the system with given group id. + # + def dscl_set_gid + unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/) + begin + possible_gid = run_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last + rescue Chef::Exceptions::DsclCommandFailed => e + raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}") + end + @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) end - safe_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + run_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'") end - def modify_home - return safe_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") if (@new_resource.home.nil? || @new_resource.home.empty?) + # + # Sets the home directory for the user. If `:manage_home` is set home + # directory is managed (moved / created) for the user. + # + def dscl_set_home + if @new_resource.home.nil? || @new_resource.home.empty? + run_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") + return + end + if @new_resource.supports[:manage_home] validate_home_dir_specification! @@ -87,199 +279,399 @@ class Chef move_home end end - safe_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") + run_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") end - def osx_shadow_hash?(string) - return !! ( string =~ /^[[:xdigit:]]{1240}$/ ) + def validate_home_dir_specification! + unless @new_resource.home =~ /^\// + raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'") + end end - def osx_salted_sha1?(string) - return !! ( string =~ /^[[:xdigit:]]{48}$/ ) + def current_home_exists? + ::File.exist?("#{@current_resource.home}") end - def guid - safe_dscl("read /Users/#{@new_resource.username} GeneratedUID").gsub(/GeneratedUID: /,"").strip + def new_home_exists? + ::File.exist?("#{@new_resource.home}") + end + + def ditto_home + skel = "/System/Library/User Template/English.lproj" + raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel) + shell_out! "ditto '#{skel}' '#{@new_resource.home}'" + ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + end + + def move_home + Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}") + + src = @current_resource.home + FileUtils.mkdir_p(@new_resource.home) + files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."] + ::FileUtils.mv(files,@new_resource.home, :force => true) + ::FileUtils.rmdir(src) + ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) end - def shadow_hash_set? - user_data = safe_dscl("read /Users/#{@new_resource.username}") - if user_data =~ /AuthenticationAuthority: / && user_data =~ /ShadowHash/ - true + # + # Sets the shell for the user using dscl. + # + def dscl_set_shell + if @new_resource.shell || ::File.exists?("#{@new_resource.shell}") + run_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'") else - false + run_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") end end - def modify_password - if @new_resource.password - shadow_hash = nil + # + # Sets the password for the user based on given password parameters. + # Chef supports specifying plain-text passwords and password shadow + # hash data. + # + def set_password + # Return if there is no password to set + return if @new_resource.password.nil? + + shadow_info = prepare_password_shadow_info + + # Shadow info is saved as binary plist. Convert the info to binary plist. + shadow_info_binary = StringIO.new + command = Mixlib::ShellOut.new("plutil -convert binary1 -o - -", + :input => shadow_info.to_plist, :live_stream => shadow_info_binary) + command.run_command + + if @user_info.nil? + # User is just created. read_user_info() will read the fresh information + # for the user with a cache flush. However with experimentation we've seen + # that dscl cache is not immediately updated after the creation of the user + # This is odd and needs to be investigated further. + sleep 3 + @user_info = read_user_info + end + + # Replace the shadow info in user's plist + dscl_set(@user_info, :shadow_hash, shadow_info_binary) + save_user_info(@user_info) + end - Chef::Log.debug("#{new_resource} updating password") - if osx_shadow_hash?(@new_resource.password) - shadow_hash = @new_resource.password.upcase + # + # Prepares the password shadow info based on the platform version. + # + def prepare_password_shadow_info + shadow_info = { } + entropy = nil + salt = nil + iterations = nil + + if mac_osx_version_10_7? + hash_value = if salted_sha512?(@new_resource.password) + @new_resource.password else - if osx_salted_sha1?(@new_resource.password) - salted_sha1 = @new_resource.password.upcase - else - hex_salt = "" - OpenSSL::Random.random_bytes(10).each_byte { |b| hex_salt << b.to_i.to_s(16) } - hex_salt = hex_salt.slice(0...8) - salt = [hex_salt].pack("H*") - sha1 = ::OpenSSL::Digest::SHA1.hexdigest(salt+@new_resource.password) - salted_sha1 = (hex_salt+sha1).upcase - end - shadow_hash = String.new("00000000"*155) - shadow_hash[168] = salted_sha1 + # Create a random 4 byte salt + salt = OpenSSL::Random.random_bytes(4) + encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + @new_resource.password) + hash_value = salt.unpack('H*').first + encoded_password end - ::File.open("/var/db/shadow/hash/#{guid}",'w',0600) do |output| - output.puts shadow_hash + shadow_info["SALTED-SHA512"] = StringIO.new + shadow_info["SALTED-SHA512"].string = convert_to_binary(hash_value) + shadow_info + else + if salted_sha512_pbkdf2?(@new_resource.password) + entropy = convert_to_binary(@new_resource.password) + salt = convert_to_binary(@new_resource.salt) + iterations = @new_resource.iterations + else + salt = OpenSSL::Random.random_bytes(32) + iterations = @new_resource.iterations # Use the default if not specified by the user + + entropy = OpenSSL::PKCS5::pbkdf2_hmac( + @new_resource.password, + salt, + iterations, + 128, + OpenSSL::Digest::SHA512.new + ) end - unless shadow_hash_set? - safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';ShadowHash;'") + pbkdf_info = { } + pbkdf_info["entropy"] = StringIO.new + pbkdf_info["entropy"].string = entropy + pbkdf_info["salt"] = StringIO.new + pbkdf_info["salt"].string = salt + pbkdf_info["iterations"] = iterations + + shadow_info["SALTED-SHA512-PBKDF2"] = pbkdf_info + end + + shadow_info + end + + # + # Removes the user from the system after removing user from his groups + # and deleting home directory if needed. + # + def remove_user + if @new_resource.supports[:manage_home] + # Remove home directory + FileUtils.rm_rf(@current_resource.home) + end + + # Remove the user from its groups + run_dscl("list /Groups").each_line do |group| + if member_of_group?(group.chomp) + run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{@new_resource.username}'") end end + + # Remove user account + run_dscl("delete /Users/#{@new_resource.username}") end - def load_current_resource - super - raise Chef::Exceptions::User, "Could not find binary /usr/bin/dscl for #{@new_resource}" unless ::File.exists?("/usr/bin/dscl") + # + # Locks the user. + # + def lock_user + run_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") end - def create_user - dscl_create_user - dscl_create_comment - set_uid - dscl_set_gid - modify_home - dscl_set_shell - modify_password + # + # Unlocks the user + # + def unlock_user + auth_string = @authentication_authority.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip + run_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'") end - def manage_user - dscl_create_user if diverged?(:username) - dscl_create_comment if diverged?(:comment) - set_uid if diverged?(:uid) - dscl_set_gid if diverged?(:gid) - modify_home if diverged?(:home) - dscl_set_shell if diverged?(:shell) - modify_password if diverged?(:password) + # + # Returns true if the user is locked, false otherwise. + # + def locked? + if @authentication_authority + !!(@authentication_authority =~ /DisabledUser/ ) + else + false + end end - def dscl_create_user - safe_dscl("create /Users/#{@new_resource.username}") + # + # This is the interface base User provider requires to provide idempotency. + # + def check_lock + return @locked = locked? end - def dscl_create_comment - safe_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") + # + # Helper functions + # + + # + # Returns true if the system state and desired state is different for + # given attribute. + # + def diverged?(parameter) + parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) end - def dscl_set_gid - unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/) - begin - possible_gid = safe_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last - rescue Chef::Exceptions::DsclCommandFailed => e - raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}") - end - @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) - end - safe_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'") + def parameter_updated?(parameter) + not (@new_resource.send(parameter) == @current_resource.send(parameter)) end - def dscl_set_shell - if @new_resource.password || ::File.exists?("#{@new_resource.shell}") - safe_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'") + # + # We need a special check function for password since we support both + # plain text and shadow hash data. + # + # Checks if password needs update based on platform version and the + # type of the password specified. + # + def diverged_password? + return false if @new_resource.password.nil? + + # Dscl provider supports both plain text passwords and shadow hashes. + if mac_osx_version_10_7? + if salted_sha512?(@new_resource.password) + diverged?(:password) + else + !salted_sha512_password_match? + end else - safe_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") + # When a system is upgraded to a version 10.7+ shadow hashes of the users + # will be updated when the user logs in. So it's possible that we will have + # SALTED-SHA512 password in the current_resource. In that case we will force + # password to be updated. + return true if salted_sha512?(@current_resource.password) + + if salted_sha512_pbkdf2?(@new_resource.password) + diverged?(:password) || diverged?(:salt) || diverged?(:iterations) + else + !salted_sha512_pbkdf2_password_match? + end end end - def remove_user - if @new_resource.supports[:manage_home] - user_info = safe_dscl("read /Users/#{@new_resource.username}") - if nfs_home_match = user_info.match(NFS_HOME_DIRECTORY) - #nfs_home = safe_dscl("read /Users/#{@new_resource.username} NFSHomeDirectory") - #nfs_home.gsub!(/NFSHomeDirectory: /,"").gsub!(/\n$/,"") - nfs_home = nfs_home_match[1] - FileUtils.rm_rf(nfs_home) - end - end - # remove the user from its groups - groups = [] - Etc.group do |group| - groups << group.name if group.mem.include?(@new_resource.username) + # + # Returns true if user is member of the specified group, false otherwise. + # + def member_of_group?(group_name) + membership_info = "" + begin + membership_info = run_dscl("read /Groups/#{group_name}") + rescue Chef::Exceptions::DsclCommandFailed + # Raised if the group doesn't contain any members end - groups.each do |group_name| - safe_dscl("delete /Groups/#{group_name} GroupMembership '#{@new_resource.username}'") - end - # remove user account - safe_dscl("delete /Users/#{@new_resource.username}") + # Output is something like: + # GroupMembership: root admin etc + members = membership_info.split(" ") + members.shift # Get rid of GroupMembership: string + members.include?(@new_resource.username) end - def locked? - user_info = safe_dscl("read /Users/#{@new_resource.username}") - if auth_authority_md = AUTHENTICATION_AUTHORITY.match(user_info) - !!(auth_authority_md[1] =~ /DisabledUser/ ) - else - false + # + # DSCL Helper functions + # + + # A simple map of Chef's terms to DSCL's terms. + DSCL_PROPERTY_MAP = { + :uid => "generateduid", + :gid => "gid", + :home => "home", + :shell => "shell", + :comment => "realname", + :password => "passwd", + :auth_authority => "authentication_authority", + :shadow_hash => "ShadowHashData" + }.freeze + + # Directory where the user plist files are stored for versions 10.7 and above + USER_PLIST_DIRECTORY = "/var/db/dslocal/nodes/Default/users".freeze + + # + # Reads the user plist and returns a hash keyed with DSCL properties specified + # in DSCL_PROPERTY_MAP. Return nil if the user is not found. + # + def read_user_info + user_info = nil + + # We flush the cache here in order to make sure that we read fresh information + # for the user. + shell_out("dscacheutil '-flushcache'") + + begin + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + user_plist_info = run_plutil("convert xml1 -o - #{user_plist_file}") + user_info = Plist::parse_xml(user_plist_info) + rescue Chef::Exceptions::PlistUtilCommandFailed end + + user_info end - def check_lock - return @locked = locked? + # + # Saves the given hash keyed with DSCL properties specified + # in DSCL_PROPERTY_MAP to the disk. + # + def save_user_info(user_info) + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + Plist::Emit.save_plist(user_info, user_plist_file) + run_plutil("convert binary1 #{user_plist_file}") end - def lock_user - safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") + # + # Sets a value in user information hash using Chef attributes as keys. + # + def dscl_set(user_hash, key, value) + raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key) + user_hash[DSCL_PROPERTY_MAP[key]] = [ value ] + user_hash end - def unlock_user - auth_info = safe_dscl("read /Users/#{@new_resource.username} AuthenticationAuthority") - auth_string = auth_info.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip#.gsub!(/[; ]*$/,"") - safe_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'") + # + # Gets a value from user information hash using Chef attributes as keys. + # + def dscl_get(user_hash, key) + raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key) + # DSCL values are set as arrays + value = user_hash[DSCL_PROPERTY_MAP[key]] + value.nil? ? value : value.first end - def validate_home_dir_specification! - unless @new_resource.home =~ /^\// - raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'") - end + # + # System Helpets + # + + def mac_osx_version + # This provider will only be invoked on node[:platform] == "mac_os_x" + # We do not check or assert that here. + node[:platform_version] end - def current_home_exists? - ::File.exist?("#{@current_resource.home}") + def mac_osx_version_10_7? + mac_osx_version.start_with?("10.7.") end - def new_home_exists? - ::File.exist?("#{@new_resource.home}") + def mac_osx_version_less_than_10_7? + versions = mac_osx_version.split(".") + # Make integer comparison in order not to report 10.10 less than 10.7 + (versions[0].to_i <= 10 && versions[1].to_i < 7) end - def ditto_home - skel = "/System/Library/User Template/English.lproj" - raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel) - shell_out! "ditto '#{skel}' '#{@new_resource.home}'" - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + def mac_osx_version_greater_than_10_7? + versions = mac_osx_version.split(".") + # Make integer comparison in order not to report 10.10 less than 10.7 + (versions[0].to_i >= 10 && versions[1].to_i > 7) end - def move_home - Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}") + def run_dscl(*args) + result = shell_out("dscl . -#{args.join(' ')}") + return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 ) + raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0 + raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: / + result.stdout + end - src = @current_resource.home - FileUtils.mkdir_p(@new_resource.home) - files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."] - ::FileUtils.mv(files,@new_resource.home, :force => true) - ::FileUtils.rmdir(src) - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + def run_plutil(*args) + result = shell_out("plutil -#{args.join(' ')}") + raise(Chef::Exceptions::PlistUtilCommandFailed,"plutil error: #{result.inspect}") unless result.exitstatus == 0 + result.stdout end - def diverged?(parameter) - parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) + def convert_binary_plist_to_xml(binary_plist_string) + Mixlib::ShellOut.new("plutil -convert xml1 -o - -", :input => binary_plist_string).run_command.stdout end - def parameter_updated?(parameter) - not (@new_resource.send(parameter) == @current_resource.send(parameter)) + def convert_to_binary(string) + string.unpack('a2'*(string.size/2)).collect { |i| i.hex.chr }.join + end + + def salted_sha512?(string) + !!(string =~ /^[[:xdigit:]]{136}$/) + end + + def salted_sha512_password_match? + # Salt is included in the first 4 bytes of shadow data + salt = @current_resource.password.slice(0,8) + shadow = OpenSSL::Digest::SHA512.hexdigest(convert_to_binary(salt) + @new_resource.password) + @current_resource.password == salt + shadow end + + def salted_sha512_pbkdf2?(string) + !!(string =~ /^[[:xdigit:]]{256}$/) + end + + def salted_sha512_pbkdf2_password_match? + salt = convert_to_binary(@current_resource.salt) + + OpenSSL::PKCS5::pbkdf2_hmac( + @new_resource.password, + salt, + @current_resource.iterations, + 128, + OpenSSL::Digest::SHA512.new + ).unpack('H*').first == @current_resource.password + end + end end end diff --git a/lib/chef/provider/user/windows.rb b/lib/chef/provider/user/windows.rb index 350f3ff4c0..66ef30c349 100644 --- a/lib/chef/provider/user/windows.rb +++ b/lib/chef/provider/user/windows.rb @@ -17,6 +17,7 @@ # require 'chef/provider/user' +require 'chef/exceptions' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ require 'chef/util/windows/net_user' end @@ -28,7 +29,7 @@ class Chef def initialize(new_resource,run_context) super - @net_user = Chef::Util::Windows::NetUser.new(@new_resource.name) + @net_user = Chef::Util::Windows::NetUser.new(@new_resource.username) end def load_current_resource @@ -37,17 +38,16 @@ class Chef user_info = nil begin user_info = @net_user.get_info - rescue - @user_exists = false - Chef::Log.debug("#{@new_resource} does not exist") - end - if user_info @current_resource.uid(user_info[:user_id]) @current_resource.gid(user_info[:primary_group_id]) @current_resource.comment(user_info[:full_name]) @current_resource.home(user_info[:home_dir]) @current_resource.shell(user_info[:script_path]) + rescue Chef::Exceptions::UserIDNotFound => e + # e.message should be "The user name could not be found" but checking for that could cause a localization bug + @user_exists = false + Chef::Log.debug("#{@new_resource} does not exist (#{e.message})") end @current_resource diff --git a/lib/chef/provider/whyrun_safe_ruby_block.rb b/lib/chef/provider/whyrun_safe_ruby_block.rb index 4b491a4f60..e5f35debd7 100644 --- a/lib/chef/provider/whyrun_safe_ruby_block.rb +++ b/lib/chef/provider/whyrun_safe_ruby_block.rb @@ -19,7 +19,7 @@ class Chef class Provider class WhyrunSafeRubyBlock < Chef::Provider::RubyBlock - def action_create + def action_run @new_resource.block.call @new_resource.updated_by_last_action(true) @run_context.events.resource_update_applied(@new_resource, :create, "execute the whyrun_safe_ruby_block #{@new_resource.name}") diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 5b95d80590..32578da5ab 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -69,7 +69,6 @@ class Chef @run_context = run_context # TODO: 5/19/2010 cw/tim: determine whether this can be removed @params = Hash.new - @node = deprecated_ivar(run_context.node, :node, :warn) end # Used in DSL mixins diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 6adf937f53..70abfbcdb0 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -23,7 +23,7 @@ require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' require 'chef/dsl/reboot_pending' require 'chef/mixin/convert_to_class_name' -require 'chef//guard_interpreter/resource_guard_interpreter' +require 'chef/guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' require 'chef/resource/conditional_action_not_nothing' require 'chef/resource_collection' @@ -121,8 +121,8 @@ F end - FORBIDDEN_IVARS = [:@run_context, :@node, :@not_if, :@only_if, :@enclosing_provider] - HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@node, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider] + FORBIDDEN_IVARS = [:@run_context, :@not_if, :@only_if, :@enclosing_provider] + HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider] include Chef::DSL::DataQuery include Chef::Mixin::ParamsValidate @@ -253,8 +253,6 @@ F @guard_interpreter = :default @elapsed_time = 0 @sensitive = false - - @node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil end # Returns a Hash of attribute => value for the state attributes declared in @@ -660,6 +658,9 @@ F end ensure @elapsed_time = Time.now - start_time + # Reporting endpoint doesn't accept a negative resource duration so set it to 0. + # A negative value can occur when a resource changes the system time backwards + @elapsed_time = 0 if @elapsed_time < 0 events.resource_completed(self) end end diff --git a/lib/chef/resource/freebsd_package.rb b/lib/chef/resource/freebsd_package.rb index c7a8d44181..70ef62ae8a 100644 --- a/lib/chef/resource/freebsd_package.rb +++ b/lib/chef/resource/freebsd_package.rb @@ -31,17 +31,26 @@ class Chef provides :package, :on_platforms => ["freebsd"] + attr_accessor :created_as_type def initialize(name, run_context=nil) super @resource_name = :freebsd_package + @created_as_type = "freebsd_package" end def after_created assign_provider end - + # This resource can be invoked with multiple names package & freebsd_package. + # We override the to_s method to ensure the key in resource collection + # matches the type resource is declared as using created_as_type. This + # logic can be removed once Chef does this for all resource in Chef 12: + # https://github.com/opscode/chef/issues/1817 + def to_s + "#{created_as_type}[#{name}]" + end private @@ -68,4 +77,3 @@ class Chef end end end - diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb index 9eafe07253..275c069f61 100644 --- a/lib/chef/resource/mount.rb +++ b/lib/chef/resource/mount.rb @@ -33,6 +33,7 @@ class Chef @mount_point = name @device = nil @device_type = :device + @fsck_device = '-' @fstype = "auto" @options = ["defaults"] @dump = 0 @@ -77,6 +78,14 @@ class Chef ) end + def fsck_device(arg=nil) + set_or_return( + :fsck_device, + arg, + :kind_of => [ String ] + ) + end + def fstype(arg=nil) set_or_return( :fstype, diff --git a/lib/chef/resource/scm.rb b/lib/chef/resource/scm.rb index 91782e4114..87c217b4cc 100644 --- a/lib/chef/resource/scm.rb +++ b/lib/chef/resource/scm.rb @@ -40,6 +40,7 @@ class Chef @allowed_actions.push(:checkout, :export, :sync, :diff, :log) @action = [:sync] @checkout_branch = "deploy" + @environment = nil end def destination(arg=nil) @@ -172,6 +173,15 @@ class Chef ) end + def environment(arg=nil) + set_or_return( + :environment, + arg, + :kind_of => [ Hash ] + ) + end + + alias :env :environment end end end diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb index 05c076229f..9d6e857de7 100644 --- a/lib/chef/resource/user.rb +++ b/lib/chef/resource/user.rb @@ -45,6 +45,8 @@ class Chef :manage_home => false, :non_unique => false } + @iterations = 27855 + @salt = nil @allowed_actions.push(:create, :remove, :modify, :manage, :lock, :unlock) end @@ -106,6 +108,22 @@ class Chef ) end + def salt(arg=nil) + set_or_return( + :salt, + arg, + :kind_of => [ String ] + ) + end + + def iterations(arg=nil) + set_or_return( + :iterations, + arg, + :kind_of => [ Integer ] + ) + end + def system(arg=nil) set_or_return( :system, diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb new file mode 100644 index 0000000000..5ed8e76cbd --- /dev/null +++ b/lib/chef/resource/windows_service.rb @@ -0,0 +1,53 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/resource/service' + +class Chef + class Resource + class WindowsService < Chef::Resource::Service + + # Until #1773 is resolved, you need to manually specify the windows_service resource + # to use action :configure_startup and attribute startup_type + + # provides :service, :on_platforms => ["windows"] + + identity_attr :service_name + + state_attrs :enabled, :running + + def initialize(name, run_context=nil) + super + @resource_name = :windows_service + @provider = Chef::Provider::Service::Windows + @allowed_actions.push(:configure_startup) + @startup_type = :automatic + end + + def startup_type(arg=nil) + # Set-Service arguments are automatic and manual + # Win32::Service returns 'auto start' or 'demand start' respectively, which the provider currently uses + set_or_return( + :startup_type, + arg, + :equal_to => [ :automatic, :manual, :disabled ] + ) + end + end + end +end diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb index 2cbd61cb0c..cc14a03962 100644 --- a/lib/chef/resource_collection.rb +++ b/lib/chef/resource_collection.rb @@ -67,24 +67,33 @@ class Chef alias_method :push, :<< def insert(resource) - is_chef_resource(resource) if @insert_after_idx # in the middle of executing a run, so any resources inserted now should # be placed after the most recent addition done by the currently executing # resource - @resources.insert(@insert_after_idx + 1, resource) - # update name -> location mappings and register new resource - @resources_by_name.each_key do |key| - @resources_by_name[key] += 1 if @resources_by_name[key] > @insert_after_idx - end - @resources_by_name[resource.to_s] = @insert_after_idx + 1 + insert_at(@insert_after_idx + 1, resource) @insert_after_idx += 1 else + is_chef_resource(resource) @resources << resource @resources_by_name[resource.to_s] = @resources.length - 1 end end + def insert_at(insert_at_index, *resources) + resources.each do |resource| + is_chef_resource(resource) + end + @resources.insert(insert_at_index, *resources) + # update name -> location mappings and register new resource + @resources_by_name.each_key do |key| + @resources_by_name[key] += resources.size if @resources_by_name[key] >= insert_at_index + end + resources.each_with_index do |resource, i| + @resources_by_name[resource.to_s] = insert_at_index + i + end + end + def each @resources.each do |resource| yield resource @@ -249,7 +258,7 @@ class Chef def is_chef_resource(arg) unless arg.kind_of?(Chef::Resource) - raise ArgumentError, "Members must be Chef::Resource's" + raise ArgumentError, "Cannot insert a #{arg.class} into a resource collection: must be a subclass of Chef::Resource" end true end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 01e8d63040..93ff682288 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -64,6 +64,7 @@ require 'chef/resource/ruby_block' require 'chef/resource/scm' require 'chef/resource/script' require 'chef/resource/service' +require 'chef/resource/windows_service' require 'chef/resource/subversion' require 'chef/resource/smartos_package' require 'chef/resource/template' diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb index 4869ec1484..cc43efe1b1 100644 --- a/lib/chef/search/query.rb +++ b/lib/chef/search/query.rb @@ -23,6 +23,7 @@ require 'chef/node' require 'chef/role' require 'chef/data_bag' require 'chef/data_bag_item' +require 'chef/exceptions' class Chef class Search @@ -34,17 +35,112 @@ class Chef @rest = Chef::REST.new(url ||Chef::Config[:chef_server_url]) end - # Search Solr for objects of a given type, for a given query. If you give - # it a block, it will handle the paging for you dynamically. - def search(type, query="*:*", sort='X_CHEF_id_CHEF_X asc', start=0, rows=1000, &block) - raise ArgumentError, "Type must be a string or a symbol!" unless (type.kind_of?(String) || type.kind_of?(Symbol)) - response = @rest.get_rest("search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}") - if block - response["rows"].each { |o| block.call(o) unless o.nil?} + # This search is only kept for backwards compatibility, since the results of the + # new filtered search method will be in a slightly different format + def partial_search(type, query='*:*', *args, &block) + Chef::Log.warn("DEPRECATED: The 'partial_search' api is deprecated, please use the search api with 'filter_result'") + # accept both types of args + if args.length == 1 && args[0].is_a?(Hash) + args_hash = args[0].dup + # partial_search implemented in the partial search cookbook uses the + # arg hash :keys instead of :filter_result to filter returned data + args_hash[:filter_result] = args_hash[:keys] + else + args_hash = {} + args_hash[:sort] = args[0] if args.length >= 1 + args_hash[:start] = args[1] if args.length >= 2 + args_hash[:rows] = args[2] if args.length >= 3 + end + + unless block.nil? + raw_results = search(type,query,args_hash) + else + raw_results = search(type,query,args_hash,&block) + end + results = Array.new + raw_results[0].each do |r| + results << r["data"] + end + return results + end + + # + # New search input, designed to be backwards compatible with the old method signature + # 'type' and 'query' are the same as before, args now will accept either a Hash of + # search arguments with symbols as the keys (ie :sort, :start, :rows) and a :filter_result + # option. + # + # :filter_result should be in the format of another Hash with the structure of: + # { + # :returned_name1 => ["path", "to", "variable"], + # :returned_name2 => ["shorter", "path"] + # } + # a real world example might be something like: + # { + # :ip_address => ["ipaddress"], + # :ruby_version => ["languages", "ruby", "version"] + # } + # this will bring back 2 variables 'ip_address' and 'ruby_version' with whatever value was found + # an example of the returned json may be: + # {"ip_address":"127.0.0.1", "ruby_version": "1.9.3"} + # + def search(type, query='*:*', *args, &block) + validate_type(type) + validate_args(args) + + scrubbed_args = Hash.new + + # argify everything + if args[0].kind_of?(Hash) + scrubbed_args = args[0] + else + # This api will be deprecated in a future release + scrubbed_args = { :sort => args[0], :start => args[1], :rows => args[2] } + end + + # set defaults, if they haven't been set yet. + scrubbed_args[:sort] ||= 'X_CHEF_id_CHEF_X asc' + scrubbed_args[:start] ||= 0 + scrubbed_args[:rows] ||= 1000 + + do_search(type, query, scrubbed_args, &block) + end + + def list_indexes + @rest.get_rest("search") + end + + private + def validate_type(t) + unless t.kind_of?(String) || t.kind_of?(Symbol) + msg = "Invalid search object type #{t.inspect} (#{t.class}), must be a String or Symbol." + + "Useage: search(:node, QUERY, [OPTIONAL_ARGS])" + + " `knife search environment QUERY (options)`" + raise Chef::Exceptions::InvalidSearchQuery, msg + end + end + + def validate_args(a) + max_args = 3 + raise Chef::Exceptions::InvalidSearchQuery, "Too many arguments! (#{a.size} for <= #{max_args})" if a.size > max_args + end + + def escape(s) + s && URI.escape(s.to_s) + end + + # new search api that allows for a cleaner implementation of things like return filters + # (formerly known as 'partial search'). + # Also args should never be nil, but that is required for Ruby 1.8 compatibility + def do_search(type, query="*:*", args=nil, &block) + query_string = create_query_string(type, query, args) + response = call_rest_service(query_string, args) + unless block.nil? + response["rows"].each { |rowset| block.call(rowset) unless rowset.nil?} unless (response["start"] + response["rows"].length) >= response["total"] - nstart = response["start"] + rows - search(type, query, sort, nstart, rows, &block) + args[:start] = response["start"] + args[:rows] + do_search(type, query, args, &block) end true else @@ -52,14 +148,26 @@ class Chef end end - def list_indexes - @rest.get_rest("search") + # create the full rest url string + def create_query_string(type, query, args) + # create some default variables just so we don't break backwards compatibility + sort = args[:sort] + start = args[:start] + rows = args[:rows] + + return "search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}" end - private - def escape(s) - s && URI.escape(s.to_s) + def call_rest_service(query_string, args) + if args.key?(:filter_result) + response = @rest.post_rest(query_string, args[:filter_result]) + response_rows = response['rows'].map { |row| row['data'] } + else + response = @rest.get_rest(query_string) + response_rows = response['rows'] end + return response + end end end end diff --git a/lib/chef/util/path_helper.rb b/lib/chef/util/path_helper.rb index 534a9087ae..e9fb4e7773 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -16,15 +16,50 @@ # limitations under the License. # -require 'chef/platform' -require 'chef/exceptions' - class Chef class Util class PathHelper # Maximum characters in a standard Windows path (260 including drive letter and NUL) WIN_MAX_PATH = 259 + def self.dirname(path) + if Chef::Platform.windows? + # Find the first slash, not counting trailing slashes + end_slash = path.size + while true + slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1) + if !slash + return end_slash == path.size ? '.' : path_separator + elsif slash == end_slash - 1 + end_slash = slash + else + return path[0..slash-1] + end + end + else + ::File.dirname(path) + end + end + + BACKSLASH = '\\'.freeze + + def self.path_separator + if Chef::Platform.windows? + File::ALT_SEPARATOR || BACKSLASH + else + File::SEPARATOR + end + end + + def self.join(*args) + args.flatten.inject do |joined_path, component| + # Joined path ends with / + joined_path = joined_path.sub(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+$/, '') + component = component.sub(/^[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+/, '') + joined_path += "#{path_separator}#{component}" + end + end + def self.validate_path(path) if Chef::Platform.windows? unless printable?(path) @@ -32,7 +67,7 @@ class Chef Chef::Log.error(msg) raise Chef::Exceptions::ValidationFailed, msg end - + if windows_max_length_exceeded?(path) Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") path.insert(0, "\\\\?\\") @@ -50,7 +85,7 @@ class Chef return true end end - + false end @@ -75,7 +110,7 @@ class Chef if Chef::Platform.windows? # Add the \\?\ API prefix on Windows unless add_prefix is false # Downcase on Windows where paths are still case-insensitive - abs_path.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR) + abs_path.gsub!(::File::SEPARATOR, path_separator) if add_prefix && abs_path !~ /^\\\\?\\/ abs_path.insert(0, "\\\\?\\") end @@ -86,9 +121,22 @@ class Chef abs_path end + def self.cleanpath(path) + path = Pathname.new(path).cleanpath.to_s + # ensure all forward slashes are backslashes + if Chef::Platform.windows? + path = path.gsub(File::SEPARATOR, path_separator) + end + path + end + def self.paths_eql?(path1, path2) canonical_path(path1) == canonical_path(path2) end end end end + +# Break a require loop when require chef/util/path_helper +require 'chef/platform' +require 'chef/exceptions' diff --git a/lib/chef/util/windows/net_user.rb b/lib/chef/util/windows/net_user.rb index 5cca348c8e..5df1a8aaa4 100644 --- a/lib/chef/util/windows/net_user.rb +++ b/lib/chef/util/windows/net_user.rb @@ -17,6 +17,7 @@ # require 'chef/util/windows' +require 'chef/exceptions' #wrapper around a subset of the NetUser* APIs. #nothing Chef specific, but not complete enough to be its own gem, so util for now. @@ -137,7 +138,9 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows ptr = 0.chr * PTR_SIZE rc = NetUserGetInfo.call(nil, @name, 3, ptr) - if rc != NERR_Success + if rc == NERR_UserNotFound + raise Chef::Exceptions::UserIDNotFound, get_last_error(rc) + elsif rc != NERR_Success raise ArgumentError, get_last_error(rc) end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index ea1002ee37..be449d4fa0 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -17,7 +17,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.0.0.alpha.0' + VERSION = '12.0.0.alpha.1' end # diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index cb028020cf..eeb2b078a4 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -33,6 +33,7 @@ class Chef MAX_PREFERRED_LENGTH = 0xFFFF NERR_Success = 0 + NERR_UserNotFound = 2221 ffi_lib "netapi32" diff --git a/lib/chef/workstation_config_loader.rb b/lib/chef/workstation_config_loader.rb new file mode 100644 index 0000000000..6715d4eec2 --- /dev/null +++ b/lib/chef/workstation_config_loader.rb @@ -0,0 +1,177 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config_fetcher' +require 'chef/config' +require 'chef/null_logger' + +class Chef + + class WorkstationConfigLoader + + # Path to a config file requested by user, (e.g., via command line option). Can be nil + attr_reader :explicit_config_file + + # TODO: initialize this with a logger for Chef and Knife + def initialize(explicit_config_file, logger=nil) + @explicit_config_file = explicit_config_file + @config_location = nil + @logger = logger || NullLogger.new + end + + def no_config_found? + config_location.nil? + end + + def config_location + @config_location ||= (explicit_config_file || locate_local_config) + end + + def chef_config_dir + if @chef_config_dir.nil? + @chef_config_dir = false + full_path = working_directory.split(File::SEPARATOR) + (full_path.length - 1).downto(0) do |i| + candidate_directory = File.join(full_path[0..i] + [".chef" ]) + if File.exist?(candidate_directory) && File.directory?(candidate_directory) + @chef_config_dir = candidate_directory + break + end + end + end + @chef_config_dir + end + + def load + # Ignore it if there's no explicit_config_file and can't find one at a + # default path. + return false if config_location.nil? + + if explicit_config_file && !path_exists?(config_location) + raise Exceptions::ConfigurationError, "Specified config file #{config_location} does not exist" + end + + # Have to set Chef::Config.config_file b/c other config is derived from it. + Chef::Config.config_file = config_location + read_config(IO.read(config_location), config_location) + end + + # (Private API, public for test purposes) + def env + ENV + end + + # (Private API, public for test purposes) + def path_exists?(path) + Pathname.new(path).expand_path.exist? + end + + private + + def have_config?(path) + if path_exists?(path) + logger.info("Using config at #{path}") + true + else + logger.debug("Config not found at #{path}, trying next option") + false + end + end + + def locate_local_config + candidate_configs = [] + + # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine) + if env['KNIFE_HOME'] + candidate_configs << File.join(env['KNIFE_HOME'], 'config.rb') + candidate_configs << File.join(env['KNIFE_HOME'], 'knife.rb') + end + # Look for $PWD/knife.rb + if Dir.pwd + candidate_configs << File.join(Dir.pwd, 'config.rb') + candidate_configs << File.join(Dir.pwd, 'knife.rb') + end + # Look for $UPWARD/.chef/knife.rb + if chef_config_dir + candidate_configs << File.join(chef_config_dir, 'config.rb') + candidate_configs << File.join(chef_config_dir, 'knife.rb') + end + # Look for $HOME/.chef/knife.rb + if env['HOME'] + candidate_configs << File.join(env['HOME'], '.chef', 'config.rb') + candidate_configs << File.join(env['HOME'], '.chef', 'knife.rb') + end + + candidate_configs.find do | candidate_config | + have_config?(candidate_config) + end + end + + def working_directory + a = if Chef::Platform.windows? + env['CD'] + else + env['PWD'] + end || Dir.pwd + + a + end + + def read_config(config_content, config_file_path) + Chef::Config.from_string(config_content, config_file_path) + rescue SignalException + raise + rescue SyntaxError => e + message = "" + message << "You have invalid ruby syntax in your config file #{config_file_path}\n\n" + message << "#{e.class.name}: #{e.message}\n" + if file_line = e.message[/#{Regexp.escape(config_file_path)}:[\d]+/] + line = file_line[/:([\d]+)$/, 1].to_i + message << highlight_config_error(config_file_path, line) + end + raise Exceptions::ConfigurationError, message + rescue Exception => e + message = "You have an error in your config file #{config_file_path}\n\n" + message << "#{e.class.name}: #{e.message}\n" + filtered_trace = e.backtrace.grep(/#{Regexp.escape(config_file_path)}/) + filtered_trace.each {|bt_line| message << " " << bt_line << "\n" } + if !filtered_trace.empty? + line_nr = filtered_trace.first[/#{Regexp.escape(config_file_path)}:([\d]+)/, 1] + message << highlight_config_error(config_file_path, line_nr.to_i) + end + raise Exceptions::ConfigurationError, message + end + + + def highlight_config_error(file, line) + config_file_lines = [] + IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"} + if line == 1 + lines = config_file_lines[0..3] + else + lines = config_file_lines[Range.new(line - 2, line)] + end + "Relevant file content:\n" + lines.join("\n") + "\n" + end + + def logger + @logger + end + + end +end |