diff options
author | Tim Smith <tsmith@chef.io> | 2018-03-29 18:10:39 -0700 |
---|---|---|
committer | Tim Smith <tsmith@chef.io> | 2018-03-29 18:10:39 -0700 |
commit | 4126c97feebb17ed368895702e1e99e38722e5ab (patch) | |
tree | 75c98f7293b646937cb7ab47dd979b1c1af4c173 | |
parent | b9d35bfa2a43cebcedad4ce03564bc5581634e9f (diff) | |
download | chef-4126c97feebb17ed368895702e1e99e38722e5ab.tar.gz |
Copy over the caching logic from the windows cookbookwindows_feature++
This gives us a 3.5x speedup. Yes we need to do this better, but it's no worse than DISM which we already landed in chef and does the exact same thing
Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r-- | lib/chef/resource/windows_feature_powershell.rb | 213 |
1 files changed, 167 insertions, 46 deletions
diff --git a/lib/chef/resource/windows_feature_powershell.rb b/lib/chef/resource/windows_feature_powershell.rb index b25574f0f5..7d384fecb4 100644 --- a/lib/chef/resource/windows_feature_powershell.rb +++ b/lib/chef/resource/windows_feature_powershell.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require "chef/mixin/powershell_out" +require "chef/json_compat" require "chef/resource" class Chef @@ -33,7 +35,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| x.is_a?(String) ? x.split(/\s*,\s*/) : x }, + coerce: proc { |x| to_lowercase_array(x) }, name_property: true property :source, String, @@ -52,82 +54,201 @@ class Chef description: "", default: false + def to_lowercase_array(x) + x = x.split(/\s*,\s*/) if x.is_a?(String) # split multiple forms of a comma separated list + x.map(&:downcase) + end + include Chef::Mixin::PowershellOut 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 + raise_on_old_powershell + + 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["platform_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 action :remove do - description "Remove a Windows role/feature using PowerShell" + raise_on_old_powershell + + 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(',')}") - 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) + 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 action :delete do - description "Remove a Windows role/feature from the image using Powershell" + raise_on_old_powershell + raise_if_delete_unsupported + + reload_cached_powershell_data unless node["powershell_features_cache"] - 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_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 + # shellout to determine the actively installed version of powershell + # we have this same data in ohai, but it doesn't get updated if powershell is installed mid run + # @return [Integer] the powershell version or 0 for nothing + def powershell_version + cmd = powershell_out("$PSVersionTable.psversion.major") + return 1 if cmd.stdout.empty? # PowerShell 1.0 doesn't have a $PSVersionTable + Regexp.last_match(1).to_i if cmd.stdout =~ /^(\d+)/ + rescue Errno::ENOENT + 0 # zero as in nothing is installed + end + + # raise if we're running powershell less than 3.0 since we need convertto-json + # check the powershell version via ohai data and if we're < 3.0 also shellout to make sure as + # a newer version could be installed post ohai run. Yes we're double checking. It's fine. + # @todo this can go away when we fully remove support for Windows 2008 R2 + # @raise [RuntimeError] Raise if powershell is < 3.0 + def raise_on_old_powershell + # be super defensive about the powershell lang plugin not being there + return if node["languages"] && node["languages"]["powershell"] && node["languages"]["powershell"]["version"].to_i > 3 + raise "The windows_feature_powershell resource requires PowerShell 3.0 or later. Please install PowerShell 3.0+ before running this resource." if powershell_version < 3 + end + def install_feature_cmdlet - node["os_version"].to_f < 6.2 ? "Import-Module ServerManager; Add-WindowsFeature" : "Install-WindowsFeature" + node["platform_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 + node["platform_version"].to_f < 6.2 ? "Import-Module ServerManager; Remove-WindowsFeature" : "Uninstall-WindowsFeature" + end + + # @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 + + # 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["platform_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 - 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 + # 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.downcase # lowercase so we can compare properly + 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["platform_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 raise_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 |