diff --git a/lib/puppet/provider/package/rpm.rb b/lib/puppet/provider/package/rpm.rb index 2a3a69785..4a544fbb1 100644 --- a/lib/puppet/provider/package/rpm.rb +++ b/lib/puppet/provider/package/rpm.rb @@ -1,197 +1,197 @@ require 'puppet/provider/package' # RPM packaging. Should work anywhere that has rpm installed. Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Provider::Package do desc "RPM packaging support; should work anywhere with a working `rpm` binary. This provider supports the `install_options` attribute, which allows command-line flags to be passed to the RPM binary. Install options should be specified as an array, where each element is either a string or a `{'--flag' => 'value'}` hash. (That hash example would be equivalent to a `'--flag=value'` string; the hash syntax is available as a convenience.)" has_feature :versionable has_feature :install_options # Note: self:: is required here to keep these constants in the context of what will # eventually become this Puppet::Type::Package::ProviderRpm class. # The query format by which we identify installed packages self::NEVRA_FORMAT = %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n} self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$} self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch] commands :rpm => "rpm" if command('rpm') confine :true => begin rpm('--version') rescue Puppet::ExecutionFailure false else true end end def self.current_version return @current_version unless @current_version.nil? output = rpm "--version" @current_version = output.gsub('RPM version ', '').strip end # rpm < 4.1 don't support --nosignature def self.nosignature '--nosignature' unless Puppet::Util::Package.versioncmp(current_version, '4.1') < 0 end # rpm < 4.0.2 don't support --nodigest def self.nodigest '--nodigest' unless Puppet::Util::Package.versioncmp(current_version, '4.0.2') < 0 end def self.instances packages = [] # list out all of the packages begin execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process| # now turn each returned line into a package object process.each_line { |line| hash = nevra_to_hash(line) packages << new(hash) unless hash.empty? } } rescue Puppet::ExecutionFailure raise Puppet::Error, "Failed to list packages", $!.backtrace end packages end # Find the fully versioned package name and the version alone. Returns # a hash with entries :instance => fully versioned package name, and # :ensure => version-release def query #NOTE: Prior to a fix for issue 1243, this method potentially returned a cached value #IF YOU CALL THIS METHOD, IT WILL CALL RPM #Use get(:property) to check if cached values are available - cmd = ["-q", @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT] + cmd = ["-q", '--whatprovides', @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT] begin output = rpm(*cmd) rescue Puppet::ExecutionFailure # rpm -q exits 1 if package not found return nil end # FIXME: We could actually be getting back multiple packages # for multilib and this will only return the first such package @property_hash.update(self.class.nevra_to_hash(output)) @property_hash.dup end # Here we just retrieve the version from the file specified in the source. def latest unless source = @resource[:source] @resource.fail "RPMs must specify a package source" end cmd = [command(:rpm), "-q", "--qf", self.class::NEVRA_FORMAT, "-p", source] h = self.class.nevra_to_hash(execfail(cmd, Puppet::Error)) h[:ensure] end def install source = nil unless source = @resource[:source] @resource.fail "RPMs must specify a package source" end # RPM gets pissy if you try to install an already # installed package if @resource.should(:ensure) == @property_hash[:ensure] or @resource.should(:ensure) == :latest && @property_hash[:ensure] == latest return end flag = ["-i"] flag = ["-U", "--oldpackage"] if @property_hash[:ensure] and @property_hash[:ensure] != :absent flag = flag + install_options rpm flag, source end def uninstall query if get(:arch) == :absent nvr = "#{get(:name)}-#{get(:version)}-#{get(:release)}" arch = ".#{get(:arch)}" # If they specified an arch in the manifest, erase that Otherwise, # erase the arch we got back from the query. If multiple arches are # installed and only the package name is specified (without the # arch), this will uninstall all of them on successive runs of the # client, one after the other # version of RPM prior to 4.2.1 can't accept the architecture as # part of the package name. unless Puppet::Util::Package.versioncmp(self.class.current_version, '4.2.1') < 0 if @resource[:name][-arch.size, arch.size] == arch nvr += arch else nvr += ".#{get(:arch)}" end end rpm "-e", nvr end def update self.install end def install_options join_options(resource[:install_options]) end private # Turns a array of options into flags to be passed to rpm install(8) and # The options can be passed as a string or hash. Note that passing a hash # should only be used in case -Dfoo=bar must be passed, # which can be accomplished with: # install_options => [ { '-Dfoo' => 'bar' } ] # Regular flags like '-L' must be passed as a string. # @param options [Array] # @return Concatenated list of options # @api private def join_options(options) return [] unless options options.collect do |val| case val when Hash val.keys.sort.collect do |k| "#{k}=#{val[k]}" end.join(' ') else val end end end # @param line [String] one line of rpm package query information # @return [Hash] of NEVRA_FIELDS strings parsed from package info # or an empty hash if we failed to parse # @api private def self.nevra_to_hash(line) line.strip! hash = {} if match = self::NEVRA_REGEX.match(line) self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v } hash[:provider] = self.name hash[:ensure] = "#{hash[:version]}-#{hash[:release]}" else Puppet.debug("Failed to match rpm line #{line}") end return hash end end diff --git a/spec/unit/provider/package/aptrpm_spec.rb b/spec/unit/provider/package/aptrpm_spec.rb index 5db0a975e..081996d25 100755 --- a/spec/unit/provider/package/aptrpm_spec.rb +++ b/spec/unit/provider/package/aptrpm_spec.rb @@ -1,45 +1,45 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:package).provider(:aptrpm) do let :type do Puppet::Type.type(:package) end let :pkg do type.new(:name => 'faff', :provider => :aptrpm, :source => '/tmp/faff.rpm') end it { should be_versionable } context "when retrieving ensure" do before(:each) do Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm") pkg.provider.stubs(:which).with("rpm").returns("/bin/rpm") Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], {:combine => true, :custom_environment => {}, :failonfail => true}).returns("4.10.1\n").at_most_once end def rpm pkg.provider.expects(:rpm). - with('-q', 'faff', '--nosignature', '--nodigest', '--qf', + with('-q', '--whatprovides', 'faff', '--nosignature', '--nodigest', '--qf', "%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n") end it "should report absent packages" do rpm.raises(Puppet::ExecutionFailure, "couldn't find rpm") pkg.property(:ensure).retrieve.should == :absent end it "should report present packages correctly" do rpm.returns("faff-1.2.3-1 0 1.2.3-1 5 i686\n") pkg.property(:ensure).retrieve.should == "1.2.3-1-5" end end it "should try and install when asked" do pkg.provider.expects(:aptget). with('-q', '-y', 'install', 'faff'). returns(0) pkg.provider.install end it "should try and purge when asked" do pkg.provider.expects(:aptget).with('-y', '-q', 'remove', '--purge', 'faff').returns(0) pkg.provider.purge end end diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index 221a77e50..0504b7650 100755 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -1,322 +1,322 @@ #! /usr/bin/env ruby require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:rpm) describe provider_class do let (:packages) do <<-RPM_OUTPUT cracklib-dicts 0 2.8.9 3.3 x86_64 basesystem 0 8.0 5.1.1.el5.centos noarch chkconfig 0 1.3.30.2 2.el5 x86_64 myresource 0 1.2.3.4 5.el4 noarch mysummaryless 0 1.2.3.4 5.el4 noarch RPM_OUTPUT end let(:resource_name) { 'myresource' } let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, :ensure => :installed, :provider => 'rpm' ) end let(:provider) do provider = provider_class.new provider.resource = resource provider end let(:nevra_format) { %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n} } let(:execute_options) do {:failonfail => true, :combine => true, :custom_environment => {}} end let(:rpm_version) { "RPM version 5.0.0\n" } before(:each) do Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm") provider_class.stubs(:which).with("rpm").returns("/bin/rpm") provider_class.instance_variable_set("@current_version", nil) Puppet::Type::Package::ProviderRpm.expects(:execute).with(["/bin/rpm", "--version"]).returns(rpm_version).at_most_once Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], execute_options).returns(rpm_version).at_most_once end describe "self.instances" do describe "with a modern version of RPM" do it "includes all the modern flags" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end describe "with a version of RPM < 4.1" do let(:rpm_version) { "RPM version 4.0.2\n" } it "excludes the --nosignature flag" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end describe "with a version of RPM < 4.0.2" do let(:rpm_version) { "RPM version 3.0.5\n" } it "excludes the --nodigest flag" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end it "returns an array of packages" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances expect(installed_packages[0].properties).to eq( { :provider => :rpm, :name => "cracklib-dicts", :epoch => "0", :version => "2.8.9", :release => "3.3", :arch => "x86_64", :ensure => "2.8.9-3.3", } ) expect(installed_packages[1].properties).to eq( { :provider => :rpm, :name => "basesystem", :epoch => "0", :version => "8.0", :release => "5.1.1.el5.centos", :arch => "noarch", :ensure => "8.0-5.1.1.el5.centos", } ) expect(installed_packages[2].properties).to eq( { :provider => :rpm, :name => "chkconfig", :epoch => "0", :version => "1.3.30.2", :release => "2.el5", :arch => "x86_64", :ensure => "1.3.30.2-2.el5", } ) expect(installed_packages.last.properties).to eq( { :provider => :rpm, :name => "mysummaryless", :epoch => "0", :version => "1.2.3.4", :release => "5.el4", :arch => "noarch", :ensure => "1.2.3.4-5.el4", } ) end end describe "#install" do let(:resource) do Puppet::Type.type(:package).new( :name => 'myresource', :ensure => :installed, :source => '/path/to/package' ) end describe "when not already installed" do it "only includes the '-i' flag" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i"], '/path/to/package'], execute_options) provider.install end end describe "when installed with options" do let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, :ensure => :installed, :provider => 'rpm', :source => '/path/to/package', :install_options => ['-D', {'--test' => 'value'}, '-Q'] ) end it "includes the options" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i", "-D", "--test=value", "-Q"], '/path/to/package'], execute_options) provider.install end end describe "when an older version is installed" do before(:each) do # Force the provider to think a version of the package is already installed # This is real hacky. I'm sorry. --jeffweiss 25 Jan 2013 provider.instance_variable_get('@property_hash')[:ensure] = '1.2.3.3' end it "includes the '-U --oldpackage' flags" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-U", "--oldpackage"], '/path/to/package'], execute_options) provider.install end end end describe "#latest" do it "retrieves version string after querying rpm for version from source file" do resource.expects(:[]).with(:source).returns('source-string') Puppet::Util::Execution.expects(:execfail).with(["/bin/rpm", "-q", "--qf", nevra_format, "-p", "source-string"], Puppet::Error).returns("myresource 0 1.2.3.4 5.el4 noarch\n") expect(provider.latest).to eq("1.2.3.4-5.el4") end end describe "#uninstall" do let(:resource) do Puppet::Type.type(:package).new( :name => 'myresource', :ensure => :installed ) end describe "on a modern RPM" do before(:each) do - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") end let(:rpm_version) { "RPM version 4.10.0\n" } it "includes the architecture in the package name" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-e", 'myresource-1.2.3.4-5.el4.noarch'], execute_options).returns('').at_most_once provider.uninstall end end describe "on an ancient RPM" do before(:each) do - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '', '', '--qf', nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", "myresource", '', '', '--qf', nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") end let(:rpm_version) { "RPM version 3.0.6\n" } it "excludes the architecture from the package name" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-e", 'myresource-1.2.3.4-5.el4'], execute_options).returns('').at_most_once provider.uninstall end end end describe "parsing" do def parser_test(rpm_output_string, gold_hash, number_of_debug_logs = 0) Puppet.expects(:debug).times(number_of_debug_logs) - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).returns(rpm_output_string) + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).returns(rpm_output_string) expect(provider.query).to eq(gold_hash) end let(:resource_name) { 'name' } let('delimiter') { ':DESC:' } let(:package_hash) do { :name => 'name', :epoch => 'epoch', :version => 'version', :release => 'release', :arch => 'arch', :provider => :rpm, :ensure => 'version-release', } end let(:line) { 'name epoch version release arch' } ['name', 'epoch', 'version', 'release', 'arch'].each do |field| it "still parses if #{field} is replaced by delimiter" do parser_test( line.gsub(field, delimiter), package_hash.merge( field.to_sym => delimiter, :ensure => 'version-release'.gsub(field, delimiter) ) ) end end it "does not fail if line is unparseable, but issues a debug log" do parser_test('bad data', {}, 1) end it "does not log or fail if rpm returns package not found" do Puppet.expects(:debug).never - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).raises Puppet::ExecutionFailure.new('package not found') + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).raises Puppet::ExecutionFailure.new('package not found') expect(provider.query).to be_nil end end describe "#install_options" do it "returns empty array by default" do expect(provider.install_options).to eq([]) end it "returns install_options when set" do provider.resource[:install_options] = ['-n'] expect(provider.install_options).to eq(['-n']) end it "returns multiple install_options when set" do provider.resource[:install_options] = ['-L', '/opt/puppet'] expect(provider.install_options).to eq(['-L', '/opt/puppet']) end it 'returns install_options when set as hash' do provider.resource[:install_options] = { '-Darch' => 'vax' } expect(provider.install_options).to eq(['-Darch=vax']) end it 'returns install_options when an array with hashes' do provider.resource[:install_options] = [ '-L', { '-Darch' => 'vax' }] expect(provider.install_options).to eq(['-L', '-Darch=vax']) end end describe ".nodigest" do { '4.0' => nil, '4.0.1' => nil, '4.0.2' => '--nodigest', '4.0.3' => '--nodigest', '4.1' => '--nodigest', '5' => '--nodigest', }.each do |version, expected| describe "when current version is #{version}" do it "returns #{expected.inspect}" do provider_class.stubs(:current_version).returns(version) expect(provider_class.nodigest).to eq(expected) end end end end describe ".nosignature" do { '4.0.3' => nil, '4.1' => '--nosignature', '4.1.1' => '--nosignature', '4.2' => '--nosignature', '5' => '--nosignature', }.each do |version, expected| describe "when current version is #{version}" do it "returns #{expected.inspect}" do provider_class.stubs(:current_version).returns(version) expect(provider_class.nosignature).to eq(expected) end end end end end