summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Edwards <adamed@opscode.com>2014-08-08 13:55:41 -0700
committerAdam Edwards <adamed@opscode.com>2014-08-29 14:42:42 -0700
commit262f174320be0f1cc0e600dc2d2527dbd3b8266a (patch)
treeb4e3d858b28dc5716d91eea6c85d5af14ef333c9
parentd1350f296095ff70baafbd4f5fa1e3737a4924d2 (diff)
downloadchef-262f174320be0f1cc0e600dc2d2527dbd3b8266a.tar.gz
Initial dsc_configuration resource implementation
-rw-r--r--lib/chef/exceptions.rb2
-rw-r--r--lib/chef/mixin/windows_architecture_helper.rb16
-rw-r--r--lib/chef/provider/dsc_script.rb103
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/dsc_script.rb101
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/util/dsc/configuration_generator.rb115
-rw-r--r--lib/chef/util/dsc/local_configuration_manager.rb125
-rw-r--r--lib/chef/util/powershell/cmdlet.rb132
-rw-r--r--lib/chef/util/powershell/cmdlet_result.rb46
-rw-r--r--spec/functional/resource/dsc_script_spec.rb115
-rw-r--r--spec/functional/util/powershell/cmdlet_spec.rb112
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/platform_helpers.rb12
-rw-r--r--spec/unit/resource/dsc_script_spec.rb69
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