227 lines
8.7 KiB
Ruby
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
|