summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/chef/resource/windows_feature.rb90
-rw-r--r--lib/chef/resource/windows_feature_dism.rb198
-rw-r--r--lib/chef/resource/windows_feature_powershell.rb133
-rw-r--r--lib/chef/resources.rb3
-rw-r--r--spec/unit/resource/windows_feature.rb41
-rw-r--r--spec/unit/resource/windows_feature_dism.rb41
-rw-r--r--spec/unit/resource/windows_feature_powershell.rb41
7 files changed, 547 insertions, 0 deletions
diff --git a/lib/chef/resource/windows_feature.rb b/lib/chef/resource/windows_feature.rb
new file mode 100644
index 0000000000..42f5524cc1
--- /dev/null
+++ b/lib/chef/resource/windows_feature.rb
@@ -0,0 +1,90 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+#
+# Copyright:: 2011-2018, 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.
+#
+class Chef
+ class Resource
+ class WindowsFeature < Chef::Resource
+ resource_name :windows_feature
+ provides :windows_feature
+
+ description "Using the windows_feature resource to add, remove or delete Windows features and roles"
+ introduced "14.0"
+
+ property :feature_name, [Array, String],
+ description: "The name of the feature/role(s) to install. The same feature may have different"\
+ " names depending on the underlying resource being used (ie DHCPServer vs DHCP;"\
+ " DNS-Server-Full-Role vs DNS).",
+ name_property: true
+
+ property :source, String,
+ description: "Use a local repository for the feature install."
+
+ property :all, [true, false],
+ description: "Install all sub features.",
+ default: false
+
+ property :management_tools, [true, false],
+ description: "Install all applicable management tools of the roles, role services, or features (PowerShell only).",
+ default: false
+
+ property :install_method, Symbol,
+ description: "If DISM or PowerShell should be used for the installation. Note feature names differ"\
+ " between the two installation methods.",
+ equal_to: [:windows_feature_dism, :windows_feature_powershell, :windows_feature_servermanagercmd]
+
+ property :timeout, Integer,
+ description: "Specifies a timeout (in seconds) for feature install.",
+ default: 600
+
+ action :install do
+ description "Install a Windows role/feature"
+
+ run_default_subresource :install
+ end
+
+ action :remove do
+ description "Remove a Windows role/feature"
+
+ run_default_subresource :remove
+ end
+
+ action :delete do
+ description "Remove a Windows role/feature from the image"
+
+ run_default_subresource :delete
+ end
+
+ action_class do
+ # call the appropriate windows_feature resource based on the specified subresource
+ # @return [void]
+ def run_default_subresource(desired_action)
+ raise "Support for Windows feature installation via servermanagercmd.exe has been removed as this support is no longer needed in Windows 2008 R2 and above. You will need to update your cookbook to install either via dism or powershell (preferred)." if new_resource.install_method == :windows_feature_servermanagercmd
+
+ subresource = new_resource.install_method || :windows_feature_dism
+ declare_resource(subresource, new_resource.name) do
+ action desired_action
+ feature_name new_resource.feature_name
+ source new_resource.source if new_resource.source
+ all new_resource.all
+ timeout new_resource.timeout
+ management_tools new_resource.management_tools if subresource == :windows_feature_powershell
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/windows_feature_dism.rb b/lib/chef/resource/windows_feature_dism.rb
new file mode 100644
index 0000000000..6ffaf318d5
--- /dev/null
+++ b/lib/chef/resource/windows_feature_dism.rb
@@ -0,0 +1,198 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+#
+# Copyright:: 2011-2018, 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.
+#
+
+class Chef
+ class Resource
+ class WindowsFeatureDism < Chef::Resource
+ resource_name :windows_feature_dism
+ provides :windows_feature_dism
+
+ description "Using the windows_feature_dism resource to add, remove or"\
+ " delete Windows features and roles using DISM"
+ introduced "14.0"
+
+ 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) },
+ name_property: true
+
+ property :source, String,
+ description: "Use a local repository for the feature install."
+
+ property :all, [true, false],
+ description: "Install all sub features. This is the equivalent of specifying the /All switch to dism.exe",
+ default: false
+
+ property :timeout, Integer,
+ description: "Specifies a timeout (in seconds) for feature install.",
+ default: 600
+
+ action :install do
+ description "Install a Windows role/feature using DISM"
+
+ reload_cached_dism_data unless node["dism_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?
+ message = "install Windows feature#{'s' if features_to_install.count > 1} #{features_to_install.join(',')}"
+ converge_by(message) do
+ addsource = new_resource.source ? "/LimitAccess /Source:\"#{new_resource.source}\"" : ""
+ addall = new_resource.all ? "/All" : ""
+ shell_out!("dism.exe /online /enable-feature #{features_to_install.map { |f| "/featurename:#{f}" }.join(' ')} /norestart #{addsource} #{addall}", returns: [0, 42, 127, 3010], timeout: new_resource.timeout)
+
+ reload_cached_dism_data # Reload cached dism feature state
+ end
+ end
+ end
+
+ action :remove do
+ description "Remove a Windows role/feature using DISM"
+
+ reload_cached_dism_data unless node["dism_features_cache"]
+
+ Chef::Log.debug("Windows features needing removal: #{features_to_remove.empty? ? 'none' : features_to_remove.join(',')}")
+ unless features_to_remove.empty?
+ message = "remove Windows feature#{'s' if features_to_remove.count > 1} #{features_to_remove.join(',')}"
+
+ converge_by(message) do
+ shell_out!("dism.exe /online /disable-feature #{features_to_remove.map { |f| "/featurename:#{f}" }.join(' ')} /norestart", returns: [0, 42, 127, 3010], timeout: new_resource.timeout)
+
+ reload_cached_dism_data # Reload cached dism feature state
+ end
+ end
+ end
+
+ action :delete do
+ description "Remove a Windows role/feature from the image using DISM"
+
+ fail_if_delete_unsupported
+
+ reload_cached_dism_data unless node["dism_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?
+ message = "delete Windows feature#{'s' if features_to_delete.count > 1} #{features_to_delete.join(',')} from the image"
+ converge_by(message) do
+ shell_out!("dism.exe /online /disable-feature #{features_to_delete.map { |f| "/featurename:#{f}" }.join(' ')} /Remove /norestart", returns: [0, 42, 127, 3010], timeout: new_resource.timeout)
+
+ reload_cached_dism_data # Reload cached dism 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 features are what needs installing
+ @install ||= new_resource.feature_name & node["dism_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["dism_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["dism_features_cache"]["enabled"] +
+ node["dism_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["dism_features_cache"]["enabled"] +
+ node["dism_features_cache"]["disabled"] +
+ node["dism_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 'dism /online /Get-Features' to see the list of available feature names." unless unavailable.empty?
+ end
+
+ # run dism.exe to get a list of all available features and their state
+ # and save that to the node at node.normal (same as ohai) level.
+ # We do this because getting a list of features in dism takes at least a second
+ # and this data will be persisted across multiple resource runs which gives us
+ # a much faster run when no features actually need to be installed / removed.
+ # @return [void]
+ def reload_cached_dism_data
+ Chef::Log.debug("Caching Windows features available via dism.exe.")
+ node.normal["dism_features_cache"] = Mash.new
+ node.normal["dism_features_cache"]["enabled"] = []
+ node.normal["dism_features_cache"]["disabled"] = []
+ node.normal["dism_features_cache"]["removed"] = []
+
+ # Grab raw feature information from dism command line
+ raw_list_of_features = shell_out("dism.exe /Get-Features /Online /Format:Table /English").stdout
+
+ # Split stdout into an array by windows line ending
+ features_list = raw_list_of_features.split("\r\n")
+ features_list.each do |feature_details_raw|
+ case feature_details_raw
+ when /Payload Removed/ # matches 'Disabled with Payload Removed'
+ add_to_feature_mash("removed", feature_details_raw)
+ when /Enable/ # matches 'Enabled' and 'Enable Pending' aka after reboot
+ add_to_feature_mash("enabled", feature_details_raw)
+ when /Disable/ # matches 'Disabled' and 'Disable Pending' aka after reboot
+ add_to_feature_mash("disabled", feature_details_raw)
+ end
+ end
+ Chef::Log.debug("The cache contains\n#{node['dism_features_cache']}")
+ end
+
+ # parse the feature string and add the values to the appropriate array
+ # in the
+ # strips trailing whitespace characters then split on n number of spaces
+ # + | + n number of spaces
+ # @return [void]
+ def add_to_feature_mash(feature_type, feature_string)
+ feature_details = feature_string.strip.split(/\s+[|]\s+/)
+ node.normal["dism_features_cache"][feature_type] << feature_details.first
+ 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
+ removed = new_resource.feature_name & node["dism_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
+ # @return [void]
+ 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
+ end
+end
diff --git a/lib/chef/resource/windows_feature_powershell.rb b/lib/chef/resource/windows_feature_powershell.rb
new file mode 100644
index 0000000000..30f8f6da83
--- /dev/null
+++ b/lib/chef/resource/windows_feature_powershell.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Greg Zapp (<greg.zapp@gmail.com>)
+#
+# Copyright:: 2015-2018, 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.
+#
+
+class Chef
+ class Resource
+ class WindowsFeaturePowershell < Chef::Resource
+ resource_name :windows_feature_powershell
+ provides :windows_feature_powershell
+
+ description "Use the windows_feature_powershell resource to add, remove or"\
+ " delete Windows features and roles using PowerShell. This resource"\
+ " offers significant speed benefits over the windows_feature_dism resource,"\
+ " but requires installing the Remote Server Administration Tools on"\
+ " non-server releases of Windows"
+ introduced "14.0"
+
+ 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) },
+ name_property: true
+
+ property :source, String,
+ description: "Use a local repository for the feature install."
+
+ property :all, [true, false],
+ description: "Install all sub features. This is equivalent to using the"\
+ " -InstallAllSubFeatures switch with Add-WindowsFeature.",
+ default: false
+
+ property :timeout, Integer,
+ description: "Specifies a timeout (in seconds) for feature install.",
+ default: 600
+
+ property :management_tools, [true, false],
+ description: "",
+ default: false
+
+ 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
+ Chef::Log.info(cmd.stdout)
+ end
+ end
+ end
+
+ 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)
+ Chef::Log.info(cmd.stdout)
+ end
+ end
+ end
+
+ 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)
+ Chef::Log.info(cmd.stdout)
+ 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
+ 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
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index ba91660df0..42150ffc82 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -110,6 +110,9 @@ require "chef/resource/cab_package"
require "chef/resource/powershell_package"
require "chef/resource/msu_package"
require "chef/resource/windows_auto_run"
+require "chef/resource/windows_feature"
+require "chef/resource/windows_feature_dism"
+require "chef/resource/windows_feature_powershell"
require "chef/resource/windows_font"
require "chef/resource/windows_pagefile"
require "chef/resource/windows_path"
diff --git a/spec/unit/resource/windows_feature.rb b/spec/unit/resource/windows_feature.rb
new file mode 100644
index 0000000000..c8b8587ed8
--- /dev/null
+++ b/spec/unit/resource/windows_feature.rb
@@ -0,0 +1,41 @@
+#
+# Copyright:: Copyright 2018, 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::WindowsFeature do
+ let(:resource) { Chef::Resource::WindowsFeature.new("SNMP") }
+
+ it "sets resource name as :windows_feature" do
+ expect(resource.resource_name).to eql(:windows_feature)
+ end
+
+ it "sets the default action as :install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "sets the feature_name property as its name" do
+ expect(resource.feature_name).to eql("SNMP")
+ end
+
+ it "supports :install, :remove, and :delete actions" do
+ expect { resource.action :install }.not_to raise_error
+ expect { resource.action :remove }.not_to raise_error
+ expect { resource.action :delete }.not_to raise_error
+ expect { resource.action :update }.to raise_error(ArgumentError)
+ end
+end
diff --git a/spec/unit/resource/windows_feature_dism.rb b/spec/unit/resource/windows_feature_dism.rb
new file mode 100644
index 0000000000..3885f4813e
--- /dev/null
+++ b/spec/unit/resource/windows_feature_dism.rb
@@ -0,0 +1,41 @@
+#
+# Copyright:: Copyright 2018, 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::WindowsFeatureDism do
+ let(:resource) { Chef::Resource::WindowsFeatureDism.new("SNMP") }
+
+ it "sets resource name as :windows_feature_dism" do
+ expect(resource.resource_name).to eql(:windows_feature_dism)
+ end
+
+ it "sets the default action as :install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "sets the feature_name property as its name and coerces it to an array" do
+ expect(resource.feature_name).to eql(["SNMP"])
+ end
+
+ it "supports :install, :remove, and :delete actions" do
+ expect { resource.action :install }.not_to raise_error
+ expect { resource.action :remove }.not_to raise_error
+ expect { resource.action :delete }.not_to raise_error
+ expect { resource.action :update }.to raise_error(ArgumentError)
+ end
+end
diff --git a/spec/unit/resource/windows_feature_powershell.rb b/spec/unit/resource/windows_feature_powershell.rb
new file mode 100644
index 0000000000..02f308ca73
--- /dev/null
+++ b/spec/unit/resource/windows_feature_powershell.rb
@@ -0,0 +1,41 @@
+#
+# Copyright:: Copyright 2018, 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::WindowsFeaturePowershell do
+ let(:resource) { Chef::Resource::WindowsFeaturePowershell.new("SNMP") }
+
+ it "sets resource name as :windows_feature_powershell" do
+ expect(resource.resource_name).to eql(:windows_feature_powershell)
+ end
+
+ it "sets the default action as :install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "sets the feature_name property as its name and coerces it to an array" do
+ expect(resource.feature_name).to eql(["SNMP"])
+ end
+
+ it "supports :install, :remove, and :delete actions" do
+ expect { resource.action :install }.not_to raise_error
+ expect { resource.action :remove }.not_to raise_error
+ expect { resource.action :delete }.not_to raise_error
+ expect { resource.action :update }.to raise_error(ArgumentError)
+ end
+end