diff options
author | Adam Edwards <adamed@opscode.com> | 2014-09-02 15:10:18 -0700 |
---|---|---|
committer | Adam Edwards <adamed@opscode.com> | 2014-09-02 15:10:18 -0700 |
commit | ee76ed291aea01b6a484f7f7a4f9c4067eb19f41 (patch) | |
tree | 528adf69f0549d4c0b76c15adab571990d751319 | |
parent | 54e7881f5fbb9a078fe93cea6d531f06646afd84 (diff) | |
parent | 08e45161fc63b7c95cf07afbfa17d41fac459c81 (diff) | |
download | chef-ee76ed291aea01b6a484f7f7a4f9c4067eb19f41.tar.gz |
Merge pull request #1958 from opscode/adamedx/dsc-resource-modules
DSC resource modules that have not been imported can cause failures
-rw-r--r-- | lib/chef/util/dsc/local_configuration_manager.rb | 36 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet_result.rb | 2 | ||||
-rw-r--r-- | spec/functional/resource/dsc_script_spec.rb | 229 | ||||
-rw-r--r-- | spec/unit/util/dsc/lcm_output_parser_spec.rb | 4 | ||||
-rw-r--r-- | spec/unit/util/dsc/local_configuration_manager_spec.rb | 134 |
5 files changed, 388 insertions, 17 deletions
diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb index 79cfcd2996..4a56b6a397 100644 --- a/lib/chef/util/dsc/local_configuration_manager.rb +++ b/lib/chef/util/dsc/local_configuration_manager.rb @@ -29,14 +29,7 @@ class Chef::Util::DSC def test_configuration(configuration_document) status = run_configuration_cmdlet(configuration_document) - unless status.succeeded? - # LCM returns an error if any of the resources do not support the opptional What-If - if status.stderr.gsub(/\s+/, ' ') =~ /A parameter cannot be found that matches parameter name 'Whatif'/ - Chef::Log::warn("Received error while testing configuration due to resource not supporting 'WhatIf'") - else - raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{status.stderr.gsub(/\s+/, ' ')}" - end - end + handle_what_if_exception!(status.stderr) unless status.succeeded? configuration_update_required?(status.return_value) end @@ -57,7 +50,7 @@ class Chef::Util::DSC test_only_parameters = ! apply_configuration ? '-whatif; if (! $?) { exit 1 }' : '' start_operation_timing - command_code = "$ProgressPreference = 'SilentlyContinue';start-dscconfiguration -path #{@configuration_path} -wait -force #{test_only_parameters}" + command_code = lcm_command_code(@configuration_path, test_only_parameters) status = nil begin @@ -79,12 +72,35 @@ class Chef::Util::DSC status end + def lcm_command_code(configuration_path, test_only_parameters) + <<-EOH +$ProgressPreference = 'SilentlyContinue';start-dscconfiguration -path #{@configuration_path} -wait -erroraction 'continue' -force #{test_only_parameters} +EOH + end + + def handle_what_if_exception!(what_if_exception_output) + if what_if_exception_output.gsub(/\s+/, ' ') =~ /A parameter cannot be found that matches parameter name 'Whatif'/i + # LCM returns an error if any of the resources do not support the opptional What-If + Chef::Log::warn("Received error while testing configuration due to resource not supporting 'WhatIf'") + elsif output_has_dsc_module_failure?(what_if_exception_output) + Chef::Log::warn("Received error while testing configuration due to a module for an imported resource possibly not being fully installed:\n#{what_if_exception_output.gsub(/\s+/, ' ')}") + else + raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{what_if_exception_output.gsub(/\s+/, ' ')}" + end + end + + def output_has_dsc_module_failure?(what_if_output) + !! (what_if_output =~ /\sCimException/ && + what_if_output =~ /ProviderOperationExecutionFailure/ && + what_if_output =~ /\smodule\s+is\s+installed/) + end + def configuration_update_required?(what_if_output) Chef::Log.debug("DSC: DSC returned the following '-whatif' output from test operation:\n#{what_if_output}") begin Parser::parse(what_if_output) rescue Chef::Util::DSC::LocalConfigurationManager::Parser => e - Chef::Log::warn("Could not parse parse LCM output: #{e}") + Chef::Log::warn("Could not parse LCM output: #{e}") [Chef::Util::DSC::ResourceInfo.new('Unknown DSC Resources', true, ['Unknown changes because LCM output was not parsable.'])] end end diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb index 390ee8cbc1..696013e684 100644 --- a/lib/chef/util/powershell/cmdlet_result.rb +++ b/lib/chef/util/powershell/cmdlet_result.rb @@ -38,7 +38,7 @@ class Chef::Util::Powershell @status.stdout end end - + def succeeded? @succeeded = @status.status.exitstatus == 0 end diff --git a/spec/functional/resource/dsc_script_spec.rb b/spec/functional/resource/dsc_script_spec.rb index 488f6aebef..79917c01df 100644 --- a/spec/functional/resource/dsc_script_spec.rb +++ b/spec/functional/resource/dsc_script_spec.rb @@ -17,6 +17,7 @@ #
require 'spec_helper'
+require 'chef/mixin/shell_out'
require 'chef/mixin/windows_architecture_helper'
describe Chef::Resource::DscScript, :windows_powershell_dsc_only do
@@ -29,15 +30,36 @@ describe Chef::Resource::DscScript, :windows_powershell_dsc_only do ::FileUtils.rm_rf(@temp_dir) if ::Dir.exist?(@temp_dir)
end
- def create_config_script_from_code(code, configuration_name)
- script_code = "Configuration '#{configuration_name}'\n{\n\t#{code}\n}\n"
- script_path = "#{@temp_dir}/dsc_functional_test.ps1"
+ include Chef::Mixin::ShellOut
+
+ def create_config_script_from_code(code, configuration_name, data = false)
+ script_code = data ? code : "Configuration '#{configuration_name}'\n{\n\t#{code}\n}\n"
+ data_suffix = data ? '_config_data' : ''
+ extension = data ? 'psd1' : 'ps1'
+ script_path = "#{@temp_dir}/dsc_functional_test#{data_suffix}.#{extension}"
::File.open(script_path, 'wt') do | script |
script.write(script_code)
end
script_path
end
+ def user_exists?(target_user)
+ result = false
+ begin
+ shell_out!("net user #{target_user}")
+ result = true
+ rescue Mixlib::ShellOut::ShellCommandFailed
+ end
+ result
+ end
+
+ def delete_user(target_user)
+ begin
+ shell_out!("net user #{target_user} /delete")
+ rescue Mixlib::ShellOut::ShellCommandFailed
+ end
+ end
+
let(:dsc_env_variable) { 'chefenvtest' }
let(:dsc_env_value1) { 'value1' }
let(:env_value2) { 'value2' }
@@ -68,6 +90,92 @@ describe Chef::Resource::DscScript, :windows_powershell_dsc_only do }
EOH
}
+
+ let(:dsc_user_prefix) { 'dsc' }
+ let(:dsc_user_suffix) { 'chefx' }
+ let(:dsc_user) {"#{dsc_user_prefix}_usr_#{dsc_user_suffix}" }
+ let(:dsc_user_prefix_env_var_name) { 'dsc_user_env_prefix' }
+ let(:dsc_user_suffix_env_var_name) { 'dsc_user_env_suffix' }
+ let(:dsc_user_prefix_env_code) { "$env:#{dsc_user_prefix_env_var_name}"}
+ let(:dsc_user_suffix_env_code) { "$env:#{dsc_user_suffix_env_var_name}"}
+ let(:dsc_user_prefix_param_name) { 'dsc_user_prefix_param' }
+ let(:dsc_user_suffix_param_name) { 'dsc_user_suffix_param' }
+ let(:dsc_user_prefix_param_code) { "$#{dsc_user_prefix_param_name}"}
+ let(:dsc_user_suffix_param_code) { "$#{dsc_user_suffix_param_name}"}
+ let(:dsc_user_env_code) { "\"$(#{dsc_user_prefix_env_code})_usr_$(#{dsc_user_suffix_env_code})\""}
+ let(:dsc_user_param_code) { "\"$(#{dsc_user_prefix_param_code})_usr_$(#{dsc_user_suffix_param_code})\""}
+
+ let(:config_flags) { nil }
+ let(:config_params) { <<-EOH
+
+ [CmdletBinding()]
+ param
+ (
+ $#{dsc_user_prefix_param_name},
+ $#{dsc_user_suffix_param_name}
+ )
+EOH
+ }
+
+ let(:config_param_section) { '' }
+ let(:dsc_user_code) { "'#{dsc_user}'" }
+ let(:dsc_user_prefix_code) { dsc_user_prefix }
+ let(:dsc_user_suffix_code) { dsc_user_suffix }
+ let(:dsc_script_environment_attribute) { nil }
+ let(:dsc_user_resources_code) { <<-EOH
+ #{config_param_section}
+node localhost
+{
+$testuser = #{dsc_user_code}
+$testpassword = ConvertTo-SecureString -String "jf9a8m49jrajf4#" -AsPlainText -Force
+$testcred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $testuser, $testpassword
+
+User dsctestusercreate
+{
+ UserName = $testuser
+ Password = $testcred
+ Description = "DSC test user"
+ Ensure = "Present"
+ Disabled = $false
+ PasswordNeverExpires = $true
+ PasswordChangeRequired = $false
+}
+}
+EOH
+ }
+
+ let(:dsc_user_config_data) {
+<<-EOH
+@{
+ AllNodes = @(
+ @{
+ NodeName = "localhost";
+ PSDscAllowPlainTextPassword = $true
+ }
+ )
+}
+
+EOH
+ }
+
+ let(:dsc_environment_env_var_name) { 'dsc_test_cwd' }
+ let(:dsc_environment_no_fail_not_etc_directory) { "#{ENV['systemroot']}\\system32" }
+ let(:dsc_environment_fail_etc_directory) { "#{ENV['systemroot']}\\system32\\drivers\\etc" }
+ let(:exception_message_signature) { 'LL927-LL928' }
+ let(:dsc_environment_config) {<<-EOH
+if (($pwd.path -eq '#{dsc_environment_fail_etc_directory}') -and (test-path('#{dsc_environment_fail_etc_directory}')))
+{
+ throw 'Signature #{exception_message_signature}: Purposefully failing because cwd == #{dsc_environment_fail_etc_directory}'
+}
+environment "whatsmydir"
+{
+ Name = '#{dsc_environment_env_var_name}'
+ Value = $pwd.path
+ Ensure = 'Present'
+}
+EOH
+ }
+
let(:dsc_config_name) {
dsc_test_resource_base.name
}
@@ -75,8 +183,10 @@ EOH dsc_test_resource_base.code(dsc_code)
dsc_test_resource_base
}
+ let(:config_name_value) { dsc_test_resource_base.name }
+
let(:dsc_resource_from_path) {
- dsc_test_resource_base.command(create_config_script_from_code(dsc_code, dsc_test_resource_base.name))
+ dsc_test_resource_base.command(create_config_script_from_code(dsc_code, config_name_value))
dsc_test_resource_base
}
@@ -100,6 +210,104 @@ EOH expect(dsc_test_resource.registry_key_exists?(test_registry_key)).to eq(true)
expect(dsc_test_resource.registry_value_exists?(test_registry_key, {:name => test_registry_value, :type => :string, :data => test_registry_data})).to eq(true)
end
+
+ it_should_behave_like 'a dsc_script resource with configuration affected by cwd'
+ end
+
+ shared_examples_for 'a dsc_script resource with configuration affected by cwd' do
+ after(:each) do
+ removal_resource = Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context)
+ removal_resource.code <<-EOH
+environment 'removethis'
+{
+ Name = '#{dsc_environment_env_var_name}'
+ Ensure = 'Absent'
+}
+EOH
+ removal_resource.run_action(:run)
+ end
+ let(:dsc_code) { dsc_environment_config }
+ it 'should not raise an exception if the cwd is not etc' do
+ dsc_test_resource.cwd(dsc_environment_no_fail_not_etc_directory)
+ expect {dsc_test_resource.run_action(:run)}.not_to raise_error
+ end
+
+ it 'should raise an exception if the cwd is etc' do
+ dsc_test_resource.cwd(dsc_environment_fail_etc_directory)
+ expect {dsc_test_resource.run_action(:run)}.to raise_error(Chef::Exceptions::PowershellCmdletException)
+ begin
+ dsc_test_resource.run_action(:run)
+ rescue Chef::Exceptions::PowershellCmdletException => e
+ expect(e.message).to match(exception_message_signature)
+ end
+ end
+ end
+
+ shared_examples_for 'a parameterized DSC configuration script' do
+ context 'when specifying environment variables in the environment attribute' do
+ let(:dsc_user_prefix_code) { dsc_user_prefix_env_code }
+ let(:dsc_user_suffix_code) { dsc_user_suffix_env_code }
+ it_behaves_like 'a dsc_script with configuration that uses environment variables'
+ end
+ end
+
+ shared_examples_for 'a dsc_script with configuration data' do
+ context 'when using the configuration_data attribute' do
+ let(:configuration_data_attribute) { 'configuration_data' }
+ it_behaves_like 'a dsc_script with configuration data set via an attribute'
+ end
+
+ context 'when using the configuration_data_script attribute' do
+ let(:configuration_data_attribute) { 'configuration_data_script' }
+ it_behaves_like 'a dsc_script with configuration data set via an attribute'
+ end
+ end
+
+ shared_examples_for 'a dsc_script with configuration data set via an attribute' do
+ it 'should run a configuration script that creates a user' do
+ config_data_value = dsc_user_config_data
+ dsc_test_resource.configuration_name(config_name_value)
+ if configuration_data_attribute == 'configuration_data_script'
+ config_data_value = create_config_script_from_code(dsc_user_config_data, '', true)
+ end
+ dsc_test_resource.environment({dsc_user_prefix_env_var_name => dsc_user_prefix,
+ dsc_user_suffix_env_var_name => dsc_user_suffix})
+ dsc_test_resource.send(configuration_data_attribute, config_data_value)
+ dsc_test_resource.flags(config_flags)
+ expect(user_exists?(dsc_user)).to eq(false)
+ expect {dsc_test_resource.run_action(:run)}.not_to raise_error
+ expect(user_exists?(dsc_user)).to eq(true)
+ end
+ end
+
+ shared_examples_for 'a dsc_script with configuration data that takes parameters' do
+ context 'when script code takes parameters for configuration' do
+ let(:dsc_user_code) { dsc_user_param_code }
+ let(:config_param_section) { config_params }
+ let(:config_flags) {{:"#{dsc_user_prefix_param_name}" => "#{dsc_user_prefix}", :"#{dsc_user_suffix_param_name}" => "#{dsc_user_suffix}"}}
+ it 'does not directly contain the user name' do
+ configuration_script_content = ::File.open(dsc_test_resource.command) do | file |
+ file.read
+ end
+ expect(configuration_script_content.include?(dsc_user)).to be(false)
+ end
+ it_behaves_like 'a dsc_script with configuration data'
+ end
+
+ end
+
+ shared_examples_for 'a dsc_script with configuration data that uses environment variables' do
+ context 'when script code uses environment variables' do
+ let(:dsc_user_code) { dsc_user_env_code }
+
+ it 'does not directly contain the user name' do
+ configuration_script_content = ::File.open(dsc_test_resource.command) do | file |
+ file.read
+ end
+ expect(configuration_script_content.include?(dsc_user)).to be(false)
+ end
+ it_behaves_like 'a dsc_script with configuration data'
+ end
end
context 'when supplying configuration through the configuration attribute' do
@@ -112,4 +320,17 @@ EOH it_behaves_like 'a dsc_script resource with specified PowerShell configuration code'
end
+ context 'when running a configuration that manages users' do
+ before(:each) do
+ delete_user(dsc_user)
+ end
+
+ let(:dsc_code) { dsc_user_resources_code }
+ let(:config_name_value) { 'DSCTestConfig' }
+ let(:dsc_test_resource) { dsc_resource_from_path }
+
+ it_behaves_like 'a dsc_script with configuration data'
+ it_behaves_like 'a dsc_script with configuration data that uses environment variables'
+ it_behaves_like 'a dsc_script with configuration data that takes parameters'
+ end
end
diff --git a/spec/unit/util/dsc/lcm_output_parser_spec.rb b/spec/unit/util/dsc/lcm_output_parser_spec.rb index 802c0ea429..05aa072a94 100644 --- a/spec/unit/util/dsc/lcm_output_parser_spec.rb +++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb @@ -20,11 +20,11 @@ require 'chef/util/dsc/lcm_output_parser' describe Chef::Util::DSC::LocalConfigurationManager::Parser do context 'empty input parameter' do - it 'returns an emtpy array for a 0 length string' do + it 'returns an empty array for a 0 length string' do Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty end - it 'returns an emtpy array for a nil input' do + it 'returns an empty array for a nil input' do Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty end end diff --git a/spec/unit/util/dsc/local_configuration_manager_spec.rb b/spec/unit/util/dsc/local_configuration_manager_spec.rb new file mode 100644 index 0000000000..1b0d8b27db --- /dev/null +++ b/spec/unit/util/dsc/local_configuration_manager_spec.rb @@ -0,0 +1,134 @@ +#
+# Author:: Adam Edwards <adamed@getchef.com>
+# Copyright:: Copyright (c) 2014 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 'chef'
+require 'chef/util/dsc/local_configuration_manager'
+
+describe Chef::Util::DSC::LocalConfigurationManager do
+
+ let(:lcm) { Chef::Util::DSC::LocalConfigurationManager.new(nil, 'tmp') }
+
+ let(:normal_lcm_output) { <<-EOH
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOH
+ }
+
+ let(:no_whatif_lcm_output) { <<-EOH
+Start-DscConfiguration : A parameter cannot be found that matches parameter name 'whatif'.
+At line:1 char:123
++ run-somecommand -whatif
++ ~~~~~~~~
+ + CategoryInfo : InvalidArgument: (:) [Start-DscConfiguration], ParameterBindingException
+ + FullyQualifiedErrorId : NamedParameterNotFound,SomeCompany.SomeAssembly.Commands.RunSomeCommand
+EOH
+ }
+
+ let(:dsc_resource_import_failure_output) { <<-EOH
+PowerShell provider MSFT_xWebsite failed to execute Test-TargetResource functionality with error message: Please ensure that WebAdministration module is installed. + CategoryInfo : InvalidOperation: (:) [], CimException + FullyQualifiedErrorId : ProviderOperationExecutionFailure + PSComputerName : . PowerShell provider MSFT_xWebsite failed to execute Test-TargetResource functionality with error message: Please ensure that WebAdministration module is installed. + CategoryInfo : InvalidOperation: (:) [], CimException + FullyQualifiedErrorId : ProviderOperationExecutionFailure + PSComputerName : . The SendConfigurationApply function did not succeed. + CategoryInfo : NotSpecified: (root/Microsoft/...gurationManager:String) [], CimException + FullyQualifiedErrorId : MI RESULT 1 + PSComputerName : .
+EOH
+ }
+
+ let(:lcm_status) {
+ double("LCM cmdlet status", :stderr => lcm_standard_error, :return_value => lcm_standard_output, :succeeded? => lcm_cmdlet_success)
+ }
+
+ describe 'test_configuration method invocation' do
+ context 'when interacting with the LCM using a PowerShell cmdlet' do
+ before(:each) do
+ allow(lcm).to receive(:run_configuration_cmdlet).and_return(lcm_status)
+ end
+ context 'that returns successfully' do
+ before(:each) do
+ allow(lcm).to receive(:run_configuration_cmdlet).and_return(lcm_status)
+ end
+
+ let(:lcm_standard_output) { normal_lcm_output }
+ let(:lcm_standard_error) { nil }
+ let(:lcm_cmdlet_success) { true }
+
+ it 'should successfully return resource information for normally formatted output when cmdlet the cmdlet succeeds' do
+ test_configuration_result = lcm.test_configuration('config')
+ expect(test_configuration_result.class).to be(Array)
+ expect(test_configuration_result.length).to be > 0
+ expect(Chef::Log).not_to receive(:warn)
+ end
+ end
+
+ context 'that fails due to missing what-if switch in DSC resource cmdlet implementation' do
+ let(:lcm_standard_output) { '' }
+ let(:lcm_standard_error) { no_whatif_lcm_output }
+ let(:lcm_cmdlet_success) { false }
+
+ it 'should should return a (possibly empty) array of ResourceInfo instances' do
+ expect(Chef::Log).to receive(:warn)
+ test_configuration_result = nil
+ expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
+ expect(test_configuration_result.class).to be(Array)
+ end
+ end
+
+ context 'that fails due to a DSC resource not being imported before StartDSCConfiguration -whatif is executed' do
+ let(:lcm_standard_output) { '' }
+ let(:lcm_standard_error) { dsc_resource_import_failure_output }
+ let(:lcm_cmdlet_success) { false }
+
+ it 'should log a warning if the message is formatted as expected when a resource import failure occurs' do
+ expect(Chef::Log).to receive(:warn)
+ expect(lcm).to receive(:output_has_dsc_module_failure?).and_call_original
+ test_configuration_result = nil
+ expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
+ end
+
+ it 'should return a (possibly empty) array of ResourceInfo instances' do
+ expect(Chef::Log).to receive(:warn)
+ test_configuration_result = nil
+ expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
+ expect(test_configuration_result.class).to be(Array)
+ end
+ end
+
+ context 'that fails due to an PowerShell cmdlet error that cannot be handled' do
+ let(:lcm_standard_output) { 'some output' }
+ let(:lcm_standard_error) { 'Abort, Retry, Fail?' }
+ let(:lcm_cmdlet_success) { false }
+
+ it 'should raise a Chef::Exceptions::PowershellCmdletException' do
+ expect(Chef::Log).not_to receive(:warn)
+ expect(lcm).to receive(:output_has_dsc_module_failure?).and_call_original
+ expect {lcm.test_configuration('config')}.to raise_error(Chef::Exceptions::PowershellCmdletException)
+ end
+ end
+ end
+
+ it 'should identify a correctly formatted error message as a resource import failure' do
+ expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output)).to be(true)
+ end
+
+ it 'should not identify an incorrectly formatted error message as a resource import failure' do
+ expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output.gsub('module', 'gibberish'))).to be(false)
+ end
+
+ it 'should not identify a message without a CimException reference as a resource import failure' do
+ expect(lcm.send(:output_has_dsc_module_failure?, dsc_resource_import_failure_output.gsub('CimException', 'ArgumentException'))).to be(false)
+ end
+ end
+end
+
|