diff options
author | Adam Edwards <adamed@opscode.com> | 2014-08-08 13:55:41 -0700 |
---|---|---|
committer | Jay Mundrawala <jdmundrawala@gmail.com> | 2014-09-19 12:47:29 -0700 |
commit | cb503c72ea1e07f670d5c20830463e3469283298 (patch) | |
tree | ead922e0111d215391a2cefc2697c3f2d0c39931 /lib | |
parent | 5534b5d0f781188e0398d5b1ade7ceba568e4b45 (diff) | |
download | chef-cb503c72ea1e07f670d5c20830463e3469283298.tar.gz |
Initial dsc_configuration resource implementation
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/exceptions.rb | 2 | ||||
-rw-r--r-- | lib/chef/mixin/windows_architecture_helper.rb | 16 | ||||
-rw-r--r-- | lib/chef/provider/dsc_script.rb | 103 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/dsc_script.rb | 101 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/util/dsc/configuration_generator.rb | 115 | ||||
-rw-r--r-- | lib/chef/util/dsc/local_configuration_manager.rb | 125 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet.rb | 132 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet_result.rb | 46 |
10 files changed, 642 insertions, 0 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 23e223f204..ea053cae64 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -118,6 +118,8 @@ class Chef class InvalidDataBagPath < ArgumentError; end class DuplicateDataBagItem < RuntimeError; end + class PowershellCmdletException < RuntimeError; end + # A different version of a cookbook was added to a # VersionedRecipeList than the one already there. class CookbookVersionConflict < ArgumentError ; end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index ff118c1d3d..65ad042910 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,6 +42,22 @@ class Chef is_i386_process_on_x86_64_windows? end + def with_os_architecture(node) + wow64_redirection_state = nil + + if wow64_architecture_override_required?(node, node_windows_architecture(node)) + wow64_redirection_state = disable_wow64_file_redirection(node) + end + + begin + yield + ensure + if wow64_redirection_state + restore_wow64_file_redirection(node, wow64_redirection_state) + end + end + end + def node_supports_windows_architecture?(node, desired_architecture) assert_valid_windows_architecture!(desired_architecture) return (node_windows_architecture(node) == :x86_64 || diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb new file mode 100644 index 0000000000..5cdc37a312 --- /dev/null +++ b/lib/chef/provider/dsc_script.rb @@ -0,0 +1,103 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/util/powershell/cmdlet' +require 'chef/util/dsc/configuration_generator' +require 'chef/util/dsc/local_configuration_manager' + +class Chef + class Provider + class DscScript < Chef::Provider + def initialize(dsc_resource, run_context) + super(dsc_resource, run_context) + @dsc_resource = dsc_resource + @resource_converged = false + @operations = { + :set => Proc.new { |config_manager, document| + config_manager.set_configuration(document) + }, + :test => Proc.new { |config_manager, document| + config_manager.test_configuration(document) + }} + end + + def action_run + if ! @resource_converged + converge_by("DSC resource script for configuration '#{configuration_friendly_name}'") do + run_configuration(:set) + Chef::Log.info("DSC resource configuration completed successfully") + end + end + end + + def load_current_resource + @resource_converged = ! run_configuration(:test) + end + + def whyrun_supported? + true + end + + protected + + def run_configuration(operation) + config_directory = ::Dir.mktmpdir("dsc-script") + + config_manager = Chef::Util::DSC::LocalConfigurationManager.new(@run_context.node, config_directory) + + begin + configuration_document = generate_configuration_document(config_directory, @dsc_resource.flags) + @operations[operation].call(config_manager, configuration_document) + rescue Exception => e + Chef::Log.error("DSC operation failed: #{e.message.to_s}") + raise e + ensure + ::FileUtils.rm_rf(config_directory) + end + end + + def generate_configuration_document(config_directory, configuration_flags) + shellout_flags = { + :cwd => @dsc_resource.cwd, + :environment => @dsc_resource.environment, + :timeout => @dsc_resource.timeout + } + + generator = Chef::Util::DSC::ConfigurationGenerator.new(@run_context.node, config_directory) + + if @dsc_resource.command + generator.configuration_document_from_script_path(@dsc_resource.command, configuration_name, configuration_flags, shellout_flags) + else + generator.configuration_document_from_script_code(@dsc_resource.code, configuration_flags, shellout_flags) + end + end + + def configuration_name + @dsc_resource.configuration_name || @dsc_resource.name + end + + def configuration_friendly_name + if @dsc_resource.code + @dsc_resource.name + else + configuration_name + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 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..10d90bd065 --- /dev/null +++ b/lib/chef/resource/dsc_script.rb @@ -0,0 +1,101 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Resource + class DscScript < Chef::Resource + + provides :dsc_script, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions.push(:run) + @action = 'run' + provider(Chef::Provider::DscScript) + end + + def code(arg=nil) + if arg && command + raise ArgumentError, "Only one of 'code' and 'command' properties may be specified" + end + if arg && configuration_name + raise ArgumentError, "Attribute `code` may not be set if `configuration_name` is set" + end + set_or_return( + :code, + arg, + :kind_of => [ String ] + ) + end + + def configuration_name(arg=nil) + if arg && code + raise ArgumentError, "Attribute `configuration_name` may not be set if `code` is set" + end + set_or_return( + :configuration_name, + arg, + :kind_of => [ String ] + ) + end + + def command(arg=nil) + if arg && code + raise ArgumentError, "Only one of 'code' and 'command' properties may be specified" + end + set_or_return( + :command, + arg, + :kind_of => [ String ] + ) + end + + def flags(arg=nil) + set_or_return( + :flags, + arg, + :kind_of => [ Hash ] + ) + end + + def cwd(arg=nil) + set_or_return( + :cwd, + arg, + :kind_of => [ String ] + ) + end + + def environment(arg=nil) + set_or_return( + :environment, + arg, + :kind_of => [ Hash ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ Integer ] + ) + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 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..ea949bb9c4 --- /dev/null +++ b/lib/chef/util/dsc/configuration_generator.rb @@ -0,0 +1,115 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/util/powershell/cmdlet' + +class Chef::Util::DSC + class ConfigurationGenerator + def initialize(node, config_directory) + @node = node + @config_directory = config_directory + end + + def configuration_document_from_script_code(code, configuration_flags, shellout_flags) + Chef::Log.debug("DSC: DSC code:\n '#{code}'") + generated_script_path = write_document_generation_script(code, 'chef_dsc') + begin + configuration_document_from_script_path(generated_script_path, 'chef_dsc', configuration_flags, shellout_flags) + ensure + ::FileUtils.rm(generated_script_path) + end + end + + def configuration_document_from_script_path(script_path, configuration_name, configuration_flags, shellout_flags) + validate_configuration_name!(configuration_name) + + document_generation_cmdlet = Chef::Util::Powershell::Cmdlet.new( + @node, + configuration_document_generation_code(script_path, configuration_name)) + + merged_configuration_flags = get_merged_configuration_flags!(configuration_flags, configuration_name) + + document_generation_cmdlet.run(merged_configuration_flags, shellout_flags) + configuration_document_location = find_configuration_document(configuration_name) + + if ! configuration_document_location + raise RuntimeError, "No DSC configuration for '#{configuration_name}' was generated from supplied DSC script" + end + + configuration_document = get_configuration_document(configuration_document_location) + ::FileUtils.rm_rf(configuration_document_location) + configuration_document + end + + protected + + # From PowerShell error help for the Configuration language element: + # Standard names may only contain letters (a-z, A-Z), numbers (0-9), and underscore (_). + # The name may not be null or empty, and should start with a letter. + def validate_configuration_name!(configuration_name) + if !!(configuration_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false + raise ArgumentError, 'Configuration `#{configuration_name}` is not a valid PowerShell cmdlet name' + end + end + + def get_merged_configuration_flags!(configuration_flags, configuration_name) + merged_configuration_flags = { :outputpath => configuration_document_directory(configuration_name) } + if configuration_flags + configuration_flags.map do | switch, value | + if merged_configuration_flags.key?(switch.to_s.downcase.to_sym) + raise ArgumentError, "The `flags` attribute for the dsc_script resource contained a command line switch :#{switch.to_s} that is disallowed." + end + merged_configuration_flags[switch.to_s.downcase.to_sym] = value + end + end + merged_configuration_flags + end + + def configuration_code(code, configuration_name) + "$ProgressPreference = 'SilentlyContinue';Configuration '#{configuration_name}'\n{\n\t#{code}\n}\n" + end + + def configuration_document_generation_code(configuration_script, configuration_name) + ". '#{configuration_script}';#{configuration_name}" + end + + def write_document_generation_script(code, configuration_name) + script_path = "#{@config_directory}/chef_dsc_config.ps1" + ::File.open(script_path, 'wt') do | script | + script.write(configuration_code(code, configuration_name)) + end + script_path + end + + def find_configuration_document(configuration_name) + document_directory = configuration_document_directory(configuration_name) + document_file_name = ::Dir.entries(document_directory).find { | path | path =~ /.*.mof/ } + ::File.join(document_directory, document_file_name) + end + + def configuration_document_directory(configuration_name) + ::File.join(@config_directory, configuration_name) + end + + def get_configuration_document(document_path) + ::File.open(document_path, 'rb') do | file | + file.read + end + end + end +end diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb new file mode 100644 index 0000000000..a9c5b867df --- /dev/null +++ b/lib/chef/util/dsc/local_configuration_manager.rb @@ -0,0 +1,125 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/util/powershell/cmdlet' + +class Chef::Util::DSC + class LocalConfigurationManager + def initialize(node, configuration_path) + @node = node + @configuration_path = configuration_path + clear_execution_time + end + + def test_configuration(configuration_document) + status = run_configuration_cmdlet(configuration_document) + configuration_update_required?(status.return_value) + end + + def set_configuration(configuration_document) + run_configuration_cmdlet(configuration_document, true) + end + + def last_operation_execution_time_seconds + if @operation_start_time && @operation_end_time + @operation_end_time - @operation_start_time + end + end + + private + + def run_configuration_cmdlet(configuration_document, apply_configuration = false) + Chef::Log.debug("DSC: Calling DSC Local Config Manager to #{apply_configuration ? "set" : "test"} configuration document.") + test_only_parameters = ! apply_configuration ? '-whatif; if (! $?) { exit 1 }' : '' + + start_operation_timing + command_code = "$ProgressPreference = 'SilentlyContinue';start-dscconfiguration -path #{@configuration_path} -wait -force #{test_only_parameters}" + status = nil + + begin + save_configuration_document(configuration_document) + cmdlet = ::Chef::Util::Powershell::Cmdlet.new(@node, "#{command_code}") + status = cmdlet.run + ensure + end_operation_timing + remove_configuration_document + if last_operation_execution_time_seconds + Chef::Log.debug("DSC: DSC operation completed in #{last_operation_execution_time_seconds} seconds.") + end + end + Chef::Log.debug("DSC: Completed call to DSC Local Config Manager") + status + end + + def configuration_update_required?(what_if_output) + Chef::Log.debug("DSC: DSC returned the following '-whatif' output from test operation:\n#{what_if_output}") + parse_what_if_output(what_if_output) + end + + def save_configuration_document(configuration_document) + ::FileUtils.mkdir_p(@configuration_path) + ::File.open(configuration_document_path, 'wb') do | file | + file.write(configuration_document) + end + end + + def remove_configuration_document + ::FileUtils.rm(configuration_document_path) + end + + def configuration_document_path + File.join(@configuration_path,'..mof') + end + + def parse_what_if_output(what_if_output) + + # What-if output for start-dscconfiguration contains lines that look like one of the following: + # + # What if: [SEA-ADAMED1]: LCM: [ Start Set ] [[Group]chef_dsc] + # What if: [SEA-ADAMED1]: [[Group]chef_dsc] Performing the operation "Add" on target "Group: demo1" + # + # The second line lacking the 'LCM:' is what happens if there is a change required to make the system consistent with the resource. + # Such a line without LCM is only present if an update to the system is required. Therefore, we test each line below + # to see if it is missing the LCM, and declare that an update is needed if so. + has_change_line = false + + what_if_output.lines.each do |line| + if (line =~ /.+\:\s+\[\S*\]\:\s+LCM\:/).nil? + has_change_line = true + break + end + end + + has_change_line + end + + def clear_execution_time + @operation_start_time = nil + @operation_end_time = nil + end + + def start_operation_timing + clear_execution_time + @operation_start_time = Time.now + end + + def end_operation_timing + @operation_end_time = Time.now + end + end +end diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb new file mode 100644 index 0000000000..0120f0aaf9 --- /dev/null +++ b/lib/chef/util/powershell/cmdlet.rb @@ -0,0 +1,132 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'mixlib/shellout' +require 'chef/mixin/windows_architecture_helper' +require 'chef/util/powershell/cmdlet_result' + +class Chef::Util::Powershell + class Cmdlet + def initialize(node, cmdlet, output_format=nil, output_format_options={}) + @output_format = output_format + @node = node + + case output_format + when nil + @json_format = false + when :json + @json_format = true + when :text + @json_format = false + when :object + @json_format = true + else + raise ArgumentError, "Invalid output format #{output_format.to_s} specified" + end + + @cmdlet = cmdlet + @output_format_options = output_format_options + end + + attr_reader :output_format + + def run(switches={}, execution_options={}, *arguments) + arguments_string = arguments.join(' ') + + switches_string = command_switches_string(switches) + + json_depth = 5 + + if @json_format && @output_format_options.has_key?(:depth) + json_depth = @output_format_options[:depth] + end + + json_command = @json_format ? " | convertto-json -compress -depth #{json_depth}" : "" + command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive -command \"trap [Exception] {write-error -exception ($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} #{arguments_string}#{json_command}\";if ( ! $? ) { exit 1 }" + + augmented_options = {:returns => [0], :live_stream => false}.merge(execution_options) + command = Mixlib::ShellOut.new(command_string, augmented_options) + + os_architecture = "#{ENV['PROCESSOR_ARCHITEW6432']}" == 'AMD64' ? :x86_64 : :i386 + + status = nil + + with_os_architecture(@node) do + status = command.run_command + end + + result = CmdletResult.new(status, @output_format) + + if ! result.succeeded? + raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{result.stderr}" + end + + result + end + + protected + + include Chef::Mixin::WindowsArchitectureHelper + + def validate_switch_name!(switch_parameter_name) + if !!(switch_parameter_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false + raise ArgumentError, "`#{switch_parameter_name}` is not a valid PowerShell cmdlet switch parameter name" + end + end + + def escape_parameter_value(parameter_value) + parameter_value.gsub(/\`|\'|\"/,'`\1') + end + + def escape_string_parameter_value(parameter_value) + "'#{escape_parameter_value(parameter_value)}'" + end + + def command_switches_string(switches) + command_switches = switches.map do | switch_name, switch_value | + if switch_name.class != Symbol + raise ArgumentError, "Invalid type `#{switch_name} `for PowerShell switch '#{switch_name.to_s}'. The switch must be specified as a Symbol'" + end + + validate_switch_name!(switch_name) + + switch_argument = '' + switch_present = true + + case switch_value + when Numeric + switch_argument = switch_value.to_s + when Float + switch_argument = switch_value.to_s + when FalseClass + switch_present = false + when TrueClass + when String + switch_argument = escape_string_parameter_value(switch_value) + else + raise ArgumentError, "Invalid argument type `#{switch_value.class}` specified for PowerShell switch `:#{switch_name.to_s}`. Arguments to PowerShell must be of type `String`, `Numeric`, `Float`, `FalseClass`, or `TrueClass`" + end + + switch_present ? ["-#{switch_name.to_s.downcase}", switch_argument].join(' ') : '' + end + + command_switches.join(' ') + end + end +end + diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb new file mode 100644 index 0000000000..390ee8cbc1 --- /dev/null +++ b/lib/chef/util/powershell/cmdlet_result.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'json' + +class Chef::Util::Powershell + class CmdletResult + attr_reader :output_format + + def initialize(status, output_format) + @status = status + @output_format = output_format + end + + def stderr + @status.stderr + end + + def return_value + if output_format == :object + JSON.parse(@status.stdout) + else + @status.stdout + end + end + + def succeeded? + @succeeded = @status.status.exitstatus == 0 + end + end +end |