diff options
author | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-03-23 16:40:16 -0700 |
---|---|---|
committer | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-03-23 16:40:16 -0700 |
commit | d0ee29bba624800fc1293d6144572aec42f602de (patch) | |
tree | 2c059eea11460c7335e30a6ceb9b338893ca22ac | |
parent | 815ad9cd0afb12cc7a9d81718d9a2179e78d9d61 (diff) | |
parent | deeb73825c5b6c7b3fc6738d4d6c484a8ba8307c (diff) | |
download | chef-d0ee29bba624800fc1293d6144572aec42f602de.tar.gz |
Merge pull request #2881 from chef/jdm/dsc_resource
DscResource in core chef
26 files changed, 1236 insertions, 10 deletions
diff --git a/lib/chef/dsl/powershell.rb b/lib/chef/dsl/powershell.rb new file mode 100644 index 0000000000..a17971c689 --- /dev/null +++ b/lib/chef/dsl/powershell.rb @@ -0,0 +1,29 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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/powershell/ps_credential' + +class Chef + module DSL + module Powershell + def ps_credential(username='placeholder', password) + Chef::Util::Powershell::PSCredential.new(username, password) + end + end + end +end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 22f090789f..eea6a2f239 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -442,5 +442,20 @@ class Chef super "PID file and lockfile are not permitted to match. Specify a different location with --pid or --lockfile" end end + + class MultipleDscResourcesFound < RuntimeError + attr_reader :resources_found + def initialize(resources_found) + @resources_found = resources_found + matches_info = @resources_found.each do |r| + if r['Module'].nil? + "Resource #{r['Name']} was found in #{r['Module']['Name']}" + else + "Resource #{r['Name']} is a binary resource" + end + end + super "Found multiple matching resources. #{matches_info.join("\n")}" + end + end end end diff --git a/lib/chef/mixin/powershell_type_coercions.rb b/lib/chef/mixin/powershell_type_coercions.rb new file mode 100644 index 0000000000..75b3276c84 --- /dev/null +++ b/lib/chef/mixin/powershell_type_coercions.rb @@ -0,0 +1,82 @@ +# +# Author:: Adam Edwards (<adamed@opscode.com>) +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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 + module Mixin + module PowershellTypeCoercions + + def type_coercions + @type_coercions ||= { + Fixnum => { :type => lambda { |x| x.to_s }}, + Float => { :type => lambda { |x| x.to_s }}, + FalseClass => { :type => lambda { |x| '$false' }}, + TrueClass => { :type => lambda { |x| '$true' }}, + Hash => {:type => Proc.new { |x| translate_hash(x)}}, + Array => {:type => Proc.new { |x| translate_array(x)}} + } + end + + def translate_type(value) + translation = type_coercions[value.class] + + if translation + translation[:type].call(value) + elsif value.respond_to? :to_psobject + "(#{value.to_psobject})" + else + safe_string(value.to_s) + end + end + + private + + def translate_hash(x) + translated = x.inject([]) do |memo, (k,v)| + memo << "#{k}=#{translate_type(v)}" + end + "@{#{translated.join(';')}}" + end + + def translate_array(x) + translated = x.map do |v| + translate_type(v) + end + "@(#{translated.join(',')})" + end + + def unsafe?(s) + ["'", '#', '`', '"'].any? do |x| + s.include? x + end + end + + def safe_string(s) + # do we need to worry about binary data? + if unsafe?(s) + encoded_str = Base64.strict_encode64(s.encode("UTF-8")) + "([System.Text.Encoding]::UTF8.GetString("\ + "[System.Convert]::FromBase64String('#{encoded_str}')"\ + "))" + else + "'#{s}'" + end + end + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index 65ad042910..a0ac34f627 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -43,6 +43,14 @@ class Chef end def with_os_architecture(node) + node ||= begin + os_arch = ENV['PROCESSOR_ARCHITEW6432'] || + ENV['PROCESSOR_ARCHITECTURE'] + Hash.new.tap do |n| + n[:kernel] = Hash.new + n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386 + end + end wow64_redirection_state = nil if wow64_architecture_override_required?(node, node_windows_architecture(node)) diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index ff83c871fa..f7c85fbe23 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -47,6 +47,13 @@ class Chef node[:languages] && node[:languages][:powershell] && node[:languages][:powershell][:version].to_i >= 4 end + + def supports_dsc_invoke_resource?(node) + require 'rubygems' + supports_dsc?(node) && + Gem::Version.new(node[:languages][:powershell][:version]) >= + Gem::Version.new("5.0.10018.0") + end end end end diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb new file mode 100644 index 0000000000..fabb695803 --- /dev/null +++ b/lib/chef/provider/dsc_resource.rb @@ -0,0 +1,157 @@ +# +# 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/local_configuration_manager' +require 'chef/mixin/powershell_type_coercions' +require 'chef/util/dsc/resource_store' + +class Chef + class Provider + class DscResource < Chef::Provider + include Chef::Mixin::PowershellTypeCoercions + + provides :dsc_resource, os: "windows" + + def initialize(new_resource, run_context) + super + @new_resource = new_resource + @module_name = new_resource.module_name + end + + def action_run + if ! test_resource + converge_by(generate_description) do + result = set_resource + end + end + end + + def load_current_resource + end + + def whyrun_supported? + true + end + + def define_resource_requirements + requirements.assert(:run) do |a| + a.assertion { supports_dsc_invoke_resource? } + err = ["You must have Powershell version >= 5.0.10018.0 to use dsc_resource."] + a.failure_message Chef::Exceptions::NoProviderAvailable, + err + a.whyrun err + ["Assuming a previous resource installs Powershell 5.0.10018.0 or higher."] + a.block_action! + end + requirements.assert(:run) do |a| + a.assertion { + meta_configuration['RefreshMode'] == 'Disabled' + } + err = ["The LCM must have its RefreshMode set to Disabled. "] + a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.whyrun err + ["Assuming a previous resource sets the RefreshMode."] + a.block_action! + end + end + + protected + + def local_configuration_manager + @local_configuration_manager ||= Chef::Util::DSC::LocalConfigurationManager.new( + node, + nil + ) + end + + def resource_store + Chef::Util::DSC::ResourceStore.instance + end + + def supports_dsc_invoke_resource? + run_context && Chef::Platform.supports_dsc_invoke_resource?(node) + end + + def generate_description + @converge_description + end + + def dsc_resource_name + new_resource.resource.to_s + end + + def module_name + @module_name ||= begin + found = resource_store.find(dsc_resource_name) + + r = case found.length + when 0 + raise Chef::Exceptions::ResourceNotFound, + "Could not find #{dsc_resource_name}. Check to make "\ + "sure that it shows up when running Get-DscResource" + when 1 + if found[0]['Module'].nil? + :none + else + found[0]['Module'] + end + else + raise Chef::Exceptions::MultipleDscResourcesFound, found + end + end + end + + def test_resource + result = invoke_resource(:test) + # We really want this information from the verbose stream, + # however Invoke-DscResource is not correctly writing to that + # stream and instead just dumping to stdout + @converge_description = result.stdout + result.return_value[0]["InDesiredState"] + end + + def set_resource + result = invoke_resource(:set) + result.return_value + end + + def invoke_resource(method, output_format=:object) + properties = translate_type(@new_resource.properties) + switches = "-Method #{method.to_s} -Name #{@new_resource.resource}"\ + " -Property #{properties} -Verbose" + + if module_name != :none + switches += " -Module #{module_name}" + end + + cmdlet = Chef::Util::Powershell::Cmdlet.new( + node, + "Invoke-DscResource #{switches}", + output_format + ) + cmdlet.run! + end + + def meta_configuration + cmdlet = Chef::Util::Powershell::Cmdlet.new(node, "Get-DscLocalConfigurationManager", :object) + result = cmdlet.run! + result.return_value + end + + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 796a0f8fa6..a5f5386de3 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -25,6 +25,7 @@ require 'chef/provider/cron/aix' require 'chef/provider/deploy' require 'chef/provider/directory' require 'chef/provider/dsc_script' +require 'chef/provider/dsc_resource' require 'chef/provider/env' require 'chef/provider/erl_call' require 'chef/provider/execute' diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 91f7f30aa9..b4d37c2d61 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -25,6 +25,7 @@ require 'chef/dsl/include_recipe' require 'chef/dsl/registry_helper' require 'chef/dsl/reboot_pending' require 'chef/dsl/audit' +require 'chef/dsl/powershell' require 'chef/mixin/from_file' @@ -42,6 +43,7 @@ class Chef include Chef::DSL::RegistryHelper include Chef::DSL::RebootPending include Chef::DSL::Audit + include Chef::DSL::Powershell include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/resource/dsc_resource.rb b/lib/chef/resource/dsc_resource.rb new file mode 100644 index 0000000000..912b683434 --- /dev/null +++ b/lib/chef/resource/dsc_resource.rb @@ -0,0 +1,83 @@ +#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Opscode, 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/dsl/powershell'
+
+class Chef
+ class Resource
+ class DscResource < Chef::Resource
+
+ provides :dsc_resource, os: "windows"
+
+ include Chef::DSL::Powershell
+
+ def initialize(name, run_context)
+ super
+ @properties = {}
+ @resource_name = :dsc_resource
+ @resource = nil
+ @allowed_actions.push(:run)
+ @action = :run
+ end
+
+ def resource(value=nil)
+ if value
+ @resource = value
+ else
+ @resource
+ end
+ end
+
+ def module_name(value=nil)
+ if value
+ @module_name = value
+ else
+ @module_name
+ end
+ end
+
+ def property(property_name, value=nil)
+ if not property_name.is_a?(Symbol)
+ raise TypeError, "A property name of type Symbol must be specified, '#{property_name.to_s}' of type #{property_name.class.to_s} was given"
+ end
+
+ if value.nil?
+ value_of(@properties[property_name])
+ else
+ @properties[property_name] = value
+ end
+ end
+
+ def properties
+ @properties.reduce({}) do |memo, (k, v)|
+ memo[k] = value_of(v)
+ memo
+ end
+ end
+
+ private
+
+ def value_of(value)
+ if value.is_a?(DelayedEvaluator)
+ value.call
+ else
+ value
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 680b393741..40b12a7c5f 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -29,6 +29,7 @@ require 'chef/resource/deploy_revision' require 'chef/resource/directory' require 'chef/resource/dpkg_package' require 'chef/resource/dsc_script' +require 'chef/resource/dsc_resource' require 'chef/resource/easy_install_package' require 'chef/resource/env' require 'chef/resource/erl_call' diff --git a/lib/chef/util/dsc/resource_store.rb b/lib/chef/util/dsc/resource_store.rb new file mode 100644 index 0000000000..fdcecc2b3c --- /dev/null +++ b/lib/chef/util/dsc/resource_store.rb @@ -0,0 +1,110 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: Copyright (c) 2015 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/powershell/cmdlet_result' +require 'chef/exceptions' + +class Chef +class Util +class DSC + class ResourceStore + + def self.instance + @@instance ||= ResourceStore.new.tap do |store| + store.send(:populate_cache) + end + end + + def resources + @resources ||= [] + end + + def find(name, module_name=nil) + found = find_resources(name, module_name, resources) + + # We don't have it, query for the resource...it might + # have been added since we last queried + if found.length == 0 + rs = query_resource(name) + add_resources(rs) + found = find_resources(name, module_name, rs) + end + + found + end + + private + + def add_resource(new_r) + count = resources.count do |r| + r['ResourceType'].casecmp(new_r['ResourceType']) == 0 + end + if count == 0 + resources << new_r + end + end + + def add_resources(rs) + rs.each do |r| + add_resource(r) + end + end + + def populate_cache + @resources = query_resources + end + + def find_resources(name, module_name, rs) + found = rs.find_all do |r| + name_matches = r['Name'].casecmp(name) == 0 + if name_matches + module_name == nil || (r['Module'] and r['Module']['Name'].casecmp(module_name) == 0) + else + false + end + end + end + + + # Returns a list of dsc resources + def query_resources + cmdlet = Chef::Util::Powershell::Cmdlet.new(nil, 'get-dscresource', + :object) + result = cmdlet.run + result.return_value + end + + # Returns a list of dsc resources matching the provided name + def query_resource(resource_name) + cmdlet = Chef::Util::Powershell::Cmdlet.new(nil, "get-dscresource #{resource_name}", + :object) + result = cmdlet.run + ret_val = result.return_value + if ret_val.nil? + [] + elsif ret_val.is_a? Array + ret_val + else + [ret_val] + end + end + end +end +end +end diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb index 40edbb13c6..47d63a2b85 100644 --- a/lib/chef/util/powershell/cmdlet.rb +++ b/lib/chef/util/powershell/cmdlet.rb @@ -20,7 +20,9 @@ require 'mixlib/shellout' require 'chef/mixin/windows_architecture_helper' require 'chef/util/powershell/cmdlet_result' -class Chef::Util::Powershell +class Chef +class Util +class Powershell class Cmdlet def initialize(node, cmdlet, output_format=nil, output_format_options={}) @output_format = output_format @@ -46,6 +48,10 @@ class Chef::Util::Powershell attr_reader :output_format def run(switches={}, execution_options={}, *arguments) + streams = { :json => CmdletStream.new('json'), + :verbose => CmdletStream.new('verbose'), + } + arguments_string = arguments.join(' ') switches_string = command_switches_string(switches) @@ -56,21 +62,25 @@ class Chef::Util::Powershell 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 }" + json_command = @json_format ? " | convertto-json -compress -depth #{json_depth} "\ + "> #{streams[:json].path}" : "" + redirections = "4> '#{streams[:verbose].path}'" + command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive "\ + "-command \"trap [Exception] {write-error -exception "\ + "($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} "\ + "#{arguments_string} #{redirections}"\ + "#{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) + CmdletResult.new(status, streams, @output_format) end def run!(switches={}, execution_options={}, *arguments) @@ -131,6 +141,30 @@ class Chef::Util::Powershell command_switches.join(' ') end + + class CmdletStream + def initialize(name) + @filename = Dir::Tmpname.create(name) {} + ObjectSpace.define_finalizer(self, self.class.destroy(@filename)) + end + + def path + @filename + end + + def read + if File.exist? @filename + File.open(@filename, 'rb:bom|UTF-16LE') do |f| + f.read.encode('UTF-8') + end + end + end + + def self.destroy(name) + proc { File.delete(name) if File.exists? name } + end + end end end - +end +end diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb index 246701a7bc..f1fdd968b1 100644 --- a/lib/chef/util/powershell/cmdlet_result.rb +++ b/lib/chef/util/powershell/cmdlet_result.rb @@ -18,22 +18,35 @@ require 'chef/json_compat' -class Chef::Util::Powershell +class Chef +class Util +class Powershell class CmdletResult attr_reader :output_format - def initialize(status, output_format) + def initialize(status, streams, output_format) @status = status @output_format = output_format + @streams = streams end + def stdout + @status.stdout + end + def stderr @status.stderr end + def stream(name) + @streams[name].read + end + def return_value if output_format == :object - Chef::JSONCompat.parse(@status.stdout) + Chef::JSONCompat.parse(stream(:json)) + elsif output_format == :json + stream(:json) else @status.stdout end @@ -44,3 +57,5 @@ class Chef::Util::Powershell end end end +end +end diff --git a/lib/chef/util/powershell/ps_credential.rb b/lib/chef/util/powershell/ps_credential.rb new file mode 100644 index 0000000000..01f8c27b6c --- /dev/null +++ b/lib/chef/util/powershell/ps_credential.rb @@ -0,0 +1,38 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: Copyright (c) 2015 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/win32/crypto' if Chef::Platform.windows? + +class Chef::Util::Powershell + class PSCredential + def initialize(username, password) + @username = username + @password = password + end + + def to_psobject + "New-Object System.Management.Automation.PSCredential('#{@username}',('#{encrypt(@password)}' | ConvertTo-SecureString))" + end + + private + + def encrypt(str) + Chef::ReservedNames::Win32::Crypto.encrypt(str) + end + end +end diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index 8b81947c9b..efa632f454 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -184,6 +184,8 @@ class Chef host.typedef :pointer, :PSTR # Pointer to a null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts. host.typedef :pointer, :PTBYTE # Pointer to a TBYTE. host.typedef :pointer, :PTCHAR # Pointer to a TCHAR. + host.typedef :pointer, :PCRYPTPROTECT_PROMPTSTRUCT # Pointer to a CRYPTOPROTECT_PROMPTSTRUCT. + host.typedef :pointer, :PDATA_BLOB # Pointer to a DATA_BLOB. host.typedef :pointer, :PTSTR # A PWSTR if UNICODE is defined, a PSTR otherwise. host.typedef :pointer, :PUCHAR # Pointer to a UCHAR. host.typedef :pointer, :PUHALF_PTR # Pointer to a UHALF_PTR. diff --git a/lib/chef/win32/api/crypto.rb b/lib/chef/win32/api/crypto.rb new file mode 100644 index 0000000000..1837a57557 --- /dev/null +++ b/lib/chef/win32/api/crypto.rb @@ -0,0 +1,63 @@ +#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright 2015 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/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Crypto
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'Crypt32'
+
+ CRYPTPROTECT_UI_FORBIDDEN = 0x1
+ CRYPTPROTECT_LOCAL_MACHINE = 0x4
+ CRYPTPROTECT_AUDIT = 0x10
+
+ class CRYPT_INTEGER_BLOB < FFI::Struct
+ layout :cbData, :DWORD, # Count, in bytes, of data
+ :pbData, :pointer # Pointer to data buffer
+ def initialize(str=nil)
+ super(nil)
+ if str
+ self[:pbData] = FFI::MemoryPointer.from_string(str)
+ self[:cbData] = str.bytesize
+ end
+ end
+
+ end
+
+ safe_attach_function :CryptProtectData, [
+ :PDATA_BLOB,
+ :LPCWSTR,
+ :PDATA_BLOB,
+ :pointer,
+ :PCRYPTPROTECT_PROMPTSTRUCT,
+ :DWORD,
+ :PDATA_BLOB
+ ], :BOOL
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/crypto.rb b/lib/chef/win32/crypto.rb new file mode 100644 index 0000000000..79cf51b002 --- /dev/null +++ b/lib/chef/win32/crypto.rb @@ -0,0 +1,49 @@ +#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright 2015 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/win32/error'
+require 'chef/win32/api/memory'
+require 'chef/win32/api/crypto'
+require 'digest'
+
+class Chef
+ module ReservedNames::Win32
+ class Crypto
+ include Chef::ReservedNames::Win32::API::Crypto
+ extend Chef::ReservedNames::Win32::API::Crypto
+
+ def self.encrypt(str, &block)
+ data_blob = CRYPT_INTEGER_BLOB.new
+ unless CryptProtectData(CRYPT_INTEGER_BLOB.new(str.to_wstring), nil, nil, nil, nil, 0, data_blob)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ bytes = data_blob[:pbData].get_bytes(0, data_blob[:cbData])
+ if block
+ block.call(bytes)
+ else
+ Digest.hexencode(bytes)
+ end
+ ensure
+ unless data_blob[:pbData].null?
+ Chef::ReservedNames::Win32::Memory.local_free(data_blob[:pbData])
+ end
+ end
+
+ end
+ end
+end
diff --git a/spec/functional/resource/dsc_resource_spec.rb b/spec/functional/resource/dsc_resource_spec.rb new file mode 100644 index 0000000000..6f453eeb9f --- /dev/null +++ b/spec/functional/resource/dsc_resource_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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::DscResource, :windows_powershell_dsc_only do + before(:all) do + @ohai = Ohai::System.new + @ohai.all_plugins(['platform', 'os', 'languages/powershell']) + end + + let(:event_dispatch) { Chef::EventDispatch::Dispatcher.new } + + let(:node) { + Chef::Node.new.tap do |n| + n.consume_external_attrs(@ohai.data, {}) + end + } + + let(:run_context) { Chef::RunContext.new(node, {}, event_dispatch) } + + let(:new_resource) { + Chef::Resource::DscResource.new("dsc_resource_test", run_context) + } + + context 'when Powershell does not support Invoke-DscResource' + context 'when Powershell supports Invoke-DscResource' do + before do + if !Chef::Platform.supports_dsc_invoke_resource?(node) + skip 'Requires Powershell >= 5.0.10018.0' + end + end + context 'with an invalid dsc resource' do + it 'raises an exception if the resource is not found' do + new_resource.resource 'thisdoesnotexist' + expect { new_resource.run_action(:run) }.to raise_error( + Chef::Exceptions::ResourceNotFound) + end + end + + context 'with a valid dsc resource' do + let(:tmp_file_name) { Dir::Tmpname.create('tmpfile') {} } + let(:test_text) { "'\"!@#$%^&*)(}{][\u2713~n"} + + before do + new_resource.resource :File + new_resource.property :Contents, test_text + new_resource.property :DestinationPath, tmp_file_name + end + + after do + File.delete(tmp_file_name) if File.exists? tmp_file_name + end + + it 'converges the resource if it is not converged' do + new_resource.run_action(:run) + contents = File.open(tmp_file_name, 'rb:bom|UTF-16LE') do |f| + f.read.encode('UTF-8') + end + expect(contents).to eq(test_text) + expect(new_resource).to be_updated + end + + it 'does not converge the resource if it is already converged' do + new_resource.run_action(:run) + expect(new_resource).to be_updated + reresource = + Chef::Resource::DscResource.new("dsc_resource_retest", run_context) + reresource.resource :File + reresource.property :Contents, test_text + reresource.property :DestinationPath, tmp_file_name + reresource.run_action(:run) + expect(reresource).not_to be_updated + end + end + + end +end diff --git a/spec/functional/win32/crypto_spec.rb b/spec/functional/win32/crypto_spec.rb new file mode 100644 index 0000000000..1492995886 --- /dev/null +++ b/spec/functional/win32/crypto_spec.rb @@ -0,0 +1,57 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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' +if Chef::Platform.windows? + require 'chef/win32/crypto' +end + +describe 'Chef::ReservedNames::Win32::Crypto', :windows_only do + describe '#encrypt' do + before(:all) do + ohai_reader = Ohai::System.new + ohai_reader.all_plugins("platform") + + new_node = Chef::Node.new + new_node.consume_external_attrs(ohai_reader.data,{}) + + events = Chef::EventDispatch::Dispatcher.new + + @run_context = Chef::RunContext.new(new_node, {}, events) + end + + let (:plaintext) { 'p@assword' } + + it 'can be decrypted by powershell' do + encrypted = Chef::ReservedNames::Win32::Crypto.encrypt(plaintext) + resource = Chef::Resource::WindowsScript::PowershellScript.new("Powershell resource functional test", @run_context) + resource.code <<-EOF +$encrypted = '#{encrypted}' | ConvertTo-SecureString +$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($encrypted) +$plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) +if ($plaintext -ne '#{plaintext}') { + Write-Error 'Got: ' $plaintext + exit 1 +} +exit 0 + EOF + resource.returns(0) + resource.run_action(:run) + end + end +end diff --git a/spec/unit/mixin/powershell_type_coercions_spec.rb b/spec/unit/mixin/powershell_type_coercions_spec.rb new file mode 100644 index 0000000000..988c3926c1 --- /dev/null +++ b/spec/unit/mixin/powershell_type_coercions_spec.rb @@ -0,0 +1,72 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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/powershell_type_coercions' +require 'base64' + +class Chef::PSTypeTester + include Chef::Mixin::PowershellTypeCoercions +end + +describe Chef::Mixin::PowershellTypeCoercions do + let (:test_class) { Chef::PSTypeTester.new } + + describe '#translate_type' do + it 'should single quote a string' do + expect(test_class.translate_type('foo')).to eq("'foo'") + end + + ["'", '"', '#', '`'].each do |c| + it "should base64 encode a string that contains #{c}" do + expect(test_class.translate_type("#{c}")).to match(Base64.strict_encode64(c)) + end + end + + it 'should not quote an integer' do + expect(test_class.translate_type(123)).to eq('123') + end + + it 'should not quote a floating point number' do + expect(test_class.translate_type(123.4)).to eq('123.4') + end + + it 'should return $false when an instance of FalseClass is provided' do + expect(test_class.translate_type(false)).to eq('$false') + end + + it 'should return $true when an instance of TrueClass is provided' do + expect(test_class.translate_type(true)).to eq('$true') + end + + it 'should translate all members of a hash and wrap them in @{} separated by ;' do + expect(test_class.translate_type({"a" => 1, "b" => 1.2, "c" => false, "d" => true + })).to eq("@{a=1;b=1.2;c=$false;d=$true}") + end + + it 'should translat all members of an array and them by a ,' do + expect(test_class.translate_type([true, false])).to eq('@($true,$false)') + end + + it 'should fall back :to_psobject if we have not defined at explicit rule' do + ps_obj = double("PSObject") + expect(ps_obj).to receive(:to_psobject).and_return('$true') + expect(test_class.translate_type(ps_obj)).to eq('($true)') + end + end +end diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb index 7aafc287ea..1dbd07a021 100644 --- a/spec/unit/platform/query_helpers_spec.rb +++ b/spec/unit/platform/query_helpers_spec.rb @@ -53,3 +53,25 @@ describe 'Chef::Platform#supports_dsc?' do end end end + +describe 'Chef::Platform#supports_dsc_invoke_resource?' do + it 'returns false if powershell is not present' do + node = Chef::Node.new + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey + end + + ['1.0', '2.0', '3.0', '4.0', '5.0.10017.9'].each do |version| + it "returns false for Powershell #{version}" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = version + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey + end + end + + it "returns true for Powershell 5.0.10018.0" do + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = "5.0.10018.0" + expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_truthy + end +end + diff --git a/spec/unit/provider/dsc_resource_spec.rb b/spec/unit/provider/dsc_resource_spec.rb new file mode 100644 index 0000000000..0a6c22bdcf --- /dev/null +++ b/spec/unit/provider/dsc_resource_spec.rb @@ -0,0 +1,84 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# 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 'spec_helper' + +describe Chef::Provider::DscResource do + let (:events) { Chef::EventDispatch::Dispatcher.new } + let (:run_context) { Chef::RunContext.new(node, {}, events) } + let (:resource) { Chef::Resource::DscResource.new("dscresource", run_context) } + let (:provider) do + Chef::Provider::DscResource.new(resource, run_context) + end + + context 'when Powershell does not support Invoke-DscResource' do + let (:node) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '4.0' + node + } + + it 'raises a NoProviderAvailable exception' do + expect(provider).not_to receive(:meta_configuration) + expect{provider.run_action(:run)}.to raise_error( + Chef::Exceptions::NoProviderAvailable, /5\.0\.10018\.0/) + end + end + + context 'when Powershell supports Invoke-DscResource' do + let (:node) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '5.0.10018.0' + node + } + + context 'when RefreshMode is not set to Disabled' do + let (:meta_configuration) { {'RefreshMode' => 'AnythingElse'}} + + it 'raises an exception' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect { provider.run_action(:run) }.to raise_error( + Chef::Exceptions::NoProviderAvailable, /Disabled/) + end + end + + context 'when RefreshMode is set to Disabled' do + let (:meta_configuration) { {'RefreshMode' => 'Disabled'}} + + it 'does not update the resource if it is up to date' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect(provider).to receive(:test_resource).and_return(true) + provider.run_action(:run) + expect(resource).not_to be_updated + end + + it 'converges the resource if it is not up to date' do + expect(provider).to receive(:meta_configuration).and_return( + meta_configuration) + expect(provider).to receive(:test_resource).and_return(false) + expect(provider).to receive(:set_resource) + provider.run_action(:run) + expect(resource).to be_updated + end + end + end +end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index 8d0b1bcfd2..e1604483f3 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -593,5 +593,9 @@ describe Chef::Recipe do expect(recipe.singleton_class.included_modules).to include(Chef::DSL::Audit) expect(recipe.respond_to?(:control_group)).to be true end + + it "should respond to :ps_credential from Chef::DSL::Powershell" do + expect(recipe.respond_to?(:ps_credential)).to be true + end end end diff --git a/spec/unit/resource/dsc_resource_spec.rb b/spec/unit/resource/dsc_resource_spec.rb new file mode 100644 index 0000000000..ae15f56eaf --- /dev/null +++ b/spec/unit/resource/dsc_resource_spec.rb @@ -0,0 +1,85 @@ +# +# 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::DscResource do + let(:dsc_test_resource_name) { 'DSCTest' } + let(:dsc_test_property_name) { :DSCTestProperty } + let(:dsc_test_property_value) { 'DSCTestValue' } + + context 'when Powershell supports Dsc' do + let(:dsc_test_run_context) { + node = Chef::Node.new + node.automatic[:languages][:powershell][:version] = '5.0.10018.0' + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + } + let(:dsc_test_resource) { + Chef::Resource::DscResource.new(dsc_test_resource_name, dsc_test_run_context) + } + + 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 resource attribute to be set" do + dsc_test_resource.resource(dsc_test_resource_name) + expect(dsc_test_resource.resource).to eq(dsc_test_resource_name) + end + + it "allows the module_name attribute to be set" do + dsc_test_resource.module_name(dsc_test_resource_name) + expect(dsc_test_resource.module_name).to eq(dsc_test_resource_name) + end + + context "when setting a dsc property" do + it "allows setting a dsc property with a property name of type Symbol" do + dsc_test_resource.property(dsc_test_property_name, dsc_test_property_value) + expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value) + expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value) + end + + it "raises a TypeError if property_name is not a symbol" do + expect{ + dsc_test_resource.property('Foo', dsc_test_property_value) + }.to raise_error(TypeError) + end + + context "when using DelayedEvaluators" do + it "allows setting a dsc property with a property name of type Symbol" do + dsc_test_resource.property(dsc_test_property_name, Chef::DelayedEvaluator.new { + dsc_test_property_value + }) + expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value) + expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value) + end + end + end + + context 'Powershell DSL methods' do + it "responds to :ps_credential" do + expect(dsc_test_resource.respond_to?(:ps_credential)).to be true + end + end + end +end diff --git a/spec/unit/util/dsc/resource_store.rb b/spec/unit/util/dsc/resource_store.rb new file mode 100644 index 0000000000..a89e73fcaa --- /dev/null +++ b/spec/unit/util/dsc/resource_store.rb @@ -0,0 +1,76 @@ +# +# Author:: Jay Mundrawala <jdm@chef.io> +# Copyright:: Copyright (c) 2015 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_store' + +describe Chef::Util::DSC::ResourceStore do + let(:resource_store) { Chef::Util::DSC::ResourceStore.new } + let(:resource_a) { { + 'ResourceType' => 'AFoo', + 'Name' => 'Foo', + 'Module' => {'Name' => 'ModuleA'} + } + } + + let(:resource_b) { { + 'ResourceType' => 'BFoo', + 'Name' => 'Foo', + 'Module' => {'Name' => 'ModuleB'} + } + } + + context 'when resources are not cached' do + context 'when calling #resources' do + it 'returns an empty array' do + expect(resource_store.resources).to eql([]) + end + end + + context 'when calling #find' do + it 'returns an empty list if it cannot find any matching resources' do + expect(resource_store).to receive(:query_resource).and_return([]) + expect(resource_store.find('foo')).to eql([]) + end + + it 'returns the resource if it is found (comparisons are case insensitive)' do + expect(resource_store).to receive(:query_resource).and_return([resource_a]) + expect(resource_store.find('foo')).to eql([resource_a]) + end + + it 'returns multiple resoures if they are found' do + expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_b]) + expect(resource_store.find('foo')).to include(resource_a, resource_b) + end + + it 'deduplicates resources by ResourceName' do + expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_a]) + resource_store.find('foo') + expect(resource_store.resources).to eq([resource_a]) + end + end + end + + context 'when resources are cached' do + it 'recalls resources from the cache if present' do + expect(resource_store).not_to receive(:query_resource) + expect(resource_store).to receive(:resources).and_return([resource_a]) + resource_store.find('foo') + end + end +end diff --git a/spec/unit/util/powershell/ps_credential_spec.rb b/spec/unit/util/powershell/ps_credential_spec.rb new file mode 100644 index 0000000000..bac58b02e5 --- /dev/null +++ b/spec/unit/util/powershell/ps_credential_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Jay Mundrawala <jdm@chef.io> +# Copyright:: Copyright (c) 2015 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/ps_credential' + +describe Chef::Util::Powershell::PSCredential do + let (:username) { 'foo' } + let (:password) { 'password' } + + context 'when username and password are provided' do + let(:ps_credential) { Chef::Util::Powershell::PSCredential.new(username, password)} + context 'when calling to_psobject' do + it 'should create the script to create a PSCredential when calling' do + allow(ps_credential).to receive(:encrypt).with(password).and_return('encrypted') + expect(ps_credential.to_psobject).to eq( + "New-Object System.Management.Automation.PSCredential("\ + "'#{username}',('encrypted' | ConvertTo-SecureString))") + end + end + end +end |