diff --git a/lib/puppet/provider/package/pkg.rb b/lib/puppet/provider/package/pkg.rb index 93592d8fa..67385b5a6 100644 --- a/lib/puppet/provider/package/pkg.rb +++ b/lib/puppet/provider/package/pkg.rb @@ -1,201 +1,201 @@ require 'puppet/provider/package' Puppet::Type.type(:package).provide :pkg, :parent => Puppet::Provider::Package do desc "OpenSolaris image packaging system. See pkg(5) for more information" # http://docs.oracle.com/cd/E19963-01/html/820-6572/managepkgs.html # A few notes before we start : # Opensolaris pkg has two slightly different formats (as of now.) # The first one is what is distributed with the Solaris 11 Express 11/10 dvd # The latest one is what you get when you update package. # To make things more interesting, pkg version just returns a sha sum. # dvd: pkg version => 052adf36c3f4 # updated: pkg version => 630e1ffc7a19 # Thankfully, solaris has not changed the commands to be used. # TODO: We still have to allow packages to specify a preferred publisher. has_feature :versionable has_feature :upgradable has_feature :holdable commands :pkg => "/usr/bin/pkg" confine :osfamily => :solaris defaultfor :osfamily => :solaris, :kernelrelease => '5.11' def self.instances pkg(:list, '-H').split("\n").map{|l| new(parse_line(l))} end # The IFO flag field is just what it names, the first field can have ether # i_nstalled or -, and second field f_rozen or -, and last # o_bsolate or r_rename or - # so this checks if the installed field is present, and also verifies that # if not the field is -, else we dont know what we are doing and exit with # out doing more damage. def self.ifo_flag(flags) ( case flags[0..0] when 'i' {:status => 'installed'} when '-' {:status => 'known'} else raise ArgumentError, 'Unknown format %s: %s[%s]' % [self.name, flags, flags[0..0]] end ).merge( case flags[1..1] when 'f' {:ensure => 'held'} when '-' {} else raise ArgumentError, 'Unknown format %s: %s[%s]' % [self.name, flags, flags[1..1]] end ) end # The UFOXI field is the field present in the older pkg # (solaris 2009.06 - snv151a) # similar to IFO, UFOXI is also an either letter or - # u_pdate indicates that an update for the package is available. # f_rozen(n/i) o_bsolete x_cluded(n/i) i_constrained(n/i) # note that u_pdate flag may not be trustable due to constraints. # so we dont rely on it # Frozen was never implemented in UFOXI so skipping frozen here. def self.ufoxi_flag(flags) {} end # pkg state was present in the older version of pkg (with UFOXI) but is # no longer available with the IFO field version. When it was present, # it was used to indicate that a particular version was present (installed) # and later versions were known. Note that according to the pkg man page, # known never lists older versions of the package. So we can rely on this # field to make sure that if a known is present, then the pkg is upgradable. def self.pkg_state(state) case state when /installed/ {:status => 'installed'} when /known/ {:status => 'known'} else raise ArgumentError, 'Unknown format %s: %s' % [self.name, state] end end # Here is (hopefully) the only place we will have to deal with multiple # formats of output for different pkg versions. def self.parse_line(line) (case line.chomp # NAME (PUBLISHER) VERSION IFO (new:630e1ffc7a19) # system/core-os 0.5.11-0.169 i-- when /^(\S+) +(\S+) +(...)$/ {:name => $1, :ensure => $2}.merge ifo_flag($3) # x11/wm/fvwm (fvwm.org) 2.6.1-3 i-- when /^(\S+) \((.+)\) +(\S+) +(...)$/ {:name => $1, :publisher => $2, :ensure => $3}.merge ifo_flag($4) # NAME (PUBLISHER) VERSION STATE UFOXI (dvd:052adf36c3f4) # SUNWcs 0.5.11-0.126 installed ----- when /^(\S+) +(\S+) +(\S+) +(.....)$/ {:name => $1, :ensure => $2}.merge pkg_state($3).merge(ufoxi_flag($4)) # web/firefox/plugin/flash (extra) 10.0.32.18-0.111 installed ----- when /^(\S+) \((.+)\) +(\S+) +(\S+) +(.....)$/ {:name => $1, :publisher => $2, :ensure => $3}.merge pkg_state($4).merge(ufoxi_flag($5)) else raise ArgumentError, 'Unknown line format %s: %s' % [self.name, line] end).merge({:provider => self.name}) end def hold pkg(:freeze, @resource[:name]) end def unhold r = exec_cmd(command(:pkg), 'unfreeze', @resource[:name]) raise Puppet::Error, "Unable to unfreeze #{r[:out]}" unless [0,4].include? r[:exit] end # Return the version of the package. Note that the bug # http://defect.opensolaris.org/bz/show_bug.cgi?id=19159% # notes that we can't use -Ha for the same even though the manual page reads that way. def latest lines = pkg(:list, "-Hn", @resource[:name]).split("\n") # remove certificate expiration warnings from the output, but report them # Note: we'd like to use select! here to modify the lines array and avoid # the second select further down. But Solaris 11 comes with ruby 1.8.7 # which doesn't support select!, so do this as two selects. cert_warnings = lines.select { |line| line =~ /^Certificate/ } unless cert_warnings.empty? - Puppet.warning("pkg warning: #{cert_warnings}") + Puppet.warning("pkg warning: #{cert_warnings.join(', ')}") end lst = lines.select { |line| line !~ /^Certificate/ }.map { |line| self.class.parse_line(line) } # Now we know there is a newer version. But is that installable? (i.e are there any constraints?) # return the first known we find. The only way that is currently available is to do a dry run of # pkg update and see if could get installed (`pkg update -n res`). known = lst.find {|p| p[:status] == 'known' } return known[:ensure] if known and exec_cmd(command(:pkg), 'update', '-n', @resource[:name])[:exit].zero? # If not, then return the installed, else nil (lst.find {|p| p[:status] == 'installed' } || {})[:ensure] end # install the package and accept all licenses. def install(nofail = false) name = @resource[:name] should = @resource[:ensure] # always unhold if explicitly told to install/update self.unhold unless should.is_a? Symbol name += "@#{should}" is = self.query unless is[:ensure].to_sym == :absent self.uninstall if Puppet::Util::Package.versioncmp(should, is[:ensure]) < 0 end end r = exec_cmd(command(:pkg), 'install', '--accept', name) return r if nofail raise Puppet::Error, "Unable to update #{r[:out]}" if r[:exit] != 0 end # uninstall the package. The complication comes from the -r_ecursive flag which is no longer # present in newer package version. def uninstall cmd = [:uninstall] case (pkg :version).chomp when /052adf36c3f4/ cmd << '-r' end cmd << @resource[:name] pkg cmd end # update the package to the latest version available def update r = install(true) # 4 == /No updates available for this image./ return if [0,4].include? r[:exit] raise Puppet::Error, "Unable to update #{r[:out]}" end # list a specific package def query r = exec_cmd(command(:pkg), 'list', '-H', @resource[:name]) return {:ensure => :absent, :name => @resource[:name]} if r[:exit] != 0 self.class.parse_line(r[:out]) end def exec_cmd(*cmd) output = Puppet::Util::Execution.execute(cmd, :failonfail => false, :combine => true) {:out => output, :exit => $CHILD_STATUS.exitstatus} end end diff --git a/spec/unit/provider/package/pkg_spec.rb b/spec/unit/provider/package/pkg_spec.rb index 092817ad9..ee0ac7778 100755 --- a/spec/unit/provider/package/pkg_spec.rb +++ b/spec/unit/provider/package/pkg_spec.rb @@ -1,301 +1,301 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:package).provider(:pkg) do let (:resource) { Puppet::Resource.new(:package, 'dummy', :parameters => {:name => 'dummy', :ensure => :latest}) } let (:provider) { described_class.new(resource) } before :each do described_class.stubs(:command).with(:pkg).returns('/bin/pkg') end def self.it_should_respond_to(*actions) actions.each do |action| it "should respond to :#{action}" do provider.should respond_to(action) end end end it_should_respond_to :install, :uninstall, :update, :query, :latest it "should be versionable" do described_class.should be_versionable end describe "#methods" do context ":pkg_state" do it "should raise error on unknown values" do expect { described_class.pkg_state('extra').should }.to raise_error(ArgumentError, /Unknown format/) end ['known', 'installed'].each do |k| it "should return known values" do described_class.pkg_state(k).should == {:status => k} end end end context ":ifo_flag" do it "should raise error on unknown values" do expect { described_class.ifo_flag('x--').should }.to raise_error(ArgumentError, /Unknown format/) end {'i--' => 'installed', '---'=> 'known'}.each do |k, v| it "should return known values" do described_class.ifo_flag(k).should == {:status => v} end end end context ":parse_line" do it "should raise error on unknown values" do expect { described_class.parse_line('pkg (mypkg) 1.2.3.4 i-- zzz').should }.to raise_error(ArgumentError, /Unknown line format/) end { 'spkg 0.0.7 i--' => {:name => 'spkg', :ensure => '0.0.7', :status => 'installed', :provider => :pkg}, 'spkg (me) 0.0.7 i--' => {:name => 'spkg', :ensure => '0.0.7', :status => 'installed', :provider => :pkg, :publisher => 'me'}, 'spkg (me) 0.0.7 if-' => {:name => 'spkg', :ensure => 'held', :status => 'installed', :provider => :pkg, :publisher => 'me'}, 'spkg 0.0.7 installed -----' => {:name => 'spkg', :ensure => '0.0.7', :status => 'installed', :provider => :pkg}, 'spkg (me) 0.0.7 installed -----' => {:name => 'spkg', :ensure => '0.0.7', :status => 'installed', :provider => :pkg, :publisher => 'me'}, }.each do |k, v| it "[#{k}] should correctly parse" do described_class.parse_line(k).should == v end end end context ":latest" do it "should work correctly for ensure latest on solaris 11 (UFOXI) when there are no further packages to install" do described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.installed')) provider.latest.should == "1.0.6-0.175.0.0.0.2.537" end it "should work correctly for ensure latest on solaris 11 in the presence of a certificate expiration warning" do described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.certificate_warning')) provider.latest.should == "1.0.6-0.175.0.0.0.2.537" end it "should work correctly for ensure latest on solaris 11(known UFOXI)" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'update', '-n', 'dummy'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 0 described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.known')) provider.latest.should == "1.0.6-0.175.0.0.0.2.537" end it "should work correctly for ensure latest on solaris 11 (IFO)" do described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.ifo.installed')) provider.latest.should == "1.0.6-0.175.0.0.0.2.537" end it "should work correctly for ensure latest on solaris 11(known IFO)" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'update', '-n', 'dummy'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 0 described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.ifo.known')) provider.latest.should == "1.0.6-0.175.0.0.0.2.537" end it "issues a warning when the certificate has expired" do warning = "Certificate '/var/pkg/ssl/871b4ed0ade09926e6adf95f86bf17535f987684' for publisher 'solarisstudio', needed to access 'https://pkg.oracle.com/solarisstudio/release/', will expire in '29' days." - Puppet.expects(:warning).with("pkg warning: [\"#{warning}\"]") + Puppet.expects(:warning).with("pkg warning: #{warning}") described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.certificate_warning')) provider.latest end it "doesn't issue a warning when the certificate hasn't expired" do Puppet.expects(:warning).with(/pkg warning/).never described_class.expects(:pkg).with(:list,'-Hn','dummy').returns File.read(my_fixture('dummy_solaris11.installed')) provider.latest end end context ":instances" do it "should correctly parse lines with preferred publisher" do described_class.expects(:pkg).with(:list,'-H').returns File.read(my_fixture('simple')) instances = described_class.instances.map { |p| {:name => p.get(:name), :ensure => p.get(:ensure)} } instances.size.should == 2 instances[0].should == {:name => 'SUNWdummy', :ensure => "2.5.5-0.111"} instances[1].should == {:name => 'dummy2', :ensure =>"9.3.6.1-0.111"} end it "should correctly parse lines with non preferred publisher" do described_class.expects(:pkg).with(:list,'-H').returns File.read(my_fixture('publisher')) instances = described_class.instances.map { |p| {:name => p.get(:name), :ensure => p.get(:ensure)} } instances.size.should == 2 instances[0].should == {:name => 'SUNWdummy', :ensure => "8.8-0.111"} instances[1].should == {:name => 'service/network/dummy', :ensure => "0.5.11-0.151.0.1"} end it "should correctly parse lines on solaris 11" do described_class.expects(:pkg).with(:list, '-H').returns File.read(my_fixture('solaris11')) described_class.expects(:warning).never instances = described_class.instances.map { |p| {:name => p.get(:name), :ensure => p.get(:ensure) }} instances.size.should == 2 instances[0].should == {:name => 'dummy/dummy', :ensure => "3.0-0.175.0.0.0.2.537"} instances[1].should == {:name => 'dummy/dummy2', :ensure => "1.8.1.2-0.175.0.0.0.2.537"} end it "should fail on incorrect lines" do fake_output = File.read(my_fixture('incomplete')) described_class.expects(:pkg).with(:list,'-H').returns fake_output expect { described_class.instances }.to raise_error(ArgumentError, /Unknown line format pkg/) end it "should fail on unknown package status" do described_class.expects(:pkg).with(:list,'-H').returns File.read(my_fixture('unknown_status')) expect { described_class.instances }.to raise_error(ArgumentError, /Unknown format pkg/) end end context ":query" do context "on solaris 10" do it "should find the package" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns File.read(my_fixture('dummy_solaris10')) $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.query.should == { :name => 'dummy', :ensure => "2.5.5-0.111", :status => "installed", :provider => :pkg, } end it "should return :absent when the package is not found" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 1 provider.query.should == {:ensure => :absent, :name => "dummy"} end end context "on solaris 11" do it "should find the package" do $CHILD_STATUS.stubs(:exitstatus).returns 0 Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns File.read(my_fixture('dummy_solaris11.installed')) provider.query.should == { :name => 'dummy', :status => 'installed', :ensure => "1.0.6-0.175.0.0.0.2.537", :provider => :pkg } end it "should return :absent when the package is not found" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 1 provider.query.should == {:ensure => :absent, :name => "dummy"} end end it "should return fail when the packageline cannot be parsed" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns(File.read(my_fixture('incomplete'))) $CHILD_STATUS.stubs(:exitstatus).returns 0 expect { provider.query }.to raise_error(ArgumentError, /Unknown line format/) end end context ":install" do it "should accept all licenses" do Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy'], {:failonfail => false, :combine => true}).returns '' Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.install end it "should install specific version(1)" do # Should install also check if the version installed is the same version we are asked to install? or should we rely on puppet for that? resource[:ensure] = '0.0.7' $CHILD_STATUS.stubs(:exitstatus).returns 0 Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}) Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns 'dummy 0.0.6 installed -----' Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy@0.0.7'], {:failonfail => false, :combine => true}).returns '' provider.install end it "should install specific version(2)" do resource[:ensure] = '0.0.8' Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}) Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'list', '-H', 'dummy'], {:failonfail => false, :combine => true}).returns 'dummy 0.0.7 installed -----' Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy@0.0.8'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.install end it "should install specific version(3)" do resource[:ensure] = '0.0.7' provider.expects(:query).with().returns({:ensure => '0.0.8'}) provider.expects(:uninstall).with() $CHILD_STATUS.stubs(:exitstatus).returns 0 Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}) Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy@0.0.7'], {:failonfail => false, :combine => true}).returns '' provider.install end it "should install any if version is not specified" do resource[:ensure] = :present Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy'], {:failonfail => false, :combine => true}).returns '' Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}) $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.install end it "should install if no version was previously installed, and a specific version was requested" do resource[:ensure] = '0.0.7' provider.expects(:query).with().returns({:ensure => :absent}) Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'unfreeze', 'dummy'], {:failonfail => false, :combine => true}) Puppet::Util::Execution.expects(:execute).with(['/bin/pkg', 'install', '--accept', 'dummy@0.0.7'], {:failonfail => false, :combine => true}).returns '' $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.install end end context ":update" do it "should not raise error if not necessary" do provider.expects(:install).with(true).returns({:exit => 0}) provider.update end it "should not raise error if not necessary (2)" do provider.expects(:install).with(true).returns({:exit => 4}) provider.update end it "should raise error if necessary" do provider.expects(:install).with(true).returns({:exit => 1}) expect { provider.update }.to raise_error(Puppet::Error, /Unable to update/) end end context ":uninstall" do it "should support current pkg version" do described_class.expects(:pkg).with(:version).returns('630e1ffc7a19') described_class.expects(:pkg).with([:uninstall, resource[:name]]) provider.uninstall end it "should support original pkg commands" do described_class.expects(:pkg).with(:version).returns('052adf36c3f4') described_class.expects(:pkg).with([:uninstall, '-r', resource[:name]]) provider.uninstall end end end end