# # Author:: Greg Zapp () # # Copyright:: Copyright (c) Chef Software Inc. # # 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_relative "../json_compat" require_relative "../resource" require_relative "../platform/query_helpers" class Chef class Resource class WindowsFeaturePowershell < Chef::Resource provides(:windows_feature_powershell) { true } description "Use the **windows_feature_powershell** resource to add, remove, or entirely delete Windows features and roles using PowerShell. This resource offers significant speed benefits over the windows_feature_dism resource, but requires installation of the Remote Server Administration Tools on non-server releases of Windows." introduced "14.0" examples <<~DOC **Add the SMTP Server feature**: ```ruby windows_feature_powershell "smtp-server" do action :install all true end ``` **Install multiple features using one resource**: ```ruby windows_feature_powershell ['Web-Asp-Net45', 'Web-Net-Ext45'] do action :install end ``` **Install the Network Policy and Access Service feature**: ```ruby windows_feature_powershell 'NPAS' do action :install management_tools true end ``` DOC property :feature_name, [Array, String], description: "The name of the feature(s) or role(s) to install if they differ from the resource block's name.", coerce: proc { |x| to_formatted_array(x) }, name_property: true property :source, String, description: "Specify a local repository for the feature install." property :all, [TrueClass, FalseClass], description: "Install all subfeatures. When set to `true`, this is the equivalent of specifying the `-InstallAllSubFeatures` switch with `Add-WindowsFeature`.", default: false property :timeout, Integer, description: "Specifies a timeout (in seconds) for the feature installation.", default: 600, desired_state: false property :management_tools, [TrueClass, FalseClass], description: "Install all applicable management tools for the roles, role services, or features.", default: false # Converts strings of features into an Array. Array objects are lowercased # @return [Array] array of features def to_formatted_array(x) x = x.split(/\s*,\s*/) if x.is_a?(String) # split multiple forms of a comma separated list # features aren't case sensitive so let's compare in lowercase x.map(&:downcase) end action :install, description: "Install a Windows role or feature using PowerShell." do 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-WindowsFeature #{features_to_install.join(",")}" install_command << " -IncludeAllSubFeature" if new_resource.all install_command << " -Source \"#{new_resource.source}\"" if new_resource.source install_command << " -IncludeManagementTools" if new_resource.management_tools cmd = powershell_exec!(install_command, timeout: new_resource.timeout) Chef::Log.info(cmd.result) reload_cached_powershell_data # Reload cached powershell feature state end end end action :remove, description: "Remove a Windows role or feature using PowerShell." do 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_exec!("Uninstall-WindowsFeature #{features_to_remove.join(",")}", timeout: new_resource.timeout) Chef::Log.info(cmd.result) reload_cached_powershell_data # Reload cached powershell feature state end end end action :delete, description: "Delete a Windows role or feature from the image using PowerShell." do 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_exec!("Uninstall-WindowsFeature #{features_to_delete.join(",")} -Remove", timeout: new_resource.timeout) Chef::Log.info(cmd.result) reload_cached_powershell_data # Reload cached powershell feature state end end end action_class do # @return [Array] features the user has requested to install which need installation def features_to_install # the intersection of the features to install & disabled/removed features are what needs installing @features_to_install ||= begin features = node["powershell_features_cache"]["disabled"] features |= node["powershell_features_cache"]["removed"] if new_resource.source new_resource.feature_name & features end 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.") # # FIXME FIXME FIXME # The node object should not be used for caching state like this and this is not a public API and may break. # FIXME FIXME FIXME # 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 WindowsFeature raw_list_of_features = powershell_exec!("Get-WindowsFeature | Select-Object -Property Name,InstallState", timeout: new_resource.timeout).result raw_list_of_features || [] end # add the features values to the appropriate array # @return [void] def add_to_feature_mash(feature_type, feature_details) # add the lowercase feature name to the mash so we can compare it lowercase later node.override["powershell_features_cache"][feature_type] << feature_details.downcase 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 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 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"} removed from the host and cannot be installed." unless removed.empty? end end end end end