summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Mundrawala <jdmundrawala@gmail.com>2014-09-19 14:44:13 -0700
committerJay Mundrawala <jdmundrawala@gmail.com>2014-09-19 14:44:13 -0700
commite7d3e2adf486131cfba78fc4eb7e31d1e36b7b0e (patch)
tree87450c0f1e85094ab75cdcf354072202ade9ee52
parent5534b5d0f781188e0398d5b1ade7ceba568e4b45 (diff)
parente473325de679718ab0695d4d3177eeeef20dc994 (diff)
downloadchef-e7d3e2adf486131cfba78fc4eb7e31d1e36b7b0e.tar.gz
Merge pull request #2091 from opscode/platform/dsc-phase-1-rebase
dsc_script resource
-rw-r--r--lib/chef/exceptions.rb4
-rw-r--r--lib/chef/mixin/windows_architecture_helper.rb16
-rw-r--r--lib/chef/platform/query_helpers.rb6
-rw-r--r--lib/chef/provider/dsc_script.rb148
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/dsc_script.rb140
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/util/dsc/configuration_generator.rb115
-rw-r--r--lib/chef/util/dsc/lcm_output_parser.rb133
-rw-r--r--lib/chef/util/dsc/local_configuration_manager.rb137
-rw-r--r--lib/chef/util/dsc/resource_info.rb26
-rw-r--r--lib/chef/util/powershell/cmdlet.rb136
-rw-r--r--lib/chef/util/powershell/cmdlet_result.rb46
-rw-r--r--spec/functional/resource/dsc_script_spec.rb337
-rw-r--r--spec/functional/util/powershell/cmdlet_spec.rb114
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/platform_helpers.rb24
-rw-r--r--spec/unit/platform/query_helpers_spec.rb23
-rw-r--r--spec/unit/provider/dsc_script_spec.rb145
-rw-r--r--spec/unit/resource/dsc_script_spec.rb127
-rw-r--r--spec/unit/util/dsc/configuration_generator_spec.rb171
-rw-r--r--spec/unit/util/dsc/lcm_output_parser_spec.rb169
-rw-r--r--spec/unit/util/dsc/local_configuration_manager_spec.rb134
-rw-r--r--spec/unit/util/powershell/cmdlet_spec.rb106
24 files changed, 2261 insertions, 1 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 23e223f204..7298f1f4d1 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
@@ -179,6 +181,8 @@ class Chef
class ChildConvergeError < RuntimeError; end
+ class NoProviderAvailable < RuntimeError; end
+
class MissingRole < RuntimeError
NULL = Object.new
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/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb
index f6f5309de5..334ab278d1 100644
--- a/lib/chef/platform/query_helpers.rb
+++ b/lib/chef/platform/query_helpers.rb
@@ -45,7 +45,11 @@ class Chef
is_server_2003
end
- end
+ def supports_dsc?(node)
+ node[:languages] && node[:languages][:powershell] &&
+ node[:languages][:powershell][:version].to_i >= 4
+ end
+ end
end
end
diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb
new file mode 100644
index 0000000000..5d7322842c
--- /dev/null
+++ b/lib/chef/provider/dsc_script.rb
@@ -0,0 +1,148 @@
+#
+# 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(generate_description) do
+ run_configuration(:set)
+ Chef::Log.info("DSC resource configuration completed successfully")
+ end
+ end
+ end
+
+ def load_current_resource
+ @dsc_resources_info = run_configuration(:test)
+ @resource_converged = @dsc_resources_info.all? do |resource|
+ !resource.changes_state?
+ end
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ protected
+
+ def run_configuration(operation)
+ config_directory = ::Dir.mktmpdir("chef-dsc-script")
+ configuration_data_path = get_configuration_data_path(config_directory)
+ configuration_flags = get_augmented_configuration_flags(configuration_data_path)
+
+ config_manager = Chef::Util::DSC::LocalConfigurationManager.new(@run_context.node, config_directory)
+
+ begin
+ configuration_document = generate_configuration_document(config_directory, configuration_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 get_augmented_configuration_flags(configuration_data_path)
+ updated_flags = nil
+ if configuration_data_path
+ updated_flags = @dsc_resource.flags.nil? ? {} : @dsc_resource.flags.dup
+ Chef::Util::PathHelper.validate_path(configuration_data_path)
+ updated_flags[:configurationdata] = configuration_data_path
+ end
+ updated_flags
+ 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
+ # If code is also not provided, we mimic what the other script resources do (execute nothing)
+ Chef::Log.warn("Neither code or command were provided for dsc_resource[#{@dsc_resource.name}].") unless @dsc_resource.code
+ generator.configuration_document_from_script_code(@dsc_resource.code || '', configuration_flags, shellout_flags)
+ end
+ end
+
+ def get_configuration_data_path(config_directory)
+ if @dsc_resource.configuration_data_script
+ @dsc_resource.configuration_data_script
+ elsif @dsc_resource.configuration_data
+ configuration_data_path = "#{config_directory}/chef_dsc_config_data.psd1"
+ ::File.open(configuration_data_path, 'wt') do | script |
+ script.write(@dsc_resource.configuration_data)
+ end
+ configuration_data_path
+ 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
+
+ private
+
+ def generate_description
+ ["converge DSC configuration '#{configuration_friendly_name}'"] +
+ @dsc_resources_info.map do |resource|
+ if resource.changes_state?
+ # We ignore the last log message because it only contains the time it took, which looks weird
+ cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, '').strip }
+ "converge DSC resource #{resource.name} by #{cleaned_messages.find_all{ |c| c != ''}.join("\n")}"
+ else
+ # This is needed because a dsc script can have resouces that are both converged and not
+ "converge DSC resource #{resource.name} by doing nothing because it is already converged"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 1b0ff3ffff..dd384c90a6 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..2972ace1aa
--- /dev/null
+++ b/lib/chef/resource/dsc_script.rb
@@ -0,0 +1,140 @@
+#
+# 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/exceptions'
+
+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
+ if(run_context && Chef::Platform.supports_dsc?(run_context.node))
+ @provider = Chef::Provider::DscScript
+ else
+ raise Chef::Exceptions::NoProviderAvailable,
+ "#{powershell_info_str(run_context)}\nPowershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource."
+ end
+ end
+
+ def code(arg=nil)
+ if arg && command
+ raise ArgumentError, "Only one of 'code' and 'command' attributes may be specified"
+ end
+ if arg && configuration_name
+ raise ArgumentError, "The 'code' and 'command' attributes may not be used together"
+ 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, "The 'code' and 'command' attributes may not be used together"
+ end
+ set_or_return(
+ :command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def configuration_data(arg=nil)
+ if arg && configuration_data_script
+ raise ArgumentError, "The 'configuration_data' and 'configuration_data_script' attributes may not be used together"
+ end
+ set_or_return(
+ :configuration_data,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def configuration_data_script(arg=nil)
+ if arg && configuration_data
+ raise ArgumentError, "The 'configuration_data' and 'configuration_data_script' attributes may not be used together"
+ end
+ set_or_return(
+ :configuration_data_script,
+ 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
+
+ private
+
+ def powershell_info_str(run_context)
+ if run_context && run_context.node[:languages] && run_context.node[:languages][:powershell]
+ install_info = "Powershell #{run_context.node[:languages][:powershell][:version]} was found on the system."
+ else
+ install_info = 'Powershell was not found.'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 8c2f71bd30..fa5c82761d 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..12cd5dc3a2
--- /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\tnode 'localhost'\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) if 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/lcm_output_parser.rb b/lib/chef/util/dsc/lcm_output_parser.rb
new file mode 100644
index 0000000000..f8f853a33a
--- /dev/null
+++ b/lib/chef/util/dsc/lcm_output_parser.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Jay Mundrawala (<jdm@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/log'
+require 'chef/util/dsc/resource_info'
+
+class Chef
+ class Util
+ class DSC
+ class LocalConfigurationManager
+ module Parser
+ # Parses the output from LCM and returns a list of Chef::Util::DSC::ResourceInfo objects
+ # that describe how the resources affected the system
+ #
+ # Example:
+ # parse <<-EOF
+ # What if: [Machine]: LCM: [Start Set ]
+ # What if: [Machine]: LCM: [Start Resource ] [[File]FileToNotBeThere]
+ # What if: [Machine]: LCM: [Start Set ] [[File]FileToNotBeThere]
+ # What if: [C:\ShouldNotExist.txt] removed
+ # What if: [Machine]: LCM: [End Set ] [[File]FileToNotBeThere] in 0.1 seconds
+ # What if: [Machine]: LCM: [End Resource ] [[File]FileToNotBeThere]
+ # What if: [Machine]: LCM: [End Set ]
+ # EOF
+ #
+ # would return
+ #
+ # [
+ # Chef::Util::DSC::ResourceInfo.new(
+ # '[[File]FileToNotBeThere]',
+ # true,
+ # [
+ # '[[File]FileToNotBeThere]',
+ # '[C:\Shouldnotexist.txt]',
+ # '[[File]FileToNotBeThere] in 0.1 seconds'
+ # ]
+ # )
+ # ]
+ #
+ def self.parse(lcm_output)
+ return [] unless lcm_output
+
+ current_resource = Hash.new
+
+ resources = []
+ lcm_output.lines.each do |line|
+ op_action, op_type, info = parse_line(line)
+
+ case op_action
+ when :start
+ case op_type
+ when :set
+ if current_resource[:name]
+ current_resource[:context] = :logging
+ current_resource[:logs] = [info]
+ end
+ when :resource
+ if current_resource[:name]
+ resources.push(current_resource)
+ end
+ current_resource = {:name => info}
+ else
+ Chef::Log.debug("Ignoring op_action #{op_action}: Read line #{line}")
+ end
+ when :end
+ # Make sure we log the last line
+ if current_resource[:context] == :logging and info.include? current_resource[:name]
+ current_resource[:logs].push(info)
+ end
+ current_resource[:context] = nil
+ when :skip
+ current_resource[:skipped] = true
+ when :info
+ if current_resource[:context] == :logging
+ current_resource[:logs].push(info)
+ end
+ end
+ end
+
+ if current_resource[:name]
+ resources.push(current_resource)
+ end
+
+ build_resource_info(resources)
+ end
+
+ def self.parse_line(line)
+ if match = line.match(/^.*?:.*?:\s*LCM:\s*\[(.*?)\](.*)/)
+ # If the line looks like
+ # What If: [machinename]: LCM: [op_action op_type] message
+ # extract op_action, op_type, and message
+ operation, info = match.captures
+ op_action, op_type = operation.strip.split(' ').map {|m| m.downcase.to_sym}
+ else
+ op_action = op_type = :info
+ if match = line.match(/^.*?:.*?: \s+(.*)/)
+ info = match.captures[0]
+ else
+ info = line
+ end
+ end
+ info.strip! # Because this was formatted for humans
+ return [op_action, op_type, info]
+ end
+ private_class_method :parse_line
+
+ def self.build_resource_info(resources)
+ resources.map do |r|
+ Chef::Util::DSC::ResourceInfo.new(r[:name], !r[:skipped], r[:logs])
+ end
+ end
+ private_class_method :build_resource_info
+
+ end
+ 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..4a56b6a397
--- /dev/null
+++ b/lib/chef/util/dsc/local_configuration_manager.rb
@@ -0,0 +1,137 @@
+#
+# 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/lcm_output_parser'
+
+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)
+ handle_what_if_exception!(status.stderr) unless status.succeeded?
+ 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 = lcm_command_code(@configuration_path, test_only_parameters)
+ status = nil
+
+ begin
+ save_configuration_document(configuration_document)
+ cmdlet = ::Chef::Util::Powershell::Cmdlet.new(@node, "#{command_code}")
+ if apply_configuration
+ status = cmdlet.run!
+ else
+ status = cmdlet.run
+ end
+ 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 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 LCM output: #{e}")
+ [Chef::Util::DSC::ResourceInfo.new('Unknown DSC Resources', true, ['Unknown changes because LCM output was not parsable.'])]
+ end
+ 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 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/dsc/resource_info.rb b/lib/chef/util/dsc/resource_info.rb
new file mode 100644
index 0000000000..4a32451721
--- /dev/null
+++ b/lib/chef/util/dsc/resource_info.rb
@@ -0,0 +1,26 @@
+
+class Chef
+ class Util
+ class DSC
+ class ResourceInfo
+ # The name is the text following [Start Set]
+ attr_reader :name
+
+ # A list of all log messages between [Start Set] and [End Set].
+ # Each line is an element in the list.
+ attr_reader :change_log
+
+ def initialize(name, sets, change_log)
+ @name = name
+ @sets = sets
+ @change_log = change_log || []
+ end
+
+ # Does this resource change the state of the system?
+ def changes_state?
+ @sets
+ end
+ end
+ 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..40edbb13c6
--- /dev/null
+++ b/lib/chef/util/powershell/cmdlet.rb
@@ -0,0 +1,136 @@
+#
+# 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
+
+ CmdletResult.new(status, @output_format)
+ end
+
+ def run!(switches={}, execution_options={}, *arguments)
+ result = run(switches, execution_options, arguments)
+
+ 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(' ').strip : ''
+ 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..af7b3607cd
--- /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..fa13296c02
--- /dev/null
+++ b/spec/functional/resource/dsc_script_spec.rb
@@ -0,0 +1,337 @@
+#
+# 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/shell_out'
+require 'chef/mixin/windows_architecture_helper'
+
+describe Chef::Resource::DscScript, :windows_powershell_dsc_only 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
+
+ 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' }
+ let(:dsc_test_run_context) {
+ node = Chef::Node.new
+ node.automatic['platform'] = 'windows'
+ node.automatic['platform_version'] = '6.1'
+ node.automatic['kernel'][:machine] =
+ is_i386_process_on_x86_64_windows? ? :x86_64 : :i386
+ node.automatic[:languages][:powershell][:version] = '4.0'
+ 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_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
+ }
+ let(:dsc_resource_from_code) {
+ 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, config_name_value))
+ 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
+
+ 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
+ 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
+
+ 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/functional/util/powershell/cmdlet_spec.rb b/spec/functional/util/powershell/cmdlet_spec.rb
new file mode 100644
index 0000000000..63d1ac09b5
--- /dev/null
+++ b/spec/functional/util/powershell/cmdlet_spec.rb
@@ -0,0 +1,114 @@
+#
+# 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
+ before(:all) do
+ ohai = Ohai::System.new
+ ohai.load_plugins
+ ohai.run_plugins(true, ['platform', 'kernel'])
+ @node = Chef::Node.new
+ @node.consume_external_attrs(ohai.data, {})
+ end
+ let(:cmd_output_format) { :text }
+ let(:simple_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-childitem', cmd_output_format, {:depth => 2}) }
+ let(:invalid_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-idontexist', cmd_output_format) }
+ let(:cmdlet_get_item_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new(@node, 'get-item', cmd_output_format, {:depth => 2}) }
+ let(:cmdlet_alias_requires_switch_or_argument) { Chef::Util::Powershell::Cmdlet.new(@node, 'alias', cmd_output_format, {:depth => 2}) }
+ let(:etc_directory) { "#{ENV['systemroot']}\\system32\\drivers\\etc" }
+ let(:architecture_cmdlet) { Chef::Util::Powershell::Cmdlet.new(@node, "$env:PROCESSOR_ARCHITECTURE")}
+
+ it "executes a simple process" do
+ result = simple_cmdlet.run
+ expect(result.succeeded?).to eq(true)
+ end
+
+ it "#run does not raise a PowershellCmdletException exception if the command cannot be executed" do
+ expect {invalid_cmdlet.run}.not_to raise_error
+ end
+
+ it "#run! raises a PowershellCmdletException exception if the command cannot be executed" do
+ expect {invalid_cmdlet.run!}.to raise_error(Chef::Exceptions::PowershellCmdletException)
+ 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", :windows_powershell_dsc_only 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", :windows_powershell_dsc_only 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..ed0a8f89f6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -107,8 +107,11 @@ 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_powershell_dsc_only => true unless windows_powershell_dsc?
+ config.filter_run_excluding :windows_powershell_no_dsc_only => true unless ! windows_powershell_dsc?
config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined?
config.filter_run_excluding :solaris_only => true unless solaris?
config.filter_run_excluding :system_windows_service_gem_only => true unless system_windows_service_gem?
diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb
index a7c616d7a7..f8cad6de7f 100644
--- a/spec/support/platform_helpers.rb
+++ b/spec/support/platform_helpers.rb
@@ -52,6 +52,30 @@ 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 windows_powershell_dsc?
+ return false unless windows?
+ supports_dsc = false
+ begin
+ wmi = WmiLite::Wmi.new('root/microsoft/windows/desiredstateconfiguration')
+ lcm = wmi.query("SELECT * FROM meta_class WHERE __this ISA 'MSFT_DSCLocalConfigurationManager'")
+ supports_dsc = !! lcm
+ rescue WmiLite::WmiException
+ end
+ supports_dsc
+end
+
def mac_osx_106?
if File.exists? "/usr/bin/sw_vers"
result = shell_out("/usr/bin/sw_vers")
diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb
index 2414bdf552..6adea5eecf 100644
--- a/spec/unit/platform/query_helpers_spec.rb
+++ b/spec/unit/platform/query_helpers_spec.rb
@@ -30,3 +30,26 @@ describe "Chef::Platform#windows_server_2003?" do
expect { Thread.fork { Chef::Platform.windows_server_2003? }.join }.not_to raise_error
end
end
+
+describe 'Chef::Platform#supports_dsc?' do
+ it 'returns false if powershell is not present' do
+ node = Chef::Node.new
+ Chef::Platform.supports_dsc?(node).should be_false
+ end
+
+ ['1.0', '2.0', '3.0'].each do |version|
+ it "returns false for Powershell #{version}" do
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = version
+ Chef::Platform.supports_dsc?(node).should be_false
+ end
+ end
+
+ ['4.0', '5.0'].each do |version|
+ it "returns true for Powershell #{version}" do
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = version
+ Chef::Platform.supports_dsc?(node).should be_true
+ end
+ end
+end
diff --git a/spec/unit/provider/dsc_script_spec.rb b/spec/unit/provider/dsc_script_spec.rb
new file mode 100644
index 0000000000..8a7a7b5c6a
--- /dev/null
+++ b/spec/unit/provider/dsc_script_spec.rb
@@ -0,0 +1,145 @@
+#
+# Author:: Jay Mundrawala (<jdm@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/resource_info'
+require 'spec_helper'
+
+describe Chef::Provider::DscScript do
+ let (:node) {
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = '4.0'
+ node
+ }
+ let (:events) { Chef::EventDispatch::Dispatcher.new }
+ let (:run_context) { Chef::RunContext.new(node, {}, events) }
+ let (:resource) { Chef::Resource::DscScript.new("script", run_context) }
+ let (:provider) do
+ Chef::Provider::DscScript.new(resource, run_context)
+ end
+
+ describe '#load_current_resource' do
+ it "describes the resource as converged if there were 0 DSC resources" do
+ allow(provider).to receive(:run_configuration).with(:test).and_return([])
+ provider.load_current_resource
+ provider.instance_variable_get('@resource_converged').should be_true
+ end
+
+ it "describes the resource as not converged if there is 1 DSC resources that is converged" do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something'])
+ allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info])
+ provider.load_current_resource
+ provider.instance_variable_get('@resource_converged').should be_true
+ end
+
+ it "describes the resource as not converged if there is 1 DSC resources that is not converged" do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something'])
+ allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info])
+ provider.load_current_resource
+ provider.instance_variable_get('@resource_converged').should be_false
+ end
+
+ it "describes the resource as not converged if there are any DSC resources that are not converged" do
+ dsc_resource_info1 = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something'])
+ dsc_resource_info2 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something'])
+
+ allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info1, dsc_resource_info2])
+ provider.load_current_resource
+ provider.instance_variable_get('@resource_converged').should be_false
+ end
+
+ it "describes the resource as converged if all DSC resources that are converged" do
+ dsc_resource_info1 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something'])
+ dsc_resource_info2 = Chef::Util::DSC::ResourceInfo.new('resource', false, ['nothing will change something'])
+
+ allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info1, dsc_resource_info2])
+ provider.load_current_resource
+ provider.instance_variable_get('@resource_converged').should be_true
+ end
+ end
+
+ describe '#generate_configuration_document' do
+ # I think integration tests should cover these cases
+
+ it 'uses configuration_document_from_script_path when a dsc script file is given' do
+ allow(provider).to receive(:load_current_resource)
+ resource.command("path_to_script")
+ generator = double('Chef::Util::DSC::ConfigurationGenerator')
+ generator.should_receive(:configuration_document_from_script_path)
+ allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator)
+ provider.send(:generate_configuration_document, 'tmp', nil)
+ end
+
+ it 'uses configuration_document_from_script_code when a the dsc resource is given' do
+ allow(provider).to receive(:load_current_resource)
+ resource.code("ImADSCResource{}")
+ generator = double('Chef::Util::DSC::ConfigurationGenerator')
+ generator.should_receive(:configuration_document_from_script_code)
+ allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator)
+ provider.send(:generate_configuration_document, 'tmp', nil)
+ end
+
+ it 'should noop if neither code or command are provided' do
+ allow(provider).to receive(:load_current_resource)
+ generator = double('Chef::Util::DSC::ConfigurationGenerator')
+ generator.should_receive(:configuration_document_from_script_code).with('', anything(), anything())
+ allow(Chef::Util::DSC::ConfigurationGenerator).to receive(:new).and_return(generator)
+ provider.send(:generate_configuration_document, 'tmp', nil)
+ end
+ end
+
+ describe 'action_run' do
+ it 'should converge the script if it is not converged' do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resource', true, ['will change something'])
+ allow(provider).to receive(:run_configuration).with(:test).and_return([dsc_resource_info])
+ allow(provider).to receive(:run_configuration).with(:set)
+
+ provider.run_action(:run)
+ resource.should be_updated
+ end
+
+ it 'should not converge if the script is already converged' do
+ allow(provider).to receive(:run_configuration).with(:test).and_return([])
+
+ provider.run_action(:run)
+ resource.should_not be_updated
+ end
+ end
+
+ describe '#generate_description' do
+ it 'removes the resource name from the beginning of any log line from the LCM' do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', true, ['resourcename doing something', 'lastline'])
+ provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info])
+ provider.send(:generate_description)[1].should match(/converge DSC resource resourcename by doing something/)
+ end
+
+ it 'ignores the last line' do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', true, ['resourcename doing something', 'lastline'])
+ provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info])
+ provider.send(:generate_description)[1].should_not match(/lastline/)
+ end
+
+ it 'reports a dsc resource has not been changed if the LCM reported no change was required' do
+ dsc_resource_info = Chef::Util::DSC::ResourceInfo.new('resourcename', false, ['resourcename does nothing', 'lastline'])
+ provider.instance_variable_set('@dsc_resources_info', [dsc_resource_info])
+ provider.send(:generate_description)[1].should match(/converge DSC resource resourcename by doing nothing/)
+ end
+ end
+end
+
diff --git a/spec/unit/resource/dsc_script_spec.rb b/spec/unit/resource/dsc_script_spec.rb
new file mode 100644
index 0000000000..cbd502a61c
--- /dev/null
+++ b/spec/unit/resource/dsc_script_spec.rb
@@ -0,0 +1,127 @@
+#
+# 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_resource_name) { 'DSCTest' }
+
+ context 'when Powershell supports Dsc' do
+ let(:dsc_test_run_context) {
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = '4.0'
+ empty_events = Chef::EventDispatch::Dispatcher.new
+ Chef::RunContext.new(node, {}, empty_events)
+ }
+ 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' }
+ let(:configuration_data) { '@{AllNodes = @( @{ NodeName = "localhost"; PSDscAllowPlainTextPassword = $true })}' }
+ let(:configuration_data_script) { 'c:/myconfigs/data/safedata.psd1' }
+
+ it "has a default action of `:run`" do
+ expect(dsc_test_resource.action).to eq(:run)
+ end
+
+ it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do
+ expect(dsc_test_resource.allowed_actions.to_set).to eq([:run,:nothing].to_set)
+ end
+
+ it "allows the code attribute to be set" do
+ dsc_test_resource.code(configuration_code)
+ expect(dsc_test_resource.code).to eq(configuration_code)
+ end
+
+ it "allows the command 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 "allows the configuration_data attribute to be set" do
+ dsc_test_resource.configuration_data(configuration_data)
+ expect(dsc_test_resource.configuration_data).to eq(configuration_data)
+ end
+
+ it "allows the configuration_data_script attribute to be set" do
+ dsc_test_resource.configuration_data_script(configuration_data_script)
+ expect(dsc_test_resource.configuration_data_script).to eq(configuration_data_script)
+ end
+
+ it "raises an ArgumentError exception if an attempt is made to set the code attribute when the command 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 command attribute when the code 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 code 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_data attribute when the configuration_data_script attribute is already set" do
+ dsc_test_resource.configuration_data_script(configuration_data_script)
+ expect { dsc_test_resource.configuration_data(configuration_data) }.to raise_error(ArgumentError)
+ end
+
+ it "raises an ArgumentError exception if an attempt is made to set the configuration_data_script attribute when the configuration_data attribute is already set" do
+ dsc_test_resource.configuration_data(configuration_data)
+ expect { dsc_test_resource.configuration_data_script(configuration_data_script) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when Powershell does not supported Dsc' do
+ ['1.0', '2.0', '3.0'].each do |version|
+ it "raises an exception for powershell version '#{version}'" do
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = version
+ empty_events = Chef::EventDispatch::Dispatcher.new
+ dsc_test_run_context = Chef::RunContext.new(node, {}, empty_events)
+
+ expect {
+ Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context)
+ }.to raise_error(Chef::Exceptions::NoProviderAvailable)
+ end
+ end
+ end
+
+ context 'when Powershell is not present' do
+ let (:dsc_test_run_context) {
+ node = Chef::Node.new
+ empty_events = Chef::EventDispatch::Dispatcher.new
+ dsc_test_run_context = Chef::RunContext.new(node, {}, empty_events)
+ }
+
+ it 'raises an exception if powershell is not present' do
+ expect {
+ Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context)
+ }.to raise_error(Chef::Exceptions::NoProviderAvailable)
+ end
+ end
+end
diff --git a/spec/unit/util/dsc/configuration_generator_spec.rb b/spec/unit/util/dsc/configuration_generator_spec.rb
new file mode 100644
index 0000000000..03f3ffe25c
--- /dev/null
+++ b/spec/unit/util/dsc/configuration_generator_spec.rb
@@ -0,0 +1,171 @@
+#
+# Author:: Jay Mundrawala <jmundrawala@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/configuration_generator'
+
+describe Chef::Util::DSC::ConfigurationGenerator do
+ let(:conf_man) do
+ node = Chef::Node.new
+ Chef::Util::DSC::ConfigurationGenerator.new(node, 'tmp')
+ end
+
+ describe '#validate_configuration_name!' do
+ it 'should not raise an error if a name contains all upper case letters' do
+ conf_man.send(:validate_configuration_name!, "HELLO")
+ end
+
+ it 'should not raise an error if the name contains all lower case letters' do
+ conf_man.send(:validate_configuration_name!, "hello")
+ end
+
+ it 'should not raise an error if no special characters are used except _' do
+ conf_man.send(:validate_configuration_name!, "hello_world")
+ end
+
+ %w{! @ # $ % ^ & * & * ( ) - = + \{ \} . ? < > \\ /}.each do |sym|
+ it "raises an Argument error if it configuration name contains #{sym}" do
+ expect {
+ conf_man.send(:validate_configuration_name!, "Hello#{sym}")
+ }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe "#get_merged_configuration_flags" do
+ context 'when strings are used as switches' do
+ it 'should merge the hash if there are no restricted switches' do
+ merged = conf_man.send(:get_merged_configuration_flags!, {'flag' => 'a'}, 'hello')
+ merged.should include(:flag)
+ merged[:flag].should eql('a')
+ merged.should include(:outputpath)
+ end
+
+ it 'should raise an ArgumentError if you try to override outputpath' do
+ expect {
+ conf_man.send(:get_merged_configuration_flags!, {'outputpath' => 'a'}, 'hello')
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'should be case insensitive for switches that are not allowed' do
+ expect {
+ conf_man.send(:get_merged_configuration_flags!, {'OutputPath' => 'a'}, 'hello')
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'should be case insensitive to switches that are allowed' do
+ merged = conf_man.send(:get_merged_configuration_flags!, {'FLAG' => 'a'}, 'hello')
+ merged.should include(:flag)
+ end
+ end
+
+ context 'when symbols are used as switches' do
+ it 'should merge the hash if there are no restricted switches' do
+ merged = conf_man.send(:get_merged_configuration_flags!, {:flag => 'a'}, 'hello')
+ merged.should include(:flag)
+ merged[:flag].should eql('a')
+ merged.should include(:outputpath)
+ end
+
+ it 'should raise an ArgumentError if you try to override outputpath' do
+ expect {
+ conf_man.send(:get_merged_configuration_flags!, {:outputpath => 'a'}, 'hello')
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'should be case insensitive for switches that are not allowed' do
+ expect {
+ conf_man.send(:get_merged_configuration_flags!, {:OutputPath => 'a'}, 'hello')
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'should be case insensitive to switches that are allowed' do
+ merged = conf_man.send(:get_merged_configuration_flags!, {:FLAG => 'a'}, 'hello')
+ merged.should include(:flag)
+ end
+ end
+
+ context 'when there are no flags' do
+ it 'should supply an output path if configuration_flags is an empty hash' do
+ merged = conf_man.send(:get_merged_configuration_flags!, {}, 'hello')
+ merged.should include(:outputpath)
+ merged.length.should eql(1)
+ end
+
+ it 'should supply an output path if configuration_flags is an empty hash' do
+ merged = conf_man.send(:get_merged_configuration_flags!, nil, 'hello')
+ merged.should include(:outputpath)
+ merged.length.should eql(1)
+ end
+ end
+
+ # What should happen if configuration flags contains duplicates?
+ # flagA => 'a', flaga => 'a'
+ # or
+ # flagA => 'a', flaga => 'b'
+ #
+ end
+
+ describe '#write_document_generation_script' do
+ let(:file_like_object) { double("file like object") }
+
+ it "should write the input to a file" do
+ allow(File).to receive(:open).and_yield(file_like_object)
+ allow(File).to receive(:join) do |a, b|
+ [a,b].join("++")
+ end
+ allow(file_like_object).to receive(:write)
+ conf_man.send(:write_document_generation_script, 'file', 'hello')
+ expect(file_like_object).to have_received(:write)
+ end
+ end
+
+ describe "#find_configuration_document" do
+ it "should find the mof file" do
+ # These tests seem way too implementation specific. Unfortunatly, File and Dir
+ # need to be mocked because they are OS specific
+ allow(File).to receive(:join) do |a, b|
+ [a,b].join("++")
+ end
+
+ allow(Dir).to receive(:entries).with("tmp++hello") {['f1', 'f2', 'hello.mof', 'f3']}
+ expect(conf_man.send(:find_configuration_document, 'hello')).to eql('tmp++hello++hello.mof')
+ end
+
+ it "should return nil if the mof file is not found" do
+ allow(File).to receive(:join) do |a, b|
+ [a,b].join("++")
+ end
+ allow(Dir).to receive(:entries).with("tmp++hello") {['f1', 'f2', 'f3']}
+ expect(conf_man.send(:find_configuration_document, 'hello')).to be_nil
+ end
+ end
+
+ describe "#configuration_code" do
+ it "should build dsc" do
+ dsc = conf_man.send(:configuration_code, 'archive{}', 'hello')
+ found_configuration = false
+ dsc.split(';').each do |command|
+ if command.downcase =~ /\s*configuration\s+'hello'\s*\{\s*node\s+'localhost'\s*\{\s*archive\s*\{\s*\}\s*\}\s*\}\s*/
+ found_configuration = true
+ end
+ end
+ expect(found_configuration).to be_true
+ end
+ end
+end
diff --git a/spec/unit/util/dsc/lcm_output_parser_spec.rb b/spec/unit/util/dsc/lcm_output_parser_spec.rb
new file mode 100644
index 0000000000..23a3dbd3ec
--- /dev/null
+++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb
@@ -0,0 +1,169 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# 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/util/dsc/lcm_output_parser'
+
+describe Chef::Util::DSC::LocalConfigurationManager::Parser do
+ context 'empty input parameter' 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 empty array for a nil input' do
+ Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty
+ end
+ end
+
+ context 'correctly formatted output from lcm' do
+ it 'returns an empty array for a log with no resources' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str).should be_empty
+ end
+
+ it 'returns a single resource when only 1 logged with the correct name' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources.length.should eq(1)
+ resources[0].name.should eq('[name]')
+ end
+
+ it 'identifies when a resource changes the state of the system' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Start Set ] [name]
+logtype: [machinename]: LCM: [ End Set ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].changes_state?.should be_true
+ end
+
+ it 'preserves the log provided for how the system changed the state' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Start Set ] [name]
+logtype: [machinename]: [message]
+logtype: [machinename]: LCM: [ End Set ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].change_log.should match_array(["[name]","[message]","[name]"])
+ end
+
+ it 'should return false for changes_state?' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Skip Set ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].changes_state?.should be_false
+ end
+
+ it 'should return an empty array for change_log if changes_state? is false' do
+ str = <<EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Skip Set ] [name]
+logtype: [machinename]: LCM: [ End Resource ] [name]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].change_log.should be_empty
+ end
+ end
+
+ context 'Incorrectly formatted output from LCM' do
+ it 'should allow missing a [End Resource] when its the last one and still find all the resource' do
+ str = <<-EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Skip Set ]
+logtype: [machinename]: LCM: [ End Resource ]
+logtype: [machinename]: LCM: [ Start Resource ] [name2]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ End Set ]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].changes_state?.should be_false
+ resources[1].changes_state?.should be_true
+ end
+
+ it 'should allow missing a [End Resource] when its the first one and still find all the resource' do
+ str = <<-EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Skip Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name2]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ End Set ]
+logtype: [machinename]: LCM: [ End Resource ]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].changes_state?.should be_false
+ resources[1].changes_state?.should be_true
+ end
+
+ it 'should allow missing set and end resource and assume an unconverged resource in this case' do
+ str = <<-EOF
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ Start Resource ] [name]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Start Resource ] [name2]
+logtype: [machinename]: LCM: [ Start Test ]
+logtype: [machinename]: LCM: [ End Test ]
+logtype: [machinename]: LCM: [ Start Set ]
+logtype: [machinename]: LCM: [ End Set ]
+logtype: [machinename]: LCM: [ End Resource ]
+logtype: [machinename]: LCM: [ End Set ]
+EOF
+ resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)
+ resources[0].changes_state?.should be_true
+ resources[0].name.should eql('[name]')
+ resources[1].changes_state?.should be_true
+ resources[1].name.should eql('[name2]')
+ end
+ 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..fb6664bd40
--- /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
+
diff --git a/spec/unit/util/powershell/cmdlet_spec.rb b/spec/unit/util/powershell/cmdlet_spec.rb
new file mode 100644
index 0000000000..a964f607c8
--- /dev/null
+++ b/spec/unit/util/powershell/cmdlet_spec.rb
@@ -0,0 +1,106 @@
+#
+# Author:: Jay Mundrawala <jdm@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/powershell/cmdlet'
+
+describe Chef::Util::Powershell::Cmdlet do
+ before (:all) do
+ @node = Chef::Node.new
+ @cmdlet = Chef::Util::Powershell::Cmdlet.new(@node, 'Some-Commandlet')
+ end
+
+ describe '#validate_switch_name!' do
+ it 'should not raise an error if a name contains all upper case letters' do
+ @cmdlet.send(:validate_switch_name!, "HELLO")
+ end
+
+ it 'should not raise an error if the name contains all lower case letters' do
+ @cmdlet.send(:validate_switch_name!, "hello")
+ end
+
+ it 'should not raise an error if no special characters are used except _' do
+ @cmdlet.send(:validate_switch_name!, "hello_world")
+ end
+
+ %w{! @ # $ % ^ & * & * ( ) - = + \{ \} . ? < > \\ /}.each do |sym|
+ it "raises an Argument error if it configuration name contains #{sym}" do
+ expect {
+ @cmdlet.send(:validate_switch_name!, "Hello#{sym}")
+ }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe '#escape_parameter_value' do
+ # Is this list really complete?
+ %w{` " # '}.each do |c|
+ it "escapse #{c}" do
+ @cmdlet.send(:escape_parameter_value, "stuff #{c}").should eql("stuff `#{c}")
+ end
+ end
+
+ it 'does not do anything to a string without special characters' do
+ @cmdlet.send(:escape_parameter_value, 'stuff').should eql('stuff')
+ end
+ end
+
+ describe '#escape_string_parameter_value' do
+ it "surrounds a string with ''" do
+ @cmdlet.send(:escape_string_parameter_value, 'stuff').should eql("'stuff'")
+ end
+ end
+
+ describe '#command_switches_string' do
+ it 'raises an ArgumentError if the key is not a symbol' do
+ expect {
+ @cmdlet.send(:command_switches_string, {'foo' => 'bar'})
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'does not allow invalid switch names' do
+ expect {
+ @cmdlet.send(:command_switches_string, {:foo! => 'bar'})
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'ignores switches with a false value' do
+ @cmdlet.send(:command_switches_string, {foo: false}).should eql('')
+ end
+
+ it 'should correctly handle a value type of string' do
+ @cmdlet.send(:command_switches_string, {foo: 'bar'}).should eql("-foo 'bar'")
+ end
+
+ it 'should correctly handle a value type of string even when it is 0 length' do
+ @cmdlet.send(:command_switches_string, {foo: ''}).should eql("-foo ''")
+ end
+
+ it 'should not quote integers' do
+ @cmdlet.send(:command_switches_string, {foo: 1}).should eql("-foo 1")
+ end
+
+ it 'should not quote floats' do
+ @cmdlet.send(:command_switches_string, {foo: 1.0}).should eql("-foo 1.0")
+ end
+
+ it 'has just the switch when the value is true' do
+ @cmdlet.send(:command_switches_string, {foo: true}).should eql("-foo")
+ end
+ end
+end