diff --git a/lib/puppet/provider/base_dsc_lite/invoke_generic_dsc_resource.ps1.erb b/lib/puppet/provider/base_dsc_lite/invoke_generic_dsc_resource.ps1.erb new file mode 100644 index 00000000..371e50a7 --- /dev/null +++ b/lib/puppet/provider/base_dsc_lite/invoke_generic_dsc_resource.ps1.erb @@ -0,0 +1,52 @@ +$script:ErrorActionPreference = 'Stop' +$script:WarningPreference = 'SilentlyContinue' + +function new-pscredential +{ + [CmdletBinding()] + param ( + [parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] + [string]$user, + [parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] + [string]$password + ) + + $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force + $credentials = New-Object System.Management.Automation.PSCredential ($user, $secpasswd) + return $credentials +} + +$response = @{ + indesiredstate = $false + rebootrequired = $false + errormessage = '' +} + +$invokeParams = @{ +Name = '<%= resource.parameters[:dsc_resource_name].value %>' +ModuleName = '<%= resource.parameters[:dsc_resource_module_name].value %>' +Method = '<%= dsc_invoke_method %>' +Property = <% provider.dsc_property_param.each do |p| -%> +<%= format_dsc_lite(p.value) %> +<% end -%> +} + +try{ + $result = Invoke-DscResource @invokeParams +}catch{ + $response.errormessage = $_.Exception.Message + return ($response | ConvertTo-Json -Compress) +} + +# keep the switch for when Test passes back changed properties +switch ($invokeParams.Method) { + 'Test' { + $response.indesiredstate = $result.InDesiredState + return ($response | ConvertTo-Json -Compress) + } + 'Set' { + $response.indesiredstate = $true + $response.rebootrequired = $result.RebootRequired + return ($response | ConvertTo-Json -Compress) + } +} diff --git a/lib/puppet/provider/base_dsc_lite/powershell.rb b/lib/puppet/provider/base_dsc_lite/powershell.rb index d5513a31..9d6edccf 100644 --- a/lib/puppet/provider/base_dsc_lite/powershell.rb +++ b/lib/puppet/provider/base_dsc_lite/powershell.rb @@ -3,6 +3,7 @@ if Puppet::Util::Platform.windows? require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_manager' require_relative '../../../puppet_x/puppetlabs/dsc_lite/compatible_powershell_version' + require_relative '../../../puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter' end Puppet::Type.type(:base_dsc_lite).provide(:powershell) do @@ -60,6 +61,12 @@ def dsc_parameters p.name.to_s =~ /dsc_/ end end + + def dsc_property_param + resource.parameters_with_value.select{ |pr| pr.name == :dsc_resource_properties }.each do |p| + p.name.to_s =~ /dsc_/ + end + end def self.template_path File.expand_path(Pathname.new(__FILE__).dirname) @@ -183,6 +190,10 @@ def self.format_dsc_value(dsc_value) fail "unsupported type #{dsc_value.class} of value '#{dsc_value}'" end end + + def self.format_dsc_lite(dsc_value) + PuppetX::PuppetLabs::DscLite::PowerShellHashFormatter.format(dsc_value) + end def self.escape_quotes(text) text.gsub("'", "''") @@ -195,7 +206,10 @@ def ps_script_content(mode) def self.ps_script_content(mode, resource, provider) dsc_invoke_method = mode @param_hash = resource - template = ERB.new(File.new(template_path + "/invoke_dsc_resource.ps1.erb").read, nil, '-') + template_name = resource.type == :dsc ? + '/invoke_generic_dsc_resource.ps1.erb' : + '/invoke_dsc_resource.ps1.erb' + template = ERB.new(File.new(template_path + template_name).read, nil, '-') template.result(binding) end end diff --git a/lib/puppet/type/dsc.rb b/lib/puppet/type/dsc.rb new file mode 100644 index 00000000..bc4e3bdd --- /dev/null +++ b/lib/puppet/type/dsc.rb @@ -0,0 +1,63 @@ +require 'pathname' + +Puppet::Type.newtype(:dsc) do + require Pathname.new(__FILE__).dirname + '../../' + 'puppet/type/base_dsc_lite' + require Pathname.new(__FILE__).dirname + '../../puppet_x/puppetlabs/dsc_lite/dsc_type_helpers' + + ensurable do + newvalue(:exists?) { provider.exists? } + newvalue(:present) { provider.create } + newvalue(:absent) { provider.destroy } + defaultto { :present } + end + + newparam(:name, :namevar => true) do + desc "Name of the declaration" + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty #{self.name.to_s} must be specified." + end + fail("#{value} is not a valid #{self.name.to_s}") unless value =~ /^[a-zA-Z0-9\.\-\_\'\s]+$/ + end + end + + newparam(:dsc_resource_name) do + desc "DSC Resource Name" + isrequired + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty #{self.name.to_s} must be specified." + end + fail "#{self.name.to_s} should be a String" unless value.is_a? ::String + end + end + + newparam(:dsc_resource_module_name) do + desc "DSC Resource Module Name" + isrequired + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty #{self.name.to_s} must be specified." + end + fail "#{self.name.to_s} should be a String" unless value.is_a? ::String + end + end + + newparam(:dsc_resource_properties, :array_matching => :all) do + desc "DSC Resource Properties" + isrequired + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty #{self.name.to_s} must be specified." + end + fail "#{self.name.to_s} should be a Hash" unless value.is_a? ::Hash + end + end +end + +Puppet::Type.type(:dsc).provide :powershell, :parent => Puppet::Type.type(:base_dsc_lite).provider(:powershell) do + confine :true => (Gem::Version.new(Facter.value(:powershell_version)) >= Gem::Version.new('5.0.10586.117')) + defaultfor :operatingsystem => :windows + + mk_resource_methods +end diff --git a/lib/puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter.rb b/lib/puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter.rb new file mode 100644 index 00000000..9bca4e1d --- /dev/null +++ b/lib/puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter.rb @@ -0,0 +1,59 @@ +module PuppetX + module PuppetLabs + module DscLite + class PowerShellHashFormatter + + def self.format(dsc_value) + case + when dsc_value.class.name == 'String' + self.format_string(dsc_value) + when dsc_value.class.ancestors.include?(Numeric) + self.format_number(dsc_value) + when [:true, :false].include?(dsc_value) + self.format_boolean(dsc_value) + when ['trueclass','falseclass'].include?(dsc_value.class.name.downcase) + "$#{dsc_value.to_s}" + when dsc_value.class.name == 'Array' + self.format_array(dsc_value) + when dsc_value.class.name == 'Hash' + self.format_hash(dsc_value) + else + fail "unsupported type #{dsc_value.class} of value '#{dsc_value}'" + end + + end + + def self.format_string(value) + "'#{escape_quotes(value)}'" + end + + def self.format_number(value) + "#{value}" + end + + def self.format_boolean(value) + "$#{value.to_s}" + end + + def self.format_array(value) + output = [] + output << "@(" + value.collect do |m| + output << format(m) + end + output.join(', ') + output << ")" + end + + def self.format_hash(value) + "@{\n" + value.collect{|k, v| format(k) + ' = ' + format(v)}.join(";\n") + "\n" + "}" + end + + def self.escape_quotes(text) + text.gsub("'", "''") + end + + end + end + end +end diff --git a/spec/integration/puppet_x/puppetlabs/powershell_hash_formatter_spec.rb b/spec/integration/puppet_x/puppetlabs/powershell_hash_formatter_spec.rb new file mode 100644 index 00000000..6ef5c87b --- /dev/null +++ b/spec/integration/puppet_x/puppetlabs/powershell_hash_formatter_spec.rb @@ -0,0 +1,56 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/type' +require 'puppet_x/puppetlabs/dsc_lite/powershell_hash_formatter' + +describe PuppetX::PuppetLabs::DscLite::PowerShellHashFormatter do + before(:each) do + @formatter = PuppetX::PuppetLabs::DscLite::PowerShellHashFormatter + end + + describe "formatting ruby hash to powershell hash string" do + + describe "when given correct hash" do + + it "should output correct syntax with simple example" do + foo = <<-HERE +@{ +'ensure' = 'present'; +'name' = 'Web-WebServer' +} +HERE + result = @formatter.format({ + "ensure" => "present", + "name" => "Web-WebServer", + }) + expect(result).to eq foo.strip + end + + it "should output correct syntax with CimInstance" do + foo = <<-HERE +@{ +'ensure' = 'Present'; +'bindinginfo' = @{ +'dsc_type' = 'MSFT_xWebBindingInformation[]'; +'dsc_properties' = @{ +'protocol' = 'HTTP'; +'port' = '80' +} +} +} +HERE + result = @formatter.format({ + "ensure" => "Present", + "bindinginfo" => { + "dsc_type" => "MSFT_xWebBindingInformation[]", + "dsc_properties" => { + "protocol" => "HTTP", + "port" => "80" + } + } + }) + expect(result).to eq foo.strip + end + end + end +end diff --git a/spec/unit/puppet/type/dsc_spec.rb b/spec/unit/puppet/type/dsc_spec.rb new file mode 100644 index 00000000..06402ce0 --- /dev/null +++ b/spec/unit/puppet/type/dsc_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' +require 'puppet/type' +require 'puppet/type/dsc' + +describe Puppet::Type.type(:dsc) do + let(:resource) { described_class.new(:name => "dsc") } + subject { resource } + + it { is_expected.to be_a_kind_of Puppet::Type::Dsc } + + describe "parameter :name" do + subject { resource.parameters[:name] } + + it { is_expected.to be_isnamevar } + + it "should not allow nil" do + expect { + resource[:name] = nil + }.to raise_error(Puppet::Error, /Got nil value for name/) + end + + it "should not allow empty" do + expect { + resource[:name] = '' + }.to raise_error(Puppet::ResourceError, /A non-empty name must/) + end + + [ 'value', 'value with spaces', 'UPPER CASE', '0123456789_-', 'With.Period' ].each do |value| + it "should accept '#{value}'" do + expect { resource[:name] = value }.not_to raise_error + end + end + + [ '*', '()', '[]', '!@' ].each do |value| + it "should reject '#{value}'" do + expect { resource[:name] = value }.to raise_error(Puppet::ResourceError, /is not a valid name/) + end + end + end + + describe "parameter :dsc_resource_name" do + subject { resource.parameters[:dsc_resource_name] } + + it "should not allow nil" do + expect { + resource[:name] = nil + }.to raise_error(Puppet::Error, /Got nil value for name/) + end + + it "should not allow empty" do + expect { + resource[:name] = '' + }.to raise_error(Puppet::ResourceError, /A non-empty name must/) + end + + [ 'value', 'value with spaces', 'UPPER CASE', '0123456789_-', 'With.Period' ].each do |value| + it "should accept '#{value}'" do + expect { resource[:name] = value }.not_to raise_error + end + end + + [ '*', '()', '[]', '!@' ].each do |value| + it "should reject '#{value}'" do + expect { resource[:name] = value }.to raise_error(Puppet::ResourceError, /is not a valid name/) + end + end + end + + describe "parameter :dsc_resource_module" do + subject { resource.parameters[:dsc_resource_module] } + + it "should not allow nil" do + expect { + resource[:name] = nil + }.to raise_error(Puppet::Error, /Got nil value for name/) + end + + it "should not allow empty" do + expect { + resource[:name] = '' + }.to raise_error(Puppet::ResourceError, /A non-empty name must/) + end + + [ 'value', 'value with spaces', 'UPPER CASE', '0123456789_-', 'With.Period' ].each do |value| + it "should accept '#{value}'" do + expect { resource[:name] = value }.not_to raise_error + end + end + + [ '*', '()', '[]', '!@' ].each do |value| + it "should reject '#{value}'" do + expect { resource[:name] = value }.to raise_error(Puppet::ResourceError, /is not a valid name/) + end + end + end + + describe "parameter :dsc_resource_properties" do + subject { resource.parameters[:dsc_resource_properties] } + + it "should not allow nil" do + expect { + resource[:dsc_resource_properties] = nil + }.to raise_error(Puppet::Error, /Got nil value for dsc_resource_properties/) + end + + it "should not allow empty" do + expect { + resource[:dsc_resource_properties] = '' + }.to raise_error(Puppet::ResourceError, /A non-empty dsc_resource_properties must be specified/) + end + + it "requires a hash or array of hashes" do + expect { + resource[:dsc_resource_properties] = "hi" + }.to raise_error(Puppet::Error, /dsc_resource_properties should be a Hash/) + expect { + resource[:dsc_resource_properties] = ["hi"] + }.to raise_error(Puppet::Error, /dsc_resource_properties should be a Hash/) + end + end +end