summaryrefslogtreecommitdiff
path: root/lib/chef/provider
diff options
context:
space:
mode:
authorSalim Alam <salam@chef.io>2015-12-08 10:17:11 -0800
committerSalim Alam <salam@chef.io>2015-12-08 10:17:11 -0800
commit6a9cec48804f097ce55d2934f97ea4409890506a (patch)
treefeb19eedf7151c47fb5db7b831a07cba8b155670 /lib/chef/provider
parent5982661df7c09bbdadd5d2b8431f2a7bbba05c0d (diff)
parent3e704d162e3ef5dff9e929eca7c82b48c4d66305 (diff)
downloadchef-6a9cec48804f097ce55d2934f97ea4409890506a.tar.gz
Merge pull request #4193 from chef/mwrock/package
adds support for installer types inno, nsis, wise and installshield to windows_package resource
Diffstat (limited to 'lib/chef/provider')
-rw-r--r--lib/chef/provider/package/windows.rb109
-rw-r--r--lib/chef/provider/package/windows/exe.rb129
-rw-r--r--lib/chef/provider/package/windows/msi.rb50
-rw-r--r--lib/chef/provider/package/windows/registry_uninstall_entry.rb89
4 files changed, 350 insertions, 27 deletions
diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb
index 7ff0b71807..ad2a855f2e 100644
--- a/lib/chef/provider/package/windows.rb
+++ b/lib/chef/provider/package/windows.rb
@@ -32,11 +32,7 @@ class Chef
provides :package, os: "windows"
provides :windows_package, os: "windows"
- # Depending on the installer, we may need to examine installer_type or
- # source attributes, or search for text strings in the installer file
- # binary to determine the installer type for the user. Since the file
- # must be on disk to do so, we have to make this choice in the provider.
- require 'chef/provider/package/windows/msi.rb'
+ require 'chef/provider/package/windows/registry_uninstall_entry.rb'
# load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode?
def load_current_resource
@@ -56,24 +52,64 @@ class Chef
@package_provider ||= begin
case installer_type
when :msi
- Chef::Provider::Package::Windows::MSI.new(resource_for_provider)
+ Chef::Log.debug("#{@new_resource} is MSI")
+ require 'chef/provider/package/windows/msi'
+ Chef::Provider::Package::Windows::MSI.new(resource_for_provider, uninstall_registry_entries)
else
- raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'"
+ Chef::Log.debug("#{@new_resource} is EXE with type '#{installer_type}'")
+ require 'chef/provider/package/windows/exe'
+ Chef::Provider::Package::Windows::Exe.new(resource_for_provider, installer_type, uninstall_registry_entries)
end
end
end
def installer_type
+ # Depending on the installer, we may need to examine installer_type or
+ # source attributes, or search for text strings in the installer file
+ # binary to determine the installer type for the user. Since the file
+ # must be on disk to do so, we have to make this choice in the provider.
@installer_type ||= begin
if @new_resource.installer_type
@new_resource.installer_type
- else
- file_extension = ::File.basename(@new_resource.source).split(".").last.downcase
+ elsif source_location.nil?
+ inferred_registry_type
+ else
+ basename = ::File.basename(source_location)
+ file_extension = basename.split(".").last.downcase
if file_extension == "msi"
:msi
else
- raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'"
+ # search the binary file for installer type
+ ::Kernel.open(::File.expand_path(source_location), 'rb') do |io|
+ filesize = io.size
+ bufsize = 4096 # read 4K buffers
+ overlap = 16 # bytes to overlap between buffer reads
+
+ until io.eof
+ contents = io.read(bufsize)
+
+ case contents
+ when /inno/i # Inno Setup
+ return :inno
+ when /wise/i # Wise InstallMaster
+ return :wise
+ when /nullsoft/i # Nullsoft Scriptable Install System
+ return :nsis
+ end
+
+ if (io.tell() < filesize)
+ io.seek(io.tell() - overlap)
+ end
+ end
+ end
+
+ # if file is named 'setup.exe' assume installshield
+ if basename == 'setup.exe'
+ :installshield
+ else
+ fail Chef::Exceptions::CannotDetermineWindowsInstallerType, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'"
+ end
end
end
end
@@ -93,11 +129,11 @@ class Chef
# Chef::Provider::Package action_install + action_remove call install_package + remove_package
# Pass those calls to the correct sub-provider
def install_package(name, version)
- package_provider.install_package(name, version)
+ package_provider.install_package
end
def remove_package(name, version)
- package_provider.remove_package(name, version)
+ package_provider.remove_package
end
# @return [Array] new_version(s) as an array
@@ -106,15 +142,59 @@ class Chef
[new_resource.version]
end
+ # @return [String] candidate_version
+ def candidate_version
+ @candidate_version ||= (@new_resource.version || 'latest')
+ end
+
+ # @return [Array] current_version(s) as an array
+ # this package provider does not support package arrays
+ # However, There may be multiple versions for a single
+ # package so the first element may be a nested array
+ def current_version_array
+ [ current_resource.version ]
+ end
+
+ # @param current_version<String> one or more versions currently installed
+ # @param new_version<String> version of the new resource
+ #
+ # @return [Boolean] true if new_version is equal to or included in current_version
+ def target_version_already_installed?(current_version, new_version)
+ Chef::Log.debug("Checking if #{@new_resource} version '#{new_version}' is already installed. #{current_version} is currently installed")
+ if current_version.is_a?(Array)
+ current_version.include?(new_version)
+ else
+ new_version == current_version
+ end
+ end
+
+ def have_any_matching_version?
+ target_version_already_installed?(current_resource.version, new_resource.version)
+ end
+
private
+ def uninstall_registry_entries
+ @uninstall_registry_entries ||= Chef::Provider::Package::Windows::RegistryUninstallEntry.find_entries(new_resource.name)
+ end
+
+ def inferred_registry_type
+ uninstall_registry_entries.each do |entry|
+ return :inno if entry.key.end_with?("_is1")
+ return :msi if entry.uninstall_string.downcase.start_with?("msiexec.exe ")
+ return :nsis if entry.uninstall_string.downcase.end_with?("uninst.exe\"")
+ end
+ nil
+ end
+
def downloadable_file_missing?
uri_scheme?(new_resource.source) && !::File.exists?(source_location)
end
def resource_for_provider
@resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r|
- r.source(Chef::Util::PathHelper.validate_path(source_location))
+ r.source(Chef::Util::PathHelper.validate_path(source_location)) unless source_location.nil?
+ r.version(new_resource.version)
r.timeout(new_resource.timeout)
r.returns(new_resource.returns)
r.options(new_resource.options)
@@ -151,7 +231,8 @@ class Chef
if uri_scheme?(new_resource.source)
source_resource.path
else
- Chef::Util::PathHelper.cleanpath(new_resource.source)
+ new_source = Chef::Util::PathHelper.cleanpath(new_resource.source)
+ ::File.exist?(new_source) ? new_source : nil
end
end
diff --git a/lib/chef/provider/package/windows/exe.rb b/lib/chef/provider/package/windows/exe.rb
new file mode 100644
index 0000000000..4495868010
--- /dev/null
+++ b/lib/chef/provider/package/windows/exe.rb
@@ -0,0 +1,129 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Author:: Matt Wrock <matt@mattwrock.com>
+# Copyright:: Copyright (c) 2011, 2015 Chef Software, 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/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Package
+ class Windows
+ class Exe
+ include Chef::Mixin::ShellOut
+
+ def initialize(resource, installer_type, uninstall_entries)
+ @new_resource = resource
+ @installer_type = installer_type
+ @uninstall_entries = uninstall_entries
+ end
+
+ attr_reader :new_resource
+ attr_reader :installer_type
+ attr_reader :uninstall_entries
+
+ # From Chef::Provider::Package
+ def expand_options(options)
+ options ? " #{options}" : ""
+ end
+
+ # Returns a version if the package is installed or nil if it is not.
+ def installed_version
+ Chef::Log.debug("#{new_resource} checking package version")
+ current_installed_version
+ end
+
+ def package_version
+ new_resource.version || install_file_version
+ end
+
+ def install_package
+ Chef::Log.debug("#{new_resource} installing #{new_resource.installer_type} package '#{new_resource.source}'")
+ shell_out!(
+ [
+ "start",
+ "\"\"",
+ "/wait",
+ "\"#{new_resource.source}\"",
+ unattended_flags,
+ expand_options(new_resource.options),
+ "& exit %%%%ERRORLEVEL%%%%"
+ ].join(" "), timeout: new_resource.timeout, returns: new_resource.returns
+ )
+ end
+
+ def remove_package
+ uninstall_version = new_resource.version || current_installed_version
+ uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) }
+ .map { |version| version.uninstall_string }.uniq.each do |uninstall_string|
+ Chef::Log.debug("Registry provided uninstall string for #{new_resource} is '#{uninstall_string}'")
+ shell_out!(uninstall_command(uninstall_string), { returns: new_resource.returns })
+ end
+ end
+
+ private
+
+ def uninstall_command(uninstall_string)
+ uninstall_string.delete!('"')
+ uninstall_string = [
+ %q{/d"},
+ ::File.dirname(uninstall_string),
+ %q{" },
+ ::File.basename(uninstall_string),
+ expand_options(new_resource.options),
+ " ",
+ unattended_flags
+ ].join
+ %Q{start "" /wait #{uninstall_string} & exit %%%%ERRORLEVEL%%%%}
+ end
+
+ def current_installed_version
+ @current_installed_version ||= uninstall_entries.count == 0 ? nil : begin
+ uninstall_entries.map { |entry| entry.display_version }.uniq
+ end
+ end
+
+ def install_file_version
+ @install_file_version ||= begin
+ if ::File.exist?(@new_resource.source)
+ version_info = Chef::ReservedNames::Win32::File.version_info(new_resource.source)
+ file_version = version_info.FileVersion || version_info.ProductVersion
+ file_version == '' ? nil : file_version
+ else
+ nil
+ end
+ end
+ end
+
+ # http://unattended.sourceforge.net/installers.php
+ def unattended_flags
+ case installer_type
+ when :installshield
+ '/s /sms'
+ when :nsis
+ '/S /NCRC'
+ when :inno
+ '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART'
+ when :wise
+ '/s'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb
index 7fdbbcff35..1cc636b92e 100644
--- a/lib/chef/provider/package/windows/msi.rb
+++ b/lib/chef/provider/package/windows/msi.rb
@@ -29,10 +29,14 @@ class Chef
include Chef::ReservedNames::Win32::API::Installer if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) && Chef::Platform.supports_msi?
include Chef::Mixin::ShellOut
- def initialize(resource)
+ def initialize(resource, uninstall_entries)
@new_resource = resource
+ @uninstall_entries = uninstall_entries
end
+ attr_reader :new_resource
+ attr_reader :uninstall_entries
+
# From Chef::Provider::Package
def expand_options(options)
options ? " #{options}" : ""
@@ -40,27 +44,47 @@ class Chef
# Returns a version if the package is installed or nil if it is not.
def installed_version
- Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}")
- product_code = get_product_property(@new_resource.source, "ProductCode")
- Chef::Log.debug("#{@new_resource} checking package status and version for #{product_code}")
- get_installed_version(product_code)
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} getting product code for package at #{new_resource.source}")
+ product_code = get_product_property(new_resource.source, "ProductCode")
+ Chef::Log.debug("#{new_resource} checking package status and version for #{product_code}")
+ get_installed_version(product_code)
+ else
+ uninstall_entries.count == 0 ? nil : begin
+ uninstall_entries.map { |entry| entry.display_version }.uniq
+ end
+ end
end
def package_version
- Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}")
- get_product_property(@new_resource.source, "ProductVersion")
+ return new_resource.version if new_resource.version
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} getting product version for package at #{new_resource.source}")
+ get_product_property(new_resource.source, "ProductVersion")
+ end
end
- def install_package(name, version)
+ def install_package
# We could use MsiConfigureProduct here, but we'll start off with msiexec
- Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'")
- shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns})
+ Chef::Log.debug("#{new_resource} installing MSI package '#{new_resource.source}'")
+ shell_out!("msiexec /qn /i \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns})
end
- def remove_package(name, version)
+ def remove_package
# We could use MsiConfigureProduct here, but we'll start off with msiexec
- Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'")
- shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns})
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} removing MSI package '#{new_resource.source}'")
+ shell_out!("msiexec /qn /x \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns})
+ else
+ uninstall_version = new_resource.version || installed_version
+ uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) }
+ .map { |version| version.uninstall_string }.uniq.each do |uninstall_string|
+ Chef::Log.debug("#{new_resource} removing MSI package version using '#{uninstall_string}'")
+ uninstall_string += expand_options(new_resource.options)
+ uninstall_string += " /Q" unless uninstall_string =~ / \/Q\b/
+ shell_out!(uninstall_string, {:timeout => new_resource.timeout, :returns => new_resource.returns})
+ end
+ end
end
end
end
diff --git a/lib/chef/provider/package/windows/registry_uninstall_entry.rb b/lib/chef/provider/package/windows/registry_uninstall_entry.rb
new file mode 100644
index 0000000000..98398adf5e
--- /dev/null
+++ b/lib/chef/provider/package/windows/registry_uninstall_entry.rb
@@ -0,0 +1,89 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Author:: Matt Wrock <matt@mattwrock.com>
+# Copyright:: Copyright (c) 2011, 2015 Chef Software, 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 'win32/registry' if (RUBY_PLATFORM =~ /mswin|mingw32|windows/)
+
+class Chef
+ class Provider
+ class Package
+ class Windows
+ class RegistryUninstallEntry
+
+ def self.find_entries(package_name)
+ Chef::Log.debug("Finding uninstall entries for #{package_name}")
+ entries = []
+ [
+ [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100)],
+ [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0200)],
+ [::Win32::Registry::HKEY_CURRENT_USER]
+ ].each do |hkey|
+ desired = hkey.length > 1 ? hkey[1] : ::Win32::Registry::Constants::KEY_READ
+ begin
+ ::Win32::Registry.open(hkey[0], UNINSTALL_SUBKEY, desired) do |reg|
+ reg.each_key do |key, _wtime|
+ begin
+ entry = reg.open(key, desired)
+ display_name = read_registry_property(entry, 'DisplayName')
+ if display_name == package_name
+ entries.push(RegistryUninstallEntry.new(hkey, key, entry))
+ end
+ rescue ::Win32::Registry::Error => ex
+ Chef::Log.debug("Registry error opening key '#{key}' on node #{desired}: #{ex}")
+ end
+ end
+ end
+ rescue ::Win32::Registry::Error
+ Chef::Log.debug("Registry error opening hive '#{hkey[0]}' :: #{desired}: #{ex}")
+ end
+ end
+ entries
+ end
+
+ def self.read_registry_property(data, property)
+ data[property]
+ rescue ::Win32::Registry::Error => ex
+ Chef::Log.debug("Failure to read property '#{property}'")
+ nil
+ end
+
+ def initialize(hive, key, registry_data)
+ Chef::Log.debug("Creating uninstall entry for #{hive}::#{key}")
+ @hive = hive
+ @key = key
+ @data = registry_data
+ @display_name = RegistryUninstallEntry.read_registry_property(registry_data, 'DisplayName')
+ @display_version = RegistryUninstallEntry.read_registry_property(registry_data, 'DisplayVersion')
+ @uninstall_string = RegistryUninstallEntry.read_registry_property(registry_data, 'UninstallString')
+ end
+
+ attr_reader :hive
+ attr_reader :key
+ attr_reader :display_name
+ attr_reader :display_version
+ attr_reader :uninstall_string
+ attr_reader :data
+
+ private
+
+ UNINSTALL_SUBKEY = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'.freeze
+ end
+ end
+ end
+ end
+end