diff --git a/lib/puppet/provider/package/msi.rb b/lib/puppet/provider/package/msi.rb index fc57b7cdb..ec516f795 100644 --- a/lib/puppet/provider/package/msi.rb +++ b/lib/puppet/provider/package/msi.rb @@ -1,82 +1,89 @@ require 'puppet/provider/package' Puppet::Type.type(:package).provide(:msi, :parent => Puppet::Provider::Package) do desc "Package management by installing and removing MSIs." confine :operatingsystem => :windows defaultfor :operatingsystem => :windows has_feature :install_options # This is just here to make sure we can find it, and fail if we # can't. Unfortunately, we need to do "special" quoting of the # install options or msiexec.exe won't know what to do with them, if # the value contains a space. commands :msiexec => "msiexec.exe" def self.instances Dir.entries(installed_listing_dir).reject {|d| d == '.' or d == '..'}.collect do |name| new(:name => File.basename(name, '.yml'), :provider => :msi, :ensure => :installed) end end def query {:name => resource[:name], :ensure => :installed} if FileTest.exists?(state_file) end def install properties_for_command = nil if resource[:install_options] properties_for_command = resource[:install_options].collect do |k,v| property = shell_quote k value = shell_quote v "#{property}=#{value}" end end # Unfortunately, we can't use the msiexec method defined earlier, # because of the special quoting we need to do around the MSI # properties to use. - execute ['msiexec.exe', '/qn', '/norestart', '/i', shell_quote(resource[:source]), properties_for_command].flatten.compact.join(' ') + execute ['msiexec.exe', '/qn', '/norestart', '/i', shell_quote(msi_source), properties_for_command].flatten.compact.join(' ') File.open(state_file, 'w') do |f| metadata = { 'name' => resource[:name], 'install_options' => resource[:install_options], - 'source' => resource[:source] + 'source' => msi_source } f.puts(YAML.dump metadata) end end def uninstall - msiexec '/qn', '/norestart', '/x', resource[:source] + msiexec '/qn', '/norestart', '/x', msi_source File.delete state_file end def validate_source(value) - fail("The source parameter is required when using the MSI provider.") if value.nil? fail("The source parameter cannot be empty when using the MSI provider.") if value.empty? end private + def msi_source + resource[:source] ||= YAML.load_file(state_file)['source'] rescue nil + + fail("The source parameter is required when using the MSI provider.") unless resource[:source] + + resource[:source] + end + def self.installed_listing_dir listing_dir = File.join(Puppet[:vardir], 'db', 'package', 'msi') FileUtils.mkdir_p listing_dir unless File.directory? listing_dir listing_dir end def state_file File.join(self.class.installed_listing_dir, "#{resource[:name]}.yml") end def shell_quote(value) value.include?(' ') ? %Q["#{value.gsub(/"/, '\"')}"] : value end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index 903834f6c..44f7d0ff5 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,336 +1,336 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the `source` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the `provider` parameter; each provider defines what it requires in order to function, and you must meet those requirements to use a given provider. **Autorequires:** If Puppet is managing the files specified as a package's `adminfile`, `responsefile`, or `source`, the package resource will autorequire those files." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying `latest` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." feature :holdable, "The provider is capable of placing packages on hold such that they are not automatically upgraded as a result of other package dependencies unless explicit action is taken by a user or another package. Held is considered a superset of installed.", :methods => [:hold] feature :install_options, "The provider accepts options to be passed to the installer command." ensurable do desc "What state the package should be in. *latest* only makes sense for those packaging formats that can retrieve new packages on their own and will throw an error on those that cannot. For those packaging systems that allow you to specify package versions, specify them here. Similarly, *purged* is only useful for packaging systems that support the notion of managing configuration files separately from 'normal' system files." attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end newvalue(:held, :event => :package_held, :required_features => :holdable) do provider.hold end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: #{detail}" end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: #{detail}" end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) @latest ||= nil @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged, :held].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: #{detail}") error.set_backtrace(detail.backtrace) raise error end end case is when @latest return true when :present # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}" end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged when is return true end } false end # This retrieves the current state. LAK: I think this method is unused. def retrieve provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you." + + validate do |value| + provider.validate_source(value) + end end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:type) do desc "Deprecated form of `provider`." munge do |value| warning "'type' is deprecated; use 'provider' instead" @resource[:provider] = value @resource[:provider] end end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in `/var/sadm/install/admin`." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end newparam(:flavor) do desc "Newer versions of OpenBSD support 'flavors', which are further specifications for which type of package you want." end newparam(:install_options, :required_features => :install_options) do desc "A hash of options to be handled by the provider when installing a package." end - validate do - provider.validate_source(self[:source]) - end - autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] if source =~ /^#{File::SEPARATOR}/ autos << source end end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end end end diff --git a/spec/unit/provider/package/msi_spec.rb b/spec/unit/provider/package/msi_spec.rb index 88e44efa5..d6384cf43 100644 --- a/spec/unit/provider/package/msi_spec.rb +++ b/spec/unit/provider/package/msi_spec.rb @@ -1,170 +1,170 @@ require 'spec_helper' describe 'Puppet::Provider::Package::Msi' do include PuppetSpec::Files before :each do Puppet::Type.type(:package).stubs(:defaultprovider).returns(Puppet::Type.type(:package).provider(:msi)) Puppet[:vardir] = tmpdir('msi') @state_dir = File.join(Puppet[:vardir], 'db', 'package', 'msi') end describe 'when installing' do it 'should create a state file' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.stubs(:execute) resource.provider.install File.should be_exists File.join(@state_dir, 'mysql-5.1.58-winx64.yml') end it 'should use the install_options as parameter/value pairs' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi', :install_options => { 'INSTALLDIR' => 'C:\mysql-here' } ) resource.provider.expects(:execute).with('msiexec.exe /qn /norestart /i E:\mysql-5.1.58-winx64.msi INSTALLDIR=C:\mysql-here') resource.provider.install end it 'should only quote the value when an install_options value has a space in it' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi', :install_options => { 'INSTALLDIR' => 'C:\mysql here' } ) resource.provider.expects(:execute).with('msiexec.exe /qn /norestart /i E:\mysql-5.1.58-winx64.msi INSTALLDIR="C:\mysql here"') resource.provider.install end it 'should escape embedded quotes in install_options values with spaces' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi', :install_options => { 'INSTALLDIR' => 'C:\mysql "here"' } ) resource.provider.expects(:execute).with('msiexec.exe /qn /norestart /i E:\mysql-5.1.58-winx64.msi INSTALLDIR="C:\mysql \"here\""') resource.provider.install end it 'should not create a state file, if the installation fails' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.stubs(:execute).raises(Puppet::ExecutionFailure.new("Execution of 'msiexec.exe' returned 128: Blargle")) expect { resource.provider.install }.to raise_error(Puppet::ExecutionFailure, /msiexec\.exe/) File.should_not be_exists File.join(@state_dir, 'mysql-5.1.58-winx64.yml') end it 'should fail if the source parameter is not set' do expect do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64' - ) + ).provider.install end.to raise_error(Puppet::Error, /The source parameter is required when using the MSI provider/) end it 'should fail if the source parameter is empty' do expect do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => '' ) end.to raise_error(Puppet::Error, /The source parameter cannot be empty when using the MSI provider/) end end describe 'when uninstalling' do before :each do FileUtils.mkdir_p(@state_dir) File.open(File.join(@state_dir, 'mysql-5.1.58-winx64.yml'), 'w') {|f| f.puts 'Hello'} end it 'should remove the state file' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.stubs(:msiexec) resource.provider.uninstall File.should_not be_exists File.join(Puppet[:vardir], 'db', 'package', 'msi', 'mysql-5.1.58-winx64.yml') end it 'should leave the state file if uninstalling fails' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.stubs(:msiexec).raises(Puppet::ExecutionFailure.new("Execution of 'msiexec.exe' returned 128: Blargle")) expect { resource.provider.uninstall }.to raise_error(Puppet::ExecutionFailure, /msiexec\.exe/) File.should be_exists File.join(@state_dir, 'mysql-5.1.58-winx64.yml') end it 'should fail if the source parameter is not set' do expect do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64' - ) + ).provider.install end.to raise_error(Puppet::Error, /The source parameter is required when using the MSI provider/) end it 'should fail if the source parameter is empty' do expect do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => '' ) end.to raise_error(Puppet::Error, /The source parameter cannot be empty when using the MSI provider/) end end describe 'when enumerating instances' do it 'should consider the base of the state file name to be the name of the package' do FileUtils.mkdir_p(@state_dir) package_names = ['GoogleChromeStandaloneEnterprise', 'mysql-5.1.58-winx64', 'postgresql-8.3'] package_names.each do |state_file| File.open(File.join(@state_dir, "#{state_file}.yml"), 'w') {|f| f.puts 'Hello'} end installed_package_names = Puppet::Type.type(:package).provider(:msi).instances.collect {|p| p.name} installed_package_names.should =~ package_names end end it 'should consider the package installed if the state file is present' do FileUtils.mkdir_p(@state_dir) File.open(File.join(@state_dir, 'mysql-5.1.58-winx64.yml'), 'w') {|f| f.puts 'Hello'} resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.query.should == { :name => 'mysql-5.1.58-winx64', :ensure => :installed } end it 'should consider the package absent if the state file is missing' do resource = Puppet::Type.type(:package).new( :name => 'mysql-5.1.58-winx64', :source => 'E:\mysql-5.1.58-winx64.msi' ) resource.provider.query.should be_nil end end