diff options
Diffstat (limited to 'lib/chef/provider/package')
-rw-r--r-- | lib/chef/provider/package/apt.rb | 147 | ||||
-rw-r--r-- | lib/chef/provider/package/dpkg.rb | 128 | ||||
-rw-r--r-- | lib/chef/provider/package/easy_install.rb | 136 | ||||
-rw-r--r-- | lib/chef/provider/package/freebsd.rb | 149 | ||||
-rw-r--r-- | lib/chef/provider/package/ips.rb | 101 | ||||
-rw-r--r-- | lib/chef/provider/package/macports.rb | 105 | ||||
-rw-r--r-- | lib/chef/provider/package/pacman.rb | 111 | ||||
-rw-r--r-- | lib/chef/provider/package/portage.rb | 138 | ||||
-rw-r--r-- | lib/chef/provider/package/rpm.rb | 121 | ||||
-rw-r--r-- | lib/chef/provider/package/rubygems.rb | 548 | ||||
-rw-r--r-- | lib/chef/provider/package/smartos.rb | 84 | ||||
-rw-r--r-- | lib/chef/provider/package/solaris.rb | 139 | ||||
-rw-r--r-- | lib/chef/provider/package/yum-dump.py | 287 | ||||
-rw-r--r-- | lib/chef/provider/package/yum.rb | 1214 | ||||
-rw-r--r-- | lib/chef/provider/package/zypper.rb | 144 |
15 files changed, 3552 insertions, 0 deletions
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 |