diff --git a/lib/puppet/provider/package/openbsd.rb b/lib/puppet/provider/package/openbsd.rb index acf9b42f0..b11a816bd 100644 --- a/lib/puppet/provider/package/openbsd.rb +++ b/lib/puppet/provider/package/openbsd.rb @@ -1,228 +1,242 @@ require 'puppet/provider/package' # Packaging on OpenBSD. Doesn't work anywhere else that I know of. Puppet::Type.type(:package).provide :openbsd, :parent => Puppet::Provider::Package do desc "OpenBSD's form of `pkg_add` support. This provider supports the `install_options` and `uninstall_options` attributes, which allow command-line flags to be passed to pkg_add and pkg_delete. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." commands :pkginfo => "pkg_info", :pkgadd => "pkg_add", :pkgdelete => "pkg_delete" defaultfor :operatingsystem => :openbsd confine :operatingsystem => :openbsd has_feature :versionable has_feature :install_options has_feature :uninstall_options has_feature :upgradeable def self.instances packages = [] begin execpipe(listcmd) do |process| # our regex for matching pkg_info output regex = /^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ fields = [:name, :ensure, :flavor ] hash = {} # now turn each returned line into a package object process.each_line { |line| if match = regex.match(line.split[0]) fields.zip(match.captures) { |field,value| hash[field] = value } hash[:provider] = self.name packages << new(hash) hash = {} else unless line =~ /Updating the pkgdb/ # Print a warning on lines we can't match, but move # on, since it should be non-fatal warning("Failed to match line #{line}") end end } end return packages rescue Puppet::ExecutionFailure return nil end end def self.listcmd [command(:pkginfo), "-a"] end def latest parse_pkgconf if @resource[:source][-1,1] == ::File::SEPARATOR e_vars = { 'PKG_PATH' => @resource[:source] } else e_vars = {} end if @resource[:flavor] query = "#{@resource[:name]}--#{@resource[:flavor]}" else query = @resource[:name] end output = Puppet::Util.withenv(e_vars) {pkginfo "-Q", query} if output.nil? or output.size == 0 or output =~ /Error from / debug "Failed to query for #{resource[:name]}" return properties[:ensure] else # Remove all fuzzy matches first. output = output.split.select {|p| p =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*)/ }.join debug "pkg_info -Q for #{resource[:name]}: #{output}" end if output =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*) \(installed\)$/ debug "Package is already the latest available" return properties[:ensure] else match = /^(.*)-(\d[^-]*)[-]?(\w*)$/.match(output) debug "Latest available for #{resource[:name]}: #{match[2]}" if properties[:ensure].to_sym == :absent return match[2] end vcmp = properties[:ensure].split('.').map{|s|s.to_i} <=> match[2].split('.').map{|s|s.to_i} if vcmp > 0 debug "ensure: #{properties[:ensure]}" # The locally installed package may actually be newer than what a mirror # has. Log it at debug, but ignore it otherwise. debug "Package #{resource[:name]} #{properties[:ensure]} newer then available #{match[2]}" return properties[:ensure] else return match[2] end end end def update self.install(true) end def parse_pkgconf unless @resource[:source] if Puppet::FileSystem.exist?("/etc/pkg.conf") File.open("/etc/pkg.conf", "rb").readlines.each do |line| if matchdata = line.match(/^installpath\s*=\s*(.+)\s*$/i) @resource[:source] = matchdata[1] elsif matchdata = line.match(/^installpath\s*\+=\s*(.+)\s*$/i) if @resource[:source].nil? @resource[:source] = matchdata[1] else @resource[:source] += ":" + matchdata[1] end end end unless @resource[:source] raise Puppet::Error, "No valid installpath found in /etc/pkg.conf and no source was set" end else raise Puppet::Error, "You must specify a package source or configure an installpath in /etc/pkg.conf" end end end def install(latest = false) cmd = [] parse_pkgconf if @resource[:source][-1,1] == ::File::SEPARATOR e_vars = { 'PKG_PATH' => @resource[:source] } - # In case of a real update (i.e., the package already exists) then - # pkg_add(8) can handle the flavors. However, if we're actually - # installing with 'latest', we do need to handle the flavors. - # So we always need to handle flavors ourselves as to not break installs. - if latest and resource[:flavor] - full_name = "#{resource[:name]}--#{resource[:flavor]}" - elsif latest - # Don't depend on get_version for updates. - full_name = @resource[:name] - else - full_name = [ @resource[:name], get_version || @resource[:ensure], @resource[:flavor] ].join('-').chomp('-').chomp('-') - end + full_name = get_full_name(latest) else e_vars = {} full_name = @resource[:source] end cmd << install_options cmd << full_name if latest cmd.unshift('-rz') end Puppet::Util.withenv(e_vars) { pkgadd cmd.flatten.compact } end + def get_full_name(latest = false) + # In case of a real update (i.e., the package already exists) then + # pkg_add(8) can handle the flavors. However, if we're actually + # installing with 'latest', we do need to handle the flavors. This is + # done so we can feed pkg_add(8) the full package name to install to + # prevent ambiguity. + if latest && resource[:flavor] + "#{resource[:name]}--#{resource[:flavor]}" + elsif latest + # Don't depend on get_version for updates. + @resource[:name] + else + # If :ensure contains a version, use that instead of looking it up. + # This allows for installing packages with the same stem, but multiple + # version such as openldap-server. + if /(\d[^-]*)$/.match(@resource[:ensure].to_s) + use_version = @resource[:ensure] + else + use_version = get_version + end + + [ @resource[:name], use_version, @resource[:flavor]].join('-').gsub(/-+$/, '') + end + end + def get_version execpipe([command(:pkginfo), "-I", @resource[:name]]) do |process| # our regex for matching pkg_info output regex = /^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ master_version = 0 version = -1 process.each_line do |line| if match = regex.match(line.split[0]) # now we return the first version, unless ensure is latest version = match.captures[1] return version unless @resource[:ensure] == "latest" master_version = version unless master_version > version end end return master_version unless master_version == 0 return '' if version == -1 raise Puppet::Error, "#{version} is not available for this package" end rescue Puppet::ExecutionFailure return nil end def query # Search for the version info if pkginfo(@resource[:name]) =~ /Information for (inst:)?#{@resource[:name]}-(\S+)/ return { :ensure => $2 } else return nil end end def install_options join_options(resource[:install_options]) end def uninstall_options join_options(resource[:uninstall_options]) end def uninstall pkgdelete uninstall_options.flatten.compact, @resource[:name] end def purge pkgdelete "-c", "-q", @resource[:name] end end diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb index 712f9cfda..7b16c289b 100755 --- a/spec/unit/provider/package/openbsd_spec.rb +++ b/spec/unit/provider/package/openbsd_spec.rb @@ -1,369 +1,403 @@ #! /usr/bin/env ruby require 'spec_helper' require 'stringio' provider_class = Puppet::Type.type(:package).provider(:openbsd) describe provider_class do let(:package) { Puppet::Type.type(:package).new(:name => 'bash', :provider => 'openbsd') } let(:provider) { provider_class.new(package) } def expect_read_from_pkgconf(lines) pkgconf = stub(:readlines => lines) Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(true) File.expects(:open).with('/etc/pkg.conf', 'rb').returns(pkgconf) end def expect_pkgadd_with_source(source) provider.expects(:pkgadd).with do |fullname| ENV.should_not be_key('PKG_PATH') fullname.should == [source] end end def expect_pkgadd_with_env_and_name(source, &block) ENV.should_not be_key('PKG_PATH') provider.expects(:pkgadd).with do |fullname| ENV.should be_key('PKG_PATH') ENV['PKG_PATH'].should == source fullname.should == [provider.resource[:name]] end provider.expects(:execpipe).with(['/bin/pkg_info', '-I', provider.resource[:name]]).yields('') yield ENV.should_not be_key('PKG_PATH') end describe 'provider features' do it { should be_installable } it { should be_install_options } it { should be_uninstallable } it { should be_uninstall_options } it { should be_upgradeable } it { should be_versionable } end before :each do # Stub some provider methods to avoid needing the actual software # installed, so we can test on whatever platform we want. provider_class.stubs(:command).with(:pkginfo).returns('/bin/pkg_info') provider_class.stubs(:command).with(:pkgadd).returns('/bin/pkg_add') provider_class.stubs(:command).with(:pkgdelete).returns('/bin/pkg_delete') end context "#instances" do it "should return nil if execution failed" do provider_class.expects(:execpipe).raises(Puppet::ExecutionFailure, 'wawawa') provider_class.instances.should be_nil end it "should return the empty set if no packages are listed" do provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(StringIO.new('')) provider_class.instances.should be_empty end it "should return all packages when invoked" do fixture = File.read(my_fixture('pkginfo.list')) provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(fixture) provider_class.instances.map(&:name).sort.should == %w{bash bzip2 expat gettext libiconv lzo openvpn python vim wget}.sort end it "should return all flavors if set" do fixture = File.read(my_fixture('pkginfo_flavors.list')) provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(fixture) instances = provider_class.instances.map {|p| {:name => p.get(:name), :ensure => p.get(:ensure), :flavor => p.get(:flavor)}} instances.size.should == 2 instances[0].should == {:name => 'bash', :ensure => '3.1.17', :flavor => 'static'} instances[1].should == {:name => 'vim', :ensure => '7.0.42', :flavor => 'no_x11'} end end context "#install" do it "should fail if the resource doesn't have a source" do Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(false) expect { provider.install }.to raise_error(Puppet::Error, /must specify a package source/) end it "should fail if /etc/pkg.conf exists, but is not readable" do Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(true) File.expects(:open).with('/etc/pkg.conf', 'rb').raises(Errno::EACCES) expect { provider.install }.to raise_error(Errno::EACCES, /Permission denied/) end it "should fail if /etc/pkg.conf exists, but there is no installpath" do expect_read_from_pkgconf([]) expect { provider.install }.to raise_error(Puppet::Error, /No valid installpath found in \/etc\/pkg\.conf and no source was set/) end it "should install correctly when given a directory-unlike source" do source = '/whatever.tgz' provider.resource[:source] = source expect_pkgadd_with_source(source) provider.install end it "should install correctly when given a directory-like source" do source = '/whatever/' provider.resource[:source] = source expect_pkgadd_with_env_and_name(source) do provider.install end end it "should install correctly when given a CDROM installpath" do dir = '/mnt/cdrom/5.2/packages/amd64/' expect_read_from_pkgconf(["installpath = #{dir}"]) expect_pkgadd_with_env_and_name(dir) do provider.install end end it "should install correctly when given a ftp mirror" do url = 'ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/' expect_read_from_pkgconf(["installpath = #{url}"]) expect_pkgadd_with_env_and_name(url) do provider.install end end it "should set the resource's source parameter" do url = 'ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/' expect_read_from_pkgconf(["installpath = #{url}"]) expect_pkgadd_with_env_and_name(url) do provider.install end provider.resource[:source].should == url end it "should strip leading whitespace in installpath" do dir = '/one/' lines = ["# Notice the extra spaces after the ='s\n", "installpath = #{dir}\n", "# And notice how each line ends with a newline\n"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(dir) do provider.install end end it "should not require spaces around the equals" do dir = '/one/' lines = ["installpath=#{dir}"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(dir) do provider.install end end it "should be case-insensitive" do dir = '/one/' lines = ["INSTALLPATH = #{dir}"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(dir) do provider.install end end it "should ignore unknown keywords" do dir = '/one/' lines = ["foo = bar\n", "installpath = #{dir}\n"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(dir) do provider.install end end it "should preserve trailing spaces" do dir = '/one/ ' lines = ["installpath = #{dir}"] expect_read_from_pkgconf(lines) expect_pkgadd_with_source(dir) provider.install end it "should append installpath" do urls = ["ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/", "http://another.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/"] lines = ["installpath = #{urls[0]}\n", "installpath += #{urls[1]}\n"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(urls.join(":")) do provider.install end end it "should handle append on first installpath" do url = "ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/" lines = ["installpath += #{url}\n"] expect_read_from_pkgconf(lines) expect_pkgadd_with_env_and_name(url) do provider.install end end %w{ installpath installpath= installpath+=}.each do |line| it "should reject '#{line}'" do expect_read_from_pkgconf([line]) expect { provider.install }.to raise_error(Puppet::Error, /No valid installpath found in \/etc\/pkg\.conf and no source was set/) end end it 'should use install_options as Array' do provider.resource[:source] = '/tma1/' provider.resource[:install_options] = ['-r', '-z'] provider.expects(:pkgadd).with(['-r', '-z', 'bash']) provider.install end end context "#latest" do before do provider.resource[:source] = '/tmp/tcsh.tgz' provider.resource[:name] = 'tcsh' provider.stubs(:pkginfo).with('tcsh') end it "should return the ensure value if the package is already installed" do provider.stubs(:properties).returns({:ensure => '4.2.45'}) provider.stubs(:pkginfo).with('-Q', 'tcsh') provider.latest.should == '4.2.45' end it "should recognize a new version" do pkginfo_query = 'tcsh-6.18.01p1' provider.stubs(:pkginfo).with('-Q', 'tcsh').returns(pkginfo_query) provider.latest.should == '6.18.01p1' end it "should recognize a newer version" do provider.stubs(:properties).returns({:ensure => '1.6.8'}) pkginfo_query = 'tcsh-1.6.10' provider.stubs(:pkginfo).with('-Q', 'tcsh').returns(pkginfo_query) provider.latest.should == '1.6.10' end it "should recognize a package that is already the newest" do pkginfo_query = 'tcsh-6.18.01p0 (installed)' provider.stubs(:pkginfo).with('-Q', 'tcsh').returns(pkginfo_query) provider.latest.should == '6.18.01p0' end end + context "#get_full_name" do + it "should return the full unversioned package name when updating with a flavor" do + provider.resource[:ensure] = 'latest' + provider.resource[:flavor] = 'static' + provider.get_full_name.should == 'bash--static' + end + + it "should return the full unversioned package name when updating without a flavor" do + provider.resource[:name] = 'puppet' + provider.resource[:ensure] = 'latest' + provider.get_full_name.should == 'puppet' + end + + it "should use the ensure parameter if it is numeric" do + provider.resource[:name] = 'zsh' + provider.resource[:ensure] = '1.0' + provider.get_full_name.should == 'zsh-1.0' + end + + it "should lookup the correct version" do + output = 'bash-3.1.17 GNU Bourne Again Shell' + provider.expects(:execpipe).with(%w{/bin/pkg_info -I bash}).yields(output) + provider.get_full_name.should == 'bash-3.1.17' + end + + it "should lookup the correction version with flavors" do + provider.resource[:name] = 'fossil' + provider.resource[:flavor] = 'static' + output = 'fossil-1.29v0-static simple distributed software configuration management' + provider.expects(:execpipe).with(%w{/bin/pkg_info -I fossil}).yields(output) + provider.get_full_name.should == 'fossil-1.29v0-static' + end + end + context "#get_version" do it "should return nil if execution fails" do provider.expects(:execpipe).raises(Puppet::ExecutionFailure, 'wawawa') provider.get_version.should be_nil end it "should return the package version if in the output" do output = 'bash-3.1.17 GNU Bourne Again Shell' provider.expects(:execpipe).with(%w{/bin/pkg_info -I bash}).yields(output) provider.get_version.should == '3.1.17' end it "should return the empty string if the package is not present" do provider.resource[:name] = 'zsh' provider.expects(:execpipe).with(%w{/bin/pkg_info -I zsh}).yields(StringIO.new('')) provider.get_version.should == '' end end context "#query" do it "should return the installed version if present" do fixture = File.read(my_fixture('pkginfo.detail')) provider.expects(:pkginfo).with('bash').returns(fixture) provider.query.should == { :ensure => '3.1.17' } end it "should return nothing if not present" do provider.resource[:name] = 'zsh' provider.expects(:pkginfo).with('zsh').returns('') provider.query.should be_nil end end context "#install_options" do it "should return nill by default" do provider.install_options.should be_nil end it "should return install_options when set" do provider.resource[:install_options] = ['-n'] provider.resource[:install_options].should == ['-n'] end it "should return multiple install_options when set" do provider.resource[:install_options] = ['-L', '/opt/puppet'] provider.resource[:install_options].should == ['-L', '/opt/puppet'] end it 'should return install_options when set as hash' do provider.resource[:install_options] = { '-Darch' => 'vax' } provider.install_options.should == ['-Darch=vax'] end end context "#uninstall_options" do it "should return nill by default" do provider.uninstall_options.should be_nil end it "should return uninstall_options when set" do provider.resource[:uninstall_options] = ['-n'] provider.resource[:uninstall_options].should == ['-n'] end it "should return multiple uninstall_options when set" do provider.resource[:uninstall_options] = ['-q', '-c'] provider.resource[:uninstall_options].should == ['-q', '-c'] end it 'should return uninstall_options when set as hash' do provider.resource[:uninstall_options] = { '-Dbaddepend' => '1' } provider.uninstall_options.should == ['-Dbaddepend=1'] end end context "#uninstall" do describe 'when uninstalling' do it 'should use erase to purge' do provider.expects(:pkgdelete).with('-c', '-q', 'bash') provider.purge end end describe 'with uninstall_options' do it 'should use uninstall_options as Array' do provider.resource[:uninstall_options] = ['-q', '-c'] provider.expects(:pkgdelete).with(['-q', '-c'], 'bash') provider.uninstall end end end end