diff options
author | Bryan McLellan <btm@loftninjas.org> | 2014-09-03 21:10:04 -0400 |
---|---|---|
committer | Bryan McLellan <btm@loftninjas.org> | 2014-09-03 21:10:04 -0400 |
commit | e0c72aa02b331cb62442b0840cecd9ef4db9d552 (patch) | |
tree | 1515129feb1f144e6e82347d53ce29be43b85f8e | |
parent | d4ab0361d1a609fb687d6e4558166074556f8d7e (diff) | |
parent | 2444d1ed86b2d3ba88e626a54f045f64b0cd51de (diff) | |
download | chef-e0c72aa02b331cb62442b0840cecd9ef4db9d552.tar.gz |
Merge pull request #1975 from opscode/platform/11-dsc
Platform/11 dsc
24 files changed, 2218 insertions, 3 deletions
diff --git a/chef.gemspec b/chef.gemspec index c5f7e7d644..8f25363124 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.add_dependency "mixlib-log", "~> 1.3" s.add_dependency "mixlib-authentication", "~> 1.3" s.add_dependency "mixlib-shellout", "~> 1.4" - s.add_dependency "ohai", "~> 7.2" + s.add_dependency "ohai", "7.4.0.rc.0" # Lock down rest-client to avoid pulling in rdoc and breaking Omnibus builds # on Solaris. See the original issue for more details: diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index ba612a6b34..bfb1bd16f3 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -117,6 +117,8 @@ class Chef class ObsoleteDependencySyntax < ArgumentError; end class InvalidDataBagPath < ArgumentError; 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 edcd596341..abdb086151 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -41,6 +41,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..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 c89037df31..8327539cdf 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..37546c3b6a --- /dev/null +++ b/lib/chef/resource/dsc_script.rb @@ -0,0 +1,123 @@ +# +# 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' 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 + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 01e8d63040..c69db83c24 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..91181b3089 --- /dev/null +++ b/lib/chef/util/dsc/lcm_output_parser.rb @@ -0,0 +1,178 @@ +# +# 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 + class ParseException < RuntimeError; end + + class Operation + attr_reader :op_type + attr_reader :resources + attr_reader :info + attr_reader :sets + attr_reader :tests + + def initialize(op_type, info) + @op_type = op_type + @info = [] + @sets = [] + @tests = [] + @resources = [] + add_info(info) + end + + def add_info(info) + @info << info + end + + def add_set(set) + raise ParseException, "add_set is not allowed in this context. Found #{@op_type}" unless [:resource, :set].include?(@op_type) + @sets << set + end + + def add_test(test) + raise ParseException, "add_test is not allowed in this context. Found #{@op_type}" unless [:resource, :set].include?(@op_type) + @tests << test + end + + def add_resource(resource) + raise ParseException, 'add_resource is only allowed to be added to the set op_type' unless @op_type == :set + @resources << resource + end + end + + # 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 + + stack = Array.new + popped_op = nil + lcm_output.lines.each do |line| + op_action, op_type, info = parse_line(line) + info.strip! # Because this was formatted for humans + + # The rules: + # - For each `start` action, there must be a matching `end` action + # - `skip` actions do not not do anything (They don't add to the stack) + case op_action + when :start + new_op = Operation.new(op_type, info) + case op_type + when :set + stack[-1].add_set(new_op) if stack[-1] + when :test + stack[-1].add_test(new_op) + when :resource + while stack[-1].op_type != :set + Chef::Log.warn("Can't add resource to set...popping until it is allowed.") + popped_op = stack.pop + end + stack[-1].add_resource(new_op) + else + Chef::Log.warn("Unknown op_action #{op_action}: Read line #{line}") + end + stack.push(new_op) + when :end + popped_op = stack.pop + popped_op.add_info(info) + while popped_op.op_type != op_type + Chef::Log::warn("Unmatching end for op_type. Expected op_type=#{op_type}, found op_type=#{popped_op.op_type}. From output:\n#{lcm_output}") + popped_op = stack.pop + end + when :skip + # We don't really have anything to do here + when :info + stack[-1].add_info(info) if stack[-1] + else + stack[-1].add_info(line) if stack[-1] + end + end + + op_to_resource_infos(popped_op) + end + + def self.parse_line(line) + if match = line.match(/^.*?:.*?:\s*LCM:\s*\[(.*?)\](.*)/) + # If the line looks like + # x: [y]: 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 + # If the line looks like + # x: [y]: message + # extract message + match = line.match(/^.*?:.*?: \s+(.*)/) + op_action = op_type = :info + info = match.captures[0] + end + info.strip! # Because this was formatted for humans + return [op_action, op_type, info] + end + private_class_method :parse_line + + def self.op_to_resource_infos(op) + resources = op ? op.resources : [] + + resources.map do |r| + name = r.info[0] + sets = r.sets.length > 0 + change_log = r.sets[-1].info if sets + Chef::Util::DSC::ResourceInfo.new(name, sets, change_log) + end + end + private_class_method :op_to_resource_infos + + 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/path_helper.rb b/lib/chef/util/path_helper.rb index 534a9087ae..1c8f6f5d08 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -32,7 +32,7 @@ class Chef Chef::Log.error(msg) raise Chef::Exceptions::ValidationFailed, msg end - + if windows_max_length_exceeded?(path) Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") path.insert(0, "\\\\?\\") @@ -50,7 +50,7 @@ class Chef return true end end - + false end diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb new file mode 100644 index 0000000000..ddca732b40 --- /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.to_s =~ /\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..2aa1f35772 --- /dev/null +++ b/spec/functional/resource/dsc_script_spec.rb @@ -0,0 +1,336 @@ +# +# 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.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_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 09e7642d98..e73010d2b5 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 75ab0c9cde..ec3b3f4a60 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -43,6 +43,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/provider/dsc_script_spec.rb b/spec/unit/provider/dsc_script_spec.rb new file mode 100644 index 0000000000..b0b4416a66 --- /dev/null +++ b/spec/unit/provider/dsc_script_spec.rb @@ -0,0 +1,124 @@ +# +# 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 + + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @resource = Chef::Resource::DscScript.new("script", @run_context) + @provider = Chef::Provider::DscScript.new(@resource, @run_context) + @provider.current_resource = @current_resource + end + + describe '#load_current_resource' do + it "describes the resource as converged if there were 0 DSC resources" do + @provider.stub(: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']) + @provider.stub(: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']) + @provider.stub(: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']) + + @provider.stub(: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']) + + @provider.stub(: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 + @provider.stub(:load_current_resource) + @resource.command("path_to_script") + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_path) + Chef::Util::DSC::ConfigurationGenerator.stub(: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 + @provider.stub(:load_current_resource) + @resource.code("ImADSCResource{}") + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_code) + Chef::Util::DSC::ConfigurationGenerator.stub(:new).and_return(generator) + @provider.send(:generate_configuration_document, 'tmp', nil) + end + + it 'should noop if neither code or command are provided' do + @provider.stub(:load_current_resource) + generator = double('Chef::Util::DSC::ConfigurationGenerator') + generator.should_receive(:configuration_document_from_script_code).with('', anything(), anything()) + Chef::Util::DSC::ConfigurationGenerator.stub(: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']) + @provider.stub(:run_configuration).with(:test).and_return([dsc_resource_info]) + @provider.stub(:run_configuration).with(:set) + + @provider.run_action(:run) + @resource.should be_updated + + end + + it 'should not converge if the script is already converged' do + @provider.stub(:run_configuration).with(:test).and_return([]) + + @provider.run_action(:run) + @resource.should_not be_updated + 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..7156079937 --- /dev/null +++ b/spec/unit/resource/dsc_script_spec.rb @@ -0,0 +1,94 @@ +# +# 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' } + 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 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..8fcc85a71c --- /dev/null +++ b/spec/unit/util/dsc/configuration_generator_spec.rb @@ -0,0 +1,173 @@ +# +# 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 + before (:all) do + @node = Chef::Node.new + @conf_man = 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 + before (:each) do + @file_like_object = double("file like object") + end + + it "should write the input to a file" do + File.stub(:open).and_yield(@file_like_object) + @file_like_object.stub(:write).with(anything()) + File.stub(:join) do |a, b| + [a,b].join("++") + end + @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 + + File.stub(:join) do |a, b| + [a,b].join("++") + end + Dir.stub(: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 + File.stub(:join) do |a, b| + [a,b].join("++") + end + Dir.stub(: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..05aa072a94 --- /dev/null +++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb @@ -0,0 +1,178 @@ +# +# 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 raise an exception if a set is found inside a test' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + it 'should raise an exception if a test is found inside a test' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + 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 + 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..39b3585c17 --- /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 |