summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2018-03-02 13:37:51 -0800
committerTim Smith <tsmith@chef.io>2018-03-02 13:41:08 -0800
commit43074e78329d3901f04080e4b3a1b04a39ba6bca (patch)
treef9fdc5b5e2fbea6fd55606ebaa43b76d80cb4945
parent6f2c11cb9c7ded1137ce2138573d3d0d41e4c551 (diff)
downloadchef-43074e78329d3901f04080e4b3a1b04a39ba6bca.tar.gz
Add windows_feature, windows_feature_powershell, and
windows_feature_dism resources Ported from the windows cookbook 4.0 Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--lib/chef/resource/windows_feature.rb81
-rw-r--r--lib/chef/resource/windows_feature_dism.rb177
-rw-r--r--lib/chef/resource/windows_feature_powershell.rb105
-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, 489 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..f79434ddc2
--- /dev/null
+++ b/lib/chef/resource/windows_feature.rb
@@ -0,0 +1,81 @@
+#
+# 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
+
+ property :feature_name, [Array, String], name_property: true
+ property :source, String
+ property :all, [true, false], default: false
+ property :management_tools, [true, false], default: false
+ property :install_method, Symbol, equal_to: [:windows_feature_dism, :windows_feature_powershell, :windows_feature_servermanagercmd]
+ property :timeout, Integer, default: 600
+
+ action :install do
+ run_default_provider :install
+ end
+
+ action :remove do
+ run_default_provider :remove
+ end
+
+ action :delete do
+ run_default_provider :delete
+ end
+
+ action_class do
+ # @return [Symbol] :windows_feature_dism or the provider specified in install_method property
+ def locate_default_provider
+ if new_resource.install_method
+ new_resource.install_method
+ else
+ :windows_feature_dism
+ end
+ end
+
+ # call the appropriate windows_feature resource based on the specified provider
+ # @return [void]
+ def run_default_provider(desired_action)
+ case locate_default_provider
+ when :windows_feature_dism
+ windows_feature_dism 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
+ end
+ when :windows_feature_servermanagercmd
+ 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)."
+ when :windows_feature_powershell
+ windows_feature_powershell 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
+ end
+ 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..aece7a78f3
--- /dev/null
+++ b/lib/chef/resource/windows_feature_dism.rb
@@ -0,0 +1,177 @@
+#
+# 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
+
+ property :feature_name, [Array, String], coerce: proc { |x| Array(x) }, name_property: true
+ property :source, String
+ property :all, [true, false], default: false
+ property :timeout, Integer, default: 600
+
+ action :install do
+ 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
+ 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
+ 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..e7c785ab3c
--- /dev/null
+++ b/lib/chef/resource/windows_feature_powershell.rb
@@ -0,0 +1,105 @@
+#
+# 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
+
+ property :feature_name, [Array, String], coerce: proc { |x| Array(x) }, name_property: true
+ property :source, String
+ property :all, [true, false], default: false
+ property :timeout, Integer, default: 600
+ property :management_tools, [true, false], default: false
+
+ include Chef::Mixin::PowershellOut
+
+ action :install do
+ 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
+ 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
+ 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