diff options
author | Adam Edwards <adamed@opscode.com> | 2014-08-08 13:55:41 -0700 |
---|---|---|
committer | Adam Edwards <adamed@opscode.com> | 2014-08-29 14:42:42 -0700 |
commit | 262f174320be0f1cc0e600dc2d2527dbd3b8266a (patch) | |
tree | b4e3d858b28dc5716d91eea6c85d5af14ef333c9 | |
parent | d1350f296095ff70baafbd4f5fa1e3737a4924d2 (diff) | |
download | chef-262f174320be0f1cc0e600dc2d2527dbd3b8266a.tar.gz |
Initial dsc_configuration resource implementation
-rw-r--r-- | lib/chef/exceptions.rb | 2 | ||||
-rw-r--r-- | lib/chef/mixin/windows_architecture_helper.rb | 16 | ||||
-rw-r--r-- | lib/chef/provider/dsc_script.rb | 103 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/dsc_script.rb | 101 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/util/dsc/configuration_generator.rb | 115 | ||||
-rw-r--r-- | lib/chef/util/dsc/local_configuration_manager.rb | 125 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet.rb | 132 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet_result.rb | 46 | ||||
-rw-r--r-- | spec/functional/resource/dsc_script_spec.rb | 115 | ||||
-rw-r--r-- | spec/functional/util/powershell/cmdlet_spec.rb | 112 | ||||
-rw-r--r-- | spec/spec_helper.rb | 1 | ||||
-rw-r--r-- | spec/support/platform_helpers.rb | 12 | ||||
-rw-r--r-- | spec/unit/resource/dsc_script_spec.rb | 69 |
15 files changed, 951 insertions, 0 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index f6db5dbe56..a535ee484a 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -118,6 +118,8 @@ class Chef class InvalidDataBagPath < ArgumentError; end class DuplicateDataBagItem < RuntimeError; end + class PowershellCmdletException < RuntimeError; end + # A different version of a cookbook was added to a # VersionedRecipeList than the one already there. class CookbookVersionConflict < ArgumentError ; end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index ff118c1d3d..65ad042910 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,6 +42,22 @@ class Chef is_i386_process_on_x86_64_windows? end + def with_os_architecture(node) + wow64_redirection_state = nil + + if wow64_architecture_override_required?(node, node_windows_architecture(node)) + wow64_redirection_state = disable_wow64_file_redirection(node) + end + + begin + yield + ensure + if wow64_redirection_state + restore_wow64_file_redirection(node, wow64_redirection_state) + end + end + end + def node_supports_windows_architecture?(node, desired_architecture) assert_valid_windows_architecture!(desired_architecture) return (node_windows_architecture(node) == :x86_64 || diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb new file mode 100644 index 0000000000..5cdc37a312 --- /dev/null +++ b/lib/chef/provider/dsc_script.rb @@ -0,0 +1,103 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, 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 'chef/util/powershell/cmdlet' +require 'chef/util/dsc/configuration_generator' +require 'chef/util/dsc/local_configuration_manager' + +class Chef + class Provider + class DscScript < Chef::Provider + def initialize(dsc_resource, run_context) + super(dsc_resource, run_context) + @dsc_resource = dsc_resource + @resource_converged = false + @operations = { + :set => Proc.new { |config_manager, document| + config_manager.set_configuration(document) + }, + :test => Proc.new { |config_manager, document| + config_manager.test_configuration(document) + }} + end + + def action_run + if ! @resource_converged + converge_by("DSC resource script for configuration '#{configuration_friendly_name}'") do + run_configuration(:set) + Chef::Log.info("DSC resource configuration completed successfully") + end + end + end + + def load_current_resource + @resource_converged = ! run_configuration(:test) + end + + def whyrun_supported? + true + end + + protected + + def run_configuration(operation) + config_directory = ::Dir.mktmpdir("dsc-script") + + config_manager = Chef::Util::DSC::LocalConfigurationManager.new(@run_context.node, config_directory) + + begin + configuration_document = generate_configuration_document(config_directory, @dsc_resource.flags) + @operations[operation].call(config_manager, configuration_document) + rescue Exception => e + Chef::Log.error("DSC operation failed: #{e.message.to_s}") + raise e + ensure + ::FileUtils.rm_rf(config_directory) + end + end + + def generate_configuration_document(config_directory, configuration_flags) + shellout_flags = { + :cwd => @dsc_resource.cwd, + :environment => @dsc_resource.environment, + :timeout => @dsc_resource.timeout + } + + generator = Chef::Util::DSC::ConfigurationGenerator.new(@run_context.node, config_directory) + + if @dsc_resource.command + generator.configuration_document_from_script_path(@dsc_resource.command, configuration_name, configuration_flags, shellout_flags) + else + generator.configuration_document_from_script_code(@dsc_resource.code, configuration_flags, shellout_flags) + end + end + + def configuration_name + @dsc_resource.configuration_name || @dsc_resource.name + end + + def configuration_friendly_name + if @dsc_resource.code + @dsc_resource.name + else + configuration_name + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 3c9e94e6f7..2218822844 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -24,6 +24,7 @@ require 'chef/provider/cron/solaris' require 'chef/provider/cron/aix' require 'chef/provider/deploy' require 'chef/provider/directory' +require 'chef/provider/dsc_script' require 'chef/provider/env' require 'chef/provider/erl_call' require 'chef/provider/execute' diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb new file mode 100644 index 0000000000..10d90bd065 --- /dev/null +++ b/lib/chef/resource/dsc_script.rb @@ -0,0 +1,101 @@ +# +# 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. +# + +class Chef + class Resource + class DscScript < Chef::Resource + + provides :dsc_script, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions.push(:run) + @action = 'run' + provider(Chef::Provider::DscScript) + end + + def code(arg=nil) + if arg && command + raise ArgumentError, "Only one of 'code' and 'command' properties may be specified" + end + if arg && configuration_name + raise ArgumentError, "Attribute `code` may not be set if `configuration_name` is set" + end + set_or_return( + :code, + arg, + :kind_of => [ String ] + ) + end + + def configuration_name(arg=nil) + if arg && code + raise ArgumentError, "Attribute `configuration_name` may not be set if `code` is set" + end + set_or_return( + :configuration_name, + arg, + :kind_of => [ String ] + ) + end + + def command(arg=nil) + if arg && code + raise ArgumentError, "Only one of 'code' and 'command' properties may be specified" + end + set_or_return( + :command, + arg, + :kind_of => [ String ] + ) + end + + def flags(arg=nil) + set_or_return( + :flags, + arg, + :kind_of => [ Hash ] + ) + end + + def cwd(arg=nil) + set_or_return( + :cwd, + arg, + :kind_of => [ String ] + ) + end + + def environment(arg=nil) + set_or_return( + :environment, + arg, + :kind_of => [ Hash ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ Integer ] + ) + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 93ff682288..289b34aaac 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -28,6 +28,7 @@ require 'chef/resource/deploy' require 'chef/resource/deploy_revision' require 'chef/resource/directory' require 'chef/resource/dpkg_package' +require 'chef/resource/dsc_script' require 'chef/resource/easy_install_package' require 'chef/resource/env' require 'chef/resource/erl_call' diff --git a/lib/chef/util/dsc/configuration_generator.rb b/lib/chef/util/dsc/configuration_generator.rb new file mode 100644 index 0000000000..ea949bb9c4 --- /dev/null +++ b/lib/chef/util/dsc/configuration_generator.rb @@ -0,0 +1,115 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, 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 'chef/util/powershell/cmdlet' + +class Chef::Util::DSC + class ConfigurationGenerator + def initialize(node, config_directory) + @node = node + @config_directory = config_directory + end + + def configuration_document_from_script_code(code, configuration_flags, shellout_flags) + Chef::Log.debug("DSC: DSC code:\n '#{code}'") + generated_script_path = write_document_generation_script(code, 'chef_dsc') + begin + configuration_document_from_script_path(generated_script_path, 'chef_dsc', configuration_flags, shellout_flags) + ensure + ::FileUtils.rm(generated_script_path) + end + end + + def configuration_document_from_script_path(script_path, configuration_name, configuration_flags, shellout_flags) + validate_configuration_name!(configuration_name) + + document_generation_cmdlet = Chef::Util::Powershell::Cmdlet.new( + @node, + configuration_document_generation_code(script_path, configuration_name)) + + merged_configuration_flags = get_merged_configuration_flags!(configuration_flags, configuration_name) + + document_generation_cmdlet.run(merged_configuration_flags, shellout_flags) + configuration_document_location = find_configuration_document(configuration_name) + + if ! configuration_document_location + raise RuntimeError, "No DSC configuration for '#{configuration_name}' was generated from supplied DSC script" + end + + configuration_document = get_configuration_document(configuration_document_location) + ::FileUtils.rm_rf(configuration_document_location) + configuration_document + end + + protected + + # From PowerShell error help for the Configuration language element: + # Standard names may only contain letters (a-z, A-Z), numbers (0-9), and underscore (_). + # The name may not be null or empty, and should start with a letter. + def validate_configuration_name!(configuration_name) + if !!(configuration_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false + raise ArgumentError, 'Configuration `#{configuration_name}` is not a valid PowerShell cmdlet name' + end + end + + def get_merged_configuration_flags!(configuration_flags, configuration_name) + merged_configuration_flags = { :outputpath => configuration_document_directory(configuration_name) } + if configuration_flags + configuration_flags.map do | switch, value | + if merged_configuration_flags.key?(switch.to_s.downcase.to_sym) + raise ArgumentError, "The `flags` attribute for the dsc_script resource contained a command line switch :#{switch.to_s} that is disallowed." + end + merged_configuration_flags[switch.to_s.downcase.to_sym] = value + end + end + merged_configuration_flags + end + + def configuration_code(code, configuration_name) + "$ProgressPreference = 'SilentlyContinue';Configuration '#{configuration_name}'\n{\n\t#{code}\n}\n" + end + + def configuration_document_generation_code(configuration_script, configuration_name) + ". '#{configuration_script}';#{configuration_name}" + end + + def write_document_generation_script(code, configuration_name) + script_path = "#{@config_directory}/chef_dsc_config.ps1" + ::File.open(script_path, 'wt') do | script | + script.write(configuration_code(code, configuration_name)) + end + script_path + end + + def find_configuration_document(configuration_name) + document_directory = configuration_document_directory(configuration_name) + document_file_name = ::Dir.entries(document_directory).find { | path | path =~ /.*.mof/ } + ::File.join(document_directory, document_file_name) + end + + def configuration_document_directory(configuration_name) + ::File.join(@config_directory, configuration_name) + end + + def get_configuration_document(document_path) + ::File.open(document_path, 'rb') do | file | + file.read + end + end + end +end diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb new file mode 100644 index 0000000000..a9c5b867df --- /dev/null +++ b/lib/chef/util/dsc/local_configuration_manager.rb @@ -0,0 +1,125 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, 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 'chef/util/powershell/cmdlet' + +class Chef::Util::DSC + class LocalConfigurationManager + def initialize(node, configuration_path) + @node = node + @configuration_path = configuration_path + clear_execution_time + end + + def test_configuration(configuration_document) + status = run_configuration_cmdlet(configuration_document) + configuration_update_required?(status.return_value) + end + + def set_configuration(configuration_document) + run_configuration_cmdlet(configuration_document, true) + end + + def last_operation_execution_time_seconds + if @operation_start_time && @operation_end_time + @operation_end_time - @operation_start_time + end + end + + private + + def run_configuration_cmdlet(configuration_document, apply_configuration = false) + Chef::Log.debug("DSC: Calling DSC Local Config Manager to #{apply_configuration ? "set" : "test"} configuration document.") + 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}" + status = nil + + begin + save_configuration_document(configuration_document) + cmdlet = ::Chef::Util::Powershell::Cmdlet.new(@node, "#{command_code}") + status = cmdlet.run + ensure + end_operation_timing + remove_configuration_document + if last_operation_execution_time_seconds + Chef::Log.debug("DSC: DSC operation completed in #{last_operation_execution_time_seconds} seconds.") + end + end + Chef::Log.debug("DSC: Completed call to DSC Local Config Manager") + status + 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}") + parse_what_if_output(what_if_output) + end + + def save_configuration_document(configuration_document) + ::FileUtils.mkdir_p(@configuration_path) + ::File.open(configuration_document_path, 'wb') do | file | + file.write(configuration_document) + end + end + + def remove_configuration_document + ::FileUtils.rm(configuration_document_path) + end + + def configuration_document_path + File.join(@configuration_path,'..mof') + end + + def parse_what_if_output(what_if_output) + + # What-if output for start-dscconfiguration contains lines that look like one of the following: + # + # What if: [SEA-ADAMED1]: LCM: [ Start Set ] [[Group]chef_dsc] + # What if: [SEA-ADAMED1]: [[Group]chef_dsc] Performing the operation "Add" on target "Group: demo1" + # + # The second line lacking the 'LCM:' is what happens if there is a change required to make the system consistent with the resource. + # Such a line without LCM is only present if an update to the system is required. Therefore, we test each line below + # to see if it is missing the LCM, and declare that an update is needed if so. + has_change_line = false + + what_if_output.lines.each do |line| + if (line =~ /.+\:\s+\[\S*\]\:\s+LCM\:/).nil? + has_change_line = true + break + end + end + + has_change_line + end + + def clear_execution_time + @operation_start_time = nil + @operation_end_time = nil + end + + def start_operation_timing + clear_execution_time + @operation_start_time = Time.now + end + + def end_operation_timing + @operation_end_time = Time.now + end + end +end diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb new file mode 100644 index 0000000000..0120f0aaf9 --- /dev/null +++ b/lib/chef/util/powershell/cmdlet.rb @@ -0,0 +1,132 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, 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 'mixlib/shellout' +require 'chef/mixin/windows_architecture_helper' +require 'chef/util/powershell/cmdlet_result' + +class Chef::Util::Powershell + class Cmdlet + def initialize(node, cmdlet, output_format=nil, output_format_options={}) + @output_format = output_format + @node = node + + case output_format + when nil + @json_format = false + when :json + @json_format = true + when :text + @json_format = false + when :object + @json_format = true + else + raise ArgumentError, "Invalid output format #{output_format.to_s} specified" + end + + @cmdlet = cmdlet + @output_format_options = output_format_options + end + + attr_reader :output_format + + def run(switches={}, execution_options={}, *arguments) + arguments_string = arguments.join(' ') + + switches_string = command_switches_string(switches) + + json_depth = 5 + + if @json_format && @output_format_options.has_key?(:depth) + json_depth = @output_format_options[:depth] + end + + json_command = @json_format ? " | convertto-json -compress -depth #{json_depth}" : "" + command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive -command \"trap [Exception] {write-error -exception ($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} #{arguments_string}#{json_command}\";if ( ! $? ) { exit 1 }" + + augmented_options = {:returns => [0], :live_stream => false}.merge(execution_options) + command = Mixlib::ShellOut.new(command_string, augmented_options) + + os_architecture = "#{ENV['PROCESSOR_ARCHITEW6432']}" == 'AMD64' ? :x86_64 : :i386 + + status = nil + + with_os_architecture(@node) do + status = command.run_command + end + + result = CmdletResult.new(status, @output_format) + + if ! result.succeeded? + raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{result.stderr}" + end + + result + end + + protected + + include Chef::Mixin::WindowsArchitectureHelper + + def validate_switch_name!(switch_parameter_name) + if !!(switch_parameter_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false + raise ArgumentError, "`#{switch_parameter_name}` is not a valid PowerShell cmdlet switch parameter name" + end + end + + def escape_parameter_value(parameter_value) + parameter_value.gsub(/\`|\'|\"/,'`\1') + end + + def escape_string_parameter_value(parameter_value) + "'#{escape_parameter_value(parameter_value)}'" + end + + def command_switches_string(switches) + command_switches = switches.map do | switch_name, switch_value | + if switch_name.class != Symbol + raise ArgumentError, "Invalid type `#{switch_name} `for PowerShell switch '#{switch_name.to_s}'. The switch must be specified as a Symbol'" + end + + validate_switch_name!(switch_name) + + switch_argument = '' + switch_present = true + + case switch_value + when Numeric + switch_argument = switch_value.to_s + when Float + switch_argument = switch_value.to_s + when FalseClass + switch_present = false + when TrueClass + when String + switch_argument = escape_string_parameter_value(switch_value) + else + raise ArgumentError, "Invalid argument type `#{switch_value.class}` specified for PowerShell switch `:#{switch_name.to_s}`. Arguments to PowerShell must be of type `String`, `Numeric`, `Float`, `FalseClass`, or `TrueClass`" + end + + switch_present ? ["-#{switch_name.to_s.downcase}", switch_argument].join(' ') : '' + end + + command_switches.join(' ') + end + end +end + diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb new file mode 100644 index 0000000000..390ee8cbc1 --- /dev/null +++ b/lib/chef/util/powershell/cmdlet_result.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: Copyright (c) 2014 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 'json' + +class Chef::Util::Powershell + class CmdletResult + attr_reader :output_format + + def initialize(status, output_format) + @status = status + @output_format = output_format + end + + def stderr + @status.stderr + end + + def return_value + if output_format == :object + JSON.parse(@status.stdout) + else + @status.stdout + end + end + + def succeeded? + @succeeded = @status.status.exitstatus == 0 + end + end +end diff --git a/spec/functional/resource/dsc_script_spec.rb b/spec/functional/resource/dsc_script_spec.rb new file mode 100644 index 0000000000..78124a08de --- /dev/null +++ b/spec/functional/resource/dsc_script_spec.rb @@ -0,0 +1,115 @@ +#
+# 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 'spec_helper'
+require 'chef/mixin/windows_architecture_helper'
+
+describe Chef::Resource::DscScript, :windows_2008r2_or_later do
+ include Chef::Mixin::WindowsArchitectureHelper
+ before(:all) do
+ @temp_dir = ::Dir.mktmpdir("dsc-functional-test")
+ end
+
+ after(:all) 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"
+ ::File.open(script_path, 'wt') do | script |
+ script.write(script_code)
+ end
+ script_path
+ end
+
+ let(:dsc_env_variable) { 'chefenvtest' }
+ let(:dsc_env_value1) { 'value1' }
+ let(:env_value2) { 'value2' }
+ let(:dsc_test_run_context) {
+ node = Chef::Node.new
+ node.default['platform'] = 'windows'
+ node.default['platform_version'] = '6.1'
+ node.default['kernel'][:machine] =
+ is_i386_process_on_x86_64_windows? ? :x86_64 : :i386
+ empty_events = Chef::EventDispatch::Dispatcher.new
+ Chef::RunContext.new(node, {}, empty_events)
+ }
+ let(:dsc_test_resource_name) { 'DSCTest' }
+ let(:dsc_test_resource_base) {
+ Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context)
+ }
+ let(:test_registry_key) { 'HKEY_LOCAL_MACHINE\Software\Chef\Spec\Functional\Resource\dsc_script_spec' }
+ let(:test_registry_value) { 'Registration' }
+ let(:test_registry_data1) { 'LL927' }
+ let(:test_registry_data2) { 'LL928' }
+ let(:dsc_code) { <<-EOH
+ Registry "ChefRegKey"
+ {
+ Key = '#{test_registry_key}'
+ ValueName = '#{test_registry_value}'
+ ValueData = '#{test_registry_data}'
+ Ensure = 'Present'
+ }
+EOH
+ }
+ let(:dsc_config_name) {
+ dsc_test_resource_base.name
+ }
+ let(:dsc_resource_from_code) {
+ dsc_test_resource_base.code(dsc_code)
+ dsc_test_resource_base
+ }
+ 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
+ }
+
+ before(:each) do
+ test_key_resource = Chef::Resource::RegistryKey.new(test_registry_key, dsc_test_run_context)
+ test_key_resource.recursive(true)
+ test_key_resource.run_action(:delete_key)
+ end
+
+ after(:each) do
+ test_key_resource = Chef::Resource::RegistryKey.new(test_registry_key, dsc_test_run_context)
+ test_key_resource.recursive(true)
+ test_key_resource.run_action(:delete_key)
+ end
+
+ shared_examples_for 'a dsc_script resource with specified PowerShell configuration code' do
+ let(:test_registry_data) { test_registry_data1 }
+ it 'should create a registry key with a specific registry value and data' do
+ expect(dsc_test_resource.registry_key_exists?(test_registry_key)).to eq(false)
+ dsc_test_resource.run_action(:run)
+ 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
+ end
+
+ context 'when supplying configuration through the configuration attribute' do
+ let(:dsc_test_resource) { dsc_resource_from_code }
+ it_behaves_like 'a dsc_script resource with specified PowerShell configuration code'
+ end
+
+ context 'when supplying configuration using the path attribute' do
+ let(:dsc_test_resource) { dsc_resource_from_path }
+ it_behaves_like 'a dsc_script resource with specified PowerShell configuration code'
+ end
+
+end
diff --git a/spec/functional/util/powershell/cmdlet_spec.rb b/spec/functional/util/powershell/cmdlet_spec.rb new file mode 100644 index 0000000000..1bc8b81bbc --- /dev/null +++ b/spec/functional/util/powershell/cmdlet_spec.rb @@ -0,0 +1,112 @@ +#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, 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 'json'
+require File.expand_path('../../../../spec_helper', __FILE__)
+
+describe Chef::Util::Powershell::Cmdlet, :windows_only do
+ let(:cmd_output_format) { :text }
+ let(:simple_cmdlet) { Chef::Util::Powershell::Cmdlet.new('get-childitem', cmd_output_format, {:depth => 2}) }
+ let(:invalid_cmdlet) { Chef::Util::Powershell::Cmdlet.new('get-idontexist', cmd_output_format) }
+ let(:cmdlet_get_item_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new('get-item', cmd_output_format, {:depth => 2}) }
+ let(:cmdlet_alias_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new('alias', cmd_output_format, {:depth => 2}) }
+ let(:etc_directory) { "#{ENV['systemroot']}\\system32\\drivers\\etc" }
+ let(:architecture_cmdlet) { Chef::Util::Powershell::Cmdlet.new("$env:PROCESSOR_ARCHITECTURE")}
+ it "executes a simple process" do
+ result = simple_cmdlet.run
+ expect(result.succeeded?).to eq(true)
+ end
+
+ it "returns a PowershellCmdletException exception if the command cannot be executed" do
+ exception_occurred = nil
+
+ begin
+ invalid_cmdlet.run
+ exception_occurred = false
+ rescue Chef::Util::Powershell::CmdletException => e
+ exception_occurred = true
+ expect(e.cmdlet_result.succeeded?).to eq(false)
+ end
+
+ expect(exception_occurred).to eq(true)
+ end
+
+ it "executes a 64-bit command on a 64-bit OS, 32-bit otherwise" do
+ os_arch = ENV['PROCESSOR_ARCHITEW6432']
+ if os_arch.nil?
+ os_arch = ENV['PROCESSOR_ARCHITECTURE']
+ end
+
+ result = architecture_cmdlet.run
+ execution_arch = result.return_value
+ execution_arch.strip!
+ expect(execution_arch).to eq(os_arch)
+ end
+
+ it "passes command line switches to the command" do
+ result = cmdlet_alias_requires_switch_or_argument.run({:name => 'ls'})
+ expect(result.succeeded?).to eq(true)
+ end
+
+ it "passes command line arguments to the command" do
+ result = cmdlet_alias_requires_switch_or_argument.run({},{},'ls')
+ expect(result.succeeded?).to eq(true)
+ end
+
+ it "passes command line arguments and switches to the command" do
+ result = cmdlet_get_item_requires_switch_or_argument.run({:path => etc_directory},{},' | select-object -property fullname | format-table -hidetableheaders')
+ expect(result.succeeded?).to eq(true)
+ returned_directory = result.return_value
+ returned_directory.strip!
+ expect(returned_directory).to eq(etc_directory)
+ end
+
+ it "passes execution options to the command" do
+ result = cmdlet_get_item_requires_switch_or_argument.run({},{:cwd => etc_directory},'. | select-object -property fullname | format-table -hidetableheaders')
+ expect(result.succeeded?).to eq(true)
+ returned_directory = result.return_value
+ returned_directory.strip!
+ expect(returned_directory).to eq(etc_directory)
+ end
+
+ context "when returning json" do
+ let(:cmd_output_format) { :json }
+ it "returns json format data" do
+ result = cmdlet_alias_requires_switch_or_argument.run({},{},'ls')
+ expect(result.succeeded?).to eq(true)
+ expect(lambda{JSON.parse(result.return_value)}).not_to raise_error
+ end
+ end
+
+ context "when returning Ruby objects" do
+ let(:cmd_output_format) { :object }
+ it "returns object format data" do
+ result = simple_cmdlet.run({},{:cwd => etc_directory}, 'hosts')
+ expect(result.succeeded?).to eq(true)
+ data = result.return_value
+ expect(data['Name']).to eq('hosts')
+ end
+ end
+
+ context "when constructor is given invalid arguments" do
+ let(:cmd_output_format) { :invalid }
+ it "throws an exception if an invalid format is passed to the constructor" do
+ expect(lambda{simple_cmdlet}).to raise_error
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7c11957997..006c2c8360 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -107,6 +107,7 @@ RSpec.configure do |config| config.filter_run_excluding :not_supported_on_win2k3 => true if windows_win2k3? config.filter_run_excluding :not_supported_on_solaris => true if solaris? config.filter_run_excluding :win2k3_only => true unless windows_win2k3? + config.filter_run_excluding :windows_2008r2_or_later => true unless windows_2008r2_or_later? config.filter_run_excluding :windows64_only => true unless windows64? config.filter_run_excluding :windows32_only => true unless windows32? config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined? diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index a7c616d7a7..984bd12e8a 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -52,6 +52,18 @@ def windows_win2k3? (host['version'] && host['version'].start_with?("5.2")) end +def windows_2008r2_or_later? + return false unless windows? + wmi = WmiLite::Wmi.new + host = wmi.first_of('Win32_OperatingSystem') + version = host['version'] + return false unless version + components = version.split('.').map do | component | + component.to_i + end + components.length >=2 && components[0] >= 6 && components[1] >= 1 +end + def mac_osx_106? if File.exists? "/usr/bin/sw_vers" result = shell_out("/usr/bin/sw_vers") diff --git a/spec/unit/resource/dsc_script_spec.rb b/spec/unit/resource/dsc_script_spec.rb new file mode 100644 index 0000000000..79ad34ee8d --- /dev/null +++ b/spec/unit/resource/dsc_script_spec.rb @@ -0,0 +1,69 @@ +# +# 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 'spec_helper' + +describe Chef::Resource::DscScript do + let(:dsc_test_run_context) { + node = Chef::Node.new + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + } + let(:dsc_test_resource_name) { 'DSCTest' } + let(:dsc_test_resource) { + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + } + let(:configuration_code) {'echo "This is supposed to create a configuration document."'} + let(:configuration_path) {'c:/myconfigs/formatc.ps1'} + let(:configuration_name) { 'formatme' } + + it "allows the configuration attribute to be set" do + dsc_test_resource.code(configuration_code) + expect(dsc_test_resource.code).to eq(configuration_code) + end + + it "allows the path attribute to be set" do + dsc_test_resource.command(configuration_path) + expect(dsc_test_resource.command).to eq(configuration_path) + end + + it "allows the configuration_name attribute to be set" do + dsc_test_resource.configuration_name(configuration_name) + expect(dsc_test_resource.configuration_name).to eq(configuration_name) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration attribute when the path attribute is already set" do + dsc_test_resource.command(configuration_path) + expect { dsc_test_resource.code(configuration_code) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the path attribute when the configuration attribute is already set" do + dsc_test_resource.code(configuration_code) + expect { dsc_test_resource.command(configuration_path) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration_name attribute when the configuration attribute is already set" do + dsc_test_resource.code(configuration_code) + expect { dsc_test_resource.configuration_name(configuration_name) }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError exception if an attempt is made to set the configuration attribute when the configuration_name attribute is already set" do + dsc_test_resource.configuration_name(configuration_name) + expect { dsc_test_resource.code(configuration_code) }.to raise_error(ArgumentError) + end +end |