diff options
author | Bryan McLellan <btm@loftninjas.org> | 2016-09-30 11:47:52 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-30 11:47:51 -0400 |
commit | 5aa336d9010496932e81f59c896bb04c6bceff57 (patch) | |
tree | 455727d618fce001f4b5d25fdccd31bd67535aed | |
parent | 4f738f8aaf0fb6f1ee0f24a123640d5abf57a800 (diff) | |
parent | 8e32fd5ed4665245523a4e02a8bba5f773afe42b (diff) | |
download | chef-5aa336d9010496932e81f59c896bb04c6bceff57.tar.gz |
Merge pull request #5285 from MsysTechnologiesllc/vj/adding_support_for_cab_files
Adding support for cab files to Chef package provider on windows
-rw-r--r-- | lib/chef/provider/package/cab.rb | 150 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/cab_package.rb | 44 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/data/templates/chef-seattle20160930-4388-1crv7ef.txt | 1 | ||||
-rw-r--r-- | spec/data/templates/chef-seattle20160930-4388-jjfoae.txt | 1 | ||||
-rw-r--r-- | spec/data/templates/chef-seattle20160930-4388-umeq2c.txt | 1 | ||||
-rw-r--r-- | spec/unit/provider/package/cab_spec.rb | 218 | ||||
-rw-r--r-- | spec/unit/resource/cab_package_spec.rb | 38 |
9 files changed, 455 insertions, 0 deletions
diff --git a/lib/chef/provider/package/cab.rb b/lib/chef/provider/package/cab.rb new file mode 100644 index 0000000000..4e2054fbc2 --- /dev/null +++ b/lib/chef/provider/package/cab.rb @@ -0,0 +1,150 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@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. +# + +require "chef/provider/package" +require "chef/resource/cab_package" +require "chef/mixin/shell_out" + +class Chef + class Provider + class Package + class Cab < Chef::Provider::Package + include Chef::Mixin::ShellOut + + provides :cab_package, os: "windows" + + def load_current_resource + @current_resource = Chef::Resource::CabPackage.new(new_resource.name) + current_resource.source(new_resource.source) + new_resource.version(package_version) + current_resource.version(installed_version) + current_resource + end + + def install_package(name, version) + dism_command("/Add-Package /PackagePath:\"#{@new_resource.source}\"") + end + + def remove_package(name, version) + dism_command("/Remove-Package /PackagePath:\"#{@new_resource.source}\"") + end + + def dism_command(command) + shellout = Mixlib::ShellOut.new("dism.exe /Online #{command} /NoRestart", { :timeout => @new_resource.timeout }) + with_os_architecture(nil) do + shellout.run_command + end + end + + def installed_version + 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 + stdout = dism_command("/Get-PackageInfo /PackageName:\"#{found_packages.first["package_identity"]}\"").stdout + find_version(stdout) + else + # Presuming this won't happen, otherwise we need to handle it + raise Chef::Exceptions::Package, "Found multiple 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 + find_version(stdout) + end + + def find_version(stdout) + package_info = parse_dism_get_package_info(stdout) + package = split_package_identity(package_info["package_information"]["package_identity"]) + package["version"] + end + + # returns a hash of package state information given the output of dism /get-packages + # expected keys: package_identity + def parse_dism_get_packages(text) + packages = Array.new + text.each_line do |line| + key, value = line.split(":") if line.start_with?("Package Identity") + unless key.nil? || value.nil? + package = Hash.new + package[key.downcase.strip.tr(" ", "_")] = value.strip.chomp + packages << package + 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 + section_headers = [ "Package information", "Custom Properties", "Features" ] + text.each_line do |line| + if line =~ /Error: (.*)/ + errors << $1.strip + elsif section_headers.any? { |header| line =~ /^(#{header})/ } + in_section = $1.downcase.tr(" ", "_") + elsif line =~ /(.*) ?: (.*)/ + v = $2 # has to be first or the gsub below replaces this variable + k = $1.downcase.strip.tr(" ", "_") + 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?("0x80070003") || errors.include?("0x80070002") + raise Chef::Exceptions::Package, "DISM: The system cannot find the path or file specified." + elsif errors.include?("740") + raise Chef::Exceptions::Package, "DISM: Error 740: Elevated permissions are required to run DISM." + else + raise Chef::Exceptions::Package, "Unknown errors encountered parsing DISM output: #{errors}" + end + end + package_data + end + + def split_package_identity(identity) + data = Hash.new + data["name"], data["publisher"], data["arch"], data["resource_id"], data["version"] = identity.split("~") + data + end + + def installed_packages + @packages ||= begin + output = dism_command("/Get-Packages").stdout + packages = parse_dism_get_packages(output) + packages + end + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index affa5ca2c1..596629290b 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -83,6 +83,7 @@ require "chef/provider/package/zypper" require "chef/provider/package/solaris" require "chef/provider/package/smartos" require "chef/provider/package/aix" +require "chef/provider/package/cab" require "chef/provider/service/arch" require "chef/provider/service/freebsd" diff --git a/lib/chef/resource/cab_package.rb b/lib/chef/resource/cab_package.rb new file mode 100644 index 0000000000..b7acfb0ec9 --- /dev/null +++ b/lib/chef/resource/cab_package.rb @@ -0,0 +1,44 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@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 CabPackage < Chef::Resource::Package + include Chef::Mixin::Uris + + provides :cab_package, os: "windows" + + allowed_actions :install, :remove + + def initialize(name, run_context = nil) + super + @resource_name = :cab_package + end + + property :source, String, + coerce: (proc do |s| + unless s.nil? + uri_scheme?(s) ? s : Chef::Util::PathHelper.canonical_path(s, false) + end + end) + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 2afd47a8f4..9ff772353b 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -95,3 +95,4 @@ require "chef/resource/yum_repository" require "chef/resource/lwrp_base" require "chef/resource/bff_package" require "chef/resource/zypper_package" +require "chef/resource/cab_package" diff --git a/spec/data/templates/chef-seattle20160930-4388-1crv7ef.txt b/spec/data/templates/chef-seattle20160930-4388-1crv7ef.txt new file mode 100644 index 0000000000..f476ccd704 --- /dev/null +++ b/spec/data/templates/chef-seattle20160930-4388-1crv7ef.txt @@ -0,0 +1 @@ +Do do do do, do do do do, do do do do, do do do do
\ No newline at end of file diff --git a/spec/data/templates/chef-seattle20160930-4388-jjfoae.txt b/spec/data/templates/chef-seattle20160930-4388-jjfoae.txt new file mode 100644 index 0000000000..f476ccd704 --- /dev/null +++ b/spec/data/templates/chef-seattle20160930-4388-jjfoae.txt @@ -0,0 +1 @@ +Do do do do, do do do do, do do do do, do do do do
\ No newline at end of file diff --git a/spec/data/templates/chef-seattle20160930-4388-umeq2c.txt b/spec/data/templates/chef-seattle20160930-4388-umeq2c.txt new file mode 100644 index 0000000000..f476ccd704 --- /dev/null +++ b/spec/data/templates/chef-seattle20160930-4388-umeq2c.txt @@ -0,0 +1 @@ +Do do do do, do do do do, do do do do, do do do do
\ No newline at end of file diff --git a/spec/unit/provider/package/cab_spec.rb b/spec/unit/provider/package/cab_spec.rb new file mode 100644 index 0000000000..5e16795aa8 --- /dev/null +++ b/spec/unit/provider/package/cab_spec.rb @@ -0,0 +1,218 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@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::Cab do + let(:timeout) {} + + let(:new_resource) { Chef::Resource::CabPackage.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::Cab.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 + + before do + new_resource.source = "C:\\Temp\\Test6.1-KB2664825-v3-x64.cab" + installed_package_list_obj = double(stdout: installed_package_list_stdout) + allow(provider).to receive(:dism_command).with("/Get-Packages").and_return(installed_package_list_obj) + package_version_obj = double(stdout: package_version_stdout) + allow(provider).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{new_resource.source}\"").and_return(package_version_obj) + end + + def allow_package_info(package_path = nil, package_name = nil) + get_package_info_stdout = <<-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 + get_package_info_obj = double(stdout: get_package_info_stdout) + if package_path + allow(provider).to receive(:dism_command).with("/Get-PackageInfo /PackagePath:\"#{package_path}\"").and_return(get_package_info_obj) + else + allow(provider).to receive(:dism_command).with("/Get-PackageInfo /PackageName:\"#{package_name}\"").and_return(get_package_info_obj) + end + 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(provider).to receive(:dism_command).with("/Get-Packages").and_return(get_packages_obj) + end + + describe "#load_current_resource" do + it "returns a current_resource" do + expect(provider.load_current_resource).to be_kind_of(Chef::Resource::CabPackage) + 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 "sets the new resource package version" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("6.1.3.0") + end + end + + describe "#initialize" do + it "returns the correct class" do + expect(provider).to be_kind_of(Chef::Provider::Package::Cab) + end + end + + describe "#package_version" do + it "returns the new package version" do + allow_package_info(new_resource.source, nil) + expect(provider.package_version).to eql("6.1.3.0") + end + end + + describe "#installed_version" do + it "returns the current installed version of package" do + allow_package_info(new_resource.source, nil) + allow_get_packages + allow_package_info(nil, "Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0") + expect(provider.installed_version).to eql("6.1.3.0") + end + end + + describe "#action_remove" do + it "does nothing when the package is already removed" 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 + allow_package_info(new_resource.source, nil) + allow_get_packages + allow_package_info(nil, "Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0") + provider.load_current_resource + expect(provider.installed_version).not_to eql(nil) + expect(provider).to receive(:remove_package) + provider.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + end + end + + describe "#action_install" do + it "installs package if already not installed" do + provider.load_current_resource + expect(provider.installed_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 + allow_package_info(new_resource.source, nil) + allow_get_packages + allow_package_info(nil, "Package_for_KB2664825~31bf3856ad364e35~amd64~~6.1.3.0") + provider.load_current_resource + expect(provider.installed_version).not_to eql(nil) + expect(provider).not_to receive(:install_package) + provider.run_action(:install) + expect(new_resource).not_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 + + before do + new_resource.source = "C:\\Temp\\Test6.1-KB2664825-v3-x64.cab" + installed_package_list_obj = double(stdout: installed_package_list_stdout) + allow(provider).to receive(:dism_command).with("/Get-Packages").and_return(installed_package_list_obj) + 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 diff --git a/spec/unit/resource/cab_package_spec.rb b/spec/unit/resource/cab_package_spec.rb new file mode 100644 index 0000000000..aa4890f171 --- /dev/null +++ b/spec/unit/resource/cab_package_spec.rb @@ -0,0 +1,38 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@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::CabPackage do + + let(:resource) { Chef::Resource::CabPackage.new("test_pkg") } + + it "creates a new Chef::Resource::CabPackage" 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::CabPackage) + end + + it "sets resource name as :cab_package" do + expect(resource.resource_name).to eql(:cab_package) + end + + it "coerce its name to a package_name" do + expect(resource.package_name).to eql("test_pkg") + end +end |