diff options
-rw-r--r-- | appveyor.yml | 2 | ||||
-rw-r--r-- | lib/chef/provider/package/msu.rb | 163 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/msu_package.rb | 47 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/data/sample_msu1.xml | 10 | ||||
-rw-r--r-- | spec/data/sample_msu2.xml | 14 | ||||
-rw-r--r-- | spec/data/sample_msu3.xml | 16 | ||||
-rw-r--r-- | spec/functional/resource/msu_package_spec.rb | 84 | ||||
-rw-r--r-- | spec/spec_helper.rb | 1 | ||||
-rw-r--r-- | spec/support/platform_helpers.rb | 24 | ||||
-rw-r--r-- | spec/unit/provider/package/msu_spec.rb | 284 | ||||
-rw-r--r-- | spec/unit/resource/msu_package_spec.rb | 49 |
13 files changed, 688 insertions, 8 deletions
diff --git a/appveyor.yml b/appveyor.yml index 831ecf67e5..08756d98ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,6 +30,8 @@ install: - SET BUNDLE_IGNORE_CONFIG=true - SET BUNDLE_FROZEN=1 - SET BUNDLE_WITHOUT=development:guard:maintenance:tools:integration:changelog:docgen:travis:style:omnibus_package:aix:bsd:linux:mac_os_x:solaris + - appveyor DownloadFile http://curl.haxx.se/ca/cacert.pem -FileName C:\cacert.pem + - set SSL_CERT_FILE=C:\cacert.pem build_script: - bundle install || bundle install || bundle install diff --git a/lib/chef/provider/package/msu.rb b/lib/chef/provider/package/msu.rb new file mode 100644 index 0000000000..e1f0d53a9c --- /dev/null +++ b/lib/chef/provider/package/msu.rb @@ -0,0 +1,163 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2015-2016, 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. +# + +# msu_package leverages cab_package +# The contents of msu file are extracted, which contains one or more cab files. +# The extracted cab files are installed using Chef::Resource::Package::CabPackage +# Reference: https://support.microsoft.com/en-in/kb/934307 +require "chef/provider/package" +require "chef/resource/msu_package" +require "chef/mixin/shell_out" +require "chef/provider/package/cab" +require "chef/util/path_helper" +require "chef/mixin/uris" +require "chef/mixin/checksum" + +class Chef + class Provider + class Package + class Msu < Chef::Provider::Package + use_inline_resources + include Chef::Mixin::ShellOut + include Chef::Mixin::Uris + include Chef::Mixin::Checksum + + provides :msu_package, os: "windows" + + def load_current_resource + @current_resource = Chef::Resource::MsuPackage.new(new_resource.name) + + # download file if source is a url + msu_file = uri_scheme?(new_resource.source) ? download_source_file : @new_resource.source + + # temp directory where the contents of msu file get extracted + @temp_directory = Dir.mktmpdir("chef") + extract_msu_contents(msu_file, @temp_directory) + @cab_files = read_cab_files_from_xml(@temp_directory) + + unless @cab_files.empty? + current_resource.version(get_current_versions) + else + raise Chef::Exceptions::Package, "Corrupt MSU package: MSU package XML does not contain any cab file" + end + current_resource + end + + def get_current_versions + @cab_files.map do |cabfile| + cab_pkg = get_cab_package(cabfile) + cab_pkg.installed_version + end + end + + def get_candidate_versions + @cab_files.map do |cabfile| + cab_pkg = get_cab_package(cabfile) + cab_pkg.package_version + end + end + + def candidate_version + @candidate_version ||= get_candidate_versions + end + + def get_cab_package(cab_file) + cab_resource = @new_resource + cab_resource.source = cab_file + cab_pkg = Chef::Provider::Package::Cab.new(cab_resource, nil) + end + + def download_source_file + source_resource.run_action(:create) + Chef::Log.debug("#{new_resource} fetched source file to #{source_resource.path}") + source_resource.path + end + + def source_resource + @source_resource ||= declare_resource(:remote_file, new_resource.name) do + path default_download_cache_path + source new_resource.source + checksum new_resource.checksum + backup false + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def install_package(name, version) + #use cab_package resource to install the extracted cab packages + @cab_files.each do |cab_file| + declare_resource(:cab_package, @new_resource.name) do + source cab_file + action :install + end + end + end + + def remove_package(name, version) + #use cab_package provider to remove the extracted cab packages + @cab_files.each do |cab_file| + declare_resource(:cab_package, @new_resource.name) do + source cab_file + action :remove + end + end + end + + def extract_msu_contents(msu_file, destination) + shellout = Mixlib::ShellOut.new("expand -f:* #{msu_file} #{destination}", { :timeout => @new_resource.timeout }) + with_os_architecture(nil) do + shellout.run_command + end + end + + # msu package can contain multiple cab files + # Reading cab files from xml to ensure the order of installation in case of multiple cab files + def read_cab_files_from_xml(msu_dir) + # get the file with .xml extension + xml_files = Dir.glob("#{msu_dir}/*.xml") + cab_files = [] + + if xml_files.empty? + raise Chef::Exceptions::Package, "Corrupt MSU package: MSU package doesn't contain any xml file" + else + # msu package contains only single xml file. So using xml_files.first is sufficient + doc = ::File.open("#{xml_files.first}") { |f| REXML::Document.new f } + locations = doc.elements.each("unattend/servicing/package/source") { |element| puts element.attributes["location"] } + locations.each do |loc| + cab_files << msu_dir + "/" + loc.attribute("location").value.split("\\")[1] + end + + cab_files + end + cab_files + end + + def cleanup_after_converge + # delete the temp directory where the contents of msu file are extracted + FileUtils.rm_rf(@temp_directory) if Dir.exists?(@temp_directory) + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 8067643bab..ebb4c45ae8 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -85,6 +85,7 @@ require "chef/provider/package/smartos" require "chef/provider/package/aix" require "chef/provider/package/cab" require "chef/provider/package/powershell" +require "chef/provider/package/msu" require "chef/provider/service/arch" require "chef/provider/service/freebsd" diff --git a/lib/chef/resource/msu_package.rb b/lib/chef/resource/msu_package.rb new file mode 100644 index 0000000000..753992c185 --- /dev/null +++ b/lib/chef/resource/msu_package.rb @@ -0,0 +1,47 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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/resource/package" +require "chef/mixin/uris" + +class Chef + class Resource + class MsuPackage < Chef::Resource::Package + include Chef::Mixin::Uris + + provides :msu_package, os: "windows" + + allowed_actions :install, :remove + + def initialize(name, run_context = nil) + super + @resource_name = :msu_package + @source = name + @action = :install + end + + property :source, String, + coerce: (proc do |s| + unless s.nil? + uri_scheme?(s) ? s : Chef::Util::PathHelper.canonical_path(s, false) + end + end) + property :checksum, String, desired_state: false + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 6634a298d1..de421839e0 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -97,3 +97,4 @@ require "chef/resource/bff_package" require "chef/resource/zypper_package" require "chef/resource/cab_package" require "chef/resource/powershell_package" +require "chef/resource/msu_package" diff --git a/spec/data/sample_msu1.xml b/spec/data/sample_msu1.xml new file mode 100644 index 0000000000..cc68dbf060 --- /dev/null +++ b/spec/data/sample_msu1.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <servicing> + <package action="install"> + <assemblyIdentity name="Package_for_KB2859903" version="10.2.1.0" language="neutral" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35"/> + <source location="%configsetroot%\IE10-Windows6.1-KB2859903-x86.CAB" /> + </package> + </servicing> +</unattend> + diff --git a/spec/data/sample_msu2.xml b/spec/data/sample_msu2.xml new file mode 100644 index 0000000000..6f95e04f93 --- /dev/null +++ b/spec/data/sample_msu2.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <servicing> + <package action="install"> + <assemblyIdentity name="Package_for_KB2859903" version="10.2.1.0" language="neutral" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35"/> + <source location="%configsetroot%\IE10-Windows6.1-KB2859903-x86.CAB" /> + </package> + <package action="install"> + <assemblyIdentity name="Package_for_abc" version="10.2.1.0" language="neutral" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35"/> + <source location="%configsetroot%\abc.CAB" /> + </package> + </servicing> +</unattend> + diff --git a/spec/data/sample_msu3.xml b/spec/data/sample_msu3.xml new file mode 100644 index 0000000000..0ef09da206 --- /dev/null +++ b/spec/data/sample_msu3.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <servicing> + <package action="install"> + <assemblyIdentity name="Package_for_KB2859903" version="10.2.1.0" language="neutral" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35"/> + <source location="%configsetroot%\IE10-Windows6.1-KB2859903-x86.CAB" /> + </package> + </servicing> + <servicing> + <package action="install"> + <assemblyIdentity name="Package_for_abc" version="10.2.1.0" language="neutral" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35"/> + <source location="%configsetroot%\abc.CAB" /> + </package> + </servicing> +</unattend> + diff --git a/spec/functional/resource/msu_package_spec.rb b/spec/functional/resource/msu_package_spec.rb new file mode 100644 index 0000000000..23342be6ae --- /dev/null +++ b/spec/functional/resource/msu_package_spec.rb @@ -0,0 +1,84 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright (c) 2016 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 "spec_helper" +require "chef/provider/package/cab" + +describe Chef::Resource::MsuPackage, :win2012r2_only do + + let(:package_name) { "Package_for_KB2959977" } + let(:package_source) { "https://download.microsoft.com/download/3/B/3/3B320C07-B7B1-41E5-81F4-79EBC02DF7D3/Windows8.1-KB2959977-x64.msu" } + + let(:new_resource) { Chef::Resource::CabPackage.new("windows_test_pkg") } + let(:cab_provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Cab.new(new_resource, run_context) + end + + subject do + new_resource = Chef::Resource::MsuPackage.new("test msu package", run_context) + new_resource.package_name package_name + new_resource.source package_source + new_resource + end + + context "installing package" do + after { remove_package } + + it "installs the package successfully" do + subject.run_action(:install) + found_packages = cab_provider.installed_packages.select { |p| p["package_identity"] =~ /^#{package_name}~/ } + expect(found_packages.length).to be == 1 + end + end + + context "removing a package" do + it "removes an installed package" do + subject.run_action(:install) + remove_package + found_packages = cab_provider.installed_packages.select { |p| p["package_identity"] =~ /^#{package_name}~/ } + expect(found_packages.length).to be == 0 + end + end + + context "when an invalid msu package is given" do + def package_name + "Package_for_KB2859903" + end + + def package_source + "https://download.microsoft.com/download/5/2/B/52BE95BF-22D8-4415-B644-0FDF398F6D96/IE10-Windows6.1-KB2859903-x86.msu" + end + + it "raises error while installing" do + expect { subject.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "raises error while removing" do + expect { subject.run_action(:remove) }.to raise_error(Chef::Exceptions::Package) + end + end + + def remove_package + pkg_to_remove = Chef::Resource::MsuPackage.new(package_name, run_context) + pkg_to_remove.source = package_source + pkg_to_remove.run_action(:remove) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7559e797bc..aac5a82f2a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -139,6 +139,7 @@ RSpec.configure do |config| config.filter_run_excluding :not_supported_on_gce => true if gce? config.filter_run_excluding :not_supported_on_nano => true if windows_nano_server? config.filter_run_excluding :win2k3_only => true unless windows_win2k3? + config.filter_run_excluding :win2012r2_only => true unless windows_2012r2? config.filter_run_excluding :windows_2008r2_or_later => true unless windows_2008r2_or_later? config.filter_run_excluding :windows64_only => true unless windows64? config.filter_run_excluding :windows32_only => true unless windows32? diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index 62b262b8a7..14f883da74 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -50,23 +50,31 @@ end def windows_win2k3? return false unless windows? - wmi = WmiLite::Wmi.new - host = wmi.first_of("Win32_OperatingSystem") - (host["version"] && host["version"].start_with?("5.2")) + (host_version && host_version.start_with?("5.2")) end def windows_2008r2_or_later? return false unless windows? - wmi = WmiLite::Wmi.new - host = wmi.first_of("Win32_OperatingSystem") - version = host["version"] - return false unless version - components = version.split(".").map do |component| + return false unless host_version + components = host_version.split(".").map do |component| component.to_i end components.length >= 2 && components[0] >= 6 && components[1] >= 1 end +def windows_2012r2? + return false unless windows? + (host_version && host_version.start_with?("6.3")) +end + +def host_version + @host_version ||= begin + wmi = WmiLite::Wmi.new + host = wmi.first_of("Win32_OperatingSystem") + host["version"] + end +end + def windows_powershell_dsc? return false unless windows? supports_dsc = false diff --git a/spec/unit/provider/package/msu_spec.rb b/spec/unit/provider/package/msu_spec.rb new file mode 100644 index 0000000000..d9b97aaee6 --- /dev/null +++ b/spec/unit/provider/package/msu_spec.rb @@ -0,0 +1,284 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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 "spec_helper" + +describe Chef::Provider::Package::Msu, :windows_only do + let(:timeout) {} + + let(:new_resource) { Chef::Resource::MsuPackage.new("windows_test_pkg") } + + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Msu.new(new_resource, run_context) + end + + let(:installed_package_list_stdout) do + <<-EOF +Packages listing: +Package Identity : Package_for_KB2999486~31bf3856ad364e35~amd64~~6.1.9768.0 +Package Identity : Package_for_KB2994825~31bf3856ad364e35~amd64~~6.1.7601.0 + EOF + end + + let(:package_version_stdout) do + <<-EOF +Package information: +Package Identity : Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0 +State : Installed +Dependency : Language Pack +The operation completed successfully + EOF + end + + let(:get_package_info_stdout) do + <<-EOF +Deployment Image Servicing and Management tool +Version: 6.1.7600.16385 + +Image Version: 6.1.7600.16385 + +Package information: +Package Identity : Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0 +Applicable : Yes +Copyright : Microsoft Corporation +Company : Microsoft Corporation +State : Installed +Dependency : Language Pack +The operation completed successfully + EOF + end + + def allow_get_packages + get_packages_stdout = <<-EOF +Deployment Image Servicing and Management tool +Version: 6.1.7600.16385 + +Image Version: 6.1.7600.16385 + +Packages listing: + +Package Identity : Package_for_KB2999486~31bf3856ad364e35~amd64~~6.1.9768.0 +State : Installed +Release Type : Language Pack +Install Time : 2/11/2015 11:33 PM + +Package Identity : Package_for_KB2994825~31bf3856ad364e35~amd64~~6.1.7601.0 +State : Installed +Release Type : Language Pack +Install Time : 2/11/2015 11:33 PM + +Package Identity : Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0 +State : Installed +Release Type : Feature Pack +Install Time : 11/21/2010 3:40 AM + +The operation completed successfully. + EOF + get_packages_obj = double(stdout: get_packages_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-Packages").and_return(get_packages_obj) + end + + before do + allow(Dir).to receive(:mktmpdir) + allow(provider).to receive(:cleanup_after_converge) + end + + describe "#initialize" do + it "returns the correct class" do + expect(provider).to be_kind_of(Chef::Provider::Package::Msu) + end + end + + describe "#load_current_resource" do + before do + new_resource.source = "C:\\Temp\\Test6.1-KB2664825-v3-x64.msu" + cab_file = "c:\\temp\\test6.1-kb2664825-v3-x64.cab" + allow(provider).to receive(:extract_msu_contents) + allow(provider).to receive(:read_cab_files_from_xml).and_return([cab_file]) + installed_package_list_obj = double(stdout: installed_package_list_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-Packages").and_return(installed_package_list_obj) + package_version_obj = double(stdout: package_version_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{cab_file}\"").and_return(package_version_obj) + end + + it "returns a current_resource" do + expect(provider.load_current_resource).to be_kind_of(Chef::Resource::MsuPackage) + end + + it "sets the current_resource.version to nil when the package is not installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql([nil]) + end + + it "calls download_source_file method if source is a URL" do + new_resource.source = "https://www.something.com/Test6.1-KB2664825-v3-x64.msu" + expect(provider).to receive(:download_source_file) + provider.load_current_resource + end + end + + describe "#source_resource" do + before do + new_resource.source = "C:\\Temp\\Test6.1-KB2664825-v3-x64.msu" + new_resource.cookbook_name = "Msu_package" + end + + it "sets the desired parameters of downloades msu file" do + allow(provider).to receive(:default_download_cache_path).and_return("C:\\chef\\cache\\package") + source_resource = provider.source_resource + expect(source_resource.path).to be == "C:\\chef\\cache\\package" + expect(source_resource.name).to be == "windows_test_pkg" + expect(source_resource.source).to be == [new_resource.source] + expect(source_resource.cookbook_name).to be == "Msu_package" + expect(source_resource.checksum).to be == nil + end + end + + describe "#default_download_cache_path" do + before do + new_resource.source = "https://www.something.com/Test6.1-KB2664825-v3-x64.msu" + end + + it "returns a clean cache path where the msu file is downloaded" do + allow(Chef::FileCache).to receive(:create_cache_path).and_return("C:\\chef\\abc\\package") + path = provider.default_download_cache_path + expect(path).to be == "C:\\chef\\abc\\package\\Test6.1-KB2664825-v3-x64.msu" + end + end + + describe "action specs" do + before do + new_resource.source = "C:\\Temp\\Test6.1-KB2664825-v3-x64.msu" + cab_file = "c:\\temp\\test6.1-kb2664825-v3-x64.cab" + allow(provider).to receive(:extract_msu_contents) + allow(provider).to receive(:read_cab_files_from_xml).and_return([cab_file]) + installed_package_list_obj = double(stdout: installed_package_list_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-Packages").and_return(installed_package_list_obj) + package_version_obj = double(stdout: package_version_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{cab_file}\"").and_return(package_version_obj) + end + + describe "#action_install" do + it "installs package if not already installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql([nil]) + expect(provider).to receive(:install_package) + provider.run_action(:install) + expect(new_resource).to be_updated_by_last_action + end + + it "does not install package if already installed" do + get_package_info_obj = double(stdout: get_package_info_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{new_resource.source}\"").and_return(get_package_info_obj) + allow_get_packages + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackageName:\"Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0\"").and_return(get_package_info_obj) + provider.load_current_resource + expect(provider.current_resource.version).to eql(["6.1.3.0"]) + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe "#action_remove" do + it "does nothing when the package is not present" do + provider.load_current_resource + expect(provider).not_to receive(:remove_package) + provider.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + + it "removes packages if package is installed" do + get_package_info_obj = double(stdout: get_package_info_stdout) + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{new_resource.source}\"").and_return(get_package_info_obj) + allow_get_packages + allow_any_instance_of(Chef::Provider::Package::Cab).to receive(:dism_command).with("/Get-PackageInfo /PackageName:\"Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0\"").and_return(get_package_info_obj) + provider.load_current_resource + expect(provider.current_resource.version).to eql(["6.1.3.0"]) + expect(provider).to receive(:remove_package) + provider.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + end + end + + context "Invalid package source" do + def package_version_stdout + package_version_stdout = <<-EOF + + Deployment Image Servicing and Management tool + Version: 6.1.7600.16385 + + Image Version: 6.1.7600.16385 + + An error occurred trying to open - c:\\temp\\test6.1-KB2664825-v3-x64.cab Error: 0x80070003 + Error: 3 + The system cannot find the path specified. + The DISM log file can be found at C:\\Windows\\Logs\\DISM\\dism.log. + EOF + end + + it "raises error for invalid source path or file" do + expect { provider.load_current_resource }.to raise_error(Chef::Exceptions::Package, "DISM: The system cannot find the path or file specified.") + end + end + end + + describe "#extract_msu_contents" do + it "extracts the msu contents by using mixlib shellout" do + expect(Mixlib::ShellOut).to receive(:new).with("expand -f:* msu_file destination", { :timeout => new_resource.timeout }) + expect(provider).to receive(:with_os_architecture) + provider.extract_msu_contents("msu_file", "destination") + end + end + + describe "#read_cab_files_from_xml" do + it "raises error if the xml file is not present" do + allow(Dir).to receive(:glob).and_return([]) + expect { provider.read_cab_files_from_xml("msu_dir") }.to raise_error(Chef::Exceptions::Package) + end + + it "parses xml file with single cab file" do + xml_file = File.join(CHEF_SPEC_DATA, "sample_msu1.xml") + allow(Dir).to receive(:glob).and_return([xml_file]) + cab_files = provider.read_cab_files_from_xml("msu_dir") + expect(cab_files).to eql(["msu_dir/IE10-Windows6.1-KB2859903-x86.CAB"]) + end + +# We couldn't find any msu file with multiple cab files in it. +# So we are not 100% sure about the structure of XML file in this case +# The specs below cover 2 possible XML formats + context "handles different xml formats for multiple cab files in the msu package" do + it "parses xml file with multiple <package> tags" do + xml_file = File.join(CHEF_SPEC_DATA, "sample_msu2.xml") + allow(Dir).to receive(:glob).and_return([xml_file]) + cab_files = provider.read_cab_files_from_xml("msu_dir") + expect(cab_files).to eql(["msu_dir/IE10-Windows6.1-KB2859903-x86.CAB", "msu_dir/abc.CAB"]) + end + + it "parses xml file with multiple <servicing> tags" do + xml_file = File.join(CHEF_SPEC_DATA, "sample_msu3.xml") + allow(Dir).to receive(:glob).and_return([xml_file]) + cab_files = provider.read_cab_files_from_xml("msu_dir") + expect(cab_files).to eql(["msu_dir/IE10-Windows6.1-KB2859903-x86.CAB", "msu_dir/abc.CAB"]) + end + end + end +end diff --git a/spec/unit/resource/msu_package_spec.rb b/spec/unit/resource/msu_package_spec.rb new file mode 100644 index 0000000000..349a382b31 --- /dev/null +++ b/spec/unit/resource/msu_package_spec.rb @@ -0,0 +1,49 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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 "spec_helper" + +describe Chef::Resource::MsuPackage do + let(:resource) { Chef::Resource::MsuPackage.new("test_pkg") } + + it "creates a new Chef::Resource::MsuPackage" do + expect(resource).to be_a_kind_of(Chef::Resource) + expect(resource).to be_a_kind_of(Chef::Resource::Package) + expect(resource).to be_a_instance_of(Chef::Resource::MsuPackage) + end + + it "sets resource name as :msu_package" do + expect(resource.resource_name).to eql(:msu_package) + end + + it "sets the source as it's name" do + expect(resource.source).to eql("test_pkg") + end + + it "sets the default action as :install" do + expect(resource.action).to eql(:install) + end + + it "raises error if invalid action is given" do + expect { resource.action "abc" }.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "coerce its name to a package_name" do + expect(resource.package_name).to eql("test_pkg") + end +end |