chef/cookbooks/windows/libraries/windows_package.rb
2016-02-19 18:09:49 +01:00

227 lines
8.7 KiB
Ruby

require 'chef/resource/lwrp_base'
require 'chef/provider/lwrp_base'
require 'win32/registry' if RUBY_PLATFORM =~ /mswin|mingw32|windows/
require 'chef/mixin/shell_out'
require 'chef/mixin/language'
class Chef
class Provider
class WindowsCookbookPackage < Chef::Provider::LWRPBase
include Chef::Mixin::ShellOut
include Windows::Helper
# the logic in all action methods mirror that of
# the Chef::Provider::Package which will make
# refactoring into core chef easy
action :install do
# If we specified a version, and it's not the current version, move to the specified version
if !@new_resource.version.nil? && @new_resource.version != @current_resource.version
install_version = @new_resource.version
# If it's not installed at all, install it
elsif @current_resource.version.nil?
install_version = candidate_version
end
if install_version
Chef::Log.info("Installing #{@new_resource} version #{install_version}")
status = install_package(@new_resource.package_name, install_version)
new_resource.updated_by_last_action(true) if status
end
end
action :upgrade do
if @current_resource.version != candidate_version
orig_version = @current_resource.version || 'uninstalled'
Chef::Log.info("Upgrading #{@new_resource} version from #{orig_version} to #{candidate_version}")
status = upgrade_package(@new_resource.package_name, candidate_version)
new_resource.updated_by_last_action(true) if status
end
end
action :remove do
if removing_package?
Chef::Log.info("Removing #{@new_resource}")
remove_package(@current_resource.package_name, @new_resource.version)
new_resource.updated_by_last_action(true)
end
end
def removing_package?
if @current_resource.version.nil?
false # nothing to remove
elsif @new_resource.version.nil?
true # remove any version of a package
elsif @new_resource.version == @current_resource.version
true # remove the version we have
else
false # we don't have the version we want to remove
end
end
def expand_options(options)
options ? " #{options}" : ''
end
# these methods are the required overrides of
# a provider that extends from Chef::Provider::Package
# so refactoring into core Chef should be easy
def load_current_resource
@current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name)
@current_resource.package_name(@new_resource.package_name)
@current_resource.version(nil)
unless current_installed_version.nil?
@current_resource.version(current_installed_version)
end
@current_resource
end
def current_installed_version
@current_installed_version ||= begin
if installed_packages.include?(@new_resource.package_name)
installed_packages[@new_resource.package_name][:version]
end
end
end
def candidate_version
@candidate_version ||= begin
@new_resource.version || 'latest'
end
end
def install_package(_name, _version)
Chef::Log.debug("Processing #{@new_resource} as a #{installer_type} installer.")
install_args = [cached_file(@new_resource.source, @new_resource.checksum), expand_options(unattended_installation_flags), expand_options(@new_resource.options)]
Chef::Log.info('Starting installation...this could take awhile.')
Chef::Log.debug "Install command: #{sprintf(install_command_template, *install_args)}"
shell_out!(sprintf(install_command_template, *install_args), timeout: @new_resource.timeout, returns: @new_resource.success_codes)
end
def remove_package(_name, _version)
uninstall_string = installed_packages[@new_resource.package_name][:uninstall_string]
Chef::Log.info("Registry provided uninstall string for #{@new_resource} is '#{uninstall_string}'")
uninstall_command = begin
if uninstall_string =~ /msiexec/i
"#{uninstall_string} /qn"
else
uninstall_string.delete!('"')
"start \"\" /wait /d\"#{::File.dirname(uninstall_string)}\" #{::File.basename(uninstall_string)}#{expand_options(@new_resource.options)} /S & exit %%%%ERRORLEVEL%%%%"
end
end
Chef::Log.info("Removing #{@new_resource} with uninstall command '#{uninstall_command}'")
shell_out!(uninstall_command, { returns: @new_resource.success_codes })
end
private
def install_command_template
case installer_type
when :msi
"msiexec%2$s \"%1$s\"%3$s"
else
"start \"\" /wait \"%1$s\"%2$s%3$s & exit %%%%ERRORLEVEL%%%%"
end
end
# http://unattended.sourceforge.net/installers.php
def unattended_installation_flags
case installer_type
when :msi
# this is no-ui
'/qn /i'
when :installshield
'/s /sms'
when :nsis
'/S /NCRC'
when :inno
# "/sp- /silent /norestart"
'/verysilent /norestart'
when :wise
'/s'
end
end
def installer_type
@installer_type || begin
if @new_resource.installer_type
@new_resource.installer_type
else
basename = ::File.basename(cached_file(@new_resource.source, @new_resource.checksum))
if basename.split('.').last.downcase == 'msi' # Microsoft MSI
:msi
else
# search the binary file for installer type
contents = ::Kernel.open(::File.expand_path(cached_file(@new_resource.source)), 'rb', &:read) # TODO: limit data read in
case contents
when /inno/i # Inno Setup
:inno
when /wise/i # Wise InstallMaster
:wise
when /nsis/i # Nullsoft Scriptable Install System
:nsis
else
# if file is named 'setup.exe' assume installshield
if basename == 'setup.exe'
:installshield
else
fail Chef::Exceptions::AttributeNotFound, 'installer_type could not be determined, please set manually'
end
end
end
end
end
end
end
end
end
class Chef
class Resource
class WindowsCookbookPackage < Chef::Resource::LWRPBase
if Gem::Version.new(Chef::VERSION) >= Gem::Version.new('12.4.0')
provides :windows_package, os: 'windows', override: true
elsif Gem::Version.new(Chef::VERSION) >= Gem::Version.new('12')
provides :windows_package, os: 'windows'
end
actions :install, :remove
default_action :install
attribute :package_name, kind_of: String, name_attribute: true
attribute :source, kind_of: String, required: true
attribute :version, kind_of: String
attribute :options, kind_of: String
attribute :installer_type, kind_of: Symbol, default: nil, equal_to: [:msi, :inno, :nsis, :wise, :installshield, :custom]
attribute :checksum, kind_of: String
attribute :timeout, kind_of: Integer, default: 600
attribute :success_codes, kind_of: Array, default: [0, 42, 127]
self.resource_name = 'windows_package'
def initialize(*args)
super
@provider = Chef::Provider::WindowsCookbookPackage
end
end
end
end
if Gem::Version.new(Chef::VERSION) < Gem::Version.new('12')
# this wires up the cookbook version of the windows_package resource as Chef::Resource::WindowsPackage,
# which is kinda hella janky
Chef::Resource.send(:remove_const, :WindowsPackage) if defined? Chef::Resource::WindowsPackage
Chef::Resource.const_set('WindowsPackage', Chef::Resource::WindowsCookbookPackage)
else
if Chef.respond_to?(:set_resource_priority_array)
# this wires up the dynamic resource resolver to favor the cookbook version of windows_package over
# the internal version (but the internal Chef::Resource::WindowsPackage is still the internal version
# and a wrapper cookbook can override this e.g. for users that want to use the windows cookbook but
# want the internal windows_package resource)
Chef.set_resource_priority_array(:windows_package, [Chef::Resource::WindowsCookbookPackage], platform: 'windows')
end
end