diff options
author | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
---|---|---|
committer | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
commit | 24dc69a9a97e82a6e4207de68d6dcc664178249b (patch) | |
tree | 19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/provider | |
parent | 9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff) | |
download | chef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz |
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code
used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib/chef/provider')
77 files changed, 11687 insertions, 0 deletions
diff --git a/lib/chef/provider/breakpoint.rb b/lib/chef/provider/breakpoint.rb new file mode 100644 index 0000000000..224e2758eb --- /dev/null +++ b/lib/chef/provider/breakpoint.rb @@ -0,0 +1,36 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class Breakpoint < Chef::Provider + + def load_current_resource + end + + def action_break + if defined?(Shell) && Shell.running? + run_context.resource_collection.iterator.pause + @new_resource.updated_by_last_action(true) + run_context.resource_collection.iterator + end + end + + end + end +end diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb new file mode 100644 index 0000000000..431f3f2367 --- /dev/null +++ b/lib/chef/provider/cookbook_file.rb @@ -0,0 +1,84 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/file_access_control' +require 'chef/provider/file' +require 'tempfile' + +class Chef + class Provider + class CookbookFile < Chef::Provider::File + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::CookbookFile.new(@new_resource.name) + super + end + + def action_create + if file_cache_location && content_stale? + description = [] + description << "create a new cookbook_file #{@new_resource.path}" + description << diff_current(file_cache_location) + converge_by(description) do + Chef::Log.debug("#{@new_resource} has new contents") + backup_new_resource + deploy_tempfile do |tempfile| + Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}") + tempfile.close + FileUtils.cp(file_cache_location, tempfile.path) + # Since the @new_resource.path file will not be updated + # at the time of converge, we must use the tempfile + update_new_file_state(tempfile.path) + end + Chef::Log.info("#{@new_resource} created file #{@new_resource.path}") + end + else + set_all_access_controls + end + end + + def file_cache_location + @file_cache_location ||= begin + cookbook = run_context.cookbook_collection[resource_cookbook] + cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path) + end + end + + # Determine the cookbook to get the file from. If new resource sets an + # explicit cookbook, use it, otherwise fall back to the implicit cookbook + # i.e., the cookbook the resource was declared in. + def resource_cookbook + @new_resource.cookbook || @new_resource.cookbook_name + end + + def backup_new_resource + if ::File.exists?(@new_resource.path) + backup @new_resource.path + end + end + + def content_stale? + ( ! ::File.exist?(@new_resource.path)) || ( ! compare_content) + end + + end + end +end diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb new file mode 100644 index 0000000000..a7218fea5a --- /dev/null +++ b/lib/chef/provider/cron.rb @@ -0,0 +1,214 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/mixin/command' +require 'chef/provider' + +class Chef + class Provider + class Cron < Chef::Provider + include Chef::Mixin::Command + + CRON_PATTERN = /\A([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+|[a-zA-Z]{3})\s([-0-9*,\/]+|[a-zA-Z]{3})\s(.*)/ + ENV_PATTERN = /\A(\S+)=(\S*)/ + + CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :command, :mailto, :path, :shell, :home, :environment] + + def initialize(new_resource, run_context) + super(new_resource, run_context) + @cron_exists = false + @cron_empty = false + end + attr_accessor :cron_exists, :cron_empty + + def whyrun_supported? + true + end + + def load_current_resource + crontab_lines = [] + @current_resource = Chef::Resource::Cron.new(@new_resource.name) + @current_resource.user(@new_resource.user) + if crontab = read_crontab + cron_found = false + crontab.each_line do |line| + case line.chomp + when "# Chef Name: #{@new_resource.name}" + Chef::Log.debug("Found cron '#{@new_resource.name}'") + cron_found = true + @cron_exists = true + next + when ENV_PATTERN + set_environment_var($1, $2) if cron_found + next + when CRON_PATTERN + if cron_found + @current_resource.minute($1) + @current_resource.hour($2) + @current_resource.day($3) + @current_resource.month($4) + @current_resource.weekday($5) + @current_resource.command($6) + cron_found=false + end + next + else + cron_found=false # We've got a Chef comment with no following crontab line + next + end + end + Chef::Log.debug("Cron '#{@new_resource.name}' not found") unless @cron_exists + else + Chef::Log.debug("Cron empty for '#{@new_resource.user}'") + @cron_empty = true + end + + @current_resource + end + + def cron_different? + CRON_ATTRIBUTES.any? do |cron_var| + !@new_resource.send(cron_var).nil? && @new_resource.send(cron_var) != @current_resource.send(cron_var) + end + end + + def action_create + crontab = String.new + newcron = String.new + cron_found = false + + newcron << "# Chef Name: #{new_resource.name}\n" + [ :mailto, :path, :shell, :home ].each do |v| + newcron << "#{v.to_s.upcase}=#{@new_resource.send(v)}\n" if @new_resource.send(v) + end + @new_resource.environment.each do |name, value| + newcron << "#{name}=#{value}\n" + end + newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + + if @cron_exists + unless cron_different? + Chef::Log.debug("Skipping existing cron entry '#{@new_resource.name}'") + return + end + read_crontab.each_line do |line| + case line.chomp + when "# Chef Name: #{@new_resource.name}" + cron_found = true + next + when ENV_PATTERN + crontab << line unless cron_found + next + when CRON_PATTERN + if cron_found + cron_found = false + crontab << newcron + next + end + else + if cron_found # We've got a Chef comment with no following crontab line + crontab << newcron + cron_found = false + end + end + crontab << line + end + + # Handle edge case where the Chef comment is the last line in the current crontab + crontab << newcron if cron_found + + converge_by("update crontab entry for #{@new_resource}") do + write_crontab crontab + Chef::Log.info("#{@new_resource} updated crontab entry") + end + + else + crontab = read_crontab unless @cron_empty + crontab << newcron + + converge_by("add crontab entry for #{@new_resource}") do + write_crontab crontab + Chef::Log.info("#{@new_resource} added crontab entry") + end + end + end + + def action_delete + if @cron_exists + crontab = String.new + cron_found = false + read_crontab.each_line do |line| + case line.chomp + when "# Chef Name: #{@new_resource.name}" + cron_found = true + next + when ENV_PATTERN + next if cron_found + when CRON_PATTERN + if cron_found + cron_found = false + next + end + else + # We've got a Chef comment with no following crontab line + cron_found = false + end + crontab << line + end + description = cron_found ? "remove #{@new_resource.name} from crontab" : + "save unmodified crontab" + converge_by(description) do + write_crontab crontab + Chef::Log.info("#{@new_resource} deleted crontab entry") + end + end + end + + private + + def set_environment_var(attr_name, attr_value) + if %w(MAILTO PATH SHELL HOME).include?(attr_name) + @current_resource.send(attr_name.downcase.to_sym, attr_value) + else + @current_resource.environment(@current_resource.environment.merge(attr_name => attr_value)) + end + end + + def read_crontab + crontab = nil + status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr| + crontab = stdout.read + end + if status.exitstatus > 1 + raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}" + end + crontab + end + + def write_crontab(crontab) + status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr| + stdin.write crontab + end + if status.exitstatus > 0 + raise Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: #{status.exitstatus}" + end + end + end + end +end diff --git a/lib/chef/provider/cron/solaris.rb b/lib/chef/provider/cron/solaris.rb new file mode 100644 index 0000000000..e0811ba0ac --- /dev/null +++ b/lib/chef/provider/cron/solaris.rb @@ -0,0 +1,56 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Author:: Toomas Pelberg (toomasp@gmx.net) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# Copyright:: Copyright (c) 2010 Toomas Pelberg +# 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/log' +require 'chef/provider' + +class Chef + class Provider + class Cron + class Solaris < Chef::Provider::Cron + + private + + def read_crontab + crontab = nil + status = popen4("crontab -l #{@new_resource.user}") do |pid, stdin, stdout, stderr| + crontab = stdout.read + end + if status.exitstatus > 1 + raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}" + end + crontab + end + + def write_crontab(crontab) + tempcron = Tempfile.new("chef-cron") + tempcron << crontab + tempcron.flush + tempcron.chmod(0644) + status = run_command(:command => "/usr/bin/crontab #{tempcron.path}",:user => @new_resource.user) + tempcron.close! + if status.exitstatus > 0 + raise Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: #{status.exitstatus}" + end + end + end + end + end +end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb new file mode 100644 index 0000000000..60c626ab08 --- /dev/null +++ b/lib/chef/provider/deploy.rb @@ -0,0 +1,480 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/mixin/command" +require "chef/mixin/from_file" +require "chef/provider/git" +require "chef/provider/subversion" +require 'chef/dsl/recipe' + +class Chef + class Provider + class Deploy < Chef::Provider + + include Chef::DSL::Recipe + include Chef::Mixin::FromFile + include Chef::Mixin::Command + + attr_reader :scm_provider, :release_path, :previous_release_path + + def initialize(new_resource, run_context) + super(new_resource, run_context) + + # will resolve to ither git or svn based on resource attributes , + # and will create a resource corresponding to that provider + @scm_provider = new_resource.scm_provider.new(new_resource, run_context) + + # @configuration is not used by Deploy, it is only for backwards compat with + # chef-deploy or capistrano hooks that might use it to get environment information + @configuration = @new_resource.to_hash + @configuration[:environment] = @configuration[:environment] && @configuration[:environment]["RAILS_ENV"] + end + + def whyrun_supported? + true + end + + def load_current_resource + @scm_provider.load_current_resource + @release_path = @new_resource.deploy_to + "/releases/#{release_slug}" + end + + def sudo(command,&block) + execute(command, &block) + end + + def run(command, &block) + exec = execute(command, &block) + exec.user(@new_resource.user) if @new_resource.user + exec.group(@new_resource.group) if @new_resource.group + exec.cwd(release_path) unless exec.cwd + exec.environment(@new_resource.environment) unless exec.environment + converge_by("execute #{command}") do + exec + end + end + + def define_resource_requirements + requirements.assert(:rollback) do |a| + a.assertion { all_releases[-2] } + a.failure_message(RuntimeError, "There is no release to rollback to!") + #There is no reason to assume 2 deployments in a single chef run, hence fails in whyrun. + end + + [ @new_resource.before_migrate, @new_resource.before_symlink, + @new_resource.before_restart, @new_resource.after_restart ].each do |script| + requirements.assert(:deploy, :force_deploy) do |a| + callback_file = "#{release_path}/#{script}" + a.assertion do + if script && script.class == String + ::File.exist?(callback_file) + else + true + end + end + a.failure_message(RuntimeError, "Can't find your callback file #{callback_file}") + a.whyrun("Would assume callback file #{callback_file} included in release") + end + end + + end + + def action_deploy + save_release_state + if deployed?(release_path ) + if current_release?(release_path ) + Chef::Log.debug("#{@new_resource} is the latest version") + else + rollback_to release_path + end + else + + with_rollback_on_error do + deploy + end + end + end + + def action_force_deploy + if deployed?(release_path) + converge_by("delete deployed app at #{release_path} prior to force-deploy") do + Chef::Log.info("Already deployed app at #{release_path}, forcing.") + FileUtils.rm_rf(release_path) + Chef::Log.info("#{@new_resource} forcing deploy of already deployed app at #{release_path}") + end + end + + # Alternatives: + # * Move release_path directory before deploy and move it back when error occurs + # * Rollback to previous commit + # * Do nothing - because deploy is force, it will be retried in short time + # Because last is simpliest, keep it + deploy + end + + def action_rollback + rollback_to all_releases[-2] + end + + def rollback_to(target_release_path) + @release_path = target_release_path + + rp_index = all_releases.index(release_path) + releases_to_nuke = all_releases[(rp_index + 1)..-1] + + rollback + + releases_to_nuke.each do |i| + converge_by("roll back by removing release #{i}") do + Chef::Log.info "#{@new_resource} removing release: #{i}" + FileUtils.rm_rf i + end + release_deleted(i) + end + end + + def deploy + verify_directories_exist + # CHEF-3435: We need to create the directories if they don't exist before calling the + # scm_provider because it expects them to be there in its own assertations + unless self.converge_actions.empty? + Chef::Log.info "#{@new_resource} running collected converge_actions before calling scm_provider" + self.converge_actions.converge! + end + update_cached_repo # no converge-by - scm provider will dothis + enforce_ownership + copy_cached_repo + install_gems + enforce_ownership + callback(:before_migrate, @new_resource.before_migrate) + migrate + callback(:before_symlink, @new_resource.before_symlink) + symlink + callback(:before_restart, @new_resource.before_restart) + restart + callback(:after_restart, @new_resource.after_restart) + cleanup! + Chef::Log.info "#{@new_resource} deployed to #{@new_resource.deploy_to}" + end + + def rollback + Chef::Log.info "#{@new_resource} rolling back to previous release #{release_path}" + symlink + Chef::Log.info "#{@new_resource} restarting with previous release" + restart + end + + + def callback(what, callback_code=nil) + @collection = Chef::ResourceCollection.new + case callback_code + when Proc + Chef::Log.info "#{@new_resource} running callback #{what}" + recipe_eval(&callback_code) + when String + run_callback_from_file("#{release_path}/#{callback_code}") + when nil + run_callback_from_file("#{release_path}/deploy/#{what}.rb") + end + end + + def migrate + run_symlinks_before_migrate + + if @new_resource.migrate + enforce_ownership + + environment = @new_resource.environment + env_info = environment && environment.map do |key_and_val| + "#{key_and_val.first}='#{key_and_val.last}'" + end.join(" ") + + converge_by("execute migration command #{@new_resource.migration_command}") do + Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}" + run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :log_level => :info)) + end + end + end + + def symlink + purge_tempfiles_from_current_release + link_tempfiles_to_current_release + link_current_release_to_production + Chef::Log.info "#{@new_resource} updated symlinks" + end + + def restart + if restart_cmd = @new_resource.restart_command + if restart_cmd.kind_of?(Proc) + Chef::Log.info("#{@new_resource} restarting app with embedded recipe") + recipe_eval(&restart_cmd) + else + converge_by("restart app using command #{@new_resource.restart_command}") do + Chef::Log.info("#{@new_resource} restarting app") + run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path)) + end + end + end + end + + def cleanup! + chop = -1 - @new_resource.keep_releases + all_releases[0..chop].each do |old_release| + converge_by("remove old release #{old_release}") do + Chef::Log.info "#{@new_resource} removing old release #{old_release}" + FileUtils.rm_rf(old_release) + end + release_deleted(old_release) + end + end + + def all_releases + Dir.glob(@new_resource.deploy_to + "/releases/*").sort + end + + def update_cached_repo + if @new_resource.svn_force_export + # TODO assertion, non-recoverable - @scm_provider must be svn if force_export? + svn_force_export + else + run_scm_sync + end + end + + def run_scm_sync + @scm_provider.run_action(:sync) + end + + def svn_force_export + Chef::Log.info "#{@new_resource} exporting source repository" + @scm_provider.run_action(:force_export) + end + + def copy_cached_repo + target_dir_path = @new_resource.deploy_to + "/releases" + converge_by("deploy from repo to #{@target_dir_path} ") do + FileUtils.mkdir_p(target_dir_path) + run_command(:command => "cp -RPp #{::File.join(@new_resource.destination, ".")} #{release_path}") + Chef::Log.info "#{@new_resource} copied the cached checkout to #{release_path}" + release_created(release_path) + end + end + + def enforce_ownership + converge_by("force ownership of #{@new_resource.deploy_to} to #{@new_resource.group}:#{@new_resource.user}") do + FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to) + Chef::Log.info("#{@new_resource} set user to #{@new_resource.user}") if @new_resource.user + Chef::Log.info("#{@new_resource} set group to #{@new_resource.group}") if @new_resource.group + end + end + + def verify_directories_exist + create_dir_unless_exists(@new_resource.deploy_to) + create_dir_unless_exists(@new_resource.shared_path) + end + + def link_current_release_to_production + converge_by(["remove existing link at #{@new_resource.current_path}", + "link release #{release_path} into production at #{@new_resource.current_path}"]) do + FileUtils.rm_f(@new_resource.current_path) + begin + FileUtils.ln_sf(release_path, @new_resource.current_path) + rescue => e + raise Chef::Exceptions::FileNotFound.new("Cannot symlink current release to production: #{e.message}") + end + Chef::Log.info "#{@new_resource} linked release #{release_path} into production at #{@new_resource.current_path}" + end + enforce_ownership + end + + def run_symlinks_before_migrate + links_info = @new_resource.symlink_before_migrate.map { |src, dst| "#{src} => #{dst}" }.join(", ") + converge_by("make pre-migration symliinks: #{links_info}") do + @new_resource.symlink_before_migrate.each do |src, dest| + begin + FileUtils.ln_sf(@new_resource.shared_path + "/#{src}", release_path + "/#{dest}") + rescue => e + raise Chef::Exceptions::FileNotFound.new("Cannot symlink #{@new_resource.shared_path}/#{src} to #{release_path}/#{dest} before migrate: #{e.message}") + end + end + Chef::Log.info "#{@new_resource} made pre-migration symlinks" + end + end + + def link_tempfiles_to_current_release + dirs_info = @new_resource.create_dirs_before_symlink.join(",") + @new_resource.create_dirs_before_symlink.each do |dir| + create_dir_unless_exists(release_path + "/#{dir}") + end + Chef::Log.info("#{@new_resource} created directories before symlinking: #{dirs_info}") + + links_info = @new_resource.symlinks.map { |src, dst| "#{src} => #{dst}" }.join(", ") + converge_by("link shared paths into current release: #{links_info}") do + @new_resource.symlinks.each do |src, dest| + begin + FileUtils.ln_sf(::File.join(@new_resource.shared_path, src), ::File.join(release_path, dest)) + rescue => e + raise Chef::Exceptions::FileNotFound.new("Cannot symlink shared data #{::File.join(@new_resource.shared_path, src)} to #{::File.join(release_path, dest)}: #{e.message}") + end + end + Chef::Log.info("#{@new_resource} linked shared paths into current release: #{links_info}") + end + run_symlinks_before_migrate + enforce_ownership + end + + def create_dirs_before_symlink + end + + def purge_tempfiles_from_current_release + log_info = @new_resource.purge_before_symlink.join(", ") + converge_by("purge directories in checkout #{log_info}") do + @new_resource.purge_before_symlink.each { |dir| FileUtils.rm_rf(release_path + "/#{dir}") } + Chef::Log.info("#{@new_resource} purged directories in checkout #{log_info}") + end + end + + protected + + # Internal callback, called after copy_cached_repo. + # Override if you need to keep state externally. + # Note that YOU are responsible for implementing whyrun-friendly behavior + # in any actions you take in this callback. + def release_created(release_path) + end + + # Note that YOU are responsible for using appropriate whyrun nomenclature + # Override if you need to keep state externally. + # Note that YOU are responsible for implementing whyrun-friendly behavior + # in any actions you take in this callback. + def release_deleted(release_path) + end + + def release_slug + raise Chef::Exceptions::Override, "You must override release_slug in #{self.to_s}" + end + + def install_gems + gem_resource_collection_runner.converge + end + + def gem_resource_collection_runner + gems_collection = Chef::ResourceCollection.new + gem_packages.each { |rbgem| gems_collection << rbgem } + gems_run_context = run_context.dup + gems_run_context.resource_collection = gems_collection + Chef::Runner.new(gems_run_context) + end + + def gem_packages + return [] unless ::File.exist?("#{release_path}/gems.yml") + gems = YAML.load(IO.read("#{release_path}/gems.yml")) + + gems.map do |g| + r = Chef::Resource::GemPackage.new(g[:name], run_context) + r.version g[:version] + r.action :install + r.source "http://gems.github.com" + r + end + end + + def run_options(run_opts={}) + run_opts[:user] = @new_resource.user if @new_resource.user + run_opts[:group] = @new_resource.group if @new_resource.group + run_opts[:environment] = @new_resource.environment if @new_resource.environment + run_opts[:log_tag] = @new_resource.to_s + run_opts[:log_level] ||= :debug + if run_opts[:log_level] == :info + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? + run_opts[:live_stream] = STDOUT + end + end + run_opts + end + + def run_callback_from_file(callback_file) + Chef::Log.info "#{@new_resource} queueing checkdeploy hook #{callback_file}" + recipe_eval do + Dir.chdir(release_path) do + from_file(callback_file) if ::File.exist?(callback_file) + end + end + end + + def create_dir_unless_exists(dir) + if ::File.directory?(dir) + Chef::Log.debug "#{@new_resource} not creating #{dir} because it already exists" + return false + end + converge_by("create new directory #{dir}") do + begin + FileUtils.mkdir_p(dir) + Chef::Log.debug "#{@new_resource} created directory #{dir}" + if @new_resource.user + FileUtils.chown(@new_resource.user, nil, dir) + Chef::Log.debug("#{@new_resource} set user to #{@new_resource.user} for #{dir}") + end + if @new_resource.group + FileUtils.chown(nil, @new_resource.group, dir) + Chef::Log.debug("#{@new_resource} set group to #{@new_resource.group} for #{dir}") + end + rescue => e + raise Chef::Exceptions::FileNotFound.new("Cannot create directory #{dir}: #{e.message}") + end + end + end + + def with_rollback_on_error + yield + rescue ::Exception => e + if @new_resource.rollback_on_error + Chef::Log.warn "Error on deploying #{release_path}: #{e.message}" + failed_release = release_path + + if previous_release_path + @release_path = previous_release_path + rollback + end + converge_by("remove failed deploy #{failed_release}") do + Chef::Log.info "Removing failed deploy #{failed_release}" + FileUtils.rm_rf failed_release + end + release_deleted(failed_release) + end + + raise + end + + def save_release_state + if ::File.exists?(@new_resource.current_path) + release = ::File.readlink(@new_resource.current_path) + @previous_release_path = release if ::File.exists?(release) + end + end + + def deployed?(release) + all_releases.include?(release) + end + + def current_release?(release) + @previous_release_path == release + end + end + end +end diff --git a/lib/chef/provider/deploy/revision.rb b/lib/chef/provider/deploy/revision.rb new file mode 100644 index 0000000000..3728fc6a31 --- /dev/null +++ b/lib/chef/provider/deploy/revision.rb @@ -0,0 +1,80 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider' +require 'chef/provider/deploy' +require 'chef/json_compat' + +class Chef + class Provider + class Deploy + class Revision < Chef::Provider::Deploy + + def all_releases + sorted_releases + end + + protected + + def release_created(release) + sorted_releases {|r| r.delete(release); r << release } + end + + def release_deleted(release) + sorted_releases { |r| r.delete(release)} + end + + def release_slug + scm_provider.revision_slug + end + + private + + def sorted_releases + cache = load_cache + if block_given? + yield cache + save_cache(cache) + end + cache + end + + def sorted_releases_from_filesystem + Dir.glob(new_resource.deploy_to + "/releases/*").sort_by { |d| ::File.ctime(d) } + end + + def load_cache + begin + Chef::JSONCompat.from_json(Chef::FileCache.load("revision-deploys/#{new_resource.name}")) + rescue Chef::Exceptions::FileNotFound + sorted_releases_from_filesystem + end + end + + def save_cache(cache) + Chef::FileCache.store("revision-deploys/#{new_resource.name}", cache.to_json) + cache + end + + end + end + end +end diff --git a/lib/chef/provider/deploy/timestamped.rb b/lib/chef/provider/deploy/timestamped.rb new file mode 100644 index 0000000000..9c2d55b490 --- /dev/null +++ b/lib/chef/provider/deploy/timestamped.rb @@ -0,0 +1,32 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# 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 + class Provider + class Deploy + class Timestamped < Chef::Provider::Deploy + + protected + + def release_slug + Time.now.utc.strftime("%Y%m%d%H%M%S") + end + end + end + end +end diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb new file mode 100644 index 0000000000..0329aeb1ad --- /dev/null +++ b/lib/chef/provider/directory.rb @@ -0,0 +1,128 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/log' +require 'chef/resource/directory' +require 'chef/provider' +require 'chef/provider/file' +require 'fileutils' + +class Chef + class Provider + class Directory < Chef::Provider::File + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::Directory.new(@new_resource.name) + @current_resource.path(@new_resource.path) + load_current_resource_attrs + setup_acl + + @current_resource + end + + def define_resource_requirements + # this must be evaluated before whyrun messages are printed + access_controls.requires_changes? + + requirements.assert(:create) do |a| + # Make sure the parent dir exists, or else fail. + # for why run, print a message explaining the potential error. + parent_directory = ::File.dirname(@new_resource.path) + a.assertion { @new_resource.recursive || ::File.directory?(parent_directory) } + a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist, cannot create #{@new_resource.path}") + a.whyrun("Assuming directory #{parent_directory} would have been created") + end + + requirements.assert(:create) do |a| + parent_directory = ::File.dirname(@new_resource.path) + a.assertion do + if @new_resource.recursive + # find the lowest-level directory in @new_resource.path that already exists + # make sure we have write permissions to that directory + is_parent_writable = lambda do |base_dir| + base_dir = ::File.dirname(base_dir) + if ::File.exist?(base_dir) + ::File.writable?(base_dir) + else + is_parent_writable.call(base_dir) + end + end + is_parent_writable.call(@new_resource.path) + else + # in why run mode & parent directory does not exist no permissions check is required + # If not in why run, permissions must be valid and we rely on prior assertion that dir exists + if !whyrun_mode? || ::File.exist?(parent_directory) + ::File.writable?(parent_directory) + else + true + end + end + end + a.failure_message(Chef::Exceptions::InsufficientPermissions, + "Cannot create #{@new_resource} at #{@new_resource.path} due to insufficient permissions") + end + + requirements.assert(:delete) do |a| + a.assertion do + if ::File.exist?(@new_resource.path) + ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path) + else + true + end + end + a.failure_message(RuntimeError, "Cannot delete #{@new_resource} at #{@new_resource.path}!") + # No why-run handling here: + # * if we don't have permissions, this is unlikely to be changed earlier in the run + # * if the target is a file (not a dir), there's no reasonable path by which this would have been changed + end + end + + def action_create + unless ::File.exist?(@new_resource.path) + converge_by("create new directory #{@new_resource.path}") do + if @new_resource.recursive == true + ::FileUtils.mkdir_p(@new_resource.path) + else + ::Dir.mkdir(@new_resource.path) + end + Chef::Log.info("#{@new_resource} created directory #{@new_resource.path}") + end + end + set_all_access_controls + end + + def action_delete + if ::File.exist?(@new_resource.path) + converge_by("delete existing directory #{@new_resource.path}") do + if @new_resource.recursive == true + FileUtils.rm_rf(@new_resource.path) + Chef::Log.info("#{@new_resource} deleted #{@new_resource.path} recursively") + else + ::Dir.delete(@new_resource.path) + Chef::Log.info("#{@new_resource} deleted #{@new_resource.path}") + end + end + end + end + end + end +end diff --git a/lib/chef/provider/env.rb b/lib/chef/provider/env.rb new file mode 100644 index 0000000000..e857d74d68 --- /dev/null +++ b/lib/chef/provider/env.rb @@ -0,0 +1,152 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider' +require 'chef/mixin/command' +require 'chef/resource/env' + +class Chef + class Provider + class Env < Chef::Provider + include Chef::Mixin::Command + attr_accessor :key_exists + + def initialize(new_resource, run_context) + super + @key_exists = true + end + + def load_current_resource + @current_resource = Chef::Resource::Env.new(@new_resource.name) + @current_resource.key_name(@new_resource.key_name) + + if env_key_exists(@new_resource.key_name) + @current_resource.value(env_value(@new_resource.key_name)) + else + @key_exists = false + Chef::Log.debug("#{@new_resource} key does not exist") + end + + @current_resource + end + + def env_value(key_name) + raise Chef::Exceptions::Env, "#{self.to_s} provider does not implement env_value!" + end + + def env_key_exists(key_name) + env_value(key_name) ? true : false + end + + # Check to see if value needs any changes + # + # ==== Returns + # <true>:: If a change is required + # <false>:: If a change is not required + def compare_value + if @new_resource.delim + #e.g. check for existing value within PATH + not @current_resource.value.split(@new_resource.delim).any? do |val| + val == @new_resource.value + end + else + @new_resource.value != @current_resource.value + end + end + + def action_create + if @key_exists + if compare_value + modify_env + Chef::Log.info("#{@new_resource} altered") + @new_resource.updated_by_last_action(true) + end + else + create_env + Chef::Log.info("#{@new_resource} created") + @new_resource.updated_by_last_action(true) + end + end + + #e.g. delete a PATH element + # + # ==== Returns + # <true>:: If we handled the element case and caller should not delete the key + # <false>:: Caller should delete the key, either no :delim was specific or value was empty + # after we removed the element. + def delete_element + return false unless @new_resource.delim #no delim: delete the key + if compare_value + Chef::Log.debug("#{@new_resource} element '#{@new_resource.value}' does not exist") + return true #do not delete the key + else + new_value = + @current_resource.value.split(@new_resource.delim).select { |item| + item != @new_resource.value + }.join(@new_resource.delim) + + if new_value.empty? + return false #nothing left here, delete the key + else + old_value = @new_resource.value(new_value) + create_env + Chef::Log.debug("#{@new_resource} deleted #{old_value} element") + @new_resource.updated_by_last_action(true) + return true #we removed the element and updated; do not delete the key + end + end + end + + def action_delete + if @key_exists && !delete_element + delete_env + Chef::Log.info("#{@new_resource} deleted") + @new_resource.updated_by_last_action(true) + end + end + + def action_modify + if @key_exists + if compare_value + modify_env + Chef::Log.info("#{@new_resource} modified") + @new_resource.updated_by_last_action(true) + end + else + raise Chef::Exceptions::Env, "Cannot modify #{@new_resource} - key does not exist!" + end + end + + def create_env + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :#{@new_resource.action}" + end + + def delete_env + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :delete" + end + + def modify_env + if @new_resource.delim + #e.g. add to PATH + @new_resource.value << @new_resource.delim << @current_resource.value + end + create_env + end + end + end +end diff --git a/lib/chef/provider/env/windows.rb b/lib/chef/provider/env/windows.rb new file mode 100644 index 0000000000..bf728b1fae --- /dev/null +++ b/lib/chef/provider/env/windows.rb @@ -0,0 +1,75 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, 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. +# + +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'ruby-wmi' + require 'Win32API' +end + +class Chef + class Provider + class Env + class Windows < Chef::Provider::Env + + def create_env + obj = env_obj(@new_resource.key_name) + unless obj + obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_ + obj.name = @new_resource.key_name + obj.username = "<System>" + end + obj.variablevalue = @new_resource.value + obj.put_ + broadcast_env_change + end + + def delete_env + obj = env_obj(@new_resource.key_name) + if obj + obj.delete_ + broadcast_env_change + end + end + + def env_value(key_name) + obj = env_obj(key_name) + return obj ? obj.variablevalue : nil + end + + def env_obj(key_name) + WMI::Win32_Environment.find(:first, + :conditions => { :name => key_name }) + end + + #see: http://msdn.microsoft.com/en-us/library/ms682653%28VS.85%29.aspx + HWND_BROADCAST = 0xffff + WM_SETTINGCHANGE = 0x001A + SMTO_BLOCK = 0x0001 + SMTO_ABORTIFHUNG = 0x0002 + SMTO_NOTIMEOUTIFNOTHUNG = 0x0008 + + def broadcast_env_change + result = 0 + flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG + @send_message ||= Win32API.new('user32', 'SendMessageTimeout', 'LLLPLLP', 'L') + @send_message.call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment', flags, 5000, result) + end + end + end + end +end diff --git a/lib/chef/provider/erl_call.rb b/lib/chef/provider/erl_call.rb new file mode 100644 index 0000000000..1ee1da500c --- /dev/null +++ b/lib/chef/provider/erl_call.rb @@ -0,0 +1,106 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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/log' +require 'chef/mixin/command' +require 'chef/provider' + +class Chef + class Provider + class ErlCall < Chef::Provider + include Chef::Mixin::Command + + def initialize(node, new_resource) + super(node, new_resource) + end + + def whyrun_supported? + true + end + + def load_current_resource + true + end + + def action_run + case @new_resource.name_type + when "sname" + node = "-sname #{@new_resource.node_name}" + when "name" + node = "-name #{@new_resource.node_name}" + end + + if @new_resource.cookie + cookie = "-c #{@new_resource.cookie}" + else + cookie = "" + end + + if @new_resource.distributed + distributed = "-s" + else + distributed = "" + end + + command = "erl_call -e #{distributed} #{node} #{cookie}" + + converge_by("run erlang block") do + begin + pid, stdin, stdout, stderr = popen4(command, :waitlast => true) + + Chef::Log.debug("#{@new_resource} running") + Chef::Log.debug("#{@new_resource} command: #{command}") + Chef::Log.debug("#{@new_resource} code: #{@new_resource.code}") + + @new_resource.code.each_line { |line| stdin.puts(line.chomp) } + + stdin.close + + Chef::Log.debug("#{@new_resource} output: ") + + stdout_output = "" + stdout.each_line { |line| stdout_output << line } + stdout.close + + stderr_output = "" + stderr.each_line { |line| stderr_output << line } + stderr.close + + # fail if stderr contains anything + if stderr_output.length > 0 + raise Chef::Exceptions::ErlCall, stderr_output + end + + # fail if the first 4 characters aren't "{ok," + unless stdout_output[0..3].include?('{ok,') + raise Chef::Exceptions::ErlCall, stdout_output + end + + @new_resource.updated_by_last_action(true) + + Chef::Log.debug("#{@new_resource} #{stdout_output}") + Chef::Log.info("#{@new_resouce} ran successfully") + ensure + Process.wait(pid) if pid + end + end + end + + end + end +end diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb new file mode 100644 index 0000000000..d6b2f91bab --- /dev/null +++ b/lib/chef/provider/execute.rb @@ -0,0 +1,68 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' +require 'chef/log' +require 'chef/provider' + +class Chef + class Provider + class Execute < Chef::Provider + + include Chef::Mixin::ShellOut + + def load_current_resource + true + end + + def whyrun_supported? + true + end + + def action_run + opts = {} + + if sentinel_file = @new_resource.creates + if ::File.exists?(sentinel_file) + Chef::Log.debug("#{@new_resource} sentinel file #{sentinel_file} exists - nothing to do") + return false + end + end + + # original implementation did not specify a timeout, but ShellOut + # *always* times out. So, set a very long default timeout + opts[:timeout] = @new_resource.timeout || 3600 + opts[:returns] = @new_resource.returns if @new_resource.returns + opts[:environment] = @new_resource.environment if @new_resource.environment + opts[:user] = @new_resource.user if @new_resource.user + opts[:group] = @new_resource.group if @new_resource.group + opts[:cwd] = @new_resource.cwd if @new_resource.cwd + opts[:umask] = @new_resource.umask if @new_resource.umask + opts[:log_level] = :info + opts[:log_tag] = @new_resource.to_s + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? + opts[:live_stream] = STDOUT + end + converge_by("execute #{@new_resource.command}") do + result = shell_out!(@new_resource.command, opts) + Chef::Log.info("#{@new_resource} ran successfully") + end + end + end + end +end diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb new file mode 100644 index 0000000000..659afc6517 --- /dev/null +++ b/lib/chef/provider/file.rb @@ -0,0 +1,338 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/log' +require 'chef/resource/file' +require 'chef/mixin/checksum' +require 'chef/provider' +require 'etc' +require 'fileutils' +require 'chef/scan_access_control' +require 'chef/mixin/shell_out' + +class Chef + + class Provider + class File < Chef::Provider + include Chef::Mixin::Checksum + include Chef::Mixin::ShellOut + + def negative_complement(big) + if big > 1073741823 # Fixnum max + big -= (2**32) # diminished radix wrap to negative + end + big + end + + def octal_mode(mode) + ((mode.respond_to?(:oct) ? mode.oct : mode.to_i) & 007777) + end + + private :negative_complement, :octal_mode + + def diff_current_from_content(new_content) + result = nil + Tempfile.open("chef-diff") do |file| + file.write new_content + file.close + result = diff_current file.path + end + result + end + + def is_binary?(path) + ::File.open(path) do |file| + + buff = file.read(Chef::Config[:diff_filesize_threshold]) + buff = "" if buff.nil? + return buff !~ /^[\r[:print:]]*$/ + end + end + + + def diff_current(temp_path) + suppress_resource_reporting = false + + return [ "(diff output suppressed by config)" ] if Chef::Config[:diff_disabled] + return [ "(no temp file with new content, diff output suppressed)" ] unless ::File.exists?(temp_path) # should never happen? + + # solaris does not support diff -N, so create tempfile to diff against if we are creating a new file + target_path = if ::File.exists?(@current_resource.path) + @current_resource.path + else + suppress_resource_reporting = true # suppress big diffs going to resource reporting service + tempfile = Tempfile.new('chef-tempfile') + tempfile.path + end + + diff_filesize_threshold = Chef::Config[:diff_filesize_threshold] + diff_output_threshold = Chef::Config[:diff_output_threshold] + + if ::File.size(target_path) > diff_filesize_threshold || ::File.size(temp_path) > diff_filesize_threshold + return [ "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)" ] + end + + # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs + return [ "(current file is binary, diff output suppressed)"] if is_binary?(target_path) + return [ "(new content is binary, diff output suppressed)"] if is_binary?(temp_path) + + begin + # -u: Unified diff format + result = shell_out("diff -u #{target_path} #{temp_path}" ) + rescue Exception => e + # Should *not* receive this, but in some circumstances it seems that + # an exception can be thrown even using shell_out instead of shell_out! + return [ "Could not determine diff. Error: #{e.message}" ] + end + + # diff will set a non-zero return code even when there's + # valid stdout results, if it encounters something unexpected + # So as long as we have output, we'll show it. + if not result.stdout.empty? + if result.stdout.length > diff_output_threshold + [ "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" ] + else + val = result.stdout.split("\n") + val.delete("\\ No newline at end of file") + @new_resource.diff(val.join("\\n")) unless suppress_resource_reporting + val + end + elsif not result.stderr.empty? + [ "Could not determine diff. Error: #{result.stderr}" ] + else + [ "(no diff)" ] + end + end + + def whyrun_supported? + true + end + + def load_current_resource + # Every child should be specifying their own constructor, so this + # should only be run in the file case. + @current_resource ||= Chef::Resource::File.new(@new_resource.name) + @new_resource.path.gsub!(/\\/, "/") # for Windows + @current_resource.path(@new_resource.path) + if !::File.directory?(@new_resource.path) + if ::File.exist?(@new_resource.path) + @current_resource.checksum(checksum(@new_resource.path)) + end + end + load_current_resource_attrs + setup_acl + + @current_resource + end + + def load_current_resource_attrs + if ::File.exist?(@new_resource.path) + stat = ::File.stat(@new_resource.path) + @current_resource.owner(stat.uid) + @current_resource.mode(stat.mode & 07777) + @current_resource.group(stat.gid) + + if @new_resource.group.nil? + @new_resource.group(@current_resource.group) + end + if @new_resource.owner.nil? + @new_resource.owner(@current_resource.owner) + end + if @new_resource.mode.nil? + @new_resource.mode(@current_resource.mode) + end + end + end + + def setup_acl + @acl_scanner = ScanAccessControl.new(@new_resource, @current_resource) + @acl_scanner.set_all! + end + + def define_resource_requirements + # this must be evaluated before whyrun messages are printed + access_controls.requires_changes? + + requirements.assert(:create, :create_if_missing, :touch) do |a| + # Make sure the parent dir exists, or else fail. + # for why run, print a message explaining the potential error. + parent_directory = ::File.dirname(@new_resource.path) + + a.assertion { ::File.directory?(parent_directory) } + a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist.") + a.whyrun("Assuming directory #{parent_directory} would have been created") + end + + # Make sure the file is deletable if it exists. Otherwise, fail. + requirements.assert(:delete) do |a| + a.assertion do + if ::File.exists?(@new_resource.path) + ::File.writable?(@new_resource.path) + else + true + end + end + a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted") + end + end + + # Compare the content of a file. Returns true if they are the same, false if they are not. + def compare_content + checksum(@current_resource.path) == new_resource_content_checksum + end + + # Set the content of the file, assuming it is not set correctly already. + def set_content + unless compare_content + description = [] + description << "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(new_resource_content_checksum)}" + description << diff_current_from_content(@new_resource.content) + converge_by(description) do + backup @new_resource.path if ::File.exists?(@new_resource.path) + ::File.open(@new_resource.path, "w") {|f| f.write @new_resource.content } + Chef::Log.info("#{@new_resource} contents updated") + end + end + end + + # if you are using a tempfile before creating, you must + # override the default with the tempfile, since the + # file at @new_resource.path will not be updated on converge + def update_new_file_state(path=@new_resource.path) + stat = ::File.stat(path) + @new_resource.owner(stat.uid) + @new_resource.mode(stat.mode & 07777) + @new_resource.group(stat.gid) + if !::File.directory?(path) + @new_resource.checksum(checksum(path)) + end + end + + def action_create + if !::File.exists?(@new_resource.path) + description = [] + desc = "create new file #{@new_resource.path}" + desc << " with content checksum #{short_cksum(new_resource_content_checksum)}" if new_resource.content + description << desc + description << diff_current_from_content(@new_resource.content) + + converge_by(description) do + Chef::Log.info("entered create") + ::File.open(@new_resource.path, "w+") {|f| f.write @new_resource.content } + access_controls.set_all + Chef::Log.info("#{@new_resource} created file #{@new_resource.path}") + update_new_file_state + end + else + set_content unless @new_resource.content.nil? + set_all_access_controls + end + end + + def set_all_access_controls + if access_controls.requires_changes? + converge_by(access_controls.describe_changes) do + access_controls.set_all + #Update file state with new access values + update_new_file_state + end + end + end + + def action_create_if_missing + if ::File.exists?(@new_resource.path) + Chef::Log.debug("#{@new_resource} exists at #{@new_resource.path} taking no action.") + else + action_create + end + end + + def action_delete + if ::File.exists?(@new_resource.path) + converge_by("delete file #{@new_resource.path}") do + backup unless ::File.symlink?(@new_resource.path) + ::File.delete(@new_resource.path) + Chef::Log.info("#{@new_resource} deleted file at #{@new_resource.path}") + end + end + end + + def action_touch + action_create + converge_by("update utime on file #{@new_resource.path}") do + time = Time.now + ::File.utime(time, time, @new_resource.path) + Chef::Log.info("#{@new_resource} updated atime and mtime to #{time}") + end + end + + def backup(file=nil) + file ||= @new_resource.path + if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(file) + time = Time.now + savetime = time.strftime("%Y%m%d%H%M%S") + backup_filename = "#{@new_resource.path}.chef-#{savetime}" + backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows + # if :file_backup_path is nil, we fallback to the old behavior of + # keeping the backup in the same directory. We also need to to_s it + # so we don't get a type error around implicit to_str conversions. + prefix = Chef::Config[:file_backup_path].to_s + backup_path = ::File.join(prefix, backup_filename) + FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path] + FileUtils.cp(file, backup_path, :preserve => true) + Chef::Log.info("#{@new_resource} backed up to #{backup_path}") + + # Clean up after the number of backups + slice_number = @new_resource.backup + backup_files = Dir[::File.join(prefix, ".#{@new_resource.path}.chef-*")].sort { |a,b| b <=> a } + if backup_files.length >= @new_resource.backup + remainder = backup_files.slice(slice_number..-1) + remainder.each do |backup_to_delete| + FileUtils.rm(backup_to_delete) + Chef::Log.info("#{@new_resource} removed backup at #{backup_to_delete}") + end + end + end + end + + def deploy_tempfile + Tempfile.open(::File.basename(@new_resource.name)) do |tempfile| + yield tempfile + + temp_res = Chef::Resource::CookbookFile.new(@new_resource.name) + temp_res.path(tempfile.path) + ac = Chef::FileAccessControl.new(temp_res, @new_resource, self) + ac.set_all! + FileUtils.mv(tempfile.path, @new_resource.path) + end + end + + private + + def short_cksum(checksum) + return "none" if checksum.nil? + checksum.slice(0,6) + end + + def new_resource_content_checksum + @new_resource.content && Digest::SHA2.hexdigest(@new_resource.content) + end + end + end +end diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb new file mode 100644 index 0000000000..cc524a2fcd --- /dev/null +++ b/lib/chef/provider/git.rb @@ -0,0 +1,260 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +require 'chef/log' +require 'chef/provider' +require 'chef/mixin/shell_out' +require 'fileutils' +require 'shellwords' + +class Chef + class Provider + class Git < Chef::Provider + + include Chef::Mixin::ShellOut + + def whyrun_supported? + true + end + + def load_current_resource + @resolved_reference = nil + @current_resource = Chef::Resource::Git.new(@new_resource.name) + if current_revision = find_current_revision + @current_resource.revision current_revision + end + end + + def define_resource_requirements + # Parent directory of the target must exist. + requirements.assert(:checkout, :sync) do |a| + dirname = ::File.dirname(@new_resource.destination) + a.assertion { ::File.directory?(dirname) } + a.whyrun("Directory #{dirname} does not exist, this run will fail unless it has been previously created. Assuming it would have been created.") + a.failure_message(Chef::Exceptions::MissingParentDirectory, + "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{dirname} does not exist") + end + + + requirements.assert(:all_actions) do |a| + a.assertion { !(@new_resource.revision =~ /^origin\//) } + a.failure_message Chef::Exceptions::InvalidRemoteGitReference, + "Deploying remote branches is not supported. " + + "Specify the remote branch as a local branch for " + + "the git repository you're deploying from " + + "(ie: '#{@new_resource.revision.gsub('origin/', '')}' rather than '#{@new_resource.revision}')." + end + + requirements.assert(:all_actions) do |a| + # this can't be recovered from in why-run mode, because nothing that + # we do in the course of a run is likely to create a valid target_revision + # if we can't resolve it up front. + a.assertion { target_revision != nil } + a.failure_message Chef::Exceptions::UnresolvableGitReference, + "Unable to parse SHA reference for '#{@new_resource.revision}' in repository '#{@new_resource.repository}'. " + + "Verify your (case-sensitive) repository URL and revision.\n" + + "`git ls-remote` output: #{@resolved_reference}" + end + end + + def action_checkout + if target_dir_non_existent_or_empty? + clone + checkout + enable_submodules + add_remotes + else + Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory" + end + end + + def action_export + action_checkout + converge_by("complete the export by removing #{@new_resource.destination}.git after checkout") do + FileUtils.rm_rf(::File.join(@new_resource.destination,".git")) + end + end + + def action_sync + if existing_git_clone? + current_rev = find_current_revision + Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{target_revision}" + unless current_revision_matches_target_revision? + fetch_updates + enable_submodules + Chef::Log.info "#{@new_resource} updated to revision #{target_revision}" + end + add_remotes + else + action_checkout + end + end + + + def existing_git_clone? + ::File.exist?(::File.join(@new_resource.destination, ".git")) + end + + def target_dir_non_existent_or_empty? + !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..'] + end + + def find_current_revision + Chef::Log.debug("#{@new_resource} finding current git revision") + if ::File.exist?(::File.join(cwd, ".git")) + # 128 is returned when we're not in a git repo. this is fine + result = shell_out!('git rev-parse HEAD', :cwd => cwd, :returns => [0,128]).stdout.strip + end + sha_hash?(result) ? result : nil + end + + def add_remotes + if (@new_resource.additional_remotes.length > 0) + @new_resource.additional_remotes.each_pair do |remote_name, remote_url| + converge_by("add remote #{remote_name} from #{remote_url}") do + Chef::Log.info "#{@new_resource} adding git remote #{remote_name} = #{remote_url}" + command = "git remote add #{remote_name} #{remote_url}" + if shell_out(command, run_options(:cwd => @new_resource.destination, :log_level => :info)).exitstatus != 0 + @new_resource.updated_by_last_action(true) + end + end + end + end + end + + def clone + converge_by("clone from #{@new_resource.repository} into #{@new_resource.destination}") do + remote = @new_resource.remote + + args = [] + args << "-o #{remote}" unless remote == 'origin' + args << "--depth #{@new_resource.depth}" if @new_resource.depth + + Chef::Log.info "#{@new_resource} cloning repo #{@new_resource.repository} to #{@new_resource.destination}" + + clone_cmd = "git clone #{args.join(' ')} #{@new_resource.repository} #{Shellwords.escape @new_resource.destination}" + shell_out!(clone_cmd, run_options(:log_level => :info)) + end + end + + def checkout + sha_ref = target_revision + converge_by("checkout ref #{sha_ref} branch #{@new_resource.revision}") do + # checkout into a local branch rather than a detached HEAD + shell_out!("git checkout -b deploy #{sha_ref}", run_options(:cwd => @new_resource.destination)) + Chef::Log.info "#{@new_resource} checked out branch: #{@new_resource.revision} reference: #{sha_ref}" + end + end + + def enable_submodules + if @new_resource.enable_submodules + converge_by("enable git submodules for #{@new_resource}") do + Chef::Log.info "#{@new_resource} enabling git submodules" + # the --recursive flag means we require git 1.6.5+ now, see CHEF-1827 + command = "git submodule update --init --recursive" + shell_out!(command, run_options(:cwd => @new_resource.destination, :log_level => :info)) + end + end + end + + def fetch_updates + setup_remote_tracking_branches if @new_resource.remote != 'origin' + converge_by("fetch updates for #{@new_resource.remote}") do + # since we're in a local branch already, just reset to specified revision rather than merge + fetch_command = "git fetch #{@new_resource.remote} && git fetch #{@new_resource.remote} --tags && git reset --hard #{target_revision}" + Chef::Log.debug "Fetching updates from #{new_resource.remote} and resetting to revision #{target_revision}" + shell_out!(fetch_command, run_options(:cwd => @new_resource.destination)) + end + end + + # Use git-config to setup a remote tracking branches. Could use + # git-remote but it complains when a remote of the same name already + # exists, git-config will just silenty overwrite the setting every + # time. This could cause wierd-ness in the remote cache if the url + # changes between calls, but as long as the repositories are all + # based from each other it should still work fine. + def setup_remote_tracking_branches + command = [] + converge_by("set up remote tracking branches for #{@new_resource.repository} at #{@new_resource.remote}") do + Chef::Log.debug "#{@new_resource} configuring remote tracking branches for repository #{@new_resource.repository} "+ + "at remote #{@new_resource.remote}" + command << "git config remote.#{@new_resource.remote}.url #{@new_resource.repository}" + command << "git config remote.#{@new_resource.remote}.fetch +refs/heads/*:refs/remotes/#{@new_resource.remote}/*" + shell_out!(command.join(" && "), run_options(:cwd => @new_resource.destination)) + end + end + + def current_revision_matches_target_revision? + (!@current_resource.revision.nil?) && (target_revision.strip.to_i(16) == @current_resource.revision.strip.to_i(16)) + end + + def target_revision + @target_revision ||= begin + if sha_hash?(@new_resource.revision) + @target_revision = @new_resource.revision + else + @target_revision = remote_resolve_reference + end + end + end + + alias :revision_slug :target_revision + + def remote_resolve_reference + Chef::Log.debug("#{@new_resource} resolving remote reference") + command = git('ls-remote', @new_resource.repository, @new_resource.revision) + @resolved_reference = shell_out!(command, run_options).stdout + if @resolved_reference =~ /^([0-9a-f]{40})\s+(\S+)/ + $1 + else + nil + end + end + + private + + def run_options(run_opts={}) + run_opts[:user] = @new_resource.user if @new_resource.user + run_opts[:group] = @new_resource.group if @new_resource.group + run_opts[:environment] = {"GIT_SSH" => @new_resource.ssh_wrapper} if @new_resource.ssh_wrapper + run_opts[:log_tag] = @new_resource.to_s + run_opts[:log_level] ||= :debug + if run_opts[:log_level] == :info + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? + run_opts[:live_stream] = STDOUT + end + end + run_opts + end + + def cwd + @new_resource.destination + end + + def git(*args) + ["git", *args].compact.join(" ") + end + + def sha_hash?(string) + string =~ /^[0-9a-f]{40}$/ + end + + end + end +end diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb new file mode 100644 index 0000000000..81d7d7a400 --- /dev/null +++ b/lib/chef/provider/group.rb @@ -0,0 +1,159 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider' +require 'chef/mixin/command' +require 'chef/resource/group' +require 'etc' + +class Chef + class Provider + class Group < Chef::Provider + include Chef::Mixin::Command + attr_accessor :group_exists + attr_accessor :change_desc + + def whyrun_supported? + true + end + + def initialize(new_resource, run_context) + super + @group_exists = true + end + + def load_current_resource + @current_resource = Chef::Resource::Group.new(@new_resource.name) + @current_resource.group_name(@new_resource.group_name) + + group_info = nil + begin + group_info = Etc.getgrnam(@new_resource.group_name) + rescue ArgumentError => e + @group_exists = false + Chef::Log.debug("#{@new_resource} group does not exist") + end + + if group_info + @new_resource.gid(group_info.gid) unless @new_resource.gid + @current_resource.gid(group_info.gid) + @current_resource.members(group_info.mem) + end + + @current_resource + end + + def define_resource_requirements + requirements.assert(:modify) do |a| + a.assertion { @group_exists } + a.failure_message(Chef::Exceptions::Group, "Cannot modify #{@new_resource} - group does not exist!") + a.whyrun("Group #{@new_resource} does not exist. Unless it would have been created earlier in this run, this attempt to modify it would fail.") + end + end + + # Check to see if a group needs any changes. Populate + # @change_desc with a description of why a change must occur + # + # ==== Returns + # <true>:: If a change is required + # <false>:: If a change is not required + def compare_group + @change_desc = nil + if @new_resource.gid != @current_resource.gid + @change_desc = "change gid #{@current_resource.gid} to #{@new_resource.gid}" + return true + end + + if(@new_resource.append) + missing_members = [] + @new_resource.members.each do |member| + next if @current_resource.members.include?(member) + missing_members << member + end + if missing_members.length > 0 + @change_desc = "add missing member(s): #{missing_members.join(", ")}" + return true + end + else + if @new_resource.members != @current_resource.members + @change_desc = "replace group members with new list of members" + return true + end + end + return false + end + + def action_create + case @group_exists + when false + converge_by("create #{@new_resource}") do + create_group + Chef::Log.info("#{@new_resource} created") + end + else + if compare_group + converge_by(["alter group #{@new_resource}", @change_desc ]) do + manage_group + Chef::Log.info("#{@new_resource} altered") + end + end + end + end + + def action_remove + if @group_exists + converge_by("remove group #{@new_resource}") do + remove_group + Chef::Log.info("#{@new_resource} removed") + end + end + end + + def action_manage + if @group_exists && compare_group + converge_by(["manage group #{@new_resource}", @change_desc]) do + manage_group + Chef::Log.info("#{@new_resource} managed") + end + end + end + + def action_modify + if compare_group + converge_by(["modify group #{@new_resource}", @change_desc]) do + manage_group + Chef::Log.info("#{@new_resource} modified") + end + end + end + + def create_group + raise NotImplementedError, "subclasses of Chef::Provider::Group should define #create_group" + end + + def manage_group + raise NotImplementedError, "subclasses of Chef::Provider::Group should define #manage_group" + end + + def remove_group + raise NotImplementedError, "subclasses of Chef::Provider::Group should define #remove_group" + end + + end + end +end diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb new file mode 100644 index 0000000000..9dedef351a --- /dev/null +++ b/lib/chef/provider/group/aix.rb @@ -0,0 +1,70 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/group/usermod' + +class Chef + class Provider + class Group + class Aix < Chef::Provider::Group::Usermod + + def required_binaries + [ "/usr/bin/mkgroup", + "/usr/bin/chgroup", + "/usr/sbin/rmgroup" ] + end + + def create_group + command = "mkgroup" + command << set_options << " #{@new_resource.group_name}" + run_command(:command => command) + modify_group_members + end + + def manage_group + command = "chgroup" + options = set_options + #Usage: chgroup [-R load_module] "attr=value" ... group + if options.size > 0 + command << options << " #{@new_resource.group_name}" + run_command(:command => command) + end + modify_group_members + end + + def remove_group + run_command(:command => "rmgroup #{@new_resource.group_name}") + end + + def set_options + opts = "" + { :gid => "id" }.sort { |a,b| a[0] <=> b[0] }.each do |field, option| + if @current_resource.send(field) != @new_resource.send(field) + if @new_resource.send(field) + Chef::Log.debug("#{@new_resource} setting #{field.to_s} to #{@new_resource.send(field)}") + opts << " '#{option}=#{@new_resource.send(field)}'" + end + end + end + opts + end + + end + end + end +end diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb new file mode 100644 index 0000000000..a8ba32641c --- /dev/null +++ b/lib/chef/provider/group/dscl.rb @@ -0,0 +1,129 @@ +# +# Author:: Dreamcat4 (<dreamcat4@gmail.com>) +# Copyright:: Copyright (c) 2009 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class Group + class Dscl < Chef::Provider::Group + + def dscl(*args) + host = "." + stdout_result = ""; stderr_result = ""; cmd = "dscl #{host} -#{args.join(' ')}" + status = popen4(cmd) do |pid, stdin, stdout, stderr| + stdout.each { |line| stdout_result << line } + stderr.each { |line| stderr_result << line } + end + return [cmd, status, stdout_result, stderr_result] + end + + def safe_dscl(*args) + result = dscl(*args) + return "" if ( args.first =~ /^delete/ ) && ( result[1].exitstatus != 0 ) + raise(Chef::Exceptions::Group,"dscl error: #{result.inspect}") unless result[1].exitstatus == 0 + raise(Chef::Exceptions::Group,"dscl error: #{result.inspect}") if result[2] =~ /No such key: / + return result[2] + end + + # This is handled in providers/group.rb by Etc.getgrnam() + # def group_exists?(group) + # groups = safe_dscl("list /Groups") + # !! ( groups =~ Regexp.new("\n#{group}\n") ) + # end + + # get a free GID greater than 200 + def get_free_gid(search_limit=1000) + gid = nil; next_gid_guess = 200 + groups_gids = safe_dscl("list /Groups gid") + while(next_gid_guess < search_limit + 200) + if groups_gids =~ Regexp.new("#{Regexp.escape(next_gid_guess.to_s)}\n") + next_gid_guess += 1 + else + gid = next_gid_guess + break + end + end + return gid || raise("gid not found. Exhausted. Searched #{search_limit} times") + end + + def gid_used?(gid) + return false unless gid + groups_gids = safe_dscl("list /Groups gid") + !! ( groups_gids =~ Regexp.new("#{Regexp.escape(gid.to_s)}\n") ) + end + + def set_gid + @new_resource.gid(get_free_gid) if [nil,""].include? @new_resource.gid + raise(Chef::Exceptions::Group,"gid is already in use") if gid_used?(@new_resource.gid) + safe_dscl("create /Groups/#{@new_resource.group_name} PrimaryGroupID #{@new_resource.gid}") + end + + def set_members + unless @new_resource.append + Chef::Log.debug("#{@new_resource} removing group members #{@current_resource.members.join(' ')}") unless @current_resource.members.empty? + safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembers ''") # clear guid list + safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembership ''") # clear user list + end + unless @new_resource.members.empty? + Chef::Log.debug("#{@new_resource} setting group members #{@new_resource.members.join(', ')}") + safe_dscl("append /Groups/#{@new_resource.group_name} GroupMembership #{@new_resource.members.join(' ')}") + end + end + + def define_resource_requirements + super + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/dscl") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/dscl for #{@new_resource.name}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + end + + def load_current_resource + super + end + + def create_group + dscl_create_group + set_gid + set_members + end + + def manage_group + if @new_resource.group_name && (@current_resource.group_name != @new_resource.group_name) + dscl_create_group + end + if @new_resource.gid && (@current_resource.gid != @new_resource.gid) + set_gid + end + if @new_resource.members && (@current_resource.members != @new_resource.members) + set_members + end + end + + def dscl_create_group + safe_dscl("create /Groups/#{@new_resource.group_name}") + safe_dscl("create /Groups/#{@new_resource.group_name} Password '*'") + end + + def remove_group + safe_dscl("delete /Groups/#{@new_resource.group_name}") + end + end + end + end +end diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb new file mode 100644 index 0000000000..7fb27a7777 --- /dev/null +++ b/lib/chef/provider/group/gpasswd.rb @@ -0,0 +1,65 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/group/groupadd' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Group + class Gpasswd < Chef::Provider::Group::Groupadd + + include Chef::Mixin::ShellOut + + def load_current_resource + super + end + + def define_resource_requirements + super + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/gpasswd") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/gpasswd for #{@new_resource}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + end + + def modify_group_members + if(@new_resource.append) + unless @new_resource.members.empty? + @new_resource.members.each do |member| + Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}") + shell_out!("gpasswd -a #{member} #{@new_resource.group_name}") + end + else + Chef::Log.debug("#{@new_resource} not changing group members, the group has no members to add") + end + else + unless @new_resource.members.empty? + Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}") + shell_out!("gpasswd -M #{@new_resource.members.join(',')} #{@new_resource.group_name}") + else + Chef::Log.debug("#{@new_resource} setting group members to: none") + shell_out!("gpasswd -M \"\" #{@new_resource.group_name}") + end + end + end + end + end + end +end diff --git a/lib/chef/provider/group/groupadd.rb b/lib/chef/provider/group/groupadd.rb new file mode 100644 index 0000000000..544fee4304 --- /dev/null +++ b/lib/chef/provider/group/groupadd.rb @@ -0,0 +1,96 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class Group + class Groupadd < Chef::Provider::Group + + def required_binaries + [ "/usr/sbin/groupadd", + "/usr/sbin/groupmod", + "/usr/sbin/groupdel" ] + end + + def load_current_resource + super + end + + def define_resource_requirements + super + required_binaries.each do |required_binary| + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?(required_binary) } + a.failure_message Chef::Exceptions::Group, "Could not find binary #{required_binary} for #{@new_resource}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + end + end + + # Create the group + def create_group + command = "groupadd" + command << set_options + command << groupadd_options + run_command(:command => command) + modify_group_members + end + + # Manage the group when it already exists + def manage_group + command = "groupmod" + command << set_options + run_command(:command => command) + modify_group_members + end + + # Remove the group + def remove_group + run_command(:command => "groupdel #{@new_resource.group_name}") + end + + def modify_group_members + raise Chef::Exceptions::Group, "you must override modify_group_members in #{self.to_s}" + end + # Little bit of magic as per Adam's useradd provider to pull the assign the command line flags + # + # ==== Returns + # <string>:: A string containing the option and then the quoted value + def set_options + opts = "" + { :gid => "-g" }.sort { |a,b| a[0] <=> b[0] }.each do |field, option| + if @current_resource.send(field) != @new_resource.send(field) + if @new_resource.send(field) + opts << " #{option} '#{@new_resource.send(field)}'" + Chef::Log.debug("#{@new_resource} set #{field.to_s} to #{@new_resource.send(field)}") + end + end + end + opts << " #{@new_resource.group_name}" + end + + def groupadd_options + opts = '' + opts << " -r" if @new_resource.system + opts + end + + end + end + end +end diff --git a/lib/chef/provider/group/groupmod.rb b/lib/chef/provider/group/groupmod.rb new file mode 100644 index 0000000000..10fc680d78 --- /dev/null +++ b/lib/chef/provider/group/groupmod.rb @@ -0,0 +1,120 @@ +# +# Author:: Dan Crosta (<dcrosta@late.am>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Group + class Groupmod < Chef::Provider::Group + + include Chef::Mixin::ShellOut + + def load_current_resource + super + [ "group", "user" ].each do |binary| + raise Chef::Exceptions::Group, "Could not find binary /usr/sbin/#{binary} for #{@new_resource}" unless ::File.exists?("/usr/sbin/#{binary}") + end + end + + # Create the group + def create_group + command = "group add" + command << set_options + shell_out!(command) + + add_group_members(@new_resource.members) + end + + # Manage the group when it already exists + def manage_group + if @new_resource.append + to_add = @new_resource.members.dup + to_add.reject! { |user| @current_resource.members.include?(user) } + + to_delete = Array.new + + Chef::Log.debug("#{@new_resource} not changing group members, the group has no members to add") if to_add.empty? + else + to_add = @new_resource.members.dup + to_add.reject! { |user| @current_resource.members.include?(user) } + + to_delete = @current_resource.members.dup + to_delete.reject! { |user| @new_resource.members.include?(user) } + + Chef::Log.debug("#{@new_resource} setting group members to: none") if @new_resource.members.empty? + end + + if to_delete.empty? + # If we are only adding new members to this group, then + # call add_group_members with only those users + add_group_members(to_add) + else + Chef::Log.debug("#{@new_resource} removing members #{to_delete.join(', ')}") + + # This is tricky, but works: rename the existing group to + # "<name>_bak", create a new group with the same GID and + # "<name>", then set correct members on that group + rename = "group mod -n #{@new_resource.group_name}_bak #{@new_resource.group_name}" + shell_out!(rename) + + create = "group add" + create << set_options(:overwrite_gid => true) + shell_out!(create) + + # Ignore to_add here, since we're replacing the group we + # have to add all members who should be in the group. + add_group_members(@new_resource.members) + + remove = "group del #{@new_resource.group_name}_bak" + shell_out!(remove) + end + end + + # Remove the group + def remove_group + shell_out!("group del #{@new_resource.group_name}") + end + + # Adds a list of usernames to the group using `user mod` + def add_group_members(members) + Chef::Log.debug("#{@new_resource} adding members #{members.join(', ')}") if !members.empty? + members.each do |user| + shell_out!("user mod -G #{@new_resource.group_name} #{user}") + end + end + + # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags + # + # ==== Returns + # <string>:: A string containing the option and then the quoted value + def set_options(overwrite_gid=false) + opts = "" + if overwrite_gid || @new_resource.gid && (@current_resource.gid != @new_resource.gid) + opts << " -g '#{@new_resource.gid}'" + end + if overwrite_gid + opts << " -o" + end + opts << " #{@new_resource.group_name}" + opts + end + end + end + end +end diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb new file mode 100644 index 0000000000..3bf67a515a --- /dev/null +++ b/lib/chef/provider/group/pw.rb @@ -0,0 +1,93 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class Group + class Pw < Chef::Provider::Group + + def load_current_resource + super + end + + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/sbin/pw") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/pw for #{@new_resource}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + end + + # Create the group + def create_group + command = "pw groupadd" + command << set_options + command << set_members_option + run_command(:command => command) + end + + # Manage the group when it already exists + def manage_group + command = "pw groupmod" + command << set_options + command << set_members_option + run_command(:command => command) + end + + # Remove the group + def remove_group + run_command(:command => "pw groupdel #{@new_resource.group_name}") + end + + # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags + # + # ==== Returns + # <string>:: A string containing the option and then the quoted value + def set_options + opts = " #{@new_resource.group_name}" + if @new_resource.gid && (@current_resource.gid != @new_resource.gid) + Chef::Log.debug("#{@new_resource}: current gid (#{@current_resource.gid}) doesnt match target gid (#{@new_resource.gid}), changing it") + opts << " -g '#{@new_resource.gid}'" + end + opts + end + + # Set the membership option depending on the current resource states + def set_members_option + opt = "" + unless @new_resource.members.empty? + opt << " -M #{@new_resource.members.join(',')}" + Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}") + else + # New member list is empty so we should delete any old group members + unless @current_resource.members.empty? + opt << " -d #{@current_resource.members.join(',')}" + Chef::Log.debug("#{@new_resource} removing group members #{@current_resource.members.join(', ')}") + else + Chef::Log.debug("#{@new_resource} not changing group members, the group has no members") + end + end + opt + end + + end + end + end +end diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb new file mode 100644 index 0000000000..0b66c1f912 --- /dev/null +++ b/lib/chef/provider/group/suse.rb @@ -0,0 +1,60 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/group/groupadd' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Group + class Suse < Chef::Provider::Group::Groupadd + + include Chef::Mixin::ShellOut + + def load_current_resource + super + end + + def define_resource_requirements + super + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/sbin/groupmod") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/groupmod for #{@new_resource.name}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + end + + def modify_group_members + unless @new_resource.members.empty? + if(@new_resource.append) + @new_resource.members.each do |member| + Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}") + shell_out!("groupmod -A #{member} #{@new_resource.group_name}") + end + else + Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}") + shell_out!("groupmod -A #{@new_resource.members.join(',')} #{@new_resource.group_name}") + end + else + Chef::Log.debug("#{@new_resource} not changing group members, the group has no members") + end + end + end + end + end +end diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb new file mode 100644 index 0000000000..f0a9282831 --- /dev/null +++ b/lib/chef/provider/group/usermod.rb @@ -0,0 +1,68 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/group/groupadd' + +class Chef + class Provider + class Group + class Usermod < Chef::Provider::Group::Groupadd + + def load_current_resource + super + end + + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/sbin/usermod") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/usermod for #{@new_resource}" + # No whyrun alternative: this component should be available in the base install of any given system that uses it + end + + requirements.assert(:modify, :create) do |a| + a.assertion { @new_resource.members.empty? || @new_resource.append } + a.failure_message Chef::Exceptions::Group, "setting group members directly is not supported by #{self.to_s}, must set append true in group" + # No whyrun alternative - this action is simply not supported. + end + end + + def modify_group_members + case node[:platform] + when "openbsd", "netbsd", "aix", "solaris2" + append_flags = "-G" + when "solaris" + append_flags = "-a -G" + end + + unless @new_resource.members.empty? + if(@new_resource.append) + @new_resource.members.each do |member| + Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}") + run_command(:command => "usermod #{append_flags} #{@new_resource.group_name} #{member}" ) + end + end + else + Chef::Log.debug("#{@new_resource} not changing group members, the group has no members") + end + end + end + end + end +end diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb new file mode 100644 index 0000000000..88280408cd --- /dev/null +++ b/lib/chef/provider/group/windows.rb @@ -0,0 +1,79 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/user' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/util/windows/net_group' +end + +class Chef + class Provider + class Group + class Windows < Chef::Provider::Group + + def initialize(new_resource,run_context) + super + @net_group = Chef::Util::Windows::NetGroup.new(@new_resource.name) + end + + def load_current_resource + @current_resource = Chef::Resource::Group.new(@new_resource.name) + @current_resource.group_name(@new_resource.group_name) + + members = nil + begin + members = @net_group.local_get_members + rescue => e + @group_exists = false + Chef::Log.debug("#{@new_resource} group does not exist") + end + + if members + @current_resource.members(members) + end + + @current_resource + end + + def create_group + @net_group.local_add + manage_group + end + + def manage_group + if @new_resource.append + begin + #ERROR_MEMBER_IN_ALIAS if a member already exists in the group + @net_group.local_add_members(@new_resource.members) + rescue + members = @new_resource.members + @current_resource.members + @net_group.local_set_members(members.uniq) + end + else + @net_group.local_set_members(@new_resource.members) + end + end + + def remove_group + @net_group.local_delete + end + + end + end + end +end diff --git a/lib/chef/provider/http_request.rb b/lib/chef/provider/http_request.rb new file mode 100644 index 0000000000..0ea5f8289f --- /dev/null +++ b/lib/chef/provider/http_request.rb @@ -0,0 +1,136 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'tempfile' + +class Chef + class Provider + class HttpRequest < Chef::Provider + + attr_accessor :rest + + def whyrun_supported? + true + end + + def load_current_resource + @rest = Chef::REST.new(@new_resource.url, nil, nil) + end + + # Send a HEAD request to @new_resource.url, with ?message=@new_resource.message + def action_head + message = check_message(@new_resource.message) + # returns true from Chef::REST if returns 2XX (Net::HTTPSuccess) + modified = @rest.run_request( + :HEAD, + @rest.create_url("#{@new_resource.url}?message=#{message}"), + @new_resource.headers, + false, + 10, + false + ) + Chef::Log.info("#{@new_resource} HEAD to #{@new_resource.url} successful") + Chef::Log.debug("#{@new_resource} HEAD request response: #{modified}") + # :head is usually used to trigger notifications, which converge_by now does + if modified + converge_by("#{@new_resource} HEAD to #{@new_resource.url} returned modified, trigger notifications") {} + end + end + + # Send a GET request to @new_resource.url, with ?message=@new_resource.message + def action_get + converge_by("#{@new_resource} GET to #{@new_resource.url}") do + + message = check_message(@new_resource.message) + body = @rest.run_request( + :GET, + @rest.create_url("#{@new_resource.url}?message=#{message}"), + @new_resource.headers, + false, + 10, + false + ) + Chef::Log.info("#{@new_resource} GET to #{@new_resource.url} successful") + Chef::Log.debug("#{@new_resource} GET request response: #{body}") + end + end + + # Send a PUT request to @new_resource.url, with the message as the payload + def action_put + converge_by("#{@new_resource} PUT to #{@new_resource.url}") do + message = check_message(@new_resource.message) + body = @rest.run_request( + :PUT, + @rest.create_url("#{@new_resource.url}"), + @new_resource.headers, + message, + 10, + false + ) + Chef::Log.info("#{@new_resource} PUT to #{@new_resource.url} successful") + Chef::Log.debug("#{@new_resource} PUT request response: #{body}") + end + end + + # Send a POST request to @new_resource.url, with the message as the payload + def action_post + converge_by("#{@new_resource} POST to #{@new_resource.url}") do + message = check_message(@new_resource.message) + body = @rest.run_request( + :POST, + @rest.create_url("#{@new_resource.url}"), + @new_resource.headers, + message, + 10, + false + ) + Chef::Log.info("#{@new_resource} POST to #{@new_resource.url} message: #{message.inspect} successful") + Chef::Log.debug("#{@new_resource} POST request response: #{body}") + end + end + + # Send a DELETE request to @new_resource.url + def action_delete + converge_by("#{@new_resource} DELETE to #{@new_resource.url}") do + body = @rest.run_request( + :DELETE, + @rest.create_url("#{@new_resource.url}"), + @new_resource.headers, + false, + 10, + false + ) + @new_resource.updated_by_last_action(true) + Chef::Log.info("#{@new_resource} DELETE to #{@new_resource.url} successful") + Chef::Log.debug("#{@new_resource} DELETE request response: #{body}") + end + end + + private + + def check_message(message) + if message.kind_of?(Proc) + message.call + else + message + end + end + + end + end +end diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb new file mode 100644 index 0000000000..86680b2229 --- /dev/null +++ b/lib/chef/provider/ifconfig.rb @@ -0,0 +1,214 @@ +# +# Author:: Jason K. Jackson (jasonjackson@gmail.com) +# Copyright:: Copyright (c) 2009 Jason K. Jackson +# 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/log' +require 'chef/mixin/command' +require 'chef/provider' +require 'chef/exceptions' +require 'erb' + +# Recipe example: +# +# int = {Hash with your network settings...} +# +# ifconfig int['ip'] do +# ignore_failure true +# device int['dev'] +# mask int['mask'] +# gateway int['gateway'] +# mtu int['mtu'] +# end + +class Chef + class Provider + class Ifconfig < Chef::Provider + include Chef::Mixin::Command + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) + + @ifconfig_success = true + @interfaces = {} + + @status = popen4("ifconfig") do |pid, stdin, stdout, stderr| + stdout.each do |line| + + if !line[0..9].strip.empty? + @int_name = line[0..9].strip + @interfaces[@int_name] = {"hwaddr" => (line =~ /(HWaddr)/ ? ($') : "nil").strip.chomp } + else + @interfaces[@int_name]["inet_addr"] = (line =~ /inet addr:(\S+)/ ? ($1) : "nil") if line =~ /inet addr:/ + @interfaces[@int_name]["bcast"] = (line =~ /Bcast:(\S+)/ ? ($1) : "nil") if line =~ /Bcast:/ + @interfaces[@int_name]["mask"] = (line =~ /Mask:(\S+)/ ? ($1) : "nil") if line =~ /Mask:/ + @interfaces[@int_name]["mtu"] = (line =~ /MTU:(\S+)/ ? ($1) : "nil") if line =~ /MTU:/ + @interfaces[@int_name]["metric"] = (line =~ /Metric:(\S+)/ ? ($1) : "nil") if line =~ /Metric:/ + end + + if @interfaces.has_key?(@new_resource.device) + @interface = @interfaces.fetch(@new_resource.device) + + @current_resource.target(@new_resource.target) + @current_resource.device(@int_name) + @current_resource.inet_addr(@interface["inet_addr"]) + @current_resource.hwaddr(@interface["hwaddr"]) + @current_resource.bcast(@interface["bcast"]) + @current_resource.mask(@interface["mask"]) + @current_resource.mtu(@interface["mtu"]) + @current_resource.metric(@interface["metric"]) + end + end + end + @current_resource + end + + def define_resource_requirements + requirements.assert(:all_actions) do |a| + a.assertion { @status.exitstatus == 0 } + a.failure_message Chef::Exceptions::Ifconfig, "ifconfig failed - #{@status.inspect}!" + # no whyrun - if the base ifconfig used in load_current_resource fails + # there's no reasonable action that could have been taken in the course of + # a chef run to fix it. + end + end + + def action_add + # check to see if load_current_resource found interface in ifconfig + unless @current_resource.inet_addr + unless @new_resource.device == "lo" + command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command << " netmask #{@new_resource.mask}" if @new_resource.mask + command << " metric #{@new_resource.metric}" if @new_resource.metric + command << " mtu #{@new_resource.mtu}" if @new_resource.mtu + end + converge_by ("run #{command} to add #{@new_resource}") do + run_command( + :command => command + ) + Chef::Log.info("#{@new_resource} added") + end + end + + # Write out the config files + generate_config + end + + def action_enable + # check to see if load_current_resource found ifconfig + # enables, but does not manage config files + unless @current_resource.inet_addr + unless @new_resource.device == "lo" + command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command << " netmask #{@new_resource.mask}" if @new_resource.mask + command << " metric #{@new_resource.metric}" if @new_resource.metric + command << " mtu #{@new_resource.mtu}" if @new_resource.mtu + end + + converge_by ("run #{command} to enable #{@new_resource}") do + run_command( + :command => command + ) + Chef::Log.info("#{@new_resource} enabled") + end + end + end + + def action_delete + # check to see if load_current_resource found the interface + if @current_resource.device + command = "ifconfig #{@new_resource.device} down" + converge_by ("run #{command} to delete #{@new_resource}") do + 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 + end + + def action_disable + # check to see if load_current_resource found the interface + # disables, but leaves config files in place. + if @current_resource.device + command = "ifconfig #{@new_resource.device} down" + converge_by ("run #{command} to disable #{@new_resource}") do + run_command( + :command => command + ) + Chef::Log.info("#{@new_resource} disabled") + end + else + Chef::Log.debug("#{@new_resource} does not exist - nothing to do") + end + end + + def generate_config + b = binding + case node[:platform] + when "centos","redhat","fedora" + content = %{ +<% if @new_resource.device %>DEVICE=<%= @new_resource.device %><% end %> +<% if @new_resource.onboot %>ONBOOT=<%= @new_resource.onboot %><% end %> +<% if @new_resource.bootproto %>BOOTPROTO=<%= @new_resource.bootproto %><% end %> +<% if @new_resource.target %>IPADDR=<%= @new_resource.target %><% end %> +<% if @new_resource.mask %>NETMASK=<%= @new_resource.mask %><% end %> +<% if @new_resource.network %>NETWORK=<%= @new_resource.network %><% end %> +<% if @new_resource.bcast %>BROADCAST=<%= @new_resource.bcast %><% end %> +<% if @new_resource.onparent %>ONPARENT=<%= @new_resource.onparent %><% end %> + } + template = ::ERB.new(content) + network_file_name = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" + converge_by ("generate configuration file : #{network_file_name}") do + network_file = ::File.new(network_file_name, "w") + network_file.puts(template.result(b)) + network_file.close + end + Chef::Log.info("#{@new_resource} created configuration file") + when "debian","ubuntu" + # template + when "slackware" + # template + end + end + + def delete_config + require 'fileutils' + case node[:platform] + when "centos","redhat","fedora" + ifcfg_file = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" + if ::File.exist?(ifcfg_file) + converge_by ("delete the #{ifcfg_file}") do + FileUtils.rm_f(ifcfg_file, :verbose => false) + end + end + when "debian","ubuntu" + # delete configs + when "slackware" + # delete configs + end + end + + end + end +end diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb new file mode 100644 index 0000000000..d6f333bd2f --- /dev/null +++ b/lib/chef/provider/link.rb @@ -0,0 +1,130 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/log' +require 'chef/mixin/shell_out' +require 'chef/mixin/file_class' +require 'chef/resource/link' +require 'chef/provider' +require 'chef/scan_access_control' + +class Chef + class Provider + class Link < Chef::Provider + include Chef::Mixin::ShellOut + include Chef::Mixin::FileClass + + def negative_complement(big) + if big > 1073741823 # Fixnum max + big -= (2**32) # diminished radix wrap to negative + end + big + end + + private :negative_complement + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::Link.new(@new_resource.name) + @current_resource.target_file(@new_resource.target_file) + if file_class.symlink?(@current_resource.target_file) + @current_resource.link_type(:symbolic) + @current_resource.to( + canonicalize(file_class.readlink(@current_resource.target_file)) + ) + else + @current_resource.link_type(:hard) + if ::File.exists?(@current_resource.target_file) + if ::File.exists?(@new_resource.to) && + file_class.stat(@current_resource.target_file).ino == + file_class.stat(@new_resource.to).ino + @current_resource.to(canonicalize(@new_resource.to)) + else + @current_resource.to("") + end + end + end + ScanAccessControl.new(@new_resource, @current_resource).set_all! + @current_resource + end + + def define_resource_requirements + requirements.assert(:delete) do |a| + a.assertion do + if @current_resource.to + @current_resource.link_type == @new_resource.link_type and + (@current_resource.link_type == :symbolic or @current_resource.to != '') + else + true + end + end + a.failure_message Chef::Exceptions::Link, "Cannot delete #{@new_resource} at #{@new_resource.target_file}! Not a #{@new_resource.link_type.to_s} link." + a.whyrun("Would assume the link at #{@new_resource.target_file} was previously created") + end + end + + def canonicalize(path) + Chef::Platform.windows? ? path.gsub('/', '\\') : path + end + + def action_create + if @current_resource.to != canonicalize(@new_resource.to) || + @current_resource.link_type != @new_resource.link_type + if @current_resource.to # nil if target_file does not exist + converge_by("unlink existing file at #{@new_resource.target_file}") do + ::File.unlink(@new_resource.target_file) + end + end + if @new_resource.link_type == :symbolic + converge_by("create symlink at #{@new_resource.target_file} to #{@new_resource.to}") do + file_class.symlink(canonicalize(@new_resource.to),@new_resource.target_file) + Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.to} -> #{@new_resource.target_file}") + Chef::Log.info("#{@new_resource} created") + end + elsif @new_resource.link_type == :hard + converge_by("create hard link at #{@new_resource.target_file} to #{@new_resource.to}") do + file_class.link(@new_resource.to, @new_resource.target_file) + Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.to} -> #{@new_resource.target_file}") + Chef::Log.info("#{@new_resource} created") + end + end + end + if @new_resource.link_type == :symbolic + if access_controls.requires_changes? + converge_by(access_controls.describe_changes) do + access_controls.set_all + end + end + end + end + + def action_delete + if @current_resource.to # Exists + converge_by ("delete link at #{@new_resource.target_file}") do + ::File.delete(@new_resource.target_file) + Chef::Log.info("#{@new_resource} deleted") + end + end + end + end + end +end diff --git a/lib/chef/provider/log.rb b/lib/chef/provider/log.rb new file mode 100644 index 0000000000..5d0417ebda --- /dev/null +++ b/lib/chef/provider/log.rb @@ -0,0 +1,54 @@ +# +# Author:: Cary Penniman (<cary@rightscale.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + + class Provider + + class Log + + # Chef log provider, allows logging to chef's logs from recipes + class ChefLog < Chef::Provider + + # No concept of a 'current' resource for logs, this is a no-op + # + # === Return + # true:: Always return true + def load_current_resource + true + end + + # Write the log to Chef's log + # + # === Return + # true:: Always return true + def action_write + Chef::Log.send(@new_resource.level, @new_resource.name) + @new_resource.updated_by_last_action(true) + end + + end + + end + + end + +end + + + diff --git a/lib/chef/provider/mdadm.rb b/lib/chef/provider/mdadm.rb new file mode 100644 index 0000000000..d93ff69c13 --- /dev/null +++ b/lib/chef/provider/mdadm.rb @@ -0,0 +1,92 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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/log' +require 'chef/mixin/shell_out' +require 'chef/provider' + +class Chef + class Provider + class Mdadm < Chef::Provider + + include Chef::Mixin::ShellOut + + def popen4 + raise Exception, "deprecated" + end + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::Mdadm.new(@new_resource.name) + @current_resource.raid_device(@new_resource.raid_device) + Chef::Log.debug("#{@new_resource} checking for software raid device #{@current_resource.raid_device}") + + device_not_found = 4 + mdadm = shell_out!("mdadm --detail --test #{@new_resource.raid_device}", :returns => [0,device_not_found]) + exists = (mdadm.status == 0) + @current_resource.exists(exists) + end + + def action_create + unless @current_resource.exists + converge_by("create RAID device #{new_resource.raid_device}") do + command = "yes | mdadm --create #{@new_resource.raid_device} --chunk=#{@new_resource.chunk} --level #{@new_resource.level}" + command << " --metadata=#{@new_resource.metadata}" + command << " --bitmap=#{@new_resource.bitmap}" if @new_resource.bitmap + command << " --raid-devices #{@new_resource.devices.length} #{@new_resource.devices.join(" ")}" + Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + shell_out!(command) + Chef::Log.info("#{@new_resource} created raid device (#{@new_resource.raid_device})") + end + else + Chef::Log.debug("#{@new_resource} raid device already exists, skipping create (#{@new_resource.raid_device})") + end + end + + def action_assemble + unless @current_resource.exists + converge_by("assemble RAID device #{new_resource.raid_device}") do + command = "yes | mdadm --assemble #{@new_resource.raid_device} #{@new_resource.devices.join(" ")}" + Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + shell_out!(command) + Chef::Log.info("#{@new_resource} assembled raid device (#{@new_resource.raid_device})") + end + else + Chef::Log.debug("#{@new_resource} raid device already exists, skipping assemble (#{@new_resource.raid_device})") + end + end + + def action_stop + if @current_resource.exists + converge_by("stop RAID device #{new_resource.raid_device}") do + command = "yes | mdadm --stop #{@new_resource.raid_device}" + Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + shell_out!(command) + Chef::Log.info("#{@new_resource} stopped raid device (#{@new_resource.raid_device})") + end + else + Chef::Log.debug("#{@new_resource} raid device doesn't exist (#{@new_resource.raid_device}) - not stopping") + end + end + + end + end +end diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb new file mode 100644 index 0000000000..fe41997d39 --- /dev/null +++ b/lib/chef/provider/mount.rb @@ -0,0 +1,128 @@ +# +# Author:: Joshua Timberman (<joshua@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/mixin/command' +require 'chef/provider' + +class Chef + class Provider + class Mount < Chef::Provider + + include Chef::Mixin::Command + + + def whyrun_supported? + true + end + + def load_current_resource + true + end + + def action_mount + unless @current_resource.mounted + converge_by("mount #{@current_resource.device} to #{@current_resource.mount_point}") do + status = mount_fs() + if status + Chef::Log.info("#{@new_resource} mounted") + end + end + else + Chef::Log.debug("#{@new_resource} is already mounted") + end + end + + def action_umount + if @current_resource.mounted + converge_by("unmount #{@current_resource.device}") do + status = umount_fs() + if status + Chef::Log.info("#{@new_resource} unmounted") + end + end + else + Chef::Log.debug("#{@new_resource} is already unmounted") + end + end + + def action_remount + unless @new_resource.supports[:remount] + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remount" + else + if @current_resource.mounted + converge_by("remount #{@current_resource.device}") do + status = remount_fs() + if status + Chef::Log.info("#{@new_resource} remounted") + end + end + else + Chef::Log.debug("#{@new_resource} not mounted, nothing to remount") + end + end + end + + def action_enable + unless @current_resource.enabled + converge_by("remount #{@current_resource.device}") do + status = enable_fs + if status + Chef::Log.info("#{@new_resource} enabled") + else + Chef::Log.debug("#{@new_resource} already enabled") + end + end + end + end + + def action_disable + if @current_resource.enabled + converge_by("remount #{@current_resource.device}") do + status = disable_fs + if status + Chef::Log.info("#{@new_resource} disabled") + else + Chef::Log.debug("#{@new_resource} already disabled") + end + end + end + end + + def mount_fs + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :mount" + end + + def umount_fs + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :umount" + end + + def remount_fs + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remount" + end + + def enable_fs + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" + end + + def disable_fs + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" + end + end + end +end diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb new file mode 100644 index 0000000000..9a85a9058a --- /dev/null +++ b/lib/chef/provider/mount/mount.rb @@ -0,0 +1,252 @@ +# +# Author:: Joshua Timberman (<joshua@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/mount' +require 'chef/log' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Mount + class Mount < Chef::Provider::Mount + include Chef::Mixin::ShellOut + + def initialize(new_resource, run_context) + super + @real_device = nil + end + attr_accessor :real_device + + def load_current_resource + @current_resource = Chef::Resource::Mount.new(@new_resource.name) + @current_resource.mount_point(@new_resource.mount_point) + @current_resource.device(@new_resource.device) + mounted? + enabled? + end + + def mountable? + # only check for existence of non-remote devices + if (device_should_exist? && !::File.exists?(device_real) ) + raise Chef::Exceptions::Mount, "Device #{@new_resource.device} does not exist" + elsif( !::File.exists?(@new_resource.mount_point) ) + raise Chef::Exceptions::Mount, "Mount point #{@new_resource.mount_point} does not exist" + end + return true + end + + def enabled? + # Check to see if there is a entry in /etc/fstab. Last entry for a volume wins. + enabled = false + ::File.foreach("/etc/fstab") do |line| + case line + when /^[#\s]/ + next + when /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ + enabled = true + @current_resource.fstype($1) + @current_resource.options($2) + @current_resource.dump($3.to_i) + @current_resource.pass($4.to_i) + Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/fstab") + next + when /^[\/\w]+\s+#{Regexp.escape(@new_resource.mount_point)}\s+/ + enabled = false + Chef::Log.debug("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab") + end + end + @current_resource.enabled(enabled) + end + + def mounted? + mounted = false + shell_out!("mount").stdout.each_line do |line| + case line + when /^#{device_mount_regex}\s+on\s+#{Regexp.escape(@new_resource.mount_point)}/ + mounted = true + Chef::Log.debug("Special device #{device_logstring} mounted as #{@new_resource.mount_point}") + when /^([\/\w])+\son\s#{Regexp.escape(@new_resource.mount_point)}\s+/ + mounted = false + Chef::Log.debug("Special device #{$~[1]} mounted as #{@new_resource.mount_point}") + end + end + @current_resource.mounted(mounted) + end + + def mount_fs + unless @current_resource.mounted + mountable? + command = "mount -t #{@new_resource.fstype}" + command << " -o #{@new_resource.options.join(',')}" unless @new_resource.options.nil? || @new_resource.options.empty? + command << case @new_resource.device_type + when :device + " #{device_real}" + when :label + " -L #{@new_resource.device}" + when :uuid + " -U #{@new_resource.device}" + end + command << " #{@new_resource.mount_point}" + shell_out!(command) + Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") + else + Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}") + end + end + + def umount_fs + if @current_resource.mounted + shell_out!("umount #{@new_resource.mount_point}") + Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") + else + Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}") + end + end + + def remount_fs + if @current_resource.mounted and @new_resource.supports[:remount] + shell_out!("mount -o remount #{@new_resource.mount_point}") + @new_resource.updated_by_last_action(true) + Chef::Log.debug("#{@new_resource} is remounted at #{@new_resource.mount_point}") + elsif @current_resource.mounted + umount_fs + sleep 1 + mount_fs + else + Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point} - nothing to do") + end + end + + def enable_fs + if @current_resource.enabled && mount_options_unchanged? + Chef::Log.debug("#{@new_resource} is already enabled - nothing to do") + return nil + end + + if @current_resource.enabled + # The current options don't match what we have, so + # disable, then enable. + disable_fs + end + ::File.open("/etc/fstab", "a") do |fstab| + fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? "defaults" : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}") + Chef::Log.debug("#{@new_resource} is enabled at #{@new_resource.mount_point}") + end + end + + def disable_fs + if @current_resource.enabled + contents = [] + + found = false + ::File.readlines("/etc/fstab").reverse_each do |line| + if !found && line =~ /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}/ + found = true + Chef::Log.debug("#{@new_resource} is removed from fstab") + next + else + contents << line + end + end + + ::File.open("/etc/fstab", "w") do |fstab| + contents.reverse_each { |line| fstab.puts line} + end + else + Chef::Log.debug("#{@new_resource} is not enabled - nothing to do") + end + end + + def network_device? + @new_resource.device =~ /:/ || @new_resource.device =~ /\/\// + end + + def device_should_exist? + ( not network_device? ) && + ( not %w[ tmpfs fuse ].include? @new_resource.fstype ) + end + + private + + def device_fstab + case @new_resource.device_type + when :device + @new_resource.device + when :label + "LABEL=#{@new_resource.device}" + when :uuid + "UUID=#{@new_resource.device}" + end + end + + def device_real + if @real_device == nil + if @new_resource.device_type == :device + @real_device = @new_resource.device + else + @real_device = "" + status = popen4("/sbin/findfs #{device_fstab}") do |pid, stdin, stdout, stderr| + device_line = stdout.first # stdout.first consumes + @real_device = device_line.chomp unless device_line.nil? + end + end + end + @real_device + end + + def device_logstring + case @new_resource.device_type + when :device + "#{device_real}" + when :label + "#{device_real} with label #{@new_resource.device}" + when :uuid + "#{device_real} with uuid #{@new_resource.device}" + end + end + + def device_mount_regex + if network_device? + # ignore trailing slash + Regexp.escape(device_real)+"/?" + elsif ::File.symlink?(device_real) + "(?:#{Regexp.escape(device_real)})|(?:#{Regexp.escape(::File.readlink(device_real))})" + else + Regexp.escape(device_real) + end + end + + def device_fstab_regex + if @new_resource.device_type == :device + device_mount_regex + else + device_fstab + end + end + + def mount_options_unchanged? + @current_resource.fstype == @new_resource.fstype and + @current_resource.options == @new_resource.options and + @current_resource.dump == @new_resource.dump and + @current_resource.pass == @new_resource.pass + end + + end + end + end +end diff --git a/lib/chef/provider/mount/windows.rb b/lib/chef/provider/mount/windows.rb new file mode 100644 index 0000000000..dced0d3596 --- /dev/null +++ b/lib/chef/provider/mount/windows.rb @@ -0,0 +1,81 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/mount' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/util/windows/net_use' + require 'chef/util/windows/volume' +end + +class Chef + class Provider + class Mount + class Windows < Chef::Provider::Mount + + def is_volume(name) + name =~ /^\\\\\?\\Volume\{[\w-]+\}\\$/ ? true : false + end + + def initialize(new_resource, run_context) + super + @mount = nil + end + + def load_current_resource + if is_volume(@new_resource.device) + @mount = Chef::Util::Windows::Volume.new(@new_resource.name) + else #assume network drive + @mount = Chef::Util::Windows::NetUse.new(@new_resource.name) + end + + @current_resource = Chef::Resource::Mount.new(@new_resource.name) + @current_resource.mount_point(@new_resource.mount_point) + Chef::Log.debug("Checking for mount point #{@current_resource.mount_point}") + + begin + @current_resource.device(@mount.device) + Chef::Log.debug("#{@current_resource.device} mounted on #{@new_resource.mount_point}") + @current_resource.mounted(true) + rescue ArgumentError => e + @current_resource.mounted(false) + Chef::Log.debug("#{@new_resource.mount_point} is not mounted: #{e.message}") + end + end + + def mount_fs + unless @current_resource.mounted + @mount.add(@new_resource.device) + Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") + else + Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}") + end + end + + def umount_fs + if @current_resource.mounted + @mount.delete + Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") + else + Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}") + end + end + + end + end + end +end diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb new file mode 100644 index 0000000000..c686f67450 --- /dev/null +++ b/lib/chef/provider/ohai.rb @@ -0,0 +1,47 @@ +# +# Author:: Michael Leianrtas (<mleinartas@gmail.com>) +# Copyright:: Copyright (c) 2010 Michael Leinartas +# 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 'ohai' + +class Chef + class Provider + class Ohai < Chef::Provider + + def whyrun_supported? + true + end + + def load_current_resource + true + end + + def action_reload + converge_by("re-run ohai and merge results into node attributes") do + ohai = ::Ohai::System.new + if @new_resource.plugin + ohai.require_plugin @new_resource.plugin + else + ohai.all_plugins + end + node.automatic_attrs.merge! ohai.data + Chef::Log.info("#{@new_resource} reloaded") + end + end + end + end +end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb new file mode 100644 index 0000000000..a28a6f93fb --- /dev/null +++ b/lib/chef/provider/package.rb @@ -0,0 +1,229 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/command' +require 'chef/log' +require 'chef/file_cache' +require 'chef/platform' + +class Chef + class Provider + class Package < Chef::Provider + + include Chef::Mixin::Command + + attr_accessor :candidate_version + def initialize(new_resource, run_context) + super + @candidate_version = nil + end + + def whyrun_supported? + true + end + + def load_current_resource + end + + def define_resource_requirements + requirements.assert(:install) do |a| + a.assertion { ((@new_resource.version != nil) && !(target_version_already_installed?)) \ + || !(@current_resource.version.nil? && candidate_version.nil?) } + a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{@new_resource.package_name}") + a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured") + end + + requirements.assert(:upgrade) do |a| + # Can't upgrade what we don't have + a.assertion { !(@current_resource.version.nil? && candidate_version.nil?) } + a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{@new_resource.package_name}") + a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured") + end + end + + def action_install + # If we specified a version, and it's not the current version, move to the specified version + if !@new_resource.version.nil? && !(target_version_already_installed?) + install_version = @new_resource.version + # If it's not installed at all, install it + elsif @current_resource.version.nil? + install_version = candidate_version + else + Chef::Log.debug("#{@new_resource} is already installed - nothing to do") + return + end + + # We need to make sure we handle the preseed file + if @new_resource.response_file + if preseed_file = get_preseed_file(@new_resource.package_name, install_version) + converge_by("preseed package #{@new_resource.package_name}") do + preseed_package(preseed_file) + end + end + end + description = install_version ? "version #{install_version} of" : "" + converge_by("install #{description} package #{@new_resource.package_name}") do + @new_resource.version(install_version) + install_package(@new_resource.package_name, install_version) + end + end + + def action_upgrade + if candidate_version.nil? + Chef::Log.debug("#{@new_resource} no candidate version - nothing to do") + elsif @current_resource.version == candidate_version + Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") + else + @new_resource.version(candidate_version) + orig_version = @current_resource.version || "uninstalled" + converge_by("upgrade package #{@new_resource.package_name} from #{orig_version} to #{candidate_version}") do + status = upgrade_package(@new_resource.package_name, candidate_version) + Chef::Log.info("#{@new_resource} upgraded from #{orig_version} to #{candidate_version}") + end + end + end + + def action_remove + if removing_package? + description = @new_resource.version ? "version #{@new_resource.version} of " : "" + converge_by("remove #{description} package #{@current_resource.package_name}") do + remove_package(@current_resource.package_name, @new_resource.version) + Chef::Log.info("#{@new_resource} removed") + end + else + Chef::Log.debug("#{@new_resource} package does not exist - nothing to do") + end + end + + def removing_package? + if @current_resource.version.nil? + false # nothing to remove + elsif @new_resource.version.nil? + true # remove any version of a package + elsif @new_resource.version == @current_resource.version + true # remove the version we have + else + false # we don't have the version we want to remove + end + end + + def action_purge + if removing_package? + description = @new_resource.version ? "version #{@new_resource.version} of" : "" + converge_by("purge #{description} package #{@current_resource.package_name}") do + purge_package(@current_resource.package_name, @new_resource.version) + Chef::Log.info("#{@new_resource} purged") + end + end + end + + def action_reconfig + if @current_resource.version == nil then + Chef::Log.debug("#{@new_resource} is NOT installed - nothing to do") + return + end + + unless @new_resource.response_file then + Chef::Log.debug("#{@new_resource} no response_file provided - nothing to do") + return + end + + if preseed_file = get_preseed_file(@new_resource.package_name, @current_resource.version) + converge_by("reconfigure package #{@new_resource.package_name}") do + preseed_package(preseed_file) + status = reconfig_package(@new_resource.package_name, @current_resource.version) + Chef::Log.info("#{@new_resource} reconfigured") + end + else + Chef::Log.debug("#{@new_resource} preseeding has not changed - nothing to do") + end + end + + def install_package(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :install" + end + + def upgrade_package(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :upgrade" + end + + def remove_package(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remove" + end + + def purge_package(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :purge" + end + + def preseed_package(file) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support pre-seeding package install/upgrade instructions" + end + + def reconfig_package(name, version) + raise( Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reconfig" ) + end + + def get_preseed_file(name, version) + resource = preseed_resource(name, version) + resource.run_action(:create) + Chef::Log.debug("#{@new_resource} fetched preseed file to #{resource.path}") + + if resource.updated_by_last_action? + resource.path + else + false + end + end + + def preseed_resource(name, version) + # A directory in our cache to store this cookbook's preseed files in + file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{@new_resource.cookbook_name}") + # The full path where the preseed file will be stored + cache_seed_to = "#{file_cache_dir}/#{name}-#{version}.seed" + + Chef::Log.debug("#{@new_resource} fetching preseed file to #{cache_seed_to}") + + begin + remote_file = Chef::Resource::Template.new(cache_seed_to, run_context) + remote_file.cookbook_name = @new_resource.cookbook_name + remote_file.source(@new_resource.response_file) + remote_file.backup(false) + provider = Chef::Platform.provider_for_resource(remote_file, :create) + provider.template_location + rescue + Chef::Log.debug("#{@new_resource} fetching preseed file via Template resource failed, fallback to CookbookFile resource") + remote_file = Chef::Resource::CookbookFile.new(cache_seed_to, run_context) + remote_file.cookbook_name = @new_resource.cookbook_name + remote_file.source(@new_resource.response_file) + remote_file.backup(false) + end + + remote_file + end + + def expand_options(options) + options ? " #{options}" : "" + end + + def target_version_already_installed? + @new_resource.version == @current_resource.version + end + + end + end +end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb new file mode 100644 index 0000000000..e8939b494e --- /dev/null +++ b/lib/chef/provider/package/apt.rb @@ -0,0 +1,147 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/shell_out' + + +class Chef + class Provider + class Package + class Apt < Chef::Provider::Package + + include Chef::Mixin::ShellOut + attr_accessor :is_virtual_package + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + check_package_state(@new_resource.package_name) + @current_resource + end + + def default_release_options + # Use apt::Default-Release option only if provider was explicitly defined + "-o APT::Default-Release=#{@new_resource.default_release}" if @new_resource.provider && @new_resource.default_release + end + + def check_package_state(package) + Chef::Log.debug("#{@new_resource} checking package status for #{package}") + installed = false + + shell_out!("apt-cache#{expand_options(default_release_options)} policy #{package}").stdout.each_line do |line| + case line + when /^\s{2}Installed: (.+)$/ + installed_version = $1 + if installed_version == '(none)' + Chef::Log.debug("#{@new_resource} current version is nil") + @current_resource.version(nil) + else + Chef::Log.debug("#{@new_resource} current version is #{installed_version}") + @current_resource.version(installed_version) + installed = true + end + when /^\s{2}Candidate: (.+)$/ + candidate_version = $1 + if candidate_version == '(none)' + # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm + @is_virtual_package = true + showpkg = shell_out!("apt-cache showpkg #{package}").stdout + providers = Hash.new + showpkg.rpartition(/Reverse Provides:? #{$/}/)[2].each_line do |line| + provider, version = line.split + providers[provider] = version + end + # Check if the package providing this virtual package is installed + num_providers = providers.length + raise Chef::Exceptions::Package, "#{@new_resource.package_name} has no candidate in the apt-cache" if num_providers == 0 + # apt will only install a virtual package if there is a single providing package + raise Chef::Exceptions::Package, "#{@new_resource.package_name} is a virtual package provided by #{num_providers} packages, you must explicitly select one to install" if num_providers > 1 + # Check if the package providing this virtual package is installed + Chef::Log.info("#{@new_resource} is a virtual package, actually acting on package[#{providers.keys.first}]") + installed = check_package_state(providers.keys.first) + else + Chef::Log.debug("#{@new_resource} candidate version is #{$1}") + @candidate_version = $1 + end + end + end + + return installed + end + + def install_package(name, version) + package_name = "#{name}=#{version}" + package_name = name if @is_virtual_package + run_command_with_systems_locale( + :command => "apt-get -q -y#{expand_options(default_release_options)}#{expand_options(@new_resource.options)} install #{package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + package_name = "#{name}" + run_command_with_systems_locale( + :command => "apt-get -q -y#{expand_options(@new_resource.options)} remove #{package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def purge_package(name, version) + run_command_with_systems_locale( + :command => "apt-get -q -y#{expand_options(@new_resource.options)} purge #{@new_resource.package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def preseed_package(preseed_file) + Chef::Log.info("#{@new_resource} pre-seeding package installation instructions") + run_command_with_systems_locale( + :command => "debconf-set-selections #{preseed_file}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def reconfig_package(name, version) + Chef::Log.info("#{@new_resource} reconfiguring") + run_command_with_systems_locale( + :command => "dpkg-reconfigure #{name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + end + end + end +end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb new file mode 100644 index 0000000000..795a7b308b --- /dev/null +++ b/lib/chef/provider/package/dpkg.rb @@ -0,0 +1,128 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +class Chef + class Provider + class Package + class Dpkg < Chef::Provider::Package::Apt + DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~-]+)/ + DPKG_INSTALLED = /^Status: install ok installed/ + DPKG_VERSION = /^Version: (.+)$/ + + include Chef::Mixin::GetSourceFromPackage + def define_resource_requirements + super + requirements.assert(:install) do |a| + a.assertion{ not @new_resource.source.nil? } + a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install" + end + + # TODO this was originally written for any action in which .source is provided + # but would it make more sense to only look at source if the action is :install? + requirements.assert(:all_actions) do |a| + a.assertion { @source_exists } + a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" + a.whyrun "Assuming it would have been previously downloaded." + end + end + + def load_current_resource + @source_exists = true + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @new_resource.version(nil) + + if @new_resource.source + @source_exists = ::File.exists?(@new_resource.source) + if @source_exists + # Get information from the package if supplied + Chef::Log.debug("#{@new_resource} checking dpkg status") + status = popen4("dpkg-deb -W #{@new_resource.source}") do |pid, stdin, stdout, stderr| + stdout.each_line do |line| + if pkginfo = DPKG_INFO.match(line) + @current_resource.package_name(pkginfo[1]) + @new_resource.version(pkginfo[2]) + end + end + end + else + # Source provided but not valid means we can't safely do further processing + return + end + + end + + # Check to see if it is installed + package_installed = nil + Chef::Log.debug("#{@new_resource} checking install state") + status = popen4("dpkg -s #{@current_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each_line do |line| + case line + when DPKG_INSTALLED + package_installed = true + when DPKG_VERSION + if package_installed + Chef::Log.debug("#{@new_resource} current version is #{$1}") + @current_resource.version($1) + end + end + end + end + + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exceptions::Package, "dpkg failed - #{status.inspect}!" + end + + @current_resource + end + + def install_package(name, version) + run_command_with_systems_locale( + :command => "dpkg -i#{expand_options(@new_resource.options)} #{@new_resource.source}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def remove_package(name, version) + run_command_with_systems_locale( + :command => "dpkg -r#{expand_options(@new_resource.options)} #{@new_resource.package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + + def purge_package(name, version) + run_command_with_systems_locale( + :command => "dpkg -P#{expand_options(@new_resource.options)} #{@new_resource.package_name}", + :environment => { + "DEBIAN_FRONTEND" => "noninteractive" + } + ) + end + end + end + end +end diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb new file mode 100644 index 0000000000..6c9dacc55d --- /dev/null +++ b/lib/chef/provider/package/easy_install.rb @@ -0,0 +1,136 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/mixin/shell_out' +require 'chef/resource/package' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Package + class EasyInstall < Chef::Provider::Package + + include Chef::Mixin::ShellOut + + def install_check(name) + check = false + + begin + # first check to see if we can import it + output = shell_out!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr + if output.include? "ImportError" + # then check to see if its on the path + output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + if output.downcase.include? "#{name.downcase}" + check = true + end + else + check = true + end + rescue + # it's probably not installed + end + + check + end + + def easy_install_binary_path + path = @new_resource.easy_install_binary + path ? path : 'easy_install' + end + + def python_binary_path + path = @new_resource.python_binary + path ? path : 'python' + end + + def module_name + m = @new_resource.module_name + m ? m : @new_resource.name + end + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @current_resource.version(nil) + + # get the currently installed version if installed + package_version = nil + if install_check(module_name) + begin + output = shell_out!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout + package_version = output.strip + rescue + output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + + output_array = output.gsub(/[\[\]]/,'').split(/\s*,\s*/) + package_path = "" + + output_array.each do |entry| + if entry.downcase.include?(@new_resource.package_name) + package_path = entry + end + end + + package_path[/\S\S(.*)\/(.*)-(.*)-py(.*).egg\S/] + package_version = $3 + end + end + + if package_version == @new_resource.version + Chef::Log.debug("#{@new_resource} at version #{@new_resource.version}") + @current_resource.version(@new_resource.version) + else + Chef::Log.debug("#{@new_resource} at version #{package_version}") + @current_resource.version(package_version) + end + + @current_resource + end + + def candidate_version + return @candidate_version if @candidate_version + + # do a dry run to get the latest version + result = shell_out!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) + @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3] + @candidate_version + end + + def install_package(name, version) + run_command(:command => "#{easy_install_binary_path}#{expand_options(@new_resource.options)} \"#{name}==#{version}\"") + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + run_command(:command => "#{easy_install_binary_path }#{expand_options(@new_resource.options)} -m #{name}") + end + + def purge_package(name, version) + remove_package(name, version) + end + + end + end + end +end diff --git a/lib/chef/provider/package/freebsd.rb b/lib/chef/provider/package/freebsd.rb new file mode 100644 index 0000000000..afdd0d812e --- /dev/null +++ b/lib/chef/provider/package/freebsd.rb @@ -0,0 +1,149 @@ +# +# Authors:: Bryan McLellan (btm@loftninjas.org) +# Matthew Landauer (matthew@openaustralia.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan, Matthew Landauer +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/shell_out' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +class Chef + class Provider + class Package + class Freebsd < Chef::Provider::Package + include Chef::Mixin::ShellOut + + include Chef::Mixin::GetSourceFromPackage + + def initialize(*args) + super + @current_resource = Chef::Resource::Package.new(@new_resource.name) + end + + def current_installed_version + pkg_info = shell_out!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) + pkg_info.stdout[/^#{package_name}-(.+)/, 1] + end + + def port_path + case @new_resource.package_name + # When the package name starts with a '/' treat it as the full path to the ports directory + when /^\// + @new_resource.package_name + # Otherwise if the package name contains a '/' not at the start (like 'www/wordpress') treat as a relative + # path from /usr/ports + when /\// + "/usr/ports/#{@new_resource.package_name}" + # Otherwise look up the path to the ports directory using 'whereis' + else + whereis = shell_out!("whereis -s #{@new_resource.package_name}", :env => nil) + unless path = whereis.stdout[/^#{@new_resource.package_name}:\s+(.+)$/, 1] + raise Chef::Exceptions::Package, "Could not find port with the name #{@new_resource.package_name}" + end + path + end + end + + def ports_makefile_variable_value(variable) + make_v = shell_out!("make -V #{variable}", :cwd => port_path, :env => nil, :returns => [0,1]) + make_v.stdout.strip.split($\).first # $\ is the line separator, i.e., newline + end + + def ports_candidate_version + ports_makefile_variable_value("PORTVERSION") + end + + def file_candidate_version_path + Dir["#{@new_resource.source}/#{@current_resource.package_name}*"][-1].to_s + end + + def file_candidate_version + file_candidate_version_path.split(/-/).last.split(/.tbz/).first + end + + def load_current_resource + @current_resource.package_name(@new_resource.package_name) + + @current_resource.version(current_installed_version) + Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version + + case @new_resource.source + when /^http/, /^ftp/ + @candidate_version = "0.0.0" + when /^\// + @candidate_version = file_candidate_version + else + @candidate_version = ports_candidate_version + end + + Chef::Log.debug("#{@new_resource} ports candidate version is #{@candidate_version}") if @candidate_version + + @current_resource + end + + def latest_link_name + ports_makefile_variable_value("LATEST_LINK") + end + + # The name of the package (without the version number) as understood by pkg_add and pkg_info + def package_name + if ::File.exist?("/usr/ports/Makefile") + if ports_makefile_variable_value("PKGNAME") =~ /^(.+)-[^-]+$/ + $1 + else + raise Chef::Exceptions::Package, "Unexpected form for PKGNAME variable in #{port_path}/Makefile" + end + else + @new_resource.package_name + end + end + + def install_package(name, version) + unless @current_resource.version + case @new_resource.source + when /^ports$/ + shell_out!("make -DBATCH install", :timeout => 1200, :env => nil, :cwd => port_path).status + when /^http/, /^ftp/ + if @new_resource.source =~ /\/$/ + shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status + else + shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status + end + Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") + when /^\// + shell_out!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status + Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") + else + shell_out!("pkg_add -r #{latest_link_name}", :env => nil).status + end + end + end + + def remove_package(name, version) + # a version is mandatory + if version + shell_out!("pkg_delete #{package_name}-#{version}", :env => nil).status + else + shell_out!("pkg_delete #{package_name}-#{@current_resource.version}", :env => nil).status + end + end + + end + end + end +end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb new file mode 100644 index 0000000000..5beb46a20a --- /dev/null +++ b/lib/chef/provider/package/ips.rb @@ -0,0 +1,101 @@ +# +# Author:: Jason J. W. Williams (<williamsjj@digitar.com>) +# Author:: Stephen Nelson-Smith (<sns@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'open3' +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Package + class Ips < Chef::Provider::Package + + include Chef::Mixin::ShellOut + attr_accessor :virtual + + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { ! @candidate_version.nil? } + a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.package_name} not found" + a.whyrun "Assuming package #{@new_resource.package_name} would have been made available." + 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 + 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 + end + + return installed + 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 + end + + def upgrade_package(name, version) + install_package(name, version) + end + + 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}" + ) + end + end + end + end +end + diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb new file mode 100644 index 0000000000..fd33788944 --- /dev/null +++ b/lib/chef/provider/package/macports.rb @@ -0,0 +1,105 @@ +class Chef + class Provider + class Package + class Macports < Chef::Provider::Package + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + + @current_resource.version(current_installed_version) + Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version + + @candidate_version = macports_candidate_version + + if !@new_resource.version and !@candidate_version + raise Chef::Exceptions::Package, "Could not get a candidate version for this package -- #{@new_resource.name} does not seem to be a valid package!" + end + + Chef::Log.debug("#{@new_resource} candidate version is #{@candidate_version}") if @candidate_version + + @current_resource + end + + def current_installed_version + command = "port installed #{@new_resource.package_name}" + output = get_response_from_command(command) + + response = nil + output.each_line do |line| + match = line.match(/^.+ @([^\s]+) \(active\)$/) + response = match[1] if match + end + response + end + + def macports_candidate_version + command = "port info --version #{@new_resource.package_name}" + output = get_response_from_command(command) + + match = output.match(/^version: (.+)$/) + + match ? match[1] : nil + end + + def install_package(name, version) + 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 + ) + 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 + ) + 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 + ) + end + + def upgrade_package(name, version) + # Saving this to a variable -- weird rSpec behavior + # happens otherwise... + current_version = @current_resource.version + + if current_version.nil? or current_version.empty? + # Macports doesn't like when you upgrade a package + # 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}" + ) + end + end + + private + def get_response_from_command(command) + output = nil + status = popen4(command) do |pid, stdin, stdout, stderr| + begin + output = stdout.read + rescue Exception + raise Chef::Exceptions::Package, "Could not read from STDOUT on command: #{command}" + end + end + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exceptions::Package, "#{command} failed - #{status.insect}!" + end + output + end + end + end + end +end diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb new file mode 100644 index 0000000000..f81486ae84 --- /dev/null +++ b/lib/chef/provider/package/pacman.rb @@ -0,0 +1,111 @@ +# +# Author:: Jan Zimmek (<jan.zimmek@web.de>) +# Copyright:: Copyright (c) 2010 Jan Zimmek +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' + +class Chef + class Provider + class Package + class Pacman < Chef::Provider::Package + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + + @current_resource.version(nil) + + Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}") + status = popen4("pacman -Qi #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + line.force_encoding(Encoding::UTF_8) if line.respond_to?(:force_encoding) + case line + when /^Version(\s?)*: (.+)$/ + Chef::Log.debug("#{@new_resource} current version is #{$2}") + @current_resource.version($2) + end + end + end + + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exceptions::Package, "pacman failed - #{status.inspect}!" + end + + @current_resource + end + + def candidate_version + return @candidate_version if @candidate_version + + repos = ["extra","core","community"] + + if(::File.exists?("/etc/pacman.conf")) + pacman = ::File.read("/etc/pacman.conf") + repos = pacman.scan(/\[(.+)\]/).flatten + end + + package_repos = repos.map {|r| Regexp.escape(r) }.join('|') + + status = popen4("pacman -Ss #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /^(#{package_repos})\/#{Regexp.escape(@new_resource.package_name)} (.+)$/ + # $2 contains a string like "4.4.0-1 (kde kdenetwork)" or "3.10-4 (base)" + # simply split by space and use first token + @candidate_version = $2.split(" ").first + end + end + end + + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exceptions::Package, "pacman failed - #{status.inspect}!" + end + + unless @candidate_version + raise Chef::Exceptions::Package, "pacman does not have a version of package #{@new_resource.package_name}" + end + + @candidate_version + + end + + def install_package(name, version) + run_command_with_systems_locale( + :command => "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" + ) + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + run_command_with_systems_locale( + :command => "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" + ) + end + + def purge_package(name, version) + remove_package(name, version) + end + + end + end + end +end diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb new file mode 100644 index 0000000000..eb13e9855a --- /dev/null +++ b/lib/chef/provider/package/portage.rb @@ -0,0 +1,138 @@ +# +# Author:: Ezra Zygmuntowicz (<ezra@engineyard.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' + +class Chef + class Provider + class Package + class Portage < Chef::Provider::Package + PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + + @current_resource.version(nil) + + category, pkg = %r{^#{PACKAGE_NAME_PATTERN}$}.match(@new_resource.package_name)[1,2] + + possibilities = Dir["/var/db/pkg/#{category || "*"}/#{pkg}-*"].map {|d| d.sub(%r{/var/db/pkg/}, "") } + versions = possibilities.map do |entry| + if(entry =~ %r{[^/]+/#{Regexp.escape(pkg)}\-(\d[\.\d]*((_(alpha|beta|pre|rc|p)\d*)*)?(-r\d+)?)}) + [$&, $1] + end + end.compact + + if versions.size > 1 + atoms = versions.map {|v| v.first }.sort + categories = atoms.map {|v| v.split('/')[0] }.uniq + if !category && categories.size > 1 + raise Chef::Exceptions::Package, "Multiple packages found for #{@new_resource.package_name}: #{atoms.join(" ")}. Specify a category." + end + elsif versions.size == 1 + @current_resource.version(versions.first.last) + Chef::Log.debug("#{@new_resource} current version #{$1}") + end + + @current_resource + end + + + def parse_emerge(package, txt) + availables = {} + package_without_category = package.split("/").last + found_package_name = nil + + txt.each_line do |line| + if line =~ /\*\s+#{PACKAGE_NAME_PATTERN}/ + found_package_name = $&.strip + if found_package_name == package || found_package_name.split("/").last == package_without_category + availables[found_package_name] = nil + end + end + + if line =~ /Latest version available: (.*)/ && availables.has_key?(found_package_name) + availables[found_package_name] = $1.strip + end + end + + if availables.size > 1 + # shouldn't happen if a category is specified so just use `package` + raise Chef::Exceptions::Package, "Multiple emerge results found for #{package}: #{availables.keys.join(" ")}. Specify a category." + end + + availables.values.first + end + + def candidate_version + return @candidate_version if @candidate_version + + status = popen4("emerge --color n --nospinner --search #{@new_resource.package_name.split('/').last}") do |pid, stdin, stdout, stderr| + available, installed = parse_emerge(@new_resource.package_name, stdout.read) + @candidate_version = available + end + + unless status.exitstatus == 0 + raise Chef::Exceptions::Package, "emerge --search failed - #{status.inspect}!" + end + + @candidate_version + + end + + + def install_package(name, version) + pkg = "=#{name}-#{version}" + + if(version =~ /^\~(.+)/) + # If we start with a tilde + pkg = "~#{name}-#{$1}" + end + + run_command_with_systems_locale( + :command => "emerge -g --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" + ) + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + if(version) + pkg = "=#{@new_resource.package_name}-#{version}" + else + pkg = "#{@new_resource.package_name}" + end + + run_command_with_systems_locale( + :command => "emerge --unmerge --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" + ) + end + + def purge_package(name, version) + remove_package(name, version) + end + + end + end + end +end diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb new file mode 100644 index 0000000000..033ce8efb9 --- /dev/null +++ b/lib/chef/provider/package/rpm.rb @@ -0,0 +1,121 @@ +# +# Author:: Joshua Timberman (<joshua@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +class Chef + class Provider + class Package + class Rpm < Chef::Provider::Package + + include Chef::Mixin::GetSourceFromPackage + + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { @package_source_exists } + a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" + a.whyrun "Assuming package #{@new_resource.name} would have been made available." + end + requirements.assert(:all_actions) do |a| + a.assertion { !@rpm_status.nil? && (@rpm_status.exitstatus == 0 || @rpm_status.exitstatus == 1) } + a.failure_message Chef::Exceptions::Package, "Unable to determine current version due to RPM failure. Detail: #{@rpm_status.inspect}" + a.whyrun "Assuming current version would have been determined for package#{@new_resource.name}." + end + end + + def load_current_resource + @package_source_provided = true + @package_source_exists = true + + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @new_resource.version(nil) + + if @new_resource.source + unless ::File.exists?(@new_resource.source) + @package_source_exists = false + return + end + + Chef::Log.debug("#{@new_resource} checking rpm status") + status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /([\w\d_.-]+)\s([\w\d_.-]+)/ + @current_resource.package_name($1) + @new_resource.version($2) + end + end + end + else + if Array(@new_resource.action).include?(:install) + @package_source_exists = false + return + end + end + + Chef::Log.debug("#{@new_resource} checking install state") + @rpm_status = popen4("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /([\w\d_.-]+)\s([\w\d_.-]+)/ + Chef::Log.debug("#{@new_resource} current version is #{$2}") + @current_resource.version($2) + end + end + end + + + @current_resource + end + + def install_package(name, version) + unless @current_resource.version + run_command_with_systems_locale( + :command => "rpm #{@new_resource.options} -i #{@new_resource.source}" + ) + else + run_command_with_systems_locale( + :command => "rpm #{@new_resource.options} -U #{@new_resource.source}" + ) + end + end + + alias_method :upgrade_package, :install_package + + def remove_package(name, version) + if version + run_command_with_systems_locale( + :command => "rpm #{@new_resource.options} -e #{name}-#{version}" + ) + else + run_command_with_systems_locale( + :command => "rpm #{@new_resource.options} -e #{name}" + ) + end + end + + end + end + end +end + diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb new file mode 100644 index 0000000000..e60d73ab62 --- /dev/null +++ b/lib/chef/provider/package/rubygems.rb @@ -0,0 +1,548 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +# Class methods on Gem are defined in rubygems +require 'rubygems' +# Ruby 1.9's gem_prelude can interact poorly with loading the full rubygems +# explicitly like this. Make sure rubygems/specification is always last in this +# list +require 'rubygems/version' +require 'rubygems/dependency' +require 'rubygems/spec_fetcher' +require 'rubygems/platform' +require 'rubygems/format' +require 'rubygems/dependency_installer' +require 'rubygems/uninstaller' +require 'rubygems/specification' + +class Chef + class Provider + class Package + class Rubygems < Chef::Provider::Package + class GemEnvironment + # HACK: trigger gem config load early. Otherwise it can get lazy + # loaded during operations where we've set Gem.sources to an + # alternate value and overwrite it with the defaults. + Gem.configuration + + DEFAULT_UNINSTALLER_OPTS = {:ignore => true, :executables => true} + + ## + # The paths where rubygems should search for installed gems. + # Implemented by subclasses. + def gem_paths + raise NotImplementedError + end + + ## + # A rubygems source index containing the list of gemspecs for all + # available gems in the gem installation. + # Implemented by subclasses + # === Returns + # Gem::SourceIndex + def gem_source_index + raise NotImplementedError + end + + ## + # A rubygems specification object containing the list of gemspecs for all + # available gems in the gem installation. + # Implemented by subclasses + # For rubygems >= 1.8.0 + # === Returns + # Gem::Specification + def gem_specification + raise NotImplementedError + end + + ## + # Lists the installed versions of +gem_name+, constrained by the + # version spec in +gem_dep+ + # === Arguments + # Gem::Dependency +gem_dep+ is a Gem::Dependency object, its version + # specification constrains which gems are returned. + # === Returns + # [Gem::Specification] an array of Gem::Specification objects + def installed_versions(gem_dep) + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + gem_specification.find_all_by_name(gem_dep.name, gem_dep.requirement) + else + gem_source_index.search(gem_dep) + end + end + + ## + # Yields to the provided block with rubygems' source list set to the + # list provided. Always resets the list when the block returns or + # raises an exception. + def with_gem_sources(*sources) + sources.compact! + original_sources = Gem.sources + Gem.sources = sources unless sources.empty? + yield + ensure + Gem.sources = original_sources + end + + ## + # Determines the candidate version for a gem from a .gem file on disk + # and checks if it matches the version contraints in +gem_dependency+ + # === Returns + # Gem::Version a singular gem version object is returned if the gem + # is available + # nil returns nil if the gem on disk doesn't match the + # version constraints for +gem_dependency+ + def candidate_version_from_file(gem_dependency, source) + spec = Gem::Format.from_file_by_path(source).spec + if spec.satisfies_requirement?(gem_dependency) + logger.debug {"#{@new_resource} found candidate gem version #{spec.version} from local gem package #{source}"} + spec.version + else + # This is probably going to end badly... + logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency.to_s}" } + nil + end + end + + ## + # Finds the newest version that satisfies the constraints of + # +gem_dependency+. The version is determined from the cache or a + # round-trip to the server as needed. The architecture and gem + # sources will be set before making the query. + # === Returns + # Gem::Version a singular gem version object is returned if the gem + # is available + # nil returns nil if the gem could not be found + def candidate_version_from_remote(gem_dependency, *sources) + raise NotImplementedError + end + + ## + # Find the newest gem version available from Gem.sources that satisfies + # the constraints of +gem_dependency+ + def find_newest_remote_version(gem_dependency, *sources) + # DependencyInstaller sorts the results such that the last one is + # always the one it considers best. + spec_with_source = dependency_installer.find_gems_with_sources(gem_dependency).last + + spec = spec_with_source && spec_with_source[0] + version = spec && spec_with_source[0].version + if version + logger.debug { "#{@new_resource} found gem #{spec.name} version #{version} for platform #{spec.platform} from #{spec_with_source[1]}" } + version + else + source_list = sources.compact.empty? ? "[#{Gem.sources.join(', ')}]" : "[#{sources.join(', ')}]" + logger.warn { "#{@new_resource} failed to find gem #{gem_dependency} from #{source_list}" } + nil + end + end + + ## + # Installs a gem via the rubygems ruby API. + # === Options + # :sources rubygems servers to use + # Other options are passed to Gem::DependencyInstaller.new + def install(gem_dependency, options={}) + with_gem_sources(*options.delete(:sources)) do + with_correct_verbosity do + dependency_installer(options).install(gem_dependency) + end + end + end + + ## + # Uninstall the gem +gem_name+ via the rubygems ruby API. If + # +gem_version+ is provided, only that version will be uninstalled. + # Otherwise, all versions are uninstalled. + # === Options + # Options are passed to Gem::Uninstaller.new + def uninstall(gem_name, gem_version=nil, opts={}) + gem_version ? opts[:version] = gem_version : opts[:all] = true + with_correct_verbosity do + uninstaller(gem_name, opts).uninstall + end + end + + ## + # Set rubygems' user interaction to ConsoleUI or SilentUI depending + # on our current debug level + def with_correct_verbosity + Gem::DefaultUserInteraction.ui = Chef::Log.debug? ? Gem::ConsoleUI.new : Gem::SilentUI.new + yield + end + + def dependency_installer(opts={}) + Gem::DependencyInstaller.new(opts) + end + + def uninstaller(gem_name, opts={}) + Gem::Uninstaller.new(gem_name, DEFAULT_UNINSTALLER_OPTS.merge(opts)) + end + + private + + def logger + Chef::Log.logger + end + + end + + class CurrentGemEnvironment < GemEnvironment + + def gem_paths + Gem.path + end + + def gem_source_index + Gem.source_index + end + + def gem_specification + Gem::Specification + end + + def candidate_version_from_remote(gem_dependency, *sources) + with_gem_sources(*sources) do + find_newest_remote_version(gem_dependency, *sources) + end + end + + end + + class AlternateGemEnvironment < GemEnvironment + JRUBY_PLATFORM = /(:?universal|x86_64|x86)\-java\-[0-9\.]+/ + + def self.gempath_cache + @gempath_cache ||= {} + end + + def self.platform_cache + @platform_cache ||= {} + end + + include Chef::Mixin::ShellOut + + attr_reader :gem_binary_location + + def initialize(gem_binary_location) + @gem_binary_location = gem_binary_location + end + + def gem_paths + if self.class.gempath_cache.key?(@gem_binary_location) + self.class.gempath_cache[@gem_binary_location] + else + # shellout! is a fork/exec which won't work on windows + shell_style_paths = shell_out!("#{@gem_binary_location} env gempath").stdout + # on windows, the path separator is (usually? always?) semicolon + paths = shell_style_paths.split(::File::PATH_SEPARATOR).map { |path| path.strip } + self.class.gempath_cache[@gem_binary_location] = paths + end + end + + def gem_source_index + @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + '/specifications' }) + end + + def gem_specification + # Only once, dirs calls a reset + unless @specification + Gem::Specification.dirs = gem_paths + @specification = Gem::Specification + end + @specification + end + + ## + # Attempt to detect the correct platform settings for the target gem + # environment. + # + # In practice, this only makes a difference if different versions are + # available depending on platform, and only if the target gem + # environment has a radically different platform (i.e., jruby), so we + # just try to detect jruby and fall back to the current platforms + # (Gem.platforms) if we don't detect it. + # + # === Returns + # [String|Gem::Platform] returns an array of Gem::Platform-compatible + # objects, i.e., Strings that are valid for Gem::Platform or actual + # Gem::Platform objects. + def gem_platforms + if self.class.platform_cache.key?(@gem_binary_location) + self.class.platform_cache[@gem_binary_location] + else + gem_environment = shell_out!("#{@gem_binary_location} env").stdout + if jruby = gem_environment[JRUBY_PLATFORM] + self.class.platform_cache[@gem_binary_location] = ['ruby', Gem::Platform.new(jruby)] + else + self.class.platform_cache[@gem_binary_location] = Gem.platforms + end + end + end + + def with_gem_platforms(*alt_gem_platforms) + alt_gem_platforms.flatten! + original_gem_platforms = Gem.platforms + Gem.platforms = alt_gem_platforms + yield + ensure + Gem.platforms = original_gem_platforms + end + + def candidate_version_from_remote(gem_dependency, *sources) + with_gem_sources(*sources) do + with_gem_platforms(*gem_platforms) do + find_newest_remote_version(gem_dependency, *sources) + end + end + end + + end + + include Chef::Mixin::ShellOut + + attr_reader :gem_env + attr_reader :cleanup_gem_env + + def logger + Chef::Log.logger + end + + include Chef::Mixin::GetSourceFromPackage + + def initialize(new_resource, run_context=nil) + super + @cleanup_gem_env = true + if new_resource.gem_binary + if new_resource.options && new_resource.options.kind_of?(Hash) + msg = "options cannot be given as a hash when using an explicit gem_binary\n" + msg << "in #{new_resource} from #{new_resource.source_line}" + raise ArgumentError, msg + end + @gem_env = AlternateGemEnvironment.new(new_resource.gem_binary) + Chef::Log.debug("#{@new_resource} using gem '#{new_resource.gem_binary}'") + elsif is_omnibus? && (!@new_resource.instance_of? Chef::Resource::ChefGem) + # Opscode Omnibus - The ruby that ships inside omnibus is only used for Chef + # Default to installing somewhere more functional + if new_resource.options && new_resource.options.kind_of?(Hash) + msg = "options should be a string instead of a hash\n" + msg << "in #{new_resource} from #{new_resource.source_line}" + raise ArgumentError, msg + end + gem_location = find_gem_by_path + @new_resource.gem_binary gem_location + @gem_env = AlternateGemEnvironment.new(gem_location) + Chef::Log.debug("#{@new_resource} using gem '#{gem_location}'") + else + @gem_env = CurrentGemEnvironment.new + @cleanup_gem_env = false + Chef::Log.debug("#{@new_resource} using gem from running ruby environment") + end + end + + def is_omnibus? + if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin! + Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + # Omnibus installs to a static path because of linking on unix, find it. + true + elsif RbConfig::CONFIG['bindir'].sub(/^[\w]:/, '') == "/opscode/chef/embedded/bin" + Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + # windows, with the drive letter removed + true + else + false + end + end + + def find_gem_by_path + Chef::Log.debug("#{@new_resource} searching for 'gem' binary in path: #{ENV['PATH']}") + separator = ::File::ALT_SEPARATOR ? ::File::ALT_SEPARATOR : ::File::SEPARATOR + path_to_first_gem = ENV['PATH'].split(::File::PATH_SEPARATOR).select { |path| ::File.exists?(path + separator + "gem") }.first + raise Chef::Exceptions::FileNotFound, "Unable to find 'gem' binary in path: #{ENV['PATH']}" if path_to_first_gem.nil? + path_to_first_gem + separator + "gem" + end + + def gem_dependency + Gem::Dependency.new(@new_resource.package_name, @new_resource.version) + end + + def source_is_remote? + return true if @new_resource.source.nil? + scheme = URI.parse(@new_resource.source).scheme + # URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if scheme =~ /^[a-z]$/ + %w{http https}.include?(scheme) + end + + def current_version + #raise 'todo' + # If one or more matching versions are installed, the newest of them + # is the current version + if !matching_installed_versions.empty? + gemspec = matching_installed_versions.last + logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}"} + gemspec + # If no version matching the requirements exists, the latest installed + # version is the current version. + elsif !all_installed_versions.empty? + gemspec = all_installed_versions.last + logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } + gemspec + else + logger.debug { "#{@new_resource} no installed version found for #{gem_dependency.to_s}"} + nil + end + end + + def matching_installed_versions + @matching_installed_versions ||= @gem_env.installed_versions(gem_dependency) + end + + def all_installed_versions + @all_installed_versions ||= begin + @gem_env.installed_versions(Gem::Dependency.new(gem_dependency.name, '>= 0')) + end + end + + def gem_sources + @new_resource.source ? Array(@new_resource.source) : nil + end + + def load_current_resource + @current_resource = Chef::Resource::Package::GemPackage.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + if current_spec = current_version + @current_resource.version(current_spec.version.to_s) + end + @current_resource + end + + def cleanup_after_converge + if @cleanup_gem_env + logger.debug { "#{@new_resource} resetting gem environment to default" } + Gem.clear_paths + end + end + + def candidate_version + @candidate_version ||= begin + if target_version_already_installed? + nil + elsif source_is_remote? + @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s + else + @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s + end + end + end + + def target_version_already_installed? + return false unless @current_resource && @current_resource.version + return false if @current_resource.version.nil? + + Gem::Requirement.new(@new_resource.version).satisfied_by?(Gem::Version.new(@current_resource.version)) + end + + ## + # Installs the gem, using either the gems API or shelling out to `gem` + # according to the following criteria: + # 1. Use gems API (Gem::DependencyInstaller) by default + # 2. shell out to `gem install` when a String of options is given + # 3. use gems API with options if a hash of options is given + def install_package(name, version) + if source_is_remote? && @new_resource.gem_binary.nil? + if @new_resource.options.nil? + @gem_env.install(gem_dependency, :sources => gem_sources) + elsif @new_resource.options.kind_of?(Hash) + options = @new_resource.options + options[:sources] = gem_sources + @gem_env.install(gem_dependency, options) + else + install_via_gem_command(name, version) + end + elsif @new_resource.gem_binary.nil? + @gem_env.install(@new_resource.source) + else + install_via_gem_command(name,version) + end + true + end + + def gem_binary_path + @new_resource.gem_binary || 'gem' + end + + def install_via_gem_command(name, version) + if @new_resource.source =~ /\.gem$/i + name = @new_resource.source + else + src = @new_resource.source && " --source=#{@new_resource.source} --source=http://rubygems.org" + end + if version + shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) + else + shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) + end + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + if @new_resource.gem_binary.nil? + if @new_resource.options.nil? + @gem_env.uninstall(name, version) + elsif @new_resource.options.kind_of?(Hash) + @gem_env.uninstall(name, version, @new_resource.options) + else + uninstall_via_gem_command(name, version) + end + else + uninstall_via_gem_command(name, version) + end + end + + def uninstall_via_gem_command(name, version) + if version + shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) + else + shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) + end + end + + def purge_package(name, version) + remove_package(name, version) + end + + private + + def opts + expand_options(@new_resource.options) + end + + end + end + end +end diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb new file mode 100644 index 0000000000..a3ef1e5e86 --- /dev/null +++ b/lib/chef/provider/package/smartos.rb @@ -0,0 +1,84 @@ +# +# Authors:: Trevor O (trevoro@joyent.com) +# Bryan McLellan (btm@loftninjas.org) +# Matthew Landauer (matthew@openaustralia.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan, Matthew Landauer +# 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. +# +# Notes +# +# * Supports installing using a local package name +# * Otherwise reverts to installing from the pkgsrc repositories URL + +require 'chef/provider/package' +require 'chef/mixin/shell_out' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +class Chef + class Provider + class Package + class SmartOS < Chef::Provider::Package + include Chef::Mixin::ShellOut + attr_accessor :is_virtual_package + + + def load_current_resource + Chef::Log.debug("#{@new_resource} loading current resource") + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @current_resource.version(nil) + check_package_state(@new_resource.package_name) + @current_resource # modified by check_package_state + end + + def check_package_state(name) + Chef::Log.debug("#{@new_resource} checking package #{name}") + # XXX + version = nil + info = shell_out!("pkg_info -E \"#{name}*\"", :env => nil, :returns => [0,1]) + + if info.stdout + version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1] + end + + if !version + @current_resource.version(nil) + else + @current_resource.version(version) + end + end + + def install_package(name, version) + Chef::Log.debug("#{@new_resource} installing package #{name}-#{version}") + package = "#{name}-#{version}" + out = shell_out!("pkgin -y install #{package}", :env => nil) + end + + def upgrade_package(name, version) + Chef::Log.debug("#{@new_resource} upgrading package #{name}-#{version}") + install_package(name, version) + end + + def remove_package(name, version) + Chef::Log.debug("#{@new_resource} removing package #{name}-#{version}") + package = "#{name}-#{version}" + out = shell_out!("pkgin -y remove #{package}", :env => nil) + end + + end + end + end +end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb new file mode 100644 index 0000000000..f502a0dc96 --- /dev/null +++ b/lib/chef/provider/package/solaris.rb @@ -0,0 +1,139 @@ +# +# Author:: Toomas Pelberg (<toomasp@gmx.net>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +class Chef + class Provider + class Package + class Solaris < Chef::Provider::Package + + include Chef::Mixin::GetSourceFromPackage + + # def initialize(*args) + # super + # @current_resource = Chef::Resource::Package.new(@new_resource.name) + # end + def define_resource_requirements + super + requirements.assert(:install) do |a| + a.assertion { @new_resource.source } + a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install" + end + requirements.assert(:all_actions) do |a| + a.assertion { !@new_resource.source || @package_source_found } + a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" + a.whyrun "would assume #{@new_resource.source} would be have previously been made available" + end + end + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @new_resource.version(nil) + + if @new_resource.source + @package_source_found = ::File.exists?(@new_resource.source) + if @package_source_found + Chef::Log.debug("#{@new_resource} checking pkg status") + status = popen4("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /VERSION:\s+(.+)/ + @new_resource.version($1) + end + end + end + end + end + + Chef::Log.debug("#{@new_resource} checking install state") + status = popen4("pkginfo -l #{@current_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /VERSION:\s+(.+)/ + Chef::Log.debug("#{@new_resource} version #{$1} is already installed") + @current_resource.version($1) + end + end + end + + unless status.exitstatus == 0 || status.exitstatus == 1 + raise Chef::Exceptions::Package, "pkginfo failed - #{status.inspect}!" + end + + unless @current_resource.version.nil? + @current_resource.version(nil) + end + + @current_resource + end + + def candidate_version + return @candidate_version if @candidate_version + status = popen4("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each_line do |line| + case line + when /VERSION:\s+(.+)/ + @candidate_version = $1 + @new_resource.version($1) + Chef::Log.debug("#{@new_resource} setting install candidate version to #{@candidate_version}") + end + end + end + unless status.exitstatus == 0 + raise Chef::Exceptions::Package, "pkginfo -l -d #{@new_resource.source} - #{status.inspect}!" + end + @candidate_version + end + + 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 => "pkgadd -n -d #{@new_resource.source} all" + ) + Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") + else + run_command_with_systems_locale( + :command => "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" + ) + 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}" + ) + 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}" + ) + Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") + end + end + + end + end + end +end diff --git a/lib/chef/provider/package/yum-dump.py b/lib/chef/provider/package/yum-dump.py new file mode 100644 index 0000000000..99136eceec --- /dev/null +++ b/lib/chef/provider/package/yum-dump.py @@ -0,0 +1,287 @@ +# +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2009, 2011 Matthew Kent +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# yum-dump.py +# Inspired by yumhelper.py by David Lutterkort +# +# Produce a list of installed, available and re-installable packages using yum +# and dump the results to stdout. +# +# yum-dump invokes yum similarly to the command line interface which makes it +# subject to most of the configuration paramaters in yum.conf. yum-dump will +# also load yum plugins in the same manor as yum - these can affect the output. +# +# Can be run as non root, but that won't update the cache. +# +# Intended to support yum 2.x and 3.x + +import os +import sys +import time +import yum +import re +import errno + +from yum import Errors +from optparse import OptionParser +from distutils import version + +YUM_PID_FILE='/var/run/yum.pid' + +# Seconds to wait for exclusive access to yum +LOCK_TIMEOUT = 10 + +YUM_VER = version.StrictVersion(yum.__version__) +YUM_MAJOR = YUM_VER.version[0] + +if YUM_MAJOR > 3 or YUM_MAJOR < 2: + print >> sys.stderr, "yum-dump Error: Can't match supported yum version" \ + " (%s)" % yum.__version__ + sys.exit(1) + +# Required for Provides output +if YUM_MAJOR == 2: + import rpm + import rpmUtils.miscutils + +def setup(yb, options): + # Only want our output + # + if YUM_MAJOR == 3: + try: + if YUM_VER >= version.StrictVersion("3.2.22"): + yb.preconf.errorlevel=0 + yb.preconf.debuglevel=0 + + # initialize the config + yb.conf + else: + yb.doConfigSetup(errorlevel=0, debuglevel=0) + except yum.Errors.ConfigError, e: + # supresses an ignored exception at exit + yb.preconf = None + print >> sys.stderr, "yum-dump Config Error: %s" % e + return 1 + except ValueError, e: + yb.preconf = None + print >> sys.stderr, "yum-dump Options Error: %s" % e + return 1 + elif YUM_MAJOR == 2: + yb.doConfigSetup() + + def __log(a,b): pass + + yb.log = __log + yb.errorlog = __log + + # Give Chef every possible package version, it can decide what to do with them + if YUM_MAJOR == 3: + yb.conf.showdupesfromrepos = True + elif YUM_MAJOR == 2: + yb.conf.setConfigOption('showdupesfromrepos', True) + + # Optionally run only on cached repositories, but non root must use the cache + if os.geteuid() != 0: + if YUM_MAJOR == 3: + yb.conf.cache = True + elif YUM_MAJOR == 2: + yb.conf.setConfigOption('cache', True) + else: + if YUM_MAJOR == 3: + yb.conf.cache = options.cache + elif YUM_MAJOR == 2: + yb.conf.setConfigOption('cache', options.cache) + + return 0 + +def dump_packages(yb, list, output_provides): + packages = {} + + if YUM_MAJOR == 2: + yb.doTsSetup() + yb.doRepoSetup() + yb.doSackSetup() + + db = yb.doPackageLists(list) + + for pkg in db.installed: + pkg.type = 'i' + packages[str(pkg)] = pkg + + if YUM_VER >= version.StrictVersion("3.2.21"): + for pkg in db.available: + pkg.type = 'a' + packages[str(pkg)] = pkg + + # These are both installed and available + for pkg in db.reinstall_available: + pkg.type = 'r' + packages[str(pkg)] = pkg + else: + # Old style method - no reinstall list + for pkg in yb.pkgSack.returnPackages(): + + if str(pkg) in packages: + if packages[str(pkg)].type == "i": + packages[str(pkg)].type = 'r' + continue + + pkg.type = 'a' + packages[str(pkg)] = pkg + + unique_packages = packages.values() + + unique_packages.sort(lambda x, y: cmp(x.name, y.name)) + + for pkg in unique_packages: + if output_provides == "all" or \ + (output_provides == "installed" and (pkg.type == "i" or pkg.type == "r")): + + # yum 2 doesn't have provides_print, implement it ourselves using methods + # based on requires gathering in packages.py + if YUM_MAJOR == 2: + provlist = [] + + # Installed and available are gathered in different ways + if pkg.type == 'i' or pkg.type == 'r': + names = pkg.hdr[rpm.RPMTAG_PROVIDENAME] + flags = pkg.hdr[rpm.RPMTAG_PROVIDEFLAGS] + ver = pkg.hdr[rpm.RPMTAG_PROVIDEVERSION] + if names is not None: + tmplst = zip(names, flags, ver) + + for (n, f, v) in tmplst: + prov = rpmUtils.miscutils.formatRequire(n, v, f) + provlist.append(prov) + # This is slow :( + elif pkg.type == 'a': + for prcoTuple in pkg.returnPrco('provides'): + prcostr = pkg.prcoPrintable(prcoTuple) + provlist.append(prcostr) + + provides = provlist + else: + provides = pkg.provides_print + else: + provides = "[]" + + print '%s %s %s %s %s %s %s %s' % ( + pkg.name, + pkg.epoch, + pkg.version, + pkg.release, + pkg.arch, + provides, + pkg.type, + pkg.repoid ) + + return 0 + +def yum_dump(options): + lock_obtained = False + + yb = yum.YumBase() + + status = setup(yb, options) + if status != 0: + return status + + if options.output_options: + print "[option installonlypkgs] %s" % " ".join(yb.conf.installonlypkgs) + + # Non root can't handle locking on rhel/centos 4 + if os.geteuid() != 0: + return dump_packages(yb, options.package_list, options.output_provides) + + # Wrap the collection and output of packages in yum's global lock to prevent + # any inconsistencies. + try: + # Spin up to LOCK_TIMEOUT + countdown = LOCK_TIMEOUT + while True: + try: + yb.doLock(YUM_PID_FILE) + lock_obtained = True + except Errors.LockError, e: + time.sleep(1) + countdown -= 1 + if countdown == 0: + print >> sys.stderr, "yum-dump Locking Error! Couldn't obtain an " \ + "exclusive yum lock in %d seconds. Giving up." % LOCK_TIMEOUT + return 200 + else: + break + + return dump_packages(yb, options.package_list, options.output_provides) + + # Ensure we clear the lock and cleanup any resources + finally: + try: + yb.closeRpmDB() + if lock_obtained == True: + yb.doUnlock(YUM_PID_FILE) + except Errors.LockError, e: + print >> sys.stderr, "yum-dump Unlock Error: %s" % e + return 200 + +def main(): + usage = "Usage: %prog [options]\n" + \ + "Output a list of installed, available and re-installable packages via yum" + parser = OptionParser(usage=usage) + parser.add_option("-C", "--cache", + action="store_true", dest="cache", default=False, + help="run entirely from cache, don't update cache") + parser.add_option("-o", "--options", + action="store_true", dest="output_options", default=False, + help="output select yum options useful to Chef") + parser.add_option("-p", "--installed-provides", + action="store_const", const="installed", dest="output_provides", default="none", + help="output Provides for installed packages, big/wide output") + parser.add_option("-P", "--all-provides", + action="store_const", const="all", dest="output_provides", default="none", + help="output Provides for all package, slow, big/wide output") + parser.add_option("-i", "--installed", + action="store_const", const="installed", dest="package_list", default="all", + help="output only installed packages") + parser.add_option("-a", "--available", + action="store_const", const="available", dest="package_list", default="all", + help="output only available and re-installable packages") + + (options, args) = parser.parse_args() + + try: + return yum_dump(options) + + except yum.Errors.RepoError, e: + print >> sys.stderr, "yum-dump Repository Error: %s" % e + return 1 + + except yum.Errors.YumBaseError, e: + print >> sys.stderr, "yum-dump General Error: %s" % e + return 1 + +try: + status = main() +# Suppress a nasty broken pipe error when output is piped to utilities like 'head' +except IOError, e: + if e.errno == errno.EPIPE: + sys.exit(1) + else: + raise + +sys.exit(status) diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb new file mode 100644 index 0000000000..9048048b83 --- /dev/null +++ b/lib/chef/provider/package/yum.rb @@ -0,0 +1,1214 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'singleton' +require 'chef/mixin/get_source_from_package' + + +class Chef + class Provider + class Package + class Yum < Chef::Provider::Package + + class RPMUtils + class << self + + # RPM::Version version_parse equivalent + def version_parse(evr) + return if evr.nil? + + epoch = nil + # assume this is a version + version = evr + release = nil + + lead = 0 + tail = evr.size + + if evr =~ %r{^([\d]+):} + epoch = $1.to_i + lead = $1.length + 1 + elsif evr[0].ord == ":".ord + epoch = 0 + lead = 1 + end + + if evr =~ %r{:?.*-(.*)$} + release = $1 + tail = evr.length - release.length - lead - 1 + + if release.empty? + release = nil + end + end + + version = evr[lead,tail] + if version.empty? + version = nil + end + + [ epoch, version, release ] + end + + # verify + def isalnum(x) + isalpha(x) or isdigit(x) + end + + def isalpha(x) + v = x.ord + (v >= 65 and v <= 90) or (v >= 97 and v <= 122) + end + + def isdigit(x) + v = x.ord + v >= 48 and v <= 57 + end + + # based on the reference spec in lib/rpmvercmp.c in rpm 4.9.0 + def rpmvercmp(x, y) + # easy! :) + return 0 if x == y + + if x.nil? + x = "" + end + + if y.nil? + y = "" + end + + # not so easy :( + # + # takes 2 strings like + # + # x = "1.20.b18.el5" + # y = "1.20.b17.el5" + # + # breaks into purely alpha and numeric segments and compares them using + # some rules + # + # * 10 > 1 + # * 1 > a + # * z > a + # * Z > A + # * z > Z + # * leading zeros are ignored + # * separators (periods, commas) are ignored + # * "1.20.b18.el5.extrastuff" > "1.20.b18.el5" + + x_pos = 0 # overall string element reference position + x_pos_max = x.length - 1 # number of elements in string, starting from 0 + x_seg_pos = 0 # segment string element reference position + x_comp = nil # segment to compare + + y_pos = 0 + y_seg_pos = 0 + y_pos_max = y.length - 1 + y_comp = nil + + while (x_pos <= x_pos_max and y_pos <= y_pos_max) + # first we skip over anything non alphanumeric + while (x_pos <= x_pos_max) and (isalnum(x[x_pos]) == false) + x_pos += 1 # +1 over pos_max if end of string + end + while (y_pos <= y_pos_max) and (isalnum(y[y_pos]) == false) + y_pos += 1 + end + + # if we hit the end of either we are done matching segments + if (x_pos == x_pos_max + 1) or (y_pos == y_pos_max + 1) + break + end + + # we are now at the start of a alpha or numeric segment + x_seg_pos = x_pos + y_seg_pos = y_pos + + # grab segment so we can compare them + if isdigit(x[x_seg_pos].ord) + x_seg_is_num = true + + # already know it's a digit + x_seg_pos += 1 + + # gather up our digits + while (x_seg_pos <= x_pos_max) and isdigit(x[x_seg_pos]) + x_seg_pos += 1 + end + # copy the segment but not the unmatched character that x_seg_pos will + # refer to + x_comp = x[x_pos,x_seg_pos - x_pos] + + while (y_seg_pos <= y_pos_max) and isdigit(y[y_seg_pos]) + y_seg_pos += 1 + end + y_comp = y[y_pos,y_seg_pos - y_pos] + else + # we are comparing strings + x_seg_is_num = false + + while (x_seg_pos <= x_pos_max) and isalpha(x[x_seg_pos]) + x_seg_pos += 1 + end + x_comp = x[x_pos,x_seg_pos - x_pos] + + while (y_seg_pos <= y_pos_max) and isalpha(y[y_seg_pos]) + y_seg_pos += 1 + end + y_comp = y[y_pos,y_seg_pos - y_pos] + end + + # if y_seg_pos didn't advance in the above loop it means the segments are + # different types + if y_pos == y_seg_pos + # numbers always win over letters + return x_seg_is_num ? 1 : -1 + end + + # move the ball forward before we mess with the segments + x_pos += x_comp.length # +1 over pos_max if end of string + y_pos += y_comp.length + + # we are comparing numbers - simply convert them + if x_seg_is_num + x_comp = x_comp.to_i + y_comp = y_comp.to_i + end + + # compares ints or strings + # don't return if equal - try the next segment + if x_comp > y_comp + return 1 + elsif x_comp < y_comp + return -1 + end + + # if we've reached here than the segments are the same - try again + end + + # we must have reached the end of one or both of the strings and they + # matched up until this point + + # segments matched completely but the segment separators were different - + # rpm reference code treats these as equal. + if (x_pos == x_pos_max + 1) and (y_pos == y_pos_max + 1) + return 0 + end + + # the most unprocessed characters left wins + if (x_pos_max - x_pos) > (y_pos_max - y_pos) + return 1 + else + return -1 + end + end + + end # self + end # RPMUtils + + class RPMVersion + include Comparable + + def initialize(*args) + if args.size == 1 + @e, @v, @r = RPMUtils.version_parse(args[0]) + elsif args.size == 3 + @e = args[0].to_i + @v = args[1] + @r = args[2] + else + raise ArgumentError, "Expecting either 'epoch-version-release' or 'epoch, " + + "version, release'" + end + end + attr_reader :e, :v, :r + alias :epoch :e + alias :version :v + alias :release :r + + def self.parse(*args) + self.new(*args) + end + + def <=>(y) + compare_versions(y) + end + + def compare(y) + compare_versions(y, false) + end + + def partial_compare(y) + compare_versions(y, true) + end + + # RPM::Version rpm_version_to_s equivalent + def to_s + if @r.nil? + @v + else + "#{@v}-#{@r}" + end + end + + def evr + "#{@e}:#{@v}-#{@r}" + end + + private + + # Rough RPM::Version rpm_version_cmp equivalent - except much slower :) + # + # partial lets epoch and version segment equality be good enough to return equal, eg: + # + # 2:1.2-1 == 2:1.2 + # 2:1.2-1 == 2: + # + def compare_versions(y, partial=false) + x = self + + # compare epoch + if (x.e.nil? == false and x.e > 0) and y.e.nil? + return 1 + elsif x.e.nil? and (y.e.nil? == false and y.e > 0) + return -1 + elsif x.e.nil? == false and y.e.nil? == false + if x.e < y.e + return -1 + elsif x.e > y.e + return 1 + end + end + + # compare version + if partial and (x.v.nil? or y.v.nil?) + return 0 + elsif x.v.nil? == false and y.v.nil? + return 1 + elsif x.v.nil? and y.v.nil? == false + return -1 + elsif x.v.nil? == false and y.v.nil? == false + cmp = RPMUtils.rpmvercmp(x.v, y.v) + return cmp if cmp != 0 + end + + # compare release + if partial and (x.r.nil? or y.r.nil?) + return 0 + elsif x.r.nil? == false and y.r.nil? + return 1 + elsif x.r.nil? and y.r.nil? == false + return -1 + elsif x.r.nil? == false and y.r.nil? == false + cmp = RPMUtils.rpmvercmp(x.r, y.r) + return cmp + end + + return 0 + end + end + + class RPMPackage + include Comparable + + def initialize(*args) + if args.size == 4 + @n = args[0] + @version = RPMVersion.new(args[1]) + @a = args[2] + @provides = args[3] + elsif args.size == 6 + @n = args[0] + e = args[1].to_i + v = args[2] + r = args[3] + @version = RPMVersion.new(e,v,r) + @a = args[4] + @provides = args[5] + else + raise ArgumentError, "Expecting either 'name, epoch-version-release, arch, provides' " + + "or 'name, epoch, version, release, arch, provides'" + end + + # We always have one, ourselves! + if @provides.empty? + @provides = [ RPMProvide.new(@n, @version.evr, :==) ] + end + end + attr_reader :n, :a, :version, :provides + alias :name :n + alias :arch :a + + def <=>(y) + compare(y) + end + + def compare(y) + x = self + + # easy! :) + return 0 if x.nevra == y.nevra + + # compare name + if x.n.nil? == false and y.n.nil? + return 1 + elsif x.n.nil? and y.n.nil? == false + return -1 + elsif x.n.nil? == false and y.n.nil? == false + if x.n < y.n + return -1 + elsif x.n > y.n + return 1 + end + end + + # compare version + if x.version > y.version + return 1 + elsif x.version < y.version + return -1 + end + + # compare arch + if x.a.nil? == false and y.a.nil? + return 1 + elsif x.a.nil? and y.a.nil? == false + return -1 + elsif x.a.nil? == false and y.a.nil? == false + if x.a < y.a + return -1 + elsif x.a > y.a + return 1 + end + end + + return 0 + end + + def to_s + nevra + end + + def nevra + "#{@n}-#{@version.evr}.#{@a}" + end + end + + # Simple implementation from rpm and ruby-rpm reference code + class RPMDependency + def initialize(*args) + if args.size == 3 + @name = args[0] + @version = RPMVersion.new(args[1]) + # Our requirement to other dependencies + @flag = args[2] || :== + elsif args.size == 5 + @name = args[0] + e = args[1].to_i + v = args[2] + r = args[3] + @version = RPMVersion.new(e,v,r) + @flag = args[4] || :== + else + raise ArgumentError, "Expecting either 'name, epoch-version-release, flag' or " + + "'name, epoch, version, release, flag'" + end + end + attr_reader :name, :version, :flag + + # Parses 2 forms: + # + # "mtr >= 2:0.71-3.0" + # "mta" + def self.parse(string) + if string =~ %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$} + name = $1 + if $2 == "=" + flag = :== + else + flag = :"#{$2}" + end + version = $3 + + return self.new(name, version, flag) + else + name = string + return self.new(name, nil, nil) + end + end + + # Test if another RPMDependency satisfies our requirements + def satisfy?(y) + unless y.kind_of?(RPMDependency) + raise ArgumentError, "Expecting an RPMDependency object" + end + + x = self + + # Easy! + if x.name != y.name + return false + end + + # Partial compare + # + # eg: x.version 2.3 == y.version 2.3-1 + sense = x.version.partial_compare(y.version) + + # Thanks to rpmdsCompare() rpmds.c + if sense < 0 and (x.flag == :> || x.flag == :>=) || (y.flag == :<= || y.flag == :<) + return true + elsif sense > 0 and (x.flag == :< || x.flag == :<=) || (y.flag == :>= || y.flag == :>) + return true + elsif sense == 0 and ( + ((x.flag == :== or x.flag == :<= or x.flag == :>=) and (y.flag == :== or y.flag == :<= or y.flag == :>=)) or + (x.flag == :< and y.flag == :<) or + (x.flag == :> and y.flag == :>) + ) + return true + end + + return false + end + end + + class RPMProvide < RPMDependency; end + class RPMRequire < RPMDependency; end + + class RPMDbPackage < RPMPackage + # <rpm parts>, installed, available + def initialize(*args) + @repoid = args.pop + # state + @available = args.pop + @installed = args.pop + super(*args) + end + attr_reader :repoid, :available, :installed + end + + # Simple storage for RPMPackage objects - keeps them unique and sorted + class RPMDb + def initialize + # package name => [ RPMPackage, RPMPackage ] of different versions + @rpms = Hash.new + # package nevra => RPMPackage for lookups + @index = Hash.new + # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature + @provides = Hash.new + # RPMPackages listed as available + @available = Set.new + # RPMPackages listed as installed + @installed = Set.new + end + + def [](package_name) + self.lookup(package_name) + end + + # Lookup package_name and return a descending array of package objects + def lookup(package_name) + pkgs = @rpms[package_name] + if pkgs + return pkgs.sort.reverse + else + return nil + end + end + + def lookup_provides(provide_name) + @provides[provide_name] + end + + # Using the package name as a key, and nevra for an index, keep a unique list of packages. + # The available/installed state can be overwritten for existing packages. + def push(*args) + args.flatten.each do |new_rpm| + unless new_rpm.kind_of?(RPMDbPackage) + raise ArgumentError, "Expecting an RPMDbPackage object" + end + + @rpms[new_rpm.n] ||= Array.new + + # we may already have this one, like when the installed list is refreshed + idx = @index[new_rpm.nevra] + if idx + # grab the existing package if it's not + curr_rpm = idx + else + @rpms[new_rpm.n] << new_rpm + + new_rpm.provides.each do |provide| + @provides[provide.name] ||= Array.new + @provides[provide.name] << new_rpm + end + + curr_rpm = new_rpm + end + + # Track the nevra -> RPMPackage association to avoid having to compare versions + # with @rpms[new_rpm.n] on the next round + @index[new_rpm.nevra] = curr_rpm + + # these are overwritten for existing packages + if new_rpm.available + @available << curr_rpm + end + if new_rpm.installed + @installed << curr_rpm + end + end + end + + def <<(*args) + self.push(args) + end + + def clear + @rpms.clear + @index.clear + @provides.clear + clear_available + clear_installed + end + + def clear_available + @available.clear + end + + def clear_installed + @installed.clear + end + + def size + @rpms.size + end + alias :length :size + + def available_size + @available.size + end + + def installed_size + @installed.size + end + + def available?(package) + @available.include?(package) + end + + def installed?(package) + @installed.include?(package) + end + + def whatprovides(rpmdep) + unless rpmdep.kind_of?(RPMDependency) + raise ArgumentError, "Expecting an RPMDependency object" + end + + what = [] + + packages = lookup_provides(rpmdep.name) + if packages + packages.each do |pkg| + pkg.provides.each do |provide| + if provide.satisfy?(rpmdep) + what << pkg + end + end + end + end + + return what + end + end + + # Cache for our installed and available packages, pulled in from yum-dump.py + class YumCache + include Chef::Mixin::Command + include Singleton + + def initialize + @rpmdb = RPMDb.new + + # Next time @rpmdb is accessed: + # :all - Trigger a run of "yum-dump.py --options --installed-provides", updates + # yum's cache and parses options from /etc/yum.conf. Pulls in Provides + # dependency data for installed packages only - this data is slow to + # gather. + # :provides - Same as :all but pulls in Provides data for available packages as well. + # Used as a last resort when we can't find a Provides match. + # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm + # db. Used between client runs for a quick refresh. + # :none - Do nothing, a call to one of the reload methods is required. + @next_refresh = :all + + @allow_multi_install = [] + + # these are for subsequent runs if we are on an interval + Chef::Client.when_run_starts do + YumCache.instance.reload + end + end + + # Cache management + # + + def refresh + case @next_refresh + when :none + return nil + when :installed + reset_installed + # fast + opts=" --installed" + when :all + reset + # medium + opts=" --options --installed-provides" + when :provides + reset + # slow! + opts=" --options --all-provides" + else + raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}" + end + + one_line = false + error = nil + + helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py') + + status = popen4("/usr/bin/python #{helper}#{opts}", :waitlast => true) do |pid, stdin, stdout, stderr| + stdout.each do |line| + one_line = true + + line.chomp! + + if line =~ %r{\[option (.*)\] (.*)} + if $1 == "installonlypkgs" + @allow_multi_install = $2.split + else + raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py" + end + next + end + + if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r]) (\S+)$} + name = $1 + epoch = $2 + version = $3 + release = $4 + arch = $5 + provides = parse_provides($6) + type = $7 + repoid = $8 + else + Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " + + "Please check your yum configuration.") + next + end + + case type + when "i" + # if yum-dump was called with --installed this may not be true, but it's okay + # since we don't touch the @available Set in reload_installed + available = false + installed = true + when "a" + available = true + installed = false + when "r" + available = true + installed = true + end + + pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available, repoid) + @rpmdb << pkg + end + + error = stderr.readlines + end + + if status.exitstatus != 0 + raise Chef::Exceptions::Package, "Yum failed - #{status.inspect} - returns: #{error}" + else + unless one_line + Chef::Log.warn("Odd, no output from yum-dump.py. Please check " + + "your yum configuration.") + end + end + + # A reload method must be called before the cache is altered + @next_refresh = :none + end + + def reload + @next_refresh = :all + end + + def reload_installed + @next_refresh = :installed + end + + def reload_provides + @next_refresh = :provides + end + + def reset + @rpmdb.clear + end + + def reset_installed + @rpmdb.clear_installed + end + + # Querying the cache + # + + # Check for package by name or name+arch + def package_available?(package_name) + refresh + + if @rpmdb.lookup(package_name) + return true + else + if package_name =~ %r{^(.*)\.(.*)$} + pkg_name = $1 + pkg_arch = $2 + + if matches = @rpmdb.lookup(pkg_name) + matches.each do |m| + return true if m.arch == pkg_arch + end + end + end + end + + return false + end + + # Returns a array of packages satisfying an RPMDependency + def packages_from_require(rpmdep) + refresh + @rpmdb.whatprovides(rpmdep) + end + + # Check if a package-version.arch is available to install + def version_available?(package_name, desired_version, arch=nil) + version(package_name, arch, true, false) do |v| + return true if desired_version == v + end + + return false + end + + # Return the source repository for a package-version.arch + def package_repository(package_name, desired_version, arch=nil) + package(package_name, arch, true, false) do |pkg| + return pkg.repoid if desired_version == pkg.version.to_s + end + + return nil + end + + # Return the latest available version for a package.arch + def available_version(package_name, arch=nil) + version(package_name, arch, true, false) + end + alias :candidate_version :available_version + + # Return the currently installed version for a package.arch + def installed_version(package_name, arch=nil) + version(package_name, arch, false, true) + end + + # Return an array of packages allowed to be installed multiple times, such as the kernel + def allow_multi_install + refresh + @allow_multi_install + end + + private + + def version(package_name, arch=nil, is_available=false, is_installed=false) + package(package_name, arch, is_available, is_installed) do |pkg| + if block_given? + yield pkg.version.to_s + else + # first match is latest version + return pkg.version.to_s + end + end + + if block_given? + return self + else + return nil + end + end + + def package(package_name, arch=nil, is_available=false, is_installed=false) + refresh + packages = @rpmdb[package_name] + if packages + packages.each do |pkg| + if is_available + next unless @rpmdb.available?(pkg) + end + if is_installed + next unless @rpmdb.installed?(pkg) + end + if arch + next unless pkg.arch == arch + end + + if block_given? + yield pkg + else + # first match is latest version + return pkg + end + end + end + + if block_given? + return self + else + return nil + end + end + + # Parse provides from yum-dump.py output + def parse_provides(string) + ret = [] + # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0'] + string.split(", ").each do |seg| + # 'atk = 1.12.2-1.fc6' + if seg =~ %r{^'(.*)'$} + ret << RPMProvide.parse($1) + end + end + + return ret + end + + end # YumCache + + include Chef::Mixin::GetSourceFromPackage + + def initialize(new_resource, run_context) + super + + @yum = YumCache.instance + end + + # Extra attributes + # + + def arch + if @new_resource.respond_to?("arch") + @new_resource.arch + else + nil + end + end + + def flush_cache + if @new_resource.respond_to?("flush_cache") + @new_resource.flush_cache + else + { :before => false, :after => false } + end + end + + def allow_downgrade + if @new_resource.respond_to?("allow_downgrade") + @new_resource.allow_downgrade + else + false + end + end + + # Helpers + # + + def yum_arch + arch ? ".#{arch}" : nil + end + + def yum_command(command) + status, stdout, stderr = output_of_command(command, {}) + + # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't + # considered fatal - meaning the rpm is still successfully installed. These issue + # cause yum to emit a non fatal warning but still exit(1). As there's currently no + # way to suppress this behavior and an exit(1) will break a Chef run we make an + # effort to trap these and re-run the same install command - it will either fail a + # second time or succeed. + # + # A cleaner solution would have to be done in python and better hook into + # yum/rpm to handle exceptions as we see fit. + if status.exitstatus == 1 + stdout.each_line do |l| + # rpm-4.4.2.3 lib/psm.c line 2182 + if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$} + Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " + + "so running install again to verify.") + status, stdout, stderr = output_of_command(command, {}) + break + end + end + end + + if status.exitstatus > 0 + command_output = "STDOUT: #{stdout}" + command_output << "STDERR: #{stderr}" + handle_command_failures(status, command_output, {}) + end + end + + # Standard Provider methods for Parent + # + + def load_current_resource + if flush_cache[:before] + @yum.reload + end + + # At this point package_name could be: + # + # 1) a package name, eg: "foo" + # 2) a package name.arch, eg: "foo.i386" + # 3) or a dependency, eg: "foo >= 1.1" + + # Check if we have name or name+arch which has a priority over a dependency + unless @yum.package_available?(@new_resource.package_name) + # If they aren't in the installed packages they could be a dependency + parse_dependency + end + + # Don't overwrite an existing arch + unless arch + parse_arch + end + + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + + if @new_resource.source + unless ::File.exists?(@new_resource.source) + raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" + end + + Chef::Log.debug("#{@new_resource} checking rpm status") + status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /([\w\d_.-]+)\s([\w\d_.-]+)/ + @current_resource.package_name($1) + @new_resource.version($2) + end + end + end + end + + if @new_resource.version + new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}" + else + new_resource = "#{@new_resource.package_name}#{yum_arch}" + end + + Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + + installed_version = @yum.installed_version(@new_resource.package_name, arch) + @current_resource.version(installed_version) + + @candidate_version = @yum.candidate_version(@new_resource.package_name, arch) + + Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: " + + "#{@candidate_version || "(none)"}") + + @current_resource + end + + def install_package(name, version) + if @new_resource.source + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") + else + # Work around yum not exiting with an error if a package doesn't exist for CHEF-2062 + if @yum.version_available?(name, version, arch) + method = "install" + log_method = "installing" + + # More Yum fun: + # + # yum install of an old name+version will exit(1) + # yum install of an old name+version+arch will exit(0) for some reason + # + # Some packages can be installed multiple times like the kernel + unless @yum.allow_multi_install.include?(name) + if RPMVersion.parse(@current_resource.version) > RPMVersion.parse(version) + # Unless they want this... + if allow_downgrade + method = "downgrade" + log_method = "downgrading" + else + # we bail like yum when the package is older + raise Chef::Exceptions::Package, "Installed package #{name}-#{@current_resource.version} is newer " + + "than candidate package #{name}-#{version}" + end + end + end + + repo = @yum.package_repository(name, version, arch) + Chef::Log.info("#{@new_resource} #{log_method} #{name}-#{version}#{yum_arch} from #{repo} repository") + + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{name}-#{version}#{yum_arch}") + else + raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " + + "and release? (version-release, e.g. 1.84-10.fc6)" + end + end + + if flush_cache[:after] + @yum.reload + else + @yum.reload_installed + end + end + + # Keep upgrades from trying to install an older candidate version. Can happen when a new + # version is installed then removed from a repository, now the older available version + # shows up as a viable install candidate. + # + # Can be done in upgrade_package but an upgraded from->to log message slips out + # + # Hacky - better overall solution? Custom compare in Package provider? + def action_upgrade + # Could be uninstalled or have no candidate + if @current_resource.version.nil? || candidate_version.nil? + super + # Ensure the candidate is newer + elsif RPMVersion.parse(candidate_version) > RPMVersion.parse(@current_resource.version) + super + else + Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") + end + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + if version + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}-#{version}#{yum_arch}") + else + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}#{yum_arch}") + end + + if flush_cache[:after] + @yum.reload + else + @yum.reload_installed + end + end + + def purge_package(name, version) + remove_package(name, version) + end + + private + + def parse_arch + # Allow for foo.x86_64 style package_name like yum uses in it's output + # + if @new_resource.package_name =~ %r{^(.*)\.(.*)$} + new_package_name = $1 + new_arch = $2 + # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. + # Ensure we don't have an existing package matching package_name, then ensure we at + # least have a match for the new_package+new_arch before we overwrite. If neither + # then fall through to standard package handling. + if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and + (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch)) + @new_resource.package_name(new_package_name) + @new_resource.arch(new_arch) + end + end + end + + # If we don't have the package we could have been passed a 'whatprovides' feature + # + # eg: yum install "perl(Config)" + # yum install "mtr = 2:0.71-3.1" + # yum install "mtr > 2:0.71" + # + # We support resolving these out of the Provides data imported from yum-dump.py and + # matching them up with an actual package so the standard resource handling can apply. + # + # There is currently no support for filename matching. + def parse_dependency + # Transform the package_name into a requirement + yum_require = RPMRequire.parse(@new_resource.package_name) + # and gather all the packages that have a Provides feature satisfying the requirement. + # It could be multiple be we can only manage one + packages = @yum.packages_from_require(yum_require) + + if packages.empty? + # Don't bother if we are just ensuring a package is removed - we don't need Provides data + actions = Array(@new_resource.action) + unless actions.size == 1 and (actions[0] == :remove || actions[0] == :purge) + Chef::Log.debug("#{@new_resource} couldn't match #{@new_resource.package_name} in " + + "installed Provides, loading available Provides - this may take a moment") + @yum.reload_provides + packages = @yum.packages_from_require(yum_require) + end + end + + unless packages.empty? + new_package_name = packages.first.name + Chef::Log.debug("#{@new_resource} no package found for #{@new_resource.package_name} " + + "but matched Provides for #{new_package_name}") + + # Ensure it's not the same package under a different architecture + unique_names = [] + packages.each do |pkg| + unique_names << "#{pkg.name}-#{pkg.version.evr}" + end + unique_names.uniq! + + if unique_names.size > 1 + Chef::Log.warn("#{@new_resource} matched multiple Provides for #{@new_resource.package_name} " + + "but we can only use the first match: #{new_package_name}. Please use a more " + + "specific version.") + end + + @new_resource.package_name(new_package_name) + end + end + + end + end + end +end diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb new file mode 100644 index 0000000000..43727466e2 --- /dev/null +++ b/lib/chef/provider/package/zypper.rb @@ -0,0 +1,144 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'singleton' + +class Chef + class Provider + class Package + class Zypper < Chef::Provider::Package + + + def load_current_resource + @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + + is_installed=false + is_out_of_date=false + version='' + oud_version='' + Chef::Log.debug("#{@new_resource} checking zypper") + status = popen4("zypper info #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /^Version: (.+)$/ + version = $1 + Chef::Log.debug("#{@new_resource} version #{$1}") + when /^Installed: Yes$/ + is_installed=true + Chef::Log.debug("#{@new_resource} is installed") + + when /^Installed: No$/ + is_installed=false + Chef::Log.debug("#{@new_resource} is not installed") + when /^Status: out-of-date \(version (.+) installed\)$/ + is_out_of_date=true + oud_version=$1 + Chef::Log.debug("#{@new_resource} out of date version #{$1}") + end + end + end + + if is_installed==false + @candidate_version=version + @current_resource.version(nil) + end + + if is_installed==true + if is_out_of_date==true + @current_resource.version(oud_version) + @candidate_version=version + else + @current_resource.version(version) + @candidate_version=version + end + end + + unless status.exitstatus == 0 + raise Chef::Exceptions::Package, "zypper failed - #{status.inspect}!" + end + + @current_resource + end + + #Gets the zypper Version from command output (Returns Floating Point number) + def zypper_version() + `zypper -V 2>&1`.scan(/\d+/).join(".").to_f + end + + def install_package(name, version) + if zypper_version < 1.0 + run_command( + :command => "zypper install -y #{name}" + ) + elsif version + run_command( + :command => "zypper -n --no-gpg-checks install -l #{name}=#{version}" + ) + else + run_command( + :command => "zypper -n --no-gpg-checks install -l #{name}" + ) + end + end + + def upgrade_package(name, version) + if zypper_version < 1.0 + run_command( + :command => "zypper install -y #{name}" + ) + elsif version + run_command( + :command => "zypper -n --no-gpg-checks install -l #{name}=#{version}" + ) + else + run_command( + :command => "zypper -n --no-gpg-checks install -l #{name}" + ) + end + end + + def remove_package(name, version) + if zypper_version < 1.0 + run_command( + :command => "zypper remove -y #{name}" + ) + elsif version + run_command( + :command => "zypper -n --no-gpg-checks remove #{name}=#{version}" + ) + else + run_command( + :command => "zypper -n --no-gpg-checks remove #{name}" + ) + end + + + end + + def purge_package(name, version) + remove_package(name, version) + end + + end + end + end +end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb new file mode 100644 index 0000000000..9ccd7ea056 --- /dev/null +++ b/lib/chef/provider/remote_directory.rb @@ -0,0 +1,174 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/file' +require 'chef/provider/directory' +require 'chef/resource/directory' +require 'chef/resource/remote_file' +require 'chef/mixin/file_class' +require 'chef/platform' +require 'uri' +require 'tempfile' +require 'net/https' +require 'set' + +class Chef + class Provider + class RemoteDirectory < Chef::Provider::Directory + include Chef::Mixin::FileClass + + def action_create + super + + files_to_purge = Set.new( + Dir.glob(::File.join(@new_resource.path, '**', '*'), ::File::FNM_DOTMATCH).select do |name| + name !~ /(?:^|#{Regexp.escape(::File::SEPARATOR)})\.\.?$/ + end + ) + files_to_transfer.each do |cookbook_file_relative_path| + create_cookbook_file(cookbook_file_relative_path) + # the file is removed from the purge list + files_to_purge.delete(::File.join(@new_resource.path, cookbook_file_relative_path)) + # parent directories are also removed from the purge list + directories=::File.dirname(::File.join(@new_resource.path, cookbook_file_relative_path)).split(::File::SEPARATOR) + for i in 0..directories.length-1 + files_to_purge.delete(::File.join(directories[0..i])) + end + end + purge_unmanaged_files(files_to_purge) + end + + def action_create_if_missing + # if this action is called, ignore the existing overwrite flag + @new_resource.overwrite(false) + action_create + end + + protected + + def purge_unmanaged_files(unmanaged_files) + if @new_resource.purge + unmanaged_files.sort.reverse.each do |f| + # file_class comes from Chef::Mixin::FileClass + if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup) + # Linux treats directory symlinks as files + # Remove a directory as a directory when not on windows if it is not a symlink + purge_directory(f) + elsif ::File.directory?(f) && Chef::Platform.windows? + # Windows treats directory symlinks as directories so we delete them here + purge_directory(f) + else + converge_by("delete unmanaged file #{f}") do + ::File.delete(f) + Chef::Log.debug("#{@new_resource} deleted file #{f}") + end + end + end + end + end + + def purge_directory(dir) + converge_by("delete unmanaged directory #{dir}") do + Dir::rmdir(dir) + Chef::Log.debug("#{@new_resource} removed directory #{dir}") + end + end + + def files_to_transfer + cookbook = run_context.cookbook_collection[resource_cookbook] + files = cookbook.relative_filenames_in_preferred_directory(node, :files, @new_resource.source) + files.sort.reverse + end + + def directory_root_in_cookbook_cache + @directory_root_in_cookbook_cache ||= begin + cookbook = run_context.cookbook_collection[resource_cookbook] + cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path) + end + end + + # Determine the cookbook to get the file from. If new resource sets an + # explicit cookbook, use it, otherwise fall back to the implicit cookbook + # i.e., the cookbook the resource was declared in. + def resource_cookbook + @new_resource.cookbook || @new_resource.cookbook_name + end + + def create_cookbook_file(cookbook_file_relative_path) + full_path = ::File.join(@new_resource.path, cookbook_file_relative_path) + + ensure_directory_exists(::File.dirname(full_path)) + + file_to_fetch = cookbook_file_resource(full_path, cookbook_file_relative_path) + if @new_resource.overwrite + file_to_fetch.run_action(:create) + else + file_to_fetch.run_action(:create_if_missing) + end + @new_resource.updated_by_last_action(true) if file_to_fetch.updated? + end + + def cookbook_file_resource(target_path, relative_source_path) + cookbook_file = Chef::Resource::CookbookFile.new(target_path, run_context) + cookbook_file.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name + cookbook_file.source(::File.join(@new_resource.source, relative_source_path)) + if Chef::Platform.windows? && @new_resource.files_rights + @new_resource.files_rights.each_pair do |permission, *args| + cookbook_file.rights(permission, *args) + end + end + cookbook_file.mode(@new_resource.files_mode) if @new_resource.files_mode + cookbook_file.group(@new_resource.files_group) if @new_resource.files_group + cookbook_file.owner(@new_resource.files_owner) if @new_resource.files_owner + cookbook_file.backup(@new_resource.files_backup) if @new_resource.files_backup + + cookbook_file + end + + def ensure_directory_exists(path) + unless ::File.directory?(path) + directory_to_create = resource_for_directory(path) + directory_to_create.run_action(:create) + @new_resource.updated_by_last_action(true) if directory_to_create.updated? + end + end + + def resource_for_directory(path) + dir = Chef::Resource::Directory.new(path, run_context) + dir.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name + if Chef::Platform.windows? && @new_resource.rights + # rights are only meant to be applied to the toppest-level directory; + # Windows will handle inheritance. + if path == @new_resource.path + @new_resource.rights.each do |rights| #rights is a hash + permissions = rights.delete(:permissions) #delete will return the value or nil if not found + principals = rights.delete(:principals) + dir.rights(permissions, principals, rights) + end + end + end + dir.mode(@new_resource.mode) if @new_resource.mode + dir.group(@new_resource.group) + dir.owner(@new_resource.owner) + dir.recursive(true) + dir + end + + end + end +end diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb new file mode 100644 index 0000000000..90e367f558 --- /dev/null +++ b/lib/chef/provider/remote_file.rb @@ -0,0 +1,138 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/file' +require 'chef/rest' +require 'uri' +require 'tempfile' +require 'net/https' + +class Chef + class Provider + class RemoteFile < Chef::Provider::File + + def load_current_resource + @current_resource = Chef::Resource::RemoteFile.new(@new_resource.name) + super + end + + def action_create + Chef::Log.debug("#{@new_resource} checking for changes") + + if current_resource_matches_target_checksum? + Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating") + else + sources = @new_resource.source + source = sources.shift + begin + rest = Chef::REST.new(source, nil, nil, http_client_opts(source)) + raw_file = rest.streaming_request(rest.create_url(source), {}) + rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Net::HTTPFatalError => e + Chef::Log.debug("#{@new_resource} cannot be downloaded from #{source}") + if source = sources.shift + Chef::Log.debug("#{@new_resource} trying to download from another mirror") + retry + else + raise e + end + end + if matches_current_checksum?(raw_file) + Chef::Log.debug "#{@new_resource} target and source checksums are the same - not updating" + else + description = [] + description << "copy file downloaded from #{@new_resource.source} into #{@new_resource.path}" + description << diff_current(raw_file.path) + converge_by(description) do + backup_new_resource + FileUtils.cp raw_file.path, @new_resource.path + Chef::Log.info "#{@new_resource} updated" + raw_file.close! + end + # whyrun mode cleanup - the temp file will never be used, + # so close/unlink it here. + if whyrun_mode? + raw_file.close! + end + end + end + set_all_access_controls + end + + def current_resource_matches_target_checksum? + @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/ + end + + def matches_current_checksum?(candidate_file) + Chef::Log.debug "#{@new_resource} checking for file existence of #{@new_resource.path}" + if ::File.exists?(@new_resource.path) + Chef::Log.debug "#{@new_resource} file exists at #{@new_resource.path}" + @new_resource.checksum(checksum(candidate_file.path)) + Chef::Log.debug "#{@new_resource} target checksum: #{@current_resource.checksum}" + Chef::Log.debug "#{@new_resource} source checksum: #{@new_resource.checksum}" + + @new_resource.checksum == @current_resource.checksum + else + Chef::Log.debug "#{@new_resource} creating #{@new_resource.path}" + false + end + end + + def backup_new_resource + if ::File.exists?(@new_resource.path) + Chef::Log.debug "#{@new_resource} checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}" + backup @new_resource.path + end + end + + def source_file(source, current_checksum, &block) + if absolute_uri?(source) + fetch_from_uri(source, &block) + elsif !Chef::Config[:solo] + fetch_from_chef_server(source, current_checksum, &block) + else + fetch_from_local_cookbook(source, &block) + end + end + + def http_client_opts(source) + opts={} + # CHEF-3140 + # 1. If it's already compressed, trying to compress it more will + # probably be counter-productive. + # 2. Some servers are misconfigured so that you GET $URL/file.tgz but + # they respond with content type of tar and content encoding of gzip, + # which tricks Chef::REST into decompressing the response body. In this + # case you'd end up with a tar archive (no gzip) named, e.g., foo.tgz, + # which is not what you wanted. + if @new_resource.path =~ /gz$/ or source =~ /gz$/ + opts[:disable_gzip] = true + end + opts + end + + private + + def absolute_uri?(source) + URI.parse(source).absolute? + rescue URI::InvalidURIError + false + end + + end + end +end diff --git a/lib/chef/provider/resource_update.rb b/lib/chef/provider/resource_update.rb new file mode 100644 index 0000000000..e2c6bffca4 --- /dev/null +++ b/lib/chef/provider/resource_update.rb @@ -0,0 +1,55 @@ + +class Chef + class Provider + + # { + # "run_id" : "1000", + # "resource" : { + # "type" : "file", + # "name" : "/etc/passwd", + # "start_time" : "2012-01-09T08:15:30-05:00", + # "end_time" : "2012-01-09T08:15:30-05:00", + # "status" : "modified", + # "initial_state" : "exists", + # "final_state" : "modified", + # "before" : { + # "group" : "root", + # "owner" : "root", + # "checksum" : "xyz" + # }, + # "after" : { + # "group" : "root", + # "owner" : "root", + # "checksum" : "abc" + # }, + # "delta" : "escaped delta goes here" + # }, + # "event_data" : "" + # } + + class ResourceUpdate + + attr_accessor :type + attr_accessor :name + attr_accessor :duration #ms + attr_accessor :status + attr_accessor :initial_state + attr_accessor :final_state + attr_accessor :initial_properties + attr_accessor :final_properties + attr_accessor :event_data # e.g., a diff. + + def initial_state_from_resource(resource) + @initial_properties = resource.to_hash + end + + def updated_state_from_resource(resource) + @final_properties = resource.to_hash + end + + end + end +end + + + diff --git a/lib/chef/provider/route.rb b/lib/chef/provider/route.rb new file mode 100644 index 0000000000..5aedcb99ec --- /dev/null +++ b/lib/chef/provider/route.rb @@ -0,0 +1,223 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org), Jesse Nelson (spheromak@gmail.com) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/mixin/command' +require 'chef/provider' +require 'ipaddr' + +class Chef::Provider::Route < Chef::Provider + include Chef::Mixin::Command + + attr_accessor :is_running + + MASK = {'0.0.0.0' => '0', + '128.0.0.0' => '1', + '192.0.0.0' => '2', + '224.0.0.0' => '3', + '240.0.0.0' => '4', + '248.0.0.0' => '5', + '252.0.0.0' => '6', + '254.0.0.0' => '7', + '255.0.0.0' => '8', + '255.128.0.0' => '9', + '255.192.0.0' => '10', + '255.224.0.0' => '11', + '255.240.0.0' => '12', + '255.248.0.0' => '13', + '255.252.0.0' => '14', + '255.254.0.0' => '15', + '255.255.0.0' => '16', + '255.255.128.0' => '17', + '255.255.192.0' => '18', + '255.255.224.0' => '19', + '255.255.240.0' => '20', + '255.255.248.0' => '21', + '255.255.252.0' => '22', + '255.255.254.0' => '23', + '255.255.255.0' => '24', + '255.255.255.128' => '25', + '255.255.255.192' => '26', + '255.255.255.224' => '27', + '255.255.255.240' => '28', + '255.255.255.248' => '29', + '255.255.255.252' => '30', + '255.255.255.254' => '31', + '255.255.255.255' => '32' } + + def hex2ip(hex_data) + # Cleanup hex data + hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, '') + + # Check hex data format (IP is a 32bit integer, so should be 8 chars long) + return nil if hex_ip.length != hex_data.length || hex_ip.length != 8 + + # Extract octets from hex data + octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack('H2').unpack("C").first } + + # Validate IP + ip = octets.join('.') + begin + IPAddr.new(ip, Socket::AF_INET).to_s + rescue ArgumentError + Chef::Log.debug("Invalid IP address data: hex=#{hex_ip}, ip=#{ip}") + return nil + end + end + + def whyrun_supported? + true + end + + def load_current_resource + self.is_running = false + + # cidr or quad dot mask + if @new_resource.netmask + new_ip = IPAddr.new("#{@new_resource.target}/#{@new_resource.netmask}") + else + new_ip = IPAddr.new(@new_resource.target) + end + + # For linux, we use /proc/net/route file to read proc table info + if node[:os] == "linux" + route_file = ::File.open("/proc/net/route", "r") + + # Read all routes + while (line = route_file.gets) + # Get all the fields for a route + iface,destination,gateway,flags,refcnt,use,metric,mask,mtu,window,irtt = line.split + + # Convert hex-encoded values to quad-dotted notation (e.g. 0064A8C0 => 192.168.100.0) + destination = hex2ip(destination) + gateway = hex2ip(gateway) + mask = hex2ip(mask) + + # Skip formatting lines (header, etc) + next unless destination && gateway && mask + Chef::Log.debug("#{@new_resource} system has route: dest=#{destination} mask=#{mask} gw=#{gateway}") + + # check if what were trying to configure is already there + # use an ipaddr object with ip/mask this way we can have + # a new resource be in cidr format (i don't feel like + # expanding bitmask by hand. + # + running_ip = IPAddr.new("#{destination}/#{mask}") + Chef::Log.debug("#{@new_resource} new ip: #{new_ip.inspect} running ip: #{running_ip.inspect}") + self.is_running = true if running_ip == new_ip && gateway == @new_resource.gateway + end + + route_file.close + end + end + + def action_add + # check to see if load_current_resource found the route + if is_running + Chef::Log.debug("#{@new_resource} route already active - nothing to do") + else + command = generate_command(:add) + converge_by ("run #{ command } to add route") do + run_command( :command => command ) + Chef::Log.info("#{@new_resource} added") + end + end + + #for now we always write the file (ugly but its what it is) + generate_config + end + + def action_delete + if is_running + command = generate_command(:delete) + converge_by ("run #{ command } to delete route ") do + run_command( :command => command ) + Chef::Log.info("#{@new_resource} removed") + end + else + Chef::Log.debug("#{@new_resource} route does not exist - nothing to do") + end + end + + def generate_config + conf = Hash.new + case node[:platform] + when "centos", "redhat", "fedora" + # walk the collection + run_context.resource_collection.each do |resource| + if resource.is_a? Chef::Resource::Route + # default to eth0 + if resource.device + dev = resource.device + else + dev = "eth0" + end + + conf[dev] = String.new if conf[dev].nil? + if resource.action == :add + conf[dev] << config_file_contents(:add, :target => resource.target, :netmask => resource.netmask, :gateway => resource.gateway) + else + # need to do this for the case when the last route on an int + # is removed + conf[dev] << config_file_contents(:delete) + end + end + end + conf.each do |k, v| + network_file_name = "/etc/sysconfig/network-scripts/route-#{k}" + converge_by ("write route route.#{k}\n#{conf[k]} to #{ network_file_name }") do + network_file = ::File.new(network_file_name, "w") + network_file.puts(conf[k]) + Chef::Log.debug("#{@new_resource} writing route.#{k}\n#{conf[k]}") + network_file.close + end + end + end + end + + def generate_command(action) + common_route_items = '' + common_route_items << "/#{MASK[@new_resource.netmask.to_s]}" if @new_resource.netmask + common_route_items << " via #{@new_resource.gateway} " if @new_resource.gateway + + case action + when :add + command = "ip route replace #{@new_resource.target}" + command << common_route_items + command << " dev #{@new_resource.device} " if @new_resource.device + when :delete + command = "ip route delete #{@new_resource.target}" + command << common_route_items + end + + return command + end + + def config_file_contents(action, options={}) + content = '' + case action + when :add + content << "#{options[:target]}" + content << "/#{options[:netmask]}" if options[:netmask] + content << " via #{options[:gateway]}" if options[:gateway] + content << "\n" + end + + return content + end +end diff --git a/lib/chef/provider/ruby_block.rb b/lib/chef/provider/ruby_block.rb new file mode 100644 index 0000000000..16908b0eff --- /dev/null +++ b/lib/chef/provider/ruby_block.rb @@ -0,0 +1,42 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode +# 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 + class Provider + class RubyBlock < Chef::Provider + def whyrun_supported? + true + end + + def load_current_resource + true + end + + def action_run + converge_by("execute the ruby block #{@new_resource.name}") do + @new_resource.block.call + Chef::Log.info("#{@new_resource} called") + end + end + + alias :action_create :action_run + + end + end +end diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb new file mode 100644 index 0000000000..9e5a7d7fe1 --- /dev/null +++ b/lib/chef/provider/script.rb @@ -0,0 +1,57 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'tempfile' +require 'chef/provider/execute' + +class Chef + class Provider + class Script < Chef::Provider::Execute + + def action_run + script_file.puts(@new_resource.code) + script_file.close + + set_owner_and_group + + @new_resource.command("\"#{@new_resource.interpreter}\" #{@new_resource.flags} \"#{script_file.path}\"") + super + converge_by(nil) do + # ensure script is unlinked at end of converge! + unlink_script_file + end + end + + def set_owner_and_group + # FileUtils itself implements a no-op if +user+ or +group+ are nil + # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') + # as an unprivileged user. + FileUtils.chown(@new_resource.user, @new_resource.group, script_file.path) + end + + def script_file + @script_file ||= Tempfile.open("chef-script") + end + + def unlink_script_file + @script_file && @script_file.close! + end + + end + end +end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb new file mode 100644 index 0000000000..decca7fd7c --- /dev/null +++ b/lib/chef/provider/service.rb @@ -0,0 +1,158 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/command' +require 'chef/provider' + +class Chef + class Provider + class Service < Chef::Provider + + include Chef::Mixin::Command + + def initialize(new_resource, run_context) + super + @enabled = nil + end + + def whyrun_supported? + true + end + + def load_new_resource_state + # If the user didn't specify a change in enabled state, + # it will be the same as the old resource + if ( @new_resource.enabled.nil? ) + @new_resource.enabled(@current_resource.enabled) + end + if ( @new_resource.running.nil? ) + @new_resource.running(@current_resource.running) + end + end + + def shared_resource_requirements + end + + def define_resource_requirements + requirements.assert(:reload) do |a| + a.assertion { @new_resource.supports[:reload] || @new_resource.reload_command } + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" + # if a service is not declared to support reload, that won't + # typically change during the course of a run - so no whyrun + # alternative here. + end + end + + def action_enable + if @current_resource.enabled + Chef::Log.debug("#{@new_resource} already enabled - nothing to do") + else + converge_by("enable service #{@new_resource}") do + enable_service + Chef::Log.info("#{@new_resource} enabled") + end + end + load_new_resource_state + @new_resource.enabled(true) + end + + def action_disable + if @current_resource.enabled + 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_start + unless @current_resource.running + converge_by("start service #{@new_resource}") do + start_service + Chef::Log.info("#{@new_resource} started") + end + else + Chef::Log.debug("#{@new_resource} already running - nothing to do") + end + load_new_resource_state + @new_resource.running(true) + end + + def action_stop + if @current_resource.running + converge_by("stop service #{@new_resource}") do + stop_service + Chef::Log.info("#{@new_resource} stopped") + end + else + Chef::Log.debug("#{@new_resource} already stopped - nothing to do") + end + load_new_resource_state + @new_resource.running(false) + end + + def action_restart + converge_by("restart service #{@new_resource}") do + restart_service + Chef::Log.info("#{@new_resource} restarted") + end + load_new_resource_state + @new_resource.running(true) + end + + def action_reload + if @current_resource.running + converge_by("disable service #{@new_resource}") do + reload_service + Chef::Log.info("#{@new_resource} reloaded") + end + end + load_new_resource_state + end + + def enable_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" + end + + def disable_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" + end + + def start_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :start" + end + + def stop_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :stop" + end + + def restart_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart" + end + + def reload_service + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart" + end + + end + end +end diff --git a/lib/chef/provider/service/arch.rb b/lib/chef/provider/service/arch.rb new file mode 100644 index 0000000000..8c8216c37f --- /dev/null +++ b/lib/chef/provider/service/arch.rb @@ -0,0 +1,113 @@ +# +# Author:: Jan Zimmek (<jan.zimmek@web.de>) +# Copyright:: Copyright (c) 2010 Jan Zimmek +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service/init' +require 'chef/mixin/command' + +class Chef::Provider::Service::Arch < Chef::Provider::Service::Init + + def initialize(new_resource, run_context) + super + @init_command = "/etc/rc.d/#{@new_resource.service_name}" + end + + def load_current_resource + raise Chef::Exceptions::Service, "Could not find /etc/rc.conf" unless ::File.exists?("/etc/rc.conf") + raise Chef::Exceptions::Service, "No DAEMONS found in /etc/rc.conf" unless ::File.read("/etc/rc.conf").match(/DAEMONS=\((.*)\)/m) + super + + @current_resource.enabled(daemons.include?(@current_resource.service_name)) + @current_resource + end + + # Get list of all daemons from the file '/etc/rc.conf'. + # Mutiple lines and background form are supported. Example: + # DAEMONS=(\ + # foobar \ + # @example \ + # !net \ + # ) + def daemons + entries = [] + if ::File.read("/etc/rc.conf").match(/DAEMONS=\((.*)\)/m) + entries += $1.gsub(/\\?[\r\n]/, ' ').gsub(/# *[^ ]+/,' ').split(' ') if $1.length > 0 + end + + yield(entries) if block_given? + + entries + end + + # FIXME: Multiple entries of DAEMONS will cause very bad results :) + def update_daemons(entries) + content = ::File.read("/etc/rc.conf").gsub(/DAEMONS=\((.*)\)/m, "DAEMONS=(#{entries.join(' ')})") + ::File.open("/etc/rc.conf", "w") do |f| + f.write(content) + end + end + + def enable_service() + new_daemons = [] + entries = daemons + + if entries.include?(new_resource.service_name) or entries.include?("@#{new_resource.service_name}") + # exists and already enabled (or already enabled as a background service) + # new_daemons += entries + else + if entries.include?("!#{new_resource.service_name}") + # exists but disabled + entries.each do |daemon| + if daemon == "!#{new_resource.service_name}" + new_daemons << new_resource.service_name + else + new_daemons << daemon + end + end + else + # does not exist + new_daemons += entries + new_daemons << new_resource.service_name + end + update_daemons(new_daemons) + end + end + + def disable_service() + new_daemons = [] + entries = daemons + + if entries.include?("!#{new_resource.service_name}") + # exists and disabled + # new_daemons += entries + else + if entries.include?(new_resource.service_name) or entries.include?("@#{new_resource.service_name}") + # exists but enabled (or enabled as a back-ground service) + # FIXME: Does arch support !@foobar ? + entries.each do |daemon| + if [new_resource.service_name, "@#{new_resource.service_name}"].include?(daemon) + new_daemons << "!#{new_resource.service_name}" + else + new_daemons << daemon + end + end + end + update_daemons(new_daemons) + end + end + +end diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb new file mode 100644 index 0000000000..e2a0f60d91 --- /dev/null +++ b/lib/chef/provider/service/debian.rb @@ -0,0 +1,152 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/init' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Debian < Chef::Provider::Service::Init + UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i + UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i + + def load_current_resource + super + @priority_success = true + @rcd_status = nil + @current_resource.priority(get_priority) + @current_resource.enabled(service_currently_enabled?(@current_resource.priority)) + @current_resource + end + + def define_resource_requirements + # do not call super here, inherit only shared_requirements + shared_resource_requirements + requirements.assert(:all_actions) do |a| + update_rcd = "/usr/sbin/update-rc.d" + a.assertion { ::File.exists? update_rcd } + a.failure_message Chef::Exceptions::Service, "#{update_rcd} does not exist!" + # no whyrun recovery - this is a base system component of debian + # distros and must be present + end + + requirements.assert(:all_actions) do |a| + a.assertion { @priority_success } + a.failure_message Chef::Exceptions::Service, "/usr/sbin/update-rc.d -n -f #{@current_resource.service_name} failed - #{@rcd_status.inspect}" + # This can happen if the service is not yet installed,so we'll fake it. + a.whyrun ["Unable to determine priority of service, assuming service would have been correctly installed earlier in the run.", + "Assigning temporary priorities to continue.", + "If this service is not properly installed prior to this point, this will fail."] do + temp_priorities = {"6"=>[:stop, "20"], + "0"=>[:stop, "20"], + "1"=>[:stop, "20"], + "2"=>[:start, "20"], + "3"=>[:start, "20"], + "4"=>[:start, "20"], + "5"=>[:start, "20"]} + @current_resource.priority(temp_priorities) + end + end + end + + def get_priority + priority = {} + + @rcd_status = popen4("/usr/sbin/update-rc.d -n -f #{@current_resource.service_name} remove") do |pid, stdin, stdout, stderr| + + [stdout, stderr].each do |iop| + iop.each_line do |line| + if UPDATE_RC_D_PRIORITIES =~ line + # priority[runlevel] = [ S|K, priority ] + # S = Start, K = Kill + # debian runlevels: 0 Halt, 1 Singleuser, 2 Multiuser, 3-5 == 2, 6 Reboot + priority[$1] = [($2 == "S" ? :start : :stop), $3] + end + if line =~ UPDATE_RC_D_ENABLED_MATCHES + enabled = true + end + end + end + end + + unless @rcd_status.exitstatus == 0 + @priority_success = false + end + priority + end + + def service_currently_enabled?(priority) + enabled = false + priority.each { |runlevel, arguments| + Chef::Log.debug("#{@new_resource} runlevel #{runlevel}, action #{arguments[0]}, priority #{arguments[1]}") + # if we are in a update-rc.d default startup runlevel && we start in this runlevel + if (2..5).include?(runlevel.to_i) && arguments[0] == :start + enabled = true + end + } + + enabled + end + + 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}") + 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") + end + + end + + 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 .") + 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 .") + end + end + + def set_priority() + args = "" + @new_resource.priority.each do |level, o| + action = o[0] + 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}") + end + end + end + end +end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb new file mode 100644 index 0000000000..b875838ec2 --- /dev/null +++ b/lib/chef/provider/service/freebsd.rb @@ -0,0 +1,175 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' +require 'chef/provider/service' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Freebsd < Chef::Provider::Service::Init + + include Chef::Mixin::ShellOut + + 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 + @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 + end + + @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" + end + + requirements.assert(:all_actions) do |a| + 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" + # No recovery in whyrun mode - the init file is present but not correct. + end + end + + def start_service + if @new_resource.start_command + super + else + shell_out!("#{@init_command} faststart") + end + end + + def stop_service + if @new_resource.stop_command + super + else + shell_out!("#{@init_command} faststop") + end + end + + def restart_service + if @new_resource.restart_command + + super + elsif @new_resource.supports[:restart] + shell_out!("#{@init_command} fastrestart") + else + stop_service + sleep 1 + start_service + end + end + + def read_rc_conf + ::File.open("/etc/rc.conf", 'r') { |file| file.readlines } + end + + def write_rc_conf(lines) + ::File.open("/etc/rc.conf", 'w') do |file| + lines.each { |line| file.puts(line) } + end + end + + # 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" + 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 + 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)}/ } + # 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 + end + + end + end + end +end diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb new file mode 100644 index 0000000000..45b5a21f9b --- /dev/null +++ b/lib/chef/provider/service/gentoo.rb @@ -0,0 +1,67 @@ +# +# Author:: Lee Jensen (<ljensen@engineyard.com>) +# Author:: AJ Christensen (<aj@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/mixin/command' + +class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init + def load_current_resource + + @new_resource.supports[:status] = true + @new_resource.supports[:restart] = true + @found_script = false + super + + @current_resource.enabled( + Dir.glob("/etc/runlevels/**/#{@current_resource.service_name}").any? do |file| + @found_script = true + exists = ::File.exists? file + readable = ::File.readable? file + Chef::Log.debug "#{@new_resource} exists: #{exists}, readable: #{readable}" + exists and readable + end + ) + Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}" + + @current_resource + end + + def define_resource_requirements + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/sbin/rc-update") } + a.failure_message Chef::Exceptions::Service, "/sbin/rc-update does not exist" + # no whyrun recovery -t his is a core component whose presence is + # unlikely to be affected by what we do in the course of a chef run + end + + requirements.assert(:all_actions) do |a| + a.assertion { @found_script } + # No failure, just informational output from whyrun + a.whyrun "Could not find service #{@new_resource.service_name} under any runlevel" + end + end + + def enable_service() + run_command(:command => "/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") + end +end diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb new file mode 100644 index 0000000000..ab843d764d --- /dev/null +++ b/lib/chef/provider/service/init.rb @@ -0,0 +1,87 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' +require 'chef/provider/service' +require 'chef/provider/service/simple' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Init < Chef::Provider::Service::Simple + + include Chef::Mixin::ShellOut + + def initialize(new_resource, run_context) + super + @init_command = "/etc/init.d/#{@new_resource.service_name}" + end + + def define_resource_requirements + # do not call super here, inherit only shared_requirements + shared_resource_requirements + requirements.assert(:start, :stop, :restart, :reload) do |a| + a.assertion { ::File.exist?(@init_command) } + a.failure_message(Chef::Exceptions::Service, "#{@init_command} does not exist!") + a.whyrun("Init script '#{@init_command}' doesn't exist, assuming a prior action would have created it.") do + # blindly assume that the service exists but is stopped in why run mode: + @status_load_success = false + end + end + end + + def start_service + if @new_resource.start_command + super + else + shell_out!("#{@init_command} start") + end + end + + def stop_service + if @new_resource.stop_command + super + else + shell_out!("#{@init_command} stop") + end + end + + def restart_service + if @new_resource.restart_command + super + elsif @new_resource.supports[:restart] + shell_out!("#{@init_command} restart") + else + stop_service + sleep 1 + start_service + end + end + + def reload_service + if @new_resource.reload_command + super + elsif @new_resource.supports[:reload] + shell_out!("#{@init_command} reload") + end + end + end + end + end +end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb new file mode 100644 index 0000000000..32152376ee --- /dev/null +++ b/lib/chef/provider/service/insserv.rb @@ -0,0 +1,52 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/init' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Insserv < Chef::Provider::Service::Init + + def load_current_resource + super + + # Look for a /etc/rc.*/SnnSERVICE link to signifiy that the service would be started in a runlevel + if Dir.glob("/etc/rc**/S*#{@current_resource.service_name}").empty? + @current_resource.enabled false + else + @current_resource.enabled true + end + + @current_resource + 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}") + end + + def disable_service() + run_command(:command => "/sbin/insserv -r -f #{@new_resource.service_name}") + end + end + end + end +end diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb new file mode 100644 index 0000000000..69a17bb4fb --- /dev/null +++ b/lib/chef/provider/service/invokercd.rb @@ -0,0 +1,35 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/init' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Invokercd < Chef::Provider::Service::Init + + def initialize(new_resource, run_context) + super + @init_command = "/usr/sbin/invoke-rc.d #{@new_resource.service_name}" + end + end + end + end +end diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb new file mode 100644 index 0000000000..72c02779c6 --- /dev/null +++ b/lib/chef/provider/service/macosx.rb @@ -0,0 +1,144 @@ +# +# Author:: Igor Afonov <afonov@gmail.com> +# Copyright:: Copyright (c) 2011 Igor Afonov +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' + +class Chef + class Provider + class Service + class Macosx < Chef::Provider::Service::Simple + include Chef::Mixin::ShellOut + + PLIST_DIRS = %w{~/Library/LaunchAgents + /Library/LaunchAgents + /Library/LaunchDaemons + /System/Library/LaunchAgents + /System/Library/LaunchDaemons } + + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + @plist_size = 0 + @plist = find_service_plist + set_service_status + + @current_resource + end + + def define_resource_requirements + #super + requirements.assert(:enable) do |a| + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" + end + + requirements.assert(:disable) do |a| + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" + end + + requirements.assert(:reload) do |a| + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" + end + + requirements.assert(:all_actions) do |a| + a.assertion { @plist_size < 2 } + a.failure_message Chef::Exceptions::Service, "Several plist files match service name. Please use full service name." + end + + requirements.assert(:all_actions) do |a| + a.assertion { @plist_size > 0 } + # No failrue here in original code - so we also will not + # fail. Instead warn that the service is potentially missing + a.whyrun "Assuming that the service would have been previously installed and is currently disabled." do + @current_resource.enabled(false) + @current_resource.running(false) + end + end + + end + + def start_service + if @current_resource.running + Chef::Log.debug("#{@new_resource} already running, not starting") + else + if @new_resource.start_command + super + else + shell_out!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + end + end + end + + def stop_service + unless @current_resource.running + Chef::Log.debug("#{@new_resource} not running, not stopping") + else + if @new_resource.stop_command + super + else + shell_out!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + end + end + end + + def restart_service + if @new_resource.restart_command + super + else + stop_service + sleep 1 + start_service + end + end + + + def set_service_status + return if @plist == nil + + @current_resource.enabled(!@plist.nil?) + + if @current_resource.enabled + @owner_uid = ::File.stat(@plist).uid + @owner_gid = ::File.stat(@plist).gid + + shell_out!("launchctl list", :user => @owner_uid, :group => @owner_gid).stdout.each_line do |line| + case line + when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@current_resource.service_name}/ + pid = $1 + @current_resource.running(!pid.to_i.zero?) + end + end + else + @current_resource.running(false) + end + end + + private + + def find_service_plist + plists = PLIST_DIRS.inject([]) do |results, dir| + entries = Dir.glob("#{::File.expand_path(dir)}/*#{@current_resource.service_name}*.plist") + entries.any? ? results << entries : results + end + plists.flatten! + @plist_size = plists.size + plists.first + end + end + end + end +end diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb new file mode 100644 index 0000000000..629e4ee0c3 --- /dev/null +++ b/lib/chef/provider/service/redhat.rb @@ -0,0 +1,77 @@ +# +# Author:: AJ Christensen (<aj@hjksolutions.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/init' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Service + class Redhat < Chef::Provider::Service::Init + include Chef::Mixin::ShellOut + + CHKCONFIG_ON = /\d:on/ + CHKCONFIG_MISSING = /No such/ + + def initialize(new_resource, run_context) + super + @init_command = "/sbin/service #{@new_resource.service_name}" + @new_resource.supports[:status] = true + @service_missing = false + end + + def define_resource_requirements + shared_resource_requirements + + requirements.assert(:all_actions) do |a| + chkconfig_file = "/sbin/chkconfig" + a.assertion { ::File.exists? chkconfig_file } + a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} does not exist!" + end + + requirements.assert(:start, :enable, :reload, :restart) do |a| + a.assertion { !@service_missing } + a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the init.d script!" + a.whyrun "Assuming service would be disabled. The init script is not presently installed." + end + end + + def load_current_resource + super + + if ::File.exists?("/sbin/chkconfig") + chkconfig = shell_out!("/sbin/chkconfig --list #{@current_resource.service_name}", :returns => [0,1]) + @current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + @service_missing = !!(chkconfig.stderr =~ CHKCONFIG_MISSING) + end + + @current_resource + end + + def enable_service() + shell_out! "/sbin/chkconfig #{@new_resource.service_name} on" + end + + def disable_service() + shell_out! "/sbin/chkconfig #{@new_resource.service_name} off" + end + end + end + end +end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb new file mode 100644 index 0000000000..670c62d480 --- /dev/null +++ b/lib/chef/provider/service/simple.rb @@ -0,0 +1,172 @@ +# +# Author:: Mathieu Sauve-Frankel <msf@kisoku.net> +# Copyright:: Copyright (c) 2009 Mathieu Sauve-Frankel +# 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/mixin/shell_out' +require 'chef/provider/service' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Simple < Chef::Provider::Service + + include Chef::Mixin::ShellOut + + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + + @status_load_success = true + @ps_command_failed = false + + determine_current_status! + + @current_resource + end + + def whyrun_supported? + true + end + + def shared_resource_requirements + super + requirements.assert(:all_actions) do |a| + a.assertion { @status_load_success } + a.whyrun ["Service status not available. Assuming a prior action would have installed the service.", "Assuming status of not running."] + end + end + + def define_resource_requirements + # FIXME? need reload from service.rb + shared_resource_requirements + requirements.assert(:start) do |a| + a.assertion { @new_resource.start_command } + a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that start_command be set" + end + requirements.assert(:stop) do |a| + a.assertion { @new_resource.stop_command } + a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that stop_command be set" + end + + requirements.assert(:restart) do |a| + a.assertion { @new_resource.restart_command || ( @new_resource.start_command && @new_resource.stop_command ) } + a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires a restart_command or both start_command and stop_command be set in order to perform a restart" + end + + requirements.assert(:reload) do |a| + a.assertion { @new_resource.reload_command } + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} requires a reload_command be set in order to perform a reload" + end + + requirements.assert(:all_actions) do |a| + a.assertion { @new_resource.status_command or @new_resource.supports[:status] or + (!ps_cmd.nil? and !ps_cmd.empty?) } + a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute" + end + requirements.assert(:all_actions) do |a| + a.assertion { !@ps_command_failed } + a.failure_message Chef::Exceptions::Service, "Command #{ps_cmd} failed to execute, cannot determine service current status" + end + end + + def start_service + shell_out!(@new_resource.start_command) + end + + def stop_service + shell_out!(@new_resource.stop_command) + end + + def restart_service + if @new_resource.restart_command + shell_out!(@new_resource.restart_command) + else + stop_service + sleep 1 + start_service + end + end + + def reload_service + shell_out!(@new_resource.reload_command) + end + + protected + def determine_current_status! + if @new_resource.status_command + Chef::Log.debug("#{@new_resource} you have specified a status command, running..") + + begin + if shell_out(@new_resource.status_command).exitstatus == 0 + @current_resource.running true + Chef::Log.debug("#{@new_resource} is running") + end + rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError + # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. + # Temporarily catching different types of exceptions here until we get Shellout fixed. + # TODO: Remove the line before one we get the ShellOut fix. + @status_load_success = false + @current_resource.running false + nil + end + + elsif @new_resource.supports[:status] + Chef::Log.debug("#{@new_resource} supports status, running") + begin + if shell_out("#{@init_command} status").exitstatus == 0 + @current_resource.running true + Chef::Log.debug("#{@new_resource} is running") + end + # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. + # Temporarily catching different types of exceptions here until we get Shellout fixed. + # TODO: Remove the line before one we get the ShellOut fix. + rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError + @status_load_success = false + @current_resource.running false + nil + end + else + Chef::Log.debug "#{@new_resource} falling back to process table inspection" + r = Regexp.new(@new_resource.pattern) + Chef::Log.debug "#{@new_resource} attempting to match '#{@new_resource.pattern}' (#{r.inspect}) against process list" + begin + shell_out!(ps_cmd).stdout.each_line do |line| + if r.match(line) + @current_resource.running true + break + end + end + + @current_resource.running false unless @current_resource.running + Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" + # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. + # Temporarily catching different types of exceptions here until we get Shellout fixed. + # TODO: Remove the line before one we get the ShellOut fix. + rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError + @ps_command_failed = true + end + end + end + + def ps_cmd + @run_context.node[:command] && @run_context.node[:command][:ps] + end + end + end + end +end diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb new file mode 100644 index 0000000000..8e131590e8 --- /dev/null +++ b/lib/chef/provider/service/solaris.rb @@ -0,0 +1,86 @@ +# +# Author:: Toomas Pelberg (<toomasp@gmx.net>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/mixin/command' + +class Chef + class Provider + class Service + class Solaris < Chef::Provider::Service + + def initialize(new_resource, run_context=nil) + super + @init_command = "/usr/sbin/svcadm" + @status_command = "/bin/svcs -l" + end + + + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + unless ::File.exists? "/bin/svcs" + raise Chef::Exceptions::Service, "/bin/svcs does not exist!" + end + @status = service_status.enabled + @current_resource + end + + def enable_service + run_command(:command => "#{@init_command} enable #{@new_resource.service_name}") + return service_status.enabled + end + + def disable_service + run_command(:command => "#{@init_command} disable #{@new_resource.service_name}") + return service_status.enabled + end + + alias_method :stop_service, :disable_service + alias_method :start_service, :enable_service + + def reload_service + run_command(:command => "#{@init_command} refresh #{@new_resource.service_name}") + end + + def restart_service + disable_service + return enable_service + end + + def service_status + status = popen4("#{@status_command} #{@current_resource.service_name}") do |pid, stdin, stdout, stderr| + stdout.each do |line| + case line + when /state\s+online/ + @current_resource.enabled(true) + @current_resource.running(true) + end + end + end + unless @current_resource.enabled + @current_resource.enabled(false) + @current_resource.running(false) + end + @current_resource + end + + end + end + end +end diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb new file mode 100644 index 0000000000..59b4fe1564 --- /dev/null +++ b/lib/chef/provider/service/systemd.rb @@ -0,0 +1,115 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/simple' +require 'chef/mixin/command' + +class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + @status_check_success = true + + if @new_resource.status_command + 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 + @current_resource.running(true) + end + rescue Chef::Exceptions::Exec + @status_check_success = false + @current_resource.running(false) + @current_resource.enabled(false) + nil + end + else + @current_resource.running(is_active?) + end + + @current_resource.enabled(is_enabled?) + @current_resource + end + + def define_resource_requirements + shared_resource_requirements + requirements.assert(:all_actions) do |a| + a.assertion { @status_check_success } + # We won't stop in any case, but in whyrun warn and tell what we're doing. + a.whyrun ["Failed to determine status of #{@new_resource}, using command #{@new_resource.status_command}.", + "Assuming service would have been installed and is disabled"] + end + end + + def start_service + if @current_resource.running + Chef::Log.debug("#{@new_resource} already running, not starting") + else + if @new_resource.start_command + super + else + run_command_with_systems_locale(:command => "/bin/systemctl start #{@new_resource.service_name}") + end + end + end + + def stop_service + unless @current_resource.running + Chef::Log.debug("#{@new_resource} not running, not stopping") + else + if @new_resource.stop_command + super + else + run_command_with_systems_locale(:command => "/bin/systemctl stop #{@new_resource.service_name}") + end + end + end + + def restart_service + if @new_resource.restart_command + super + else + run_command_with_systems_locale(:command => "/bin/systemctl restart #{@new_resource.service_name}") + end + end + + def reload_service + if @new_resource.reload_command + super + else + run_command_with_systems_locale(:command => "/bin/systemctl reload #{@new_resource.service_name}") + end + end + + def enable_service + run_command_with_systems_locale(:command => "/bin/systemctl enable #{@new_resource.service_name}") + end + + def disable_service + run_command_with_systems_locale(:command => "/bin/systemctl disable #{@new_resource.service_name}") + end + + def is_active? + run_command_with_systems_locale({:command => "/bin/systemctl is-active #{@new_resource.service_name}", :ignore_failure => true}) == 0 + end + + def is_enabled? + run_command_with_systems_locale({:command => "/bin/systemctl is-enabled #{@new_resource.service_name}", :ignore_failure => true}) == 0 + end +end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb new file mode 100644 index 0000000000..763a2aa92b --- /dev/null +++ b/lib/chef/provider/service/upstart.rb @@ -0,0 +1,232 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2010 Bryan McLellan +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/service' +require 'chef/provider/service/simple' +require 'chef/mixin/command' +require 'chef/util/file_edit' + +class Chef + class Provider + class Service + class Upstart < Chef::Provider::Service::Simple + UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ + + # Upstart does more than start or stop a service, creating multiple 'states' [1] that a service can be in. + # In chef, when we ask a service to start, we expect it to have started before performing the next step + # since we have top down dependencies. Which is to say we may follow witha resource next that requires + # that service to be running. According to [2] we can trust that sending a 'goal' such as start will not + # return until that 'goal' is reached, or some error has occured. + # + # [1] http://upstart.ubuntu.com/wiki/JobStates + # [2] http://www.netsplit.com/2008/04/27/upstart-05-events/ + + def initialize(new_resource, run_context) + # TODO: re-evaluate if this is needed after integrating cookbook fix + raise ArgumentError, "run_context cannot be nil" unless run_context + super + + run_context.node + + @job = @new_resource.service_name + + if @new_resource.parameters + @new_resource.parameters.each do |key, value| + @job << " #{key}=#{value}" + end + end + + platform, version = Chef::Platform.find_platform_and_version(run_context.node) + if platform == "ubuntu" && (8.04..9.04).include?(version.to_f) + @upstart_job_dir = "/etc/event.d" + @upstart_conf_suffix = "" + else + @upstart_job_dir = "/etc/init" + @upstart_conf_suffix = ".conf" + end + + @command_success = true # new_resource.status_command= false, means upstart used + @config_file_found = true + @upstart_command_success = true + end + + def define_resource_requirements + # Do not call super, only call shared requirements + shared_resource_requirements + requirements.assert(:all_actions) do |a| + if !@command_success + whyrun_msg = @new_resource.status_command ? "Provided status command #{@new_resource.status_command} failed." : + "Could not determine upstart state for service" + end + a.assertion { @command_success } + # no failure here, just document the assumptions made. + a.whyrun "#{whyrun_msg} Assuming service installed and not running." + end + + requirements.assert(:all_actions) do |a| + a.assertion { @config_file_found } + # no failure here, just document the assumptions made. + a.whyrun "Could not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}. Assuming service is disabled." + end + end + + def load_current_resource + @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource.service_name(@new_resource.service_name) + + # Get running/stopped state + # We do not support searching for a service via ps when using upstart since status is a native + # upstart function. We will however support status_command in case someone wants to do something special. + if @new_resource.status_command + 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 + @current_resource.running true + end + rescue Chef::Exceptions::Exec + @command_success = false + @current_resource.running false + nil + end + else + begin + if upstart_state == "running" + @current_resource.running true + else + @current_resource.running false + end + rescue Chef::Exceptions::Exec + @command_success = false + @current_resource.running false + nil + end + end + # Get enabled/disabled state by reading job configuration file + if ::File.exists?("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + Chef::Log.debug("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}",'r') do |file| + while line = file.gets + case line + when /^start on/ + Chef::Log.debug("#{@new_resource} enabled: #{line.chomp}") + @current_resource.enabled true + break + when /^#start on/ + Chef::Log.debug("#{@new_resource} disabled: #{line.chomp}") + @current_resource.enabled false + break + end + end + end + else + @config_file_found = false + Chef::Log.debug("#{@new_resource} did not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + @current_resource.enabled false + end + + @current_resource + end + + def start_service + # Calling start on a service that is already started will return 1 + # Our 'goal' when we call start is to ensure the service is started + if @current_resource.running + Chef::Log.debug("#{@new_resource} already running, not starting") + else + if @new_resource.start_command + super + else + run_command_with_systems_locale(:command => "/sbin/start #{@job}") + end + end + end + + def stop_service + # Calling stop on a service that is already stopped will return 1 + # Our 'goal' when we call stop is to ensure the service is stopped + unless @current_resource.running + Chef::Log.debug("#{@new_resource} not running, not stopping") + else + if @new_resource.stop_command + super + else + run_command_with_systems_locale(:command => "/sbin/stop #{@job}") + end + end + end + + def restart_service + if @new_resource.restart_command + super + # Upstart always provides restart functionality so we don't need to mimic it with stop/sleep/start. + # Older versions of upstart would fail on restart if the service was currently stopped, check for that. LP:430883 + else @new_resource.supports[:restart] + if @current_resource.running + run_command_with_systems_locale(:command => "/sbin/restart #{@job}") + else + start_service + end + end + end + + def reload_service + if @new_resource.reload_command + super + else + # upstart >= 0.6.3-4 supports reload (HUP) + run_command_with_systems_locale(:command => "/sbin/reload #{@job}") + end + end + + # https://bugs.launchpad.net/upstart/+bug/94065 + + def enable_service + Chef::Log.debug("#{@new_resource} upstart lacks inherent support for enabling services, editing job config file") + conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + conf.search_file_replace(/^#start on/, "start on") + conf.write_file + end + + def disable_service + Chef::Log.debug("#{@new_resource} upstart lacks inherent support for disabling services, editing job config file") + conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + conf.search_file_replace(/^start on/, "#start on") + conf.write_file + end + + def upstart_state + command = "/sbin/status #{@job}" + status = popen4(command) do |pid, stdin, stdout, stderr| + stdout.each_line do |line| + # rsyslog stop/waiting + # service goal/state + # OR + # rsyslog (stop) waiting + # service (goal) state + line =~ UPSTART_STATE_FORMAT + data = Regexp.last_match + return data[2] + end + end + end + + end + end + end +end diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb new file mode 100644 index 0000000000..ba51e53bed --- /dev/null +++ b/lib/chef/provider/service/windows.rb @@ -0,0 +1,163 @@ +# +# Author:: Nuo Yan <nuo@opscode.com> +# Author:: Bryan McLellan <btm@loftninjas.org> +# Author:: Seth Chisamore <schisamo@opscode.com> +# Copyright:: Copyright (c) 2010-2011 Opscode, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' +require 'chef/provider/service/simple' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'win32/service' +end + +class Chef::Provider::Service::Windows < Chef::Provider::Service + + include Chef::Mixin::ShellOut + + RUNNING = 'running' + STOPPED = 'stopped' + AUTO_START = 'auto start' + DISABLED = 'disabled' + + def whyrun_supported? + false + end + + def load_current_resource + @current_resource = Chef::Resource::Service.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) + Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}" + @current_resource + end + + def start_service + if Win32::Service.exists?(@new_resource.service_name) + if current_state == RUNNING + Chef::Log.debug "#{@new_resource} already started - nothing to do" + else + if @new_resource.start_command + Chef::Log.debug "#{@new_resource} starting service using the given start_command" + shell_out!(@new_resource.start_command) + else + spawn_command_thread do + Win32::Service.start(@new_resource.service_name) + wait_for_state(RUNNING) + end + end + @new_resource.updated_by_last_action(true) + end + else + Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + end + end + + def stop_service + if Win32::Service.exists?(@new_resource.service_name) + if current_state == RUNNING + if @new_resource.stop_command + Chef::Log.debug "#{@new_resource} stopping service using the given stop_command" + shell_out!(@new_resource.stop_command) + else + spawn_command_thread do + Win32::Service.stop(@new_resource.service_name) + wait_for_state(STOPPED) + end + end + @new_resource.updated_by_last_action(true) + else + Chef::Log.debug "#{@new_resource} already stopped - nothing to do" + end + else + Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + end + end + + def restart_service + if Win32::Service.exists?(@new_resource.service_name) + if @new_resource.restart_command + Chef::Log.debug "#{@new_resource} restarting service using the given restart_command" + shell_out!(@new_resource.restart_command) + else + stop_service + start_service + end + @new_resource.updated_by_last_action(true) + else + Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + end + end + + 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 + else + Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + end + end + + 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 + else + Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + end + end + + private + def current_state + Win32::Service.status(@new_resource.service_name).current_state + end + + def start_type + Win32::Service.config_info(@new_resource.service_name).start_type + end + + # Helper method that waits for a status to change its state since state + # changes aren't usually instantaneous. + def wait_for_state(desired_state) + sleep 1 until current_state == desired_state + end + + # There ain't no party like a thread party... + def spawn_command_thread + worker = Thread.new do + yield + end + Timeout.timeout(60) do + worker.join + end + end +end diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb new file mode 100644 index 0000000000..e1f87b4dd8 --- /dev/null +++ b/lib/chef/provider/subversion.rb @@ -0,0 +1,214 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +#TODO subversion and git should both extend from a base SCM provider. + +require 'chef/log' +require 'chef/provider' +require 'chef/mixin/command' +require 'fileutils' + +class Chef + class Provider + class Subversion < Chef::Provider + + SVN_INFO_PATTERN = /^([\w\s]+): (.+)$/ + + include Chef::Mixin::Command + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::Subversion.new(@new_resource.name) + + unless [:export, :force_export].include?(Array(@new_resource.action).first) + if current_revision = find_current_revision + @current_resource.revision current_revision + end + end + end + + def define_resource_requirements + requirements.assert(:all_actions) do |a| + # Make sure the parent dir exists, or else fail. + # for why run, print a message explaining the potential error. + parent_directory = ::File.dirname(@new_resource.destination) + a.assertion { ::File.directory?(parent_directory) } + a.failure_message(Chef::Exceptions::MissingParentDirectory, + "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{parent_directory} does not exist") + a.whyrun("Directory #{parent_directory} does not exist, assuming it would have been created") + end + end + + 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)) + end + else + Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do" + end + end + + def action_export + if target_dir_non_existent_or_empty? + action_force_export + else + Chef::Log.debug "#{@new_resource} export destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do" + end + end + + def action_force_export + converge_by("export #{@new_resource.repository} into #{@new_resource.destination}") do + run_command(run_options(:command => export_command)) + end + end + + def action_sync + assert_target_directory_valid! + if ::File.exist?(::File.join(@new_resource.destination, ".svn")) + current_rev = find_current_revision + 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)) + Chef::Log.info "#{@new_resource} updated to revision: #{revision_int}" + end + end + else + action_checkout + end + end + + 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 + 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 + end + + def export_command + args = ["--force"] + args << @new_resource.svn_arguments << verbose << authentication << + "-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 + end + + # If the specified revision isn't an integer ("HEAD" for example), look + # up the revision id by asking the server + # If the specified revision is an integer, trust it. + def revision_int + @revision_int ||= begin + if @new_resource.revision =~ /^\d+$/ + @new_resource.revision + else + command = scm(:info, @new_resource.repository, @new_resource.svn_info_args, authentication, "-r#{@new_resource.revision}") + status, svn_info, error_message = output_of_command(command, run_options) + handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}") + extract_revision_info(svn_info) + end + end + end + + alias :revision_slug :revision_int + + def find_current_revision + return nil unless ::File.exist?(::File.join(@new_resource.destination, ".svn")) + command = scm(:info) + status, svn_info, error_message = output_of_command(command, run_options(:cwd => cwd)) + + unless [0,1].include?(status.exitstatus) + handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}") + end + extract_revision_info(svn_info) + end + + def current_revision_matches_target_revision? + (!@current_resource.revision.nil?) && (revision_int.strip.to_i == @current_resource.revision.strip.to_i) + end + + def run_options(run_opts={}) + run_opts[:user] = @new_resource.user if @new_resource.user + run_opts[:group] = @new_resource.group if @new_resource.group + run_opts + end + + private + + def cwd + @new_resource.destination + end + + def verbose + "-q" + end + + def extract_revision_info(svn_info) + repo_attrs = svn_info.lines.inject({}) do |attrs, line| + if line =~ SVN_INFO_PATTERN + property, value = $1, $2 + attrs[property] = value + end + attrs + end + rev = (repo_attrs['Last Changed Rev'] || repo_attrs['Revision']) + raise "Could not parse `svn info` data: #{svn_info}" if repo_attrs.empty? + Chef::Log.debug "#{@new_resource} resolved revision #{@new_resource.revision} to #{rev}" + rev + end + + # If a username is configured for the SCM, return the command-line + # switches for that. Note that we don't need to return the password + # switch, since Capistrano will check for that prompt in the output + # and will respond appropriately. + def authentication + return "" unless @new_resource.svn_username + result = "--username #{@new_resource.svn_username} " + result << "--password #{@new_resource.svn_password} " + result + end + + def scm(*args) + ['svn', *args].compact.join(" ") + end + + + def target_dir_non_existent_or_empty? + !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..'] + end + def assert_target_directory_valid! + target_parent_directory = ::File.dirname(@new_resource.destination) + unless ::File.directory?(target_parent_directory) + msg = "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{target_parent_directory} does not exist" + raise Chef::Exceptions::MissingParentDirectory, msg + end + end + end + end +end diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb new file mode 100644 index 0000000000..c937b9d980 --- /dev/null +++ b/lib/chef/provider/template.rb @@ -0,0 +1,117 @@ +#-- +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/file' +require 'chef/mixin/template' +require 'chef/mixin/checksum' +require 'chef/file_access_control' + +class Chef + class Provider + + class Template < Chef::Provider::File + + include Chef::Mixin::Checksum + include Chef::Mixin::Template + + def load_current_resource + @current_resource = Chef::Resource::Template.new(@new_resource.name) + super + end + + def define_resource_requirements + super + + requirements.assert(:create, :create_if_missing) do |a| + a.assertion { ::File::exist?(template_location) } + a.failure_message "Template source #{template_location} could not be found." + a.whyrun "Template source #{template_location} does not exist. Assuming it would have been created." + a.block_action! + end + end + + def action_create + render_with_context(template_location) do |rendered_template| + rendered(rendered_template) + update = ::File.exist?(@new_resource.path) + if update && content_matches? + Chef::Log.debug("#{@new_resource} content has not changed.") + set_all_access_controls + else + description = [] + action_message = update ? "update #{@current_resource} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(@new_resource.checksum)}" : + "create #{@new_resource}" + description << action_message + description << diff_current(rendered_template.path) + converge_by(description) do + backup + FileUtils.mv(rendered_template.path, @new_resource.path) + Chef::Log.info("#{@new_resource} updated content") + access_controls.set_all! + stat = ::File.stat(@new_resource.path) + + # template depends on the checksum not changing, and updates it + # itself later in the code, so we cannot set it here, as we do with + # all other < File child provider classes + @new_resource.owner(stat.uid) + @new_resource.mode(stat.mode & 07777) + @new_resource.group(stat.gid) + end + end + end + end + + + def template_location + @template_file_cache_location ||= begin + if @new_resource.local + @new_resource.source + else + cookbook = run_context.cookbook_collection[resource_cookbook] + cookbook.preferred_filename_on_disk_location(node, :templates, @new_resource.source) + end + end + end + + def resource_cookbook + @new_resource.cookbook || @new_resource.cookbook_name + end + + def rendered(rendered_template) + @new_resource.checksum(checksum(rendered_template.path)) + Chef::Log.debug("Current content's checksum: #{@current_resource.checksum}") + Chef::Log.debug("Rendered content's checksum: #{@new_resource.checksum}") + end + + def content_matches? + @current_resource.checksum == @new_resource.checksum + end + + private + + def render_with_context(template_location, &block) + context = {} + context.merge!(@new_resource.variables) + context[:node] = node + render_template(IO.read(template_location), context, &block) + end + + end + end +end diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb new file mode 100644 index 0000000000..e73c9de57e --- /dev/null +++ b/lib/chef/provider/user.rb @@ -0,0 +1,207 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider' +require 'chef/mixin/command' +require 'chef/resource/user' +require 'etc' + +class Chef + class Provider + class User < Chef::Provider + + include Chef::Mixin::Command + + attr_accessor :user_exists, :locked + + def initialize(new_resource, run_context) + super + @user_exists = true + @locked = nil + @shadow_lib_ok = true + @group_name_resolved = true + end + + def convert_group_name + if @new_resource.gid.is_a? String + @new_resource.gid(Etc.getgrnam(@new_resource.gid).gid) + end + rescue ArgumentError => e + @group_name_resolved = false + end + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::User.new(@new_resource.name) + @current_resource.username(@new_resource.username) + + begin + user_info = Etc.getpwnam(@new_resource.username) + rescue ArgumentError => e + @user_exists = false + Chef::Log.debug("#{@new_resource} user does not exist") + user_info = nil + end + + if user_info + @current_resource.uid(user_info.uid) + @current_resource.gid(user_info.gid) + @current_resource.comment(user_info.gecos) + @current_resource.home(user_info.dir) + @current_resource.shell(user_info.shell) + @current_resource.password(user_info.passwd) + + if @new_resource.password && @current_resource.password == 'x' + begin + require 'shadow' + rescue LoadError + @shadow_lib_ok = false + else + shadow_info = Shadow::Passwd.getspnam(@new_resource.username) + @current_resource.password(shadow_info.sp_pwdp) + end + end + + if @new_resource.gid + convert_group_name + end + end + + @current_resource + end + + def define_resource_requirements + requirements.assert(:all_actions) do |a| + a.assertion { @group_name_resolved } + a.failure_message Chef::Exceptions::User, "Couldn't lookup integer GID for group name #{@new_resource.gid}" + a.whyrun "group name #{@new_resource.gid} does not exist. This will cause group assignment to fail. Assuming this group will have been created previously." + end + requirements.assert(:all_actions) do |a| + a.assertion { @shadow_lib_ok } + a.failure_message Chef::Exceptions::MissingLibrary, "You must have ruby-shadow installed for password support!" + a.whyrun "ruby-shadow is not installed. Attempts to set user password will cause failure. Assuming that this gem will have been previously installed." + + "Note that user update converge may report false-positive on the basis of mismatched password. " + end + requirements.assert(:modify, :lock, :unlock) do |a| + a.assertion { @user_exists } + a.failure_message(Chef::Exceptions::User, "Cannot modify user #{@new_resource} - does not exist!") + a.whyrun("Assuming user #{@new_resource} would have been created") + end + end + + # Check to see if the user needs any changes + # + # === Returns + # <true>:: If a change is required + # <false>:: If the users are identical + def compare_user + [ :uid, :gid, :comment, :home, :shell, :password ].any? do |user_attrib| + !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib) + end + end + + def action_create + + if !@user_exists + converge_by("create user #{@new_resource}") do + create_user + Chef::Log.info("#{@new_resource} created") + end + elsif compare_user + converge_by("alter user #{@new_resource}") do + manage_user + Chef::Log.info("#{@new_resource} altered") + end + end + end + + def action_remove + if @user_exists + converge_by("remove user #{@new_resource}") do + remove_user + Chef::Log.info("#{@new_resource} removed") + end + end + end + + def remove_user + raise NotImplementedError + end + + def action_manage + if @user_exists && compare_user + converge_by("manage user #{@new_resource}") do + manage_user + Chef::Log.info("#{@new_resource} managed") + end + end + end + + def manage_user + raise NotImplementedError + end + + def action_modify + if compare_user + converge_by("modify user #{@new_resource}") do + manage_user + Chef::Log.info("#{@new_resource} modified") + end + end + end + + def action_lock + if check_lock() == false + converge_by("lock the user #{@new_resource}") do + lock_user + Chef::Log.info("#{@new_resource} locked") + end + else + Chef::Log.debug("#{@new_resource} already locked - nothing to do") + end + end + + def check_lock + raise NotImplementedError + end + + def lock_user + raise NotImplementedError + end + + def action_unlock + if check_lock() == true + converge_by("unlock user #{@new_resource}") do + unlock_user + Chef::Log.info("#{@new_resource} unlocked") + end + else + Chef::Log.debug("#{@new_resource} already unlocked - nothing to do") + end + end + + def unlock_user + raise NotImplementedError + end + + end + end +end diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb new file mode 100644 index 0000000000..94e8420c43 --- /dev/null +++ b/lib/chef/provider/user/dscl.rb @@ -0,0 +1,288 @@ +# +# Author:: Dreamcat4 (<dreamcat4@gmail.com>) +# Copyright:: Copyright (c) 2009 OpsCode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' +require 'chef/provider/user' +require 'openssl' + +class Chef + class Provider + class User + class Dscl < Chef::Provider::User + include Chef::Mixin::ShellOut + + NFS_HOME_DIRECTORY = %r{^NFSHomeDirectory: (.*)$} + AUTHENTICATION_AUTHORITY = %r{^AuthenticationAuthority: (.*)$} + + 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 + 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 + + # get a free UID greater than 200 + 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) + if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n") + next_uid_guess += 1 + else + uid = next_uid_guess + break + end + end + return uid || raise("uid not found. Exhausted. Searched #{search_limit} times") + end + + def uid_used?(uid) + return false unless uid + users_uids = safe_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") + end + safe_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + end + + def modify_home + return safe_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") if (@new_resource.home.nil? || @new_resource.home.empty?) + if @new_resource.supports[:manage_home] + validate_home_dir_specification! + + if (@current_resource.home == @new_resource.home) && !new_home_exists? + ditto_home + elsif !current_home_exists? && !new_home_exists? + ditto_home + elsif current_home_exists? + move_home + end + end + safe_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") + end + + def osx_shadow_hash?(string) + return !! ( string =~ /^[[:xdigit:]]{1240}$/ ) + end + + def osx_salted_sha1?(string) + return !! ( string =~ /^[[:xdigit:]]{48}$/ ) + end + + def guid + safe_dscl("read /Users/#{@new_resource.username} GeneratedUID").gsub(/GeneratedUID: /,"").strip + end + + def shadow_hash_set? + user_data = safe_dscl("read /Users/#{@new_resource.username}") + if user_data =~ /AuthenticationAuthority: / && user_data =~ /ShadowHash/ + true + else + false + end + end + + def modify_password + if @new_resource.password + shadow_hash = nil + + Chef::Log.debug("#{new_resource} updating password") + if osx_shadow_hash?(@new_resource.password) + shadow_hash = @new_resource.password.upcase + 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 + end + + ::File.open("/var/db/shadow/hash/#{guid}",'w',0600) do |output| + output.puts shadow_hash + end + + unless shadow_hash_set? + safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';ShadowHash;'") + end + end + 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") + end + + def create_user + dscl_create_user + dscl_create_comment + set_uid + dscl_set_gid + modify_home + dscl_set_shell + modify_password + 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) + end + + def dscl_create_user + safe_dscl("create /Users/#{@new_resource.username}") + end + + def dscl_create_comment + safe_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") + 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}'") + 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}'") + else + safe_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") + 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) + 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}") + 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 + end + end + + def check_lock + return @locked = locked? + end + + def lock_user + safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") + 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}'") + 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 + end + + def current_home_exists? + ::File.exist?("#{@current_resource.home}") + end + + 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 diverged?(parameter) + parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) + end + + def parameter_updated?(parameter) + not (@new_resource.send(parameter) == @current_resource.send(parameter)) + end + end + end + end +end diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb new file mode 100644 index 0000000000..4f6393da89 --- /dev/null +++ b/lib/chef/provider/user/pw.rb @@ -0,0 +1,113 @@ +# +# Author:: Stephen Haynes (<sh@nomitor.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/user' + +class Chef + class Provider + class User + class Pw < Chef::Provider::User + + def load_current_resource + super + raise Chef::Exceptions::User, "Could not find binary /usr/sbin/pw for #{@new_resource}" unless ::File.exists?("/usr/sbin/pw") + end + + def create_user + command = "pw useradd" + command << set_options + run_command(:command => command) + modify_password + end + + def manage_user + command = "pw usermod" + command << set_options + run_command(:command => command) + modify_password + end + + def remove_user + command = "pw userdel #{@new_resource.username}" + command << " -r" if @new_resource.supports[:manage_home] + run_command(:command => command) + end + + def check_lock + case @current_resource.password + when /^\*LOCKED\*/ + @locked = true + else + @locked = false + end + @locked + end + + def lock_user + run_command(:command => "pw lock #{@new_resource.username}") + end + + def unlock_user + run_command(:command => "pw unlock #{@new_resource.username}") + end + + def set_options + opts = " #{@new_resource.username}" + + field_list = { + 'comment' => "-c", + 'home' => "-d", + 'gid' => "-g", + 'uid' => "-u", + 'shell' => "-s" + } + field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option| + field_symbol = field.to_sym + if @current_resource.send(field_symbol) != @new_resource.send(field_symbol) + if @new_resource.send(field_symbol) + Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}") + opts << " #{option} '#{@new_resource.send(field_symbol)}'" + end + end + end + if @new_resource.supports[:manage_home] + Chef::Log.debug("#{@new_resource} is managing the users home directory") + opts << " -m" + end + opts + end + + def modify_password + if @current_resource.password != @new_resource.password + Chef::Log.debug("#{new_resource} updating password") + command = "pw usermod #{@new_resource.username} -H 0" + status = popen4(command, :waitlast => true) do |pid, stdin, stdout, stderr| + stdin.puts "#{@new_resource.password}" + end + + unless status.exitstatus == 0 + raise Chef::Exceptions::User, "pw failed - #{status.inspect}!" + end + else + Chef::Log.debug("#{new_resource} no change needed to password") + end + end + end + end + end +end diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb new file mode 100644 index 0000000000..489632f722 --- /dev/null +++ b/lib/chef/provider/user/useradd.rb @@ -0,0 +1,144 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'pathname' +require 'chef/provider/user' + +class Chef + class Provider + class User + class Useradd < Chef::Provider::User + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]] + + def create_user + command = compile_command("useradd") do |useradd| + useradd << universal_options + useradd << useradd_options + end + run_command(:command => command) + end + + def manage_user + command = compile_command("usermod") do |u| + u << universal_options + end + run_command(:command => command) + end + + def remove_user + command = "userdel" + command << " -r" if managing_home_dir? + command << " #{@new_resource.username}" + run_command(:command => command) + end + + def check_lock + status = popen4("passwd -S #{@new_resource.username}") do |pid, stdin, stdout, stderr| + status_line = stdout.gets.split(' ') + case status_line[1] + when /^P/ + @locked = false + when /^N/ + @locked = false + when /^L/ + @locked = true + end + end + + unless status.exitstatus == 0 + raise_lock_error = false + # we can get an exit code of 1 even when it's successful on rhel/centos (redhat bug 578534) + if status.exitstatus == 1 && ['redhat', 'centos'].include?(node[:platform]) + passwd_version_status = popen4('rpm -q passwd') do |pid, stdin, stdout, stderr| + passwd_version = stdout.gets.chomp + + unless passwd_version == 'passwd-0.73-1' + raise_lock_error = true + end + end + else + raise_lock_error = true + end + + raise Chef::Exceptions::User, "Cannot determine if #{@new_resource} is locked!" if raise_lock_error + end + + @locked + end + + def lock_user + run_command(:command => "usermod -L #{@new_resource.username}") + end + + def unlock_user + run_command(:command => "usermod -U #{@new_resource.username}") + end + + def compile_command(base_command) + yield base_command + base_command << " #{@new_resource.username}" + base_command + end + + def universal_options + opts = '' + + UNIVERSAL_OPTIONS.each do |field, option| + if @current_resource.send(field) != @new_resource.send(field) + if @new_resource.send(field) + Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field)}") + opts << " #{option} '#{@new_resource.send(field)}'" + end + end + end + if updating_home? + if managing_home_dir? + Chef::Log.debug("#{@new_resource} managing the users home directory") + opts << " -m -d '#{@new_resource.home}'" + else + Chef::Log.debug("#{@new_resource} setting home to #{@new_resource.home}") + opts << " -d '#{@new_resource.home}'" + end + end + opts << " -o" if @new_resource.non_unique || @new_resource.supports[:non_unique] + opts + end + + def useradd_options + opts = '' + opts << " -r" if @new_resource.system + opts + end + + def updating_home? + # will return false if paths are equivalent + # Pathname#cleanpath does a better job than ::File::expand_path (on both unix and windows) + # ::File.expand_path("///tmp") == ::File.expand_path("/tmp") => false + # ::File.expand_path("\\tmp") => "C:/tmp" + return true if @current_resource.home.nil? && @new_resource.home + @new_resource.home and Pathname.new(@current_resource.home).cleanpath != Pathname.new(@new_resource.home).cleanpath + end + + def managing_home_dir? + @new_resource.manage_home || @new_resource.supports[:manage_home] + end + + end + end + end +end diff --git a/lib/chef/provider/user/windows.rb b/lib/chef/provider/user/windows.rb new file mode 100644 index 0000000000..6bbb2a088c --- /dev/null +++ b/lib/chef/provider/user/windows.rb @@ -0,0 +1,124 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/user' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/util/windows/net_user' +end + +class Chef + class Provider + class User + class Windows < Chef::Provider::User + + def initialize(new_resource,run_context) + super + @net_user = Chef::Util::Windows::NetUser.new(@new_resource.name) + end + + def load_current_resource + @current_resource = Chef::Resource::User.new(@new_resource.name) + @current_resource.username(@new_resource.username) + 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]) + end + + @current_resource + end + + # Check to see if the user needs any changes + # + # === Returns + # <true>:: If a change is required + # <false>:: If the users are identical + def compare_user + unless @net_user.validate_credentials(@new_resource.password) + Chef::Log.debug("#{@new_resource} password has changed") + return true + end + [ :uid, :gid, :comment, :home, :shell ].any? do |user_attrib| + !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib) + end + end + + def create_user + @net_user.add(set_options) + end + + def manage_user + @net_user.update(set_options) + end + + def remove_user + @net_user.delete + end + + def check_lock + @net_user.check_enabled + end + + def lock_user + @net_user.disable_account + end + + def unlock_user + @net_user.enable_account + end + + def set_options + opts = {:name => @new_resource.username} + + field_list = { + 'comment' => 'full_name', + 'home' => 'home_dir', + 'gid' => 'primary_group_id', + 'uid' => 'user_id', + 'shell' => 'script_path', + 'password' => 'password' + } + + field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option| + field_symbol = field.to_sym + if @current_resource.send(field_symbol) != @new_resource.send(field_symbol) + if @new_resource.send(field_symbol) + unless field_symbol == :password + Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}") + end + opts[option.to_sym] = @new_resource.send(field_symbol) + end + end + end + opts + end + + end + end + end +end |