diff options
author | Bryan McLellan <btm@loftninjas.org> | 2014-10-15 15:50:59 -0400 |
---|---|---|
committer | Bryan McLellan <btm@loftninjas.org> | 2014-12-19 11:16:39 -0500 |
commit | 3fe7f021b84371bbdb7faafa4de10ed2a0ff50b3 (patch) | |
tree | 2d539baabdf896651f85812a5d681d594a720d0c | |
parent | 399674d53dfb15c731915ea6d95749774e19876c (diff) | |
download | chef-btm/win_package_updates.tar.gz |
WIP -- Adding support for MSUbtm/win_package_updates
-rw-r--r-- | lib/chef/provider/package/windows.rb | 13 | ||||
-rw-r--r-- | lib/chef/provider/package/windows/cab.rb | 191 | ||||
-rw-r--r-- | lib/chef/provider/package/windows/msu.rb | 75 | ||||
-rw-r--r-- | lib/chef/win32/windows_update.rb | 49 |
4 files changed, 327 insertions, 1 deletions
diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index 143d82f111..77f58a54c6 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -33,6 +33,8 @@ class Chef # 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/cab.rb' + require 'chef/provider/package/windows/msu.rb' # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? def load_current_resource @@ -49,6 +51,10 @@ class Chef case installer_type when :msi Chef::Provider::Package::Windows::MSI.new(@new_resource) + when :cab + Chef::Provider::Package::Windows::CAB.new(@new_resource) + when :msu + Chef::Provider::Package::Windows::MSU.new(@new_resource) else raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" end @@ -62,8 +68,13 @@ class Chef else file_extension = ::File.basename(@new_resource.source).split(".").last.downcase - if file_extension == "msi" + case file_extension + when "msi" :msi + when "cab" + :cab + when "msu" + :msu else raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" end diff --git a/lib/chef/provider/package/windows/cab.rb b/lib/chef/provider/package/windows/cab.rb new file mode 100644 index 0000000000..17d0e71640 --- /dev/null +++ b/lib/chef/provider/package/windows/cab.rb @@ -0,0 +1,191 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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/windows_architecture_helper' +require 'chef/util/path_helper' +require 'chef/mixin/shell_out' + +class Chef + class Provider + class Package + class Windows + class CAB + include Chef::Mixin::ShellOut + + @@package_cache = nil + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # TODO: how do we find a GUID or KB from an MSU or CAB? + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + stdout = dism_command("/Get-PackageInfo /PackagePath:\"#{@new_resource.source}\"").stdout + package_info = parse_dism_get_package_info(stdout) + # e.g. Package_for_KB2975719~31bf3856ad364e35~amd64~~6.3.1.8 + package = split_package_identity(package_info['package_information']['package_identity']) + + # Search for just the package name to catch a different version being installed + Chef::Log.debug("#{@new_resource} searching for installed package #{package['name']}") + found_packages = installed_packages.select { |p| p['package_identity'] =~ /^#{package['name']}~/ } + + + if found_packages.length == 0 + nil + elsif found_packages.length == 1 + found_packages['version'] + else + # Presuming this won't happen, otherwise we need to handle it + raise Chef::Exceptions::Package, "Found mutliple packages installed matching name #{package['name']}, found: #{found_packages.length} matches" + end + end + + def package_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + stdout = dism_command("/Get-PackageInfo /PackagePath:\"#{@new_resource.source}\"").stdout + package_info = parse_dism_get_package_info(stdout) + package = split_package_identity(package_info['package_information']['package_identity']) + package['version'] + end + + def install_package(name, version) + Chef::Log.debug("#{@new_resource} installing Windows CAB package '#{@new_resource.source}'") + dism_command("/Add-Package /PackagePath:\"#{@new_resource.source}\"") + end + + def remove_package(name, version) + if name =~ /#{Chef::Util::PathHelper.path_separator}/ # paths have path separators in them, so this is a file + Chef::Log.debug("#{@new_resource} removing Windows CAB package '#{@new_resource.source}'") + dism_command("/Remove-Package /PackagePath:\"#{@new_resource.source}\"") + else + # Must be a package identity + Chef::Log.debug("#{@new_resource} installing Windows CAB package '#{@new_resource.source}'") + end + end + + # Runs a command through DISM, and returns a Mixlib::ShellOut object + # For example, output can be accessed like dism_command("/Get-CurrentEdition").stdout + def dism_command(command) + shellout = Mixlib::ShellOut.new("dism.exe /Online #{command} /NoRestart", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + + # We must use the 64-bit version of DISM + with_os_architecture(@node) do + shellout.run_command + end + end + + def installed_packages + @@package_cache ||= begin + # TODO: A resource attribute to invalidate the cache + output = dism_command("/Get-Packages").stdout + packages = parse_dism_get_packages(output) + packages + end + end + + # returns a hash of package state information given the output of dism /get-packages + # expected keys: package_identity, state, release_type, install_time + def parse_dism_get_packages(text) + packages = Array.new + package = Hash.new + inside_package_listing = false + + text.each_line do |line| + # Skip the headers + if line =~ /^Packages listing:/ + inside_package_listing = true + next + end + + next unless inside_package_listing + + if line.chomp.empty? + # Between packages so save it to the collection + unless package.empty? + packages << package + package = Hash.new + end + end + + if line =~ /(.*) : (.*)/ + v = $2.chomp # has to be first or the gsub below replaces this variable + k = $1.downcase.strip.gsub(/ /,"_") + package[k] = v + end + end + + packages + end + + # returns a hash of package information given the output of dism /get-packageinfo + def parse_dism_get_package_info(text) + package_data = Hash.new + errors = Array.new + in_section = false + # Version/Image Version are Windows Versions + # TODO: Verfiy we're parsing Features in a useful way + section_headers = [ "Package information", "Custom Properties", "Features" ] + + text.each_line do |line| + if line =~ /Error: (.*)/ + errors << $1 + elsif section_headers.any? { |header| line =~ /^(#{header})/ } + in_section = $1.downcase.gsub(/ /,"_") + elsif line =~ /(.*) ?: (.*)/ + v = $2 # has to be first or the gsub below replaces this variable + k = $1.downcase.strip.gsub(/ /,"_") + if in_section + package_data[in_section] = Hash.new unless package_data[in_section] + package_data[in_section][k] = v + else + package_data[k] = v + end + end + end + + unless errors.empty? + if errors.include?("87") + Chef::Log.debug("DISM: Error 87: Package unknown (not installed)") + return nil + else + raise Chef::Exceptions::Package, "Unknown errors encountered parsing DISM output: #{errors}" + end + end + + package_data + end + + # Spints a package identity, e.g. Package_for_KB2975719~31bf3856ad364e35~amd64~~6.3.1.8, returning a hash of the parts + def split_package_identity(identity) + data = Hash.new + data['name'], data['publisher'], data['arch'], data['resource_id'], data['version'] = identity.split('~') + data + end + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msu.rb b/lib/chef/provider/package/windows/msu.rb new file mode 100644 index 0000000000..5c1b99755d --- /dev/null +++ b/lib/chef/provider/package/windows/msu.rb @@ -0,0 +1,75 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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/win32/windows_update' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ +require 'chef/mixin/shell_out' + +# It could be possible to use DismAddPackage in the future instead of running wusa.exe +# new_resource.name could support both paths and guid's by searching for a slash (path) + +class Chef + class Provider + class Package + class Windows + class MSU + include Chef::Mixin::ShellOut + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # TODO: how do we find a GUID or KB from an MSU or CAB? + + # Returns a version if the package is installed or nil if it is not. + def installed_version # FIXME + 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 verion for #{product_code}") + get_installed_version(product_code) + end + + def package_version # FIXME + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + get_product_property(@new_resource.source, "ProductVersion") + end + + def install_package(name, version) + Chef::Log.debug("#{@new_resource} installing Windows Update package '#{@new_resource.source}'") + shell_out!("wusa \"#{@new_resource.source}\" /quiet /norestart", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + + def remove_package(name, version) + if version + # Presume version is Microsoft Knowledge Base (KB) number + Chef::Log.debug("#{@new_resource} removing Windows Update KB '#{version}'") + shell_out!("wusa /uninstall /kb:#{version} /quiet /norestart", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + else + Chef::Log.debug("#{@new_resource} removing Windows Update package '#{@new_resource.source}'") + shell_out!("wusa /uninstall \"#{@new_resource.source}\" /quiet /norestart", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + end + end + end + end + end +end diff --git a/lib/chef/win32/windows_update.rb b/lib/chef/win32/windows_update.rb new file mode 100644 index 0000000000..7d77d8ee3c --- /dev/null +++ b/lib/chef/win32/windows_update.rb @@ -0,0 +1,49 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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. +# + + +class Chef + module ReservedNames::Win32 + class WindowsUpdate + + # Returns an IUpdateCollection + def search_updates(query_string) + update_session = WIN32OLE.new("Microsoft.Update.Session") + update_searcher = update_session.CreateUpdateSearcher + search_results = update_searcher.Search(query_string) + search_results.Updates + end + + def kb_installed?(target_kb) + # Ensure we have just the KB number + target_kb.gsub(/^KB/, '') + + update_collection = search_updates('IsInstalled=1') + update_collection.each do |item| + item.KBArticleIDs.each do |kb| + if kb == target_kb + return true + end + end + end + + return false + end + end + end +end |