summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2018-03-21 11:18:21 -0700
committerTim Smith <tsmith@chef.io>2018-03-21 11:18:21 -0700
commit3335b3c56d2bb17ed675add0e2ad7e1d3e9d82cc (patch)
treeeb97c9400feea3f04ce53cf8ffc522bac88c1525
parent8ab0742507f08cb7860cdcb2991837a0e0924723 (diff)
downloadchef-3335b3c56d2bb17ed675add0e2ad7e1d3e9d82cc.tar.gz
Use the same caching logic we use in feature_dism
This work was done in the Windows cookbook by @jakauppila. It gives us a 3.5X speedup if nothing actually needs to be installed. Huge win. Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--lib/chef/resource/windows_feature_powershell.rb169
1 files changed, 131 insertions, 38 deletions
diff --git a/lib/chef/resource/windows_feature_powershell.rb b/lib/chef/resource/windows_feature_powershell.rb
index fdb2069292..358fd58436 100644
--- a/lib/chef/resource/windows_feature_powershell.rb
+++ b/lib/chef/resource/windows_feature_powershell.rb
@@ -34,7 +34,7 @@ class Chef
property :feature_name, [Array, String],
description: "The name of the feature/role(s) to install if it differs from the resource name.",
- coerce: proc { |x| Array(x) },
+ coerce: proc { |x| x.is_a?(String) ? x.split(/\s*,\s*/) : x },
name_property: true
property :source, String,
@@ -58,18 +58,26 @@ class Chef
action :install do
description "Install a Windows role/feature using PowerShell"
- Chef::Log.warn("Requested feature #{new_resource.feature_name.join(',')} is not available on this system.") unless available?
- unless !available? || installed?
- converge_by("install Windows feature#{'s' if new_resource.feature_name.count > 1} #{new_resource.feature_name.join(',')}") do
- addsource = new_resource.source ? "-Source \"#{new_resource.source}\"" : ""
- addall = new_resource.all ? "-IncludeAllSubFeature" : ""
- addmanagementtools = new_resource.management_tools ? "-IncludeManagementTools" : ""
- cmd = if node["os_version"].to_f < 6.2
- powershell_out!("#{install_feature_cmdlet} #{new_resource.feature_name.join(',')} #{addall}", timeout: new_resource.timeout)
- else
- powershell_out!("#{install_feature_cmdlet} #{new_resource.feature_name.join(',')} #{addsource} #{addall} #{addmanagementtools}", timeout: new_resource.timeout)
- end
+ reload_cached_powershell_data unless node["powershell_features_cache"]
+ fail_if_unavailable # fail if the features don't exist
+ fail_if_removed # fail if the features are in removed state
+
+ Chef::Log.debug("Windows features needing installation: #{features_to_install.empty? ? 'none' : features_to_install.join(',')}")
+ unless features_to_install.empty?
+ converge_by("install Windows feature#{'s' if features_to_install.count > 1} #{features_to_install.join(',')}") do
+ install_command = "#{install_feature_cmdlet} #{features_to_install.join(',')}"
+ install_command << " -IncludeAllSubFeature" if new_resource.all
+ if node["os_version"].to_f < 6.2 && (new_resource.source || new_resource.management_tools)
+ Chef::Log.warn("The 'source' and 'management_tools' properties are not available on Windows 2012R2 or great. Skipping these properties!")
+ else
+ install_command << " -Source \"#{new_resource.source}\"" if new_resource.source
+ install_command << " -IncludeManagementTools" if new_resource.management_tools
+ end
+
+ cmd = powershell_out!(install_command, timeout: new_resource.timeout)
Chef::Log.info(cmd.stdout)
+
+ reload_cached_powershell_data # Reload cached powershell feature state
end
end
end
@@ -77,10 +85,16 @@ class Chef
action :remove do
description "Remove a Windows role/feature using PowerShell"
- if installed?
- converge_by("remove Windows feature#{'s' if new_resource.feature_name.count > 1} #{new_resource.feature_name.join(',')}") do
- cmd = powershell_out!("#{remove_feature_cmdlet} #{new_resource.feature_name.join(',')}", timeout: new_resource.timeout)
+ reload_cached_powershell_data unless node["powershell_features_cache"]
+
+ Chef::Log.debug("Windows features needing removal: #{features_to_remove.empty? ? 'none' : features_to_remove.join(',')}")
+
+ unless features_to_remove.empty?
+ converge_by("remove Windows feature#{'s' if features_to_remove.count > 1} #{features_to_remove.join(',')}") do
+ cmd = powershell_out!("#{remove_feature_cmdlet} #{features_to_remove.join(',')}", timeout: new_resource.timeout)
Chef::Log.info(cmd.stdout)
+
+ reload_cached_powershell_data # Reload cached powershell feature state
end
end
end
@@ -88,47 +102,126 @@ class Chef
action :delete do
description "Remove a Windows role/feature from the image using Powershell"
- if available?
- converge_by("delete Windows feature#{'s' if new_resource.feature_name.count > 1} #{new_resource.feature_name.join(',')} from the image") do
- cmd = powershell_out!("Uninstall-WindowsFeature #{new_resource.feature_name.join(',')} -Remove", timeout: new_resource.timeout)
+ fail_if_delete_unsupported
+
+ reload_cached_powershell_data unless node["powershell_features_cache"]
+
+ fail_if_unavailable # fail if the features don't exist
+
+ Chef::Log.debug("Windows features needing deletion: #{features_to_delete.empty? ? 'none' : features_to_delete.join(',')}")
+
+ unless features_to_delete.empty?
+ converge_by("delete Windows feature#{'s' if features_to_delete.count > 1} #{features_to_delete.join(',')} from the image") do
+ cmd = powershell_out!("Uninstall-WindowsFeature #{features_to_delete.join(',')} -Remove", timeout: new_resource.timeout)
Chef::Log.info(cmd.stdout)
+
+ reload_cached_powershell_data # Reload cached powershell feature state
end
end
end
action_class do
- # @todo remove this when we're ready to drop windows 8/2012
def install_feature_cmdlet
node["os_version"].to_f < 6.2 ? "Import-Module ServerManager; Add-WindowsFeature" : "Install-WindowsFeature"
end
- # @todo remove this when we're ready to drop windows 8/2012
def remove_feature_cmdlet
node["os_version"].to_f < 6.2 ? "Import-Module ServerManager; Remove-WindowsFeature" : "Uninstall-WindowsFeature"
end
- def installed?
- @installed ||= begin
- # @todo remove this when we're ready to drop windows 8/2012
- cmd = if node["os_version"].to_f < 6.2
- powershell_out("Import-Module ServerManager; @(Get-WindowsFeature #{new_resource.feature_name.join(',')} | ?{$_.Installed -ne $TRUE}).count", timeout: new_resource.timeout)
- else
- powershell_out("@(Get-WindowsFeature #{new_resource.feature_name.join(',')} | ?{$_.InstallState -ne \'Installed\'}).count", timeout: new_resource.timeout)
- end
- cmd.stderr.empty? && cmd.stdout.chomp.to_i == 0
+ # @return [Array] features the user has requested to install which need installation
+ def features_to_install
+ # the intersection of the features to install & disabled features are what needs installing
+ @install ||= new_resource.feature_name & node["powershell_features_cache"]["disabled"]
+ end
+
+ # @return [Array] features the user has requested to remove which need removing
+ def features_to_remove
+ # the intersection of the features to remove & enabled features are what needs removing
+ @remove ||= new_resource.feature_name & node["powershell_features_cache"]["enabled"]
+ end
+
+ # @return [Array] features the user has requested to delete which need deleting
+ def features_to_delete
+ # the intersection of the features to remove & enabled/disabled features are what needs removing
+ @remove ||= begin
+ all_available = node["powershell_features_cache"]["enabled"] +
+ node["powershell_features_cache"]["disabled"]
+ new_resource.feature_name & all_available
end
end
- def available?
- @available ||= begin
- # @todo remove this when we're ready to drop windows 8/2012
- cmd = if node["os_version"].to_f < 6.2
- powershell_out("Import-Module ServerManager; @(Get-WindowsFeature #{new_resource.feature_name.join(',')}).count", timeout: new_resource.timeout)
- else
- powershell_out("@(Get-WindowsFeature #{new_resource.feature_name.join(',')} | ?{$_.InstallState -ne \'Removed\'}).count", timeout: new_resource.timeout)
- end
- cmd.stderr.empty? && cmd.stdout.chomp.to_i > 0
+ # if any features are not supported on this release of Windows or
+ # have been deleted raise with a friendly message. At one point in time
+ # we just warned, but this goes against the behavior of ever other package
+ # provider in Chef and it isn't clear what you'd want if you passed an array
+ # and some features were available and others were not.
+ # @return [void]
+ def fail_if_unavailable
+ all_available = node["powershell_features_cache"]["enabled"] +
+ node["powershell_features_cache"]["disabled"] +
+ node["powershell_features_cache"]["removed"]
+
+ # the difference of desired features to install to all features is what's not available
+ unavailable = (new_resource.feature_name - all_available)
+ raise "The Windows feature#{'s' if unavailable.count > 1} #{unavailable.join(',')} #{unavailable.count > 1 ? 'are' : 'is'} not available on this version of Windows. Run 'Get-WindowsFeature' to see the list of available feature names." unless unavailable.empty?
+ end
+
+ # run Get-WindowsFeature to get a list of all available features and their state
+ # and save that to the node at node.override level.
+ # @return [void]
+ def reload_cached_powershell_data
+ Chef::Log.debug("Caching Windows features available via Get-WindowsFeature.")
+ node.override["powershell_features_cache"] = Mash.new
+ node.override["powershell_features_cache"]["enabled"] = []
+ node.override["powershell_features_cache"]["disabled"] = []
+ node.override["powershell_features_cache"]["removed"] = []
+
+ parsed_feature_list.each do |feature_details_raw|
+ case feature_details_raw["InstallState"]
+ when 5 # matches 'Removed' InstallState
+ add_to_feature_mash("removed", feature_details_raw["Name"])
+ when 1, 3 # matches 'Installed' or 'InstallPending' states
+ add_to_feature_mash("enabled", feature_details_raw["Name"])
+ when 0, 2 # matches 'Available' or 'UninstallPending' states
+ add_to_feature_mash("disabled", feature_details_raw["Name"])
+ end
+ end
+ Chef::Log.debug("The powershell cache contains\n#{node['powershell_features_cache']}")
+ end
+
+ # fetch the list of available feature names and state in JSON and parse the JSON
+ def parsed_feature_list
+ # Grab raw feature information from dism command line
+ raw_list_of_features = if node["os_version"].to_f < 6.2
+ powershell_out!("Import-Module ServerManager; Get-WindowsFeature | Select-Object -Property Name,InstallState | ConvertTo-Json -Compress", timeout: new_resource.timeout).stdout
+ else
+ powershell_out!("Get-WindowsFeature | Select-Object -Property Name,InstallState | ConvertTo-Json -Compress", timeout: new_resource.timeout).stdout
+ end
+
+ Chef::JSONCompat.from_json(raw_list_of_features)
+ end
+
+ # add the features values to the appropriate array
+ # @return [void]
+ def add_to_feature_mash(feature_type, feature_details)
+ node.override["powershell_features_cache"][feature_type] << feature_details
+ end
+
+ # Fail if any of the packages are in a removed state
+ # @return [void]
+ def fail_if_removed
+ return if new_resource.source # if someone provides a source then all is well
+ if node["os_version"].to_f > 6.2
+ return if registry_key_exists?('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Servicing') && registry_value_exists?('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Servicing', name: "LocalSourcePath") # if source is defined in the registry, still fine
end
+ removed = new_resource.feature_name & node["powershell_features_cache"]["removed"]
+ raise "The Windows feature#{'s' if removed.count > 1} #{removed.join(',')} #{removed.count > 1 ? 'are' : 'is'} have been removed from the host and cannot be installed." unless removed.empty?
+ end
+
+ # Fail unless we're on windows 8+ / 2012+ where deleting a feature is supported
+ def fail_if_delete_unsupported
+ raise Chef::Exceptions::UnsupportedAction, "#{self} :delete action not support on Windows releases before Windows 8/2012. Cannot continue!" unless node["platform_version"].to_f >= 6.2
end
end
end