summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McLellan <btm@loftninjas.org>2014-10-15 15:50:59 -0400
committerBryan McLellan <btm@loftninjas.org>2014-12-19 11:16:39 -0500
commit3fe7f021b84371bbdb7faafa4de10ed2a0ff50b3 (patch)
tree2d539baabdf896651f85812a5d681d594a720d0c
parent399674d53dfb15c731915ea6d95749774e19876c (diff)
downloadchef-btm/win_package_updates.tar.gz
WIP -- Adding support for MSUbtm/win_package_updates
-rw-r--r--lib/chef/provider/package/windows.rb13
-rw-r--r--lib/chef/provider/package/windows/cab.rb191
-rw-r--r--lib/chef/provider/package/windows/msu.rb75
-rw-r--r--lib/chef/win32/windows_update.rb49
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