ruby-pwsh 0.2.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.pmtignore +2 -0
- data/.rubocop.yml +12 -2
- data/.travis.yml +1 -0
- data/CHANGELOG.md +46 -2
- data/Gemfile +4 -9
- data/README.md +50 -0
- data/Rakefile +1 -3
- data/appveyor.yml +38 -0
- data/lib/puppet/feature/pwshlib.rb +5 -0
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +670 -0
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_functions.ps1 +120 -0
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_postscript.ps1 +23 -0
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_preamble.ps1 +8 -0
- data/lib/pwsh.rb +4 -3
- data/lib/pwsh/util.rb +48 -26
- data/lib/pwsh/version.rb +1 -1
- data/metadata.json +3 -3
- data/pwshlib.md +2 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37a20d5c79da4ea0b065cb85afc8e99933128576ef73c759a9f8113b7f738eed
|
4
|
+
data.tar.gz: 9da59c07a8037eee9bb03591726ed83e4909b95009579b362fb49a4d2e4ac171
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 760bbbdd326042ff79e4e0f28bbfe48447ddc17fb6968f569ce1c5d75848a83e3b3042022250fb309d98124d97580c9ad1a7a9a8a629aadbbdc57b7de7718f60
|
7
|
+
data.tar.gz: 1c57dec6a7bd3db1e4b32adfd0e61abb996157317fa2f74f20110f246a3daf8f6302aa0569aef07e863299e9a8ba126631b89f9df3055f5b013a8c720ff308e0
|
data/.gitignore
CHANGED
data/.pmtignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
Gemspec/RequiredRubyVersion:
|
2
|
+
Enabled: false
|
1
3
|
Layout/EndOfLine:
|
2
4
|
Description: Don't enforce CRLF on Windows.
|
3
5
|
Enabled: false
|
@@ -19,7 +21,7 @@ Metrics/AbcSize:
|
|
19
21
|
|
20
22
|
# requires 2.3's squiggly HEREDOC support, which we can't use, yet
|
21
23
|
# see http://www.virtuouscode.com/2016/01/06/about-the-ruby-squiggly-heredoc-syntax/
|
22
|
-
Layout/
|
24
|
+
Layout/HeredocIndentation:
|
23
25
|
Enabled: false
|
24
26
|
# Need to ignore rubocop complaining about the name of the library.
|
25
27
|
Naming/FileName:
|
@@ -28,4 +30,12 @@ Naming/FileName:
|
|
28
30
|
Style/RescueStandardError:
|
29
31
|
Enabled: false
|
30
32
|
Style/ExpandPathArguments:
|
31
|
-
Enabled: false
|
33
|
+
Enabled: false
|
34
|
+
Style/Documentation:
|
35
|
+
Enabled: false
|
36
|
+
Style/ClassAndModuleChildren:
|
37
|
+
Exclude:
|
38
|
+
- lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
|
39
|
+
Style/ClassVars:
|
40
|
+
Exclude:
|
41
|
+
- lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,51 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
-
## [0.
|
5
|
+
## [0.5.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.1) (2020-09-25)
|
6
|
+
|
7
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.5.0...0.5.1)
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
- \(MAINT\) Ensure dsc provider finds dsc resources during agent run [\#45](https://github.com/puppetlabs/ruby-pwsh/pull/45) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
12
|
+
|
13
|
+
## [0.5.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.0) (2020-08-20)
|
14
|
+
|
15
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.1...0.5.0)
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
20
|
+
|
21
|
+
## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
|
22
|
+
|
23
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.0...0.4.1)
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
|
27
|
+
- Ensure ruby versions older than 2.3 function correctly [\#30](https://github.com/puppetlabs/ruby-pwsh/pull/30) ([binford2k](https://github.com/binford2k))
|
28
|
+
|
29
|
+
## [0.4.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.0) (2020-01-14)
|
30
|
+
|
31
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.3.0...0.4.0)
|
32
|
+
|
33
|
+
### Added
|
34
|
+
|
35
|
+
- \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
|
36
|
+
|
37
|
+
## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
|
38
|
+
|
39
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.2.0...0.3.0)
|
40
|
+
|
41
|
+
### Added
|
42
|
+
|
43
|
+
- \(FEAT\) Add method for symbolizing hash keys [\#16](https://github.com/puppetlabs/ruby-pwsh/pull/16) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
44
|
+
|
45
|
+
### Fixed
|
46
|
+
|
47
|
+
- \(FEAT\) Ensure hash key casing methods work on arrays [\#15](https://github.com/puppetlabs/ruby-pwsh/pull/15) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
48
|
+
|
49
|
+
## [0.2.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.2.0) (2019-11-26)
|
6
50
|
|
7
51
|
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.1.0...0.2.0)
|
8
52
|
|
@@ -13,4 +57,4 @@ All notable changes to this project will be documented in this file.The format i
|
|
13
57
|
|
14
58
|
|
15
59
|
|
16
|
-
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/
|
60
|
+
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
data/Gemfile
CHANGED
@@ -11,31 +11,26 @@ group :test do
|
|
11
11
|
gem 'rspec', '~> 3.0'
|
12
12
|
gem 'rspec-collection_matchers', '~> 1.0'
|
13
13
|
gem 'rspec-its', '~> 1.0'
|
14
|
-
gem 'rubocop'
|
14
|
+
gem 'rubocop', '>= 0.77'
|
15
15
|
gem 'rubocop-rspec'
|
16
16
|
gem 'simplecov'
|
17
17
|
end
|
18
18
|
|
19
19
|
group :development do
|
20
|
-
|
21
|
-
gem 'github_changelog_generator', git: 'https://github.com/skywinder/github-changelog-generator.git', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018'
|
20
|
+
gem 'github_changelog_generator', '~> 1.15' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.3.0')
|
22
21
|
gem 'yard'
|
23
22
|
end
|
24
23
|
|
25
24
|
group :puppet do
|
26
25
|
gem 'pdk', '~> 1.0'
|
26
|
+
gem 'puppet'
|
27
27
|
end
|
28
28
|
|
29
29
|
group :pry do
|
30
30
|
gem 'fuubar'
|
31
31
|
|
32
|
-
if RUBY_VERSION
|
33
|
-
gem 'debugger'
|
34
|
-
elsif RUBY_VERSION =~ /^2\.[01]/
|
35
|
-
gem 'byebug', '~> 9.0.0'
|
32
|
+
if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.4.0')
|
36
33
|
gem 'pry-byebug'
|
37
|
-
elsif RUBY_VERSION =~ /^2\.[23456789]/
|
38
|
-
gem 'pry-byebug' # rubocop:disable Bundler/DuplicatedGem
|
39
34
|
else
|
40
35
|
gem 'pry-debugger'
|
41
36
|
end
|
data/README.md
CHANGED
@@ -60,10 +60,60 @@ ps_version = posh.execute('[String]$PSVersionTable.PSVersion')[:stdout].strip
|
|
60
60
|
pp("The PowerShell version of the currently running Manager is #{ps_version}")
|
61
61
|
```
|
62
62
|
|
63
|
+
## Reference
|
64
|
+
|
65
|
+
You can find the full reference documentation online, [here](https://rubydoc.info/gems/ruby-pwsh).
|
66
|
+
|
63
67
|
<!-- ## Development
|
64
68
|
|
65
69
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
66
70
|
|
67
71
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). -->
|
68
72
|
|
73
|
+
## Releasing the Gem and Puppet Module
|
74
|
+
|
75
|
+
Steps to release an update to the gem and module include:
|
76
|
+
|
77
|
+
1. Ensure that the release branch is up to date with the master:
|
78
|
+
```bash
|
79
|
+
git push upstream upstream/master:release --force
|
80
|
+
```
|
81
|
+
1. Checkout a new working branch for the release prep (where xyz is the appropriate version, sans periods):
|
82
|
+
```bash
|
83
|
+
git checkout -b maint/release/prep-xyz upstream/release
|
84
|
+
```
|
85
|
+
1. Update the version in `lib/pwsh/version.rb` and `metadata.json` to the appropriate version for the new release.
|
86
|
+
1. Run the changelog update task (make sure to verify the changelog, correctly tagging PRs as needed):
|
87
|
+
```bash
|
88
|
+
bundle exec rake changelog
|
89
|
+
```
|
90
|
+
1. Commit your changes with a short, sensible commit message, like:
|
91
|
+
```text
|
92
|
+
git add lib/pwsh/version.rb
|
93
|
+
git add metadata.json
|
94
|
+
git add CHANGELOG.md
|
95
|
+
git commit -m '(MAINT) Prep for x.y.z release'
|
96
|
+
```
|
97
|
+
1. Push your changes and submit a pull request for review _against the **release** branch_:
|
98
|
+
```bash
|
99
|
+
git push -u origin maint/release-prep-xyz
|
100
|
+
```
|
101
|
+
1. Ensure tests pass and the code is merged to `release`.
|
102
|
+
1. Grab the commit hash from the merge commit on release, use that as the tag for the version (replacing `x.y.z` with the appropriate version and `commithash` with the relevant one), then push the tags to upstream:
|
103
|
+
```bash
|
104
|
+
bundle exec rake tag['x.y.z', 'commithash']
|
105
|
+
```
|
106
|
+
1. Build the Ruby gem and publish:
|
107
|
+
```bash
|
108
|
+
bundle exec rake build
|
109
|
+
bundle exec rake push['ruby-pwsh-x.y.z.gem']
|
110
|
+
```
|
111
|
+
1. Verify that the correct version now exists on [RubyGems](https://rubygems.org/search?query=ruby-pwsh)
|
112
|
+
1. Build the Puppet module:
|
113
|
+
```bash
|
114
|
+
bundle exec rake build_module
|
115
|
+
```
|
116
|
+
1. Publish the updated module version (found in the `pkg` folder) to [the Forge](https://forge.puppet.com/puppetlabs/pwshlib).
|
117
|
+
1. Submit the [mergeback PR from the release branch to master](https://github.com/puppetlabs/ruby-pwsh/compare/master...release).
|
118
|
+
|
69
119
|
## Known Issues
|
data/Rakefile
CHANGED
@@ -61,8 +61,6 @@ end
|
|
61
61
|
desc 'Build the gem'
|
62
62
|
task :build do
|
63
63
|
gemspec_path = File.join(Dir.pwd, 'ruby-pwsh.gemspec')
|
64
|
-
# Delete the puppet-specific code if it exists
|
65
|
-
FileUtils.rm_r('lib/puppet') if File.exist?('lib/puppet')
|
66
64
|
run_local_command("bundle exec gem build '#{gemspec_path}'")
|
67
65
|
end
|
68
66
|
|
@@ -75,7 +73,7 @@ task :tag, [:version, :sha] do |_task, args|
|
|
75
73
|
raise "Invalid version #{args[:version]} - must be like '1.2.3'" unless args[:version] =~ /^\d+\.\d+\.\d+$/
|
76
74
|
|
77
75
|
run_local_command('git fetch upstream')
|
78
|
-
run_local_command("git tag -a version -m #{args[:version]} #{args[:sha]}")
|
76
|
+
run_local_command("git tag -a #{args[:version]} -m #{args[:version]} #{args[:sha]}")
|
79
77
|
run_local_command('git push upstream --tags')
|
80
78
|
end
|
81
79
|
|
data/appveyor.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
---
|
2
|
+
version: 1.1.x.{build}
|
3
|
+
branches:
|
4
|
+
only:
|
5
|
+
- master
|
6
|
+
- release
|
7
|
+
clone_depth: 10
|
8
|
+
environment:
|
9
|
+
matrix:
|
10
|
+
-
|
11
|
+
RUBY_VERSION: 25-x64
|
12
|
+
CHECK: rubocop
|
13
|
+
-
|
14
|
+
RUBY_VERSION: 25
|
15
|
+
CHECK: spec
|
16
|
+
COVERAGE: yes
|
17
|
+
matrix:
|
18
|
+
fast_finish: true
|
19
|
+
install:
|
20
|
+
- set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH%
|
21
|
+
- bundle install --jobs 4 --retry 2
|
22
|
+
- type Gemfile.lock
|
23
|
+
build: off
|
24
|
+
build_script:
|
25
|
+
- dir .
|
26
|
+
test_script:
|
27
|
+
- ruby -v
|
28
|
+
- gem -v
|
29
|
+
- bundle -v
|
30
|
+
- pwsh -v
|
31
|
+
- bundle exec rake %CHECK%
|
32
|
+
notifications:
|
33
|
+
- provider: Email
|
34
|
+
to:
|
35
|
+
- nobody@nowhere.com
|
36
|
+
on_build_success: false
|
37
|
+
on_build_failure: false
|
38
|
+
on_build_status_changed: false
|
@@ -0,0 +1,670 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puppet/resource_api/simple_provider'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'ruby-pwsh'
|
6
|
+
require 'pathname'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
class Puppet::Provider::DscBaseProvider < Puppet::ResourceApi::SimpleProvider
|
10
|
+
# Initializes the provider, preparing the class variables which cache:
|
11
|
+
# - the canonicalized resources across calls
|
12
|
+
# - query results
|
13
|
+
# - logon failures
|
14
|
+
def initialize
|
15
|
+
@@cached_canonicalized_resource = []
|
16
|
+
@@cached_query_results = []
|
17
|
+
@@logon_failures = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Look through a cache to retrieve the hashes specified, if they have been cached.
|
21
|
+
# Does so by seeing if each of the specified hashes is a subset of any of the hashes
|
22
|
+
# in the cache, so {foo: 1, bar: 2} would return if {foo: 1} was the search hash.
|
23
|
+
#
|
24
|
+
# @param cache [Array] the class variable containing cached hashes to search through
|
25
|
+
# @param hashes [Array] the list of hashes to search the cache for
|
26
|
+
# @return [Array] an array containing the matching hashes for the search condition, if any
|
27
|
+
def fetch_cached_hashes(cache, hashes)
|
28
|
+
cache.reject do |item|
|
29
|
+
matching_hash = hashes.select { |hash| (item.to_a - hash.to_a).empty? || (hash.to_a - item.to_a).empty? }
|
30
|
+
matching_hash.empty?
|
31
|
+
end.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
# Implements the canonicalize feature of the Resource API; this method is called first against any resources
|
35
|
+
# defined in the manifest, then again to conform the results from a get call. The method attempts to retrieve
|
36
|
+
# the DSC resource from the machine; if the resource is found, this method then compares the downcased values
|
37
|
+
# of the two hashes, overwriting the manifest value with the discovered one if they are case insensitively
|
38
|
+
# equivalent; this enables case insensitive but preserving behavior where a manifest declaration of a path as
|
39
|
+
# "c:/foo/bar" if discovered on disk as "C:\Foo\Bar" will canonicalize to the latter and prevent any flapping.
|
40
|
+
#
|
41
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
42
|
+
# @param resources [Hash] the hash of the resource to canonicalize from either manifest or invocation
|
43
|
+
# @return [Hash] returns a hash representing the current state of the object, if it exists
|
44
|
+
def canonicalize(context, resources)
|
45
|
+
canonicalized_resources = []
|
46
|
+
resources.collect do |r|
|
47
|
+
if fetch_cached_hashes(@@cached_canonicalized_resource, [r]).empty?
|
48
|
+
canonicalized = invoke_get_method(context, r)
|
49
|
+
if canonicalized.nil?
|
50
|
+
canonicalized = r.dup
|
51
|
+
@@cached_canonicalized_resource << r.dup
|
52
|
+
else
|
53
|
+
canonicalized[:name] = r[:name]
|
54
|
+
if r[:dsc_psdscrunascredential].nil?
|
55
|
+
canonicalized.delete(:dsc_psdscrunascredential)
|
56
|
+
else
|
57
|
+
canonicalized[:dsc_psdscrunascredential] = r[:dsc_psdscrunascredential]
|
58
|
+
end
|
59
|
+
downcased_result = recursively_downcase(canonicalized)
|
60
|
+
downcased_resource = recursively_downcase(r)
|
61
|
+
downcased_result.each do |key, value|
|
62
|
+
canonicalized[key] = r[key] unless downcased_resource[key] == value
|
63
|
+
canonicalized.delete(key) unless downcased_resource.keys.include?(key)
|
64
|
+
end
|
65
|
+
# Cache the actually canonicalized resource separately
|
66
|
+
@@cached_canonicalized_resource << canonicalized.dup
|
67
|
+
end
|
68
|
+
else
|
69
|
+
canonicalized = r
|
70
|
+
end
|
71
|
+
canonicalized_resources << canonicalized
|
72
|
+
end
|
73
|
+
context.debug("Canonicalized Resources: #{canonicalized_resources}")
|
74
|
+
canonicalized_resources
|
75
|
+
end
|
76
|
+
|
77
|
+
# Attempts to retrieve an instance of the DSC resource, invoking the `Get` method and passing any
|
78
|
+
# namevars as the Properties to Invoke-DscResource. The result object, if any, is compared to the
|
79
|
+
# specified properties in the Puppet Resource to decide whether it needs to be created, updated,
|
80
|
+
# deleted, or whether it is in the desired state.
|
81
|
+
#
|
82
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
83
|
+
# @param names [Hash] the hash of namevar properties and their values to use to get the resource
|
84
|
+
# @return [Hash] returns a hash representing the current state of the object, if it exists
|
85
|
+
def get(context, names = nil)
|
86
|
+
# Relies on the get_simple_filter feature to pass the namevars
|
87
|
+
# as an array containing the namevar parameters as a hash.
|
88
|
+
# This hash is functionally the same as a should hash as
|
89
|
+
# passed to the should_to_resource method.
|
90
|
+
context.debug('Collecting data from the DSC Resource')
|
91
|
+
|
92
|
+
# If the resource has already been queried, do not bother querying for it again
|
93
|
+
cached_results = fetch_cached_hashes(@@cached_query_results, names)
|
94
|
+
return cached_results unless cached_results.empty?
|
95
|
+
|
96
|
+
if @@cached_canonicalized_resource.empty?
|
97
|
+
mandatory_properties = {}
|
98
|
+
else
|
99
|
+
canonicalized_resource = @@cached_canonicalized_resource[0].dup
|
100
|
+
mandatory_properties = canonicalized_resource.select do |attribute, _value|
|
101
|
+
(mandatory_get_attributes(context) - namevar_attributes(context)).include?(attribute)
|
102
|
+
end
|
103
|
+
# If dsc_psdscrunascredential was specified, re-add it here.
|
104
|
+
mandatory_properties[:dsc_psdscrunascredential] = canonicalized_resource[:dsc_psdscrunascredential] if canonicalized_resource.keys.include?(:dsc_psdscrunascredential)
|
105
|
+
end
|
106
|
+
names.collect do |name|
|
107
|
+
name = { name: name } if name.is_a? String
|
108
|
+
invoke_get_method(context, name.merge(mandatory_properties))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
113
|
+
# the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
|
114
|
+
# up to the Resource API based on the results
|
115
|
+
#
|
116
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
117
|
+
# @param name [String] the name of the resource being created
|
118
|
+
# @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
119
|
+
def create(context, name, should)
|
120
|
+
context.debug("Creating '#{name}' with #{should.inspect}")
|
121
|
+
invoke_set_method(context, name, should)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
125
|
+
# the `invoke_set_method` method; whether this method, `create`, or `delete` is called is entirely
|
126
|
+
# up to the Resource API based on the results
|
127
|
+
#
|
128
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
129
|
+
# @param name [String] the name of the resource being created
|
130
|
+
# @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
131
|
+
def update(context, name, should)
|
132
|
+
context.debug("Updating '#{name}' with #{should.inspect}")
|
133
|
+
invoke_set_method(context, name, should)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
137
|
+
# the `invoke_set_method` method; whether this method, `create`, or `update` is called is entirely
|
138
|
+
# up to the Resource API based on the results
|
139
|
+
#
|
140
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
141
|
+
# @param name [String] the name of the resource being created
|
142
|
+
# @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
143
|
+
def delete(context, name)
|
144
|
+
context.debug("Deleting '#{name}'")
|
145
|
+
invoke_set_method(context, name, name.merge({ dsc_ensure: 'Absent' }))
|
146
|
+
end
|
147
|
+
|
148
|
+
# Invokes the `Get` method, passing the name_hash as the properties to use with `Invoke-DscResource`
|
149
|
+
# The PowerShell script returns a JSON representation of the DSC Resource's CIM Instance munged as
|
150
|
+
# best it can be for Ruby. Once that JSON is parsed into a hash this method further munges it to
|
151
|
+
# fit the expected property definitions. Finally, it returns the object for the Resource API to
|
152
|
+
# compare against and determine what future actions, if any, are needed.
|
153
|
+
#
|
154
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
155
|
+
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
156
|
+
# @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
|
157
|
+
def invoke_get_method(context, name_hash)
|
158
|
+
context.debug("retrieving #{name_hash.inspect}")
|
159
|
+
|
160
|
+
# Do not bother running if the logon credentials won't work
|
161
|
+
unless name_hash[:dsc_psdscrunascredential].nil?
|
162
|
+
return name_hash if logon_failed_already?(name_hash[:dsc_psdscrunascredential])
|
163
|
+
end
|
164
|
+
|
165
|
+
query_props = name_hash.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
|
166
|
+
resource = should_to_resource(query_props, context, 'get')
|
167
|
+
script_content = ps_script_content(resource)
|
168
|
+
context.debug("Script:\n #{redact_secrets(script_content)}")
|
169
|
+
output = ps_manager.execute(script_content)[:stdout]
|
170
|
+
context.err('Nothing returned') if output.nil?
|
171
|
+
|
172
|
+
data = JSON.parse(output)
|
173
|
+
context.debug("raw data received: #{data.inspect}")
|
174
|
+
error = data['errormessage']
|
175
|
+
unless error.nil?
|
176
|
+
# NB: We should have a way to stop processing this resource *now* without blowing up the whole Puppet run
|
177
|
+
# Raising an error stops processing but blows things up while context.err alerts but continues to process
|
178
|
+
if error =~ /Logon failure: the user has not been granted the requested logon type at this computer/
|
179
|
+
logon_error = "PSDscRunAsCredential account specified (#{name_hash[:dsc_psdscrunascredential]['user']}) does not have appropriate logon rights; are they an administrator?"
|
180
|
+
name_hash[:name].nil? ? context.err(logon_error) : context.err(name_hash[:name], logon_error)
|
181
|
+
@@logon_failures << name_hash[:dsc_psdscrunascredential].dup
|
182
|
+
# This is a hack to handle the query cache to prevent a second lookup
|
183
|
+
@@cached_query_results << name_hash # if fetch_cached_hashes(@@cached_query_results, [data]).empty?
|
184
|
+
else
|
185
|
+
context.err(error)
|
186
|
+
end
|
187
|
+
# Either way, something went wrong and we didn't get back a good result, so return nil
|
188
|
+
return nil
|
189
|
+
end
|
190
|
+
# DSC gives back information we don't care about; filter down to only
|
191
|
+
# those properties exposed in the type definition.
|
192
|
+
valid_attributes = context.type.attributes.keys.collect(&:to_s)
|
193
|
+
data.select! { |key, _value| valid_attributes.include?("dsc_#{key.downcase}") }
|
194
|
+
# Canonicalize the results to match the type definition representation;
|
195
|
+
# failure to do so will prevent the resource_api from comparing the result
|
196
|
+
# to the should hash retrieved from the resource definition in the manifest.
|
197
|
+
data.keys.each do |key|
|
198
|
+
type_key = "dsc_#{key.downcase}".to_sym
|
199
|
+
data[type_key] = data.delete(key)
|
200
|
+
camelcase_hash_keys!(data[type_key]) if data[type_key].is_a?(Enumerable)
|
201
|
+
end
|
202
|
+
# If a resource is found, it's present, so refill these two Puppet-only keys
|
203
|
+
data.merge!({ name: name_hash[:name] })
|
204
|
+
if ensurable?(context)
|
205
|
+
ensure_value = data.key?(:dsc_ensure) ? data[:dsc_ensure].downcase : 'present'
|
206
|
+
data.merge!({ ensure: ensure_value })
|
207
|
+
end
|
208
|
+
# TODO: Handle PSDscRunAsCredential flapping
|
209
|
+
# Resources do not return the account under which they were discovered, so re-add that
|
210
|
+
if name_hash[:dsc_psdscrunascredential].nil?
|
211
|
+
data.delete(:dsc_psdscrunascredential)
|
212
|
+
else
|
213
|
+
data.merge!({ dsc_psdscrunascredential: name_hash[:dsc_psdscrunascredential] })
|
214
|
+
end
|
215
|
+
# Cache the query to prevent a second lookup
|
216
|
+
@@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
|
217
|
+
context.debug("Returned to Puppet as #{data}")
|
218
|
+
data
|
219
|
+
end
|
220
|
+
|
221
|
+
# Invokes the `Set` method, passing the should hash as the properties to use with `Invoke-DscResource`
|
222
|
+
# The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
|
223
|
+
# is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
224
|
+
#
|
225
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
226
|
+
# @param should [Hash] the desired state represented definition to pass as properties to Invoke-DscResource
|
227
|
+
# @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
|
228
|
+
def invoke_set_method(context, name, should)
|
229
|
+
context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
|
230
|
+
|
231
|
+
# Do not bother running if the logon credentials won't work
|
232
|
+
unless should[:dsc_psdscrunascredential].nil?
|
233
|
+
return nil if logon_failed_already?(should[:dsc_psdscrunascredential])
|
234
|
+
end
|
235
|
+
|
236
|
+
apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
|
237
|
+
resource = should_to_resource(apply_props, context, 'set')
|
238
|
+
script_content = ps_script_content(resource)
|
239
|
+
context.debug("Script:\n #{redact_secrets(script_content)}")
|
240
|
+
|
241
|
+
output = ps_manager.execute(script_content)[:stdout]
|
242
|
+
context.err('Nothing returned') if output.nil?
|
243
|
+
|
244
|
+
data = JSON.parse(output)
|
245
|
+
context.debug(data)
|
246
|
+
|
247
|
+
context.err(data['errormessage']) unless data['errormessage'].empty?
|
248
|
+
# TODO: Implement this functionality for notifying a DSC reboot?
|
249
|
+
# notify_reboot_pending if data['rebootrequired'] == true
|
250
|
+
data
|
251
|
+
end
|
252
|
+
|
253
|
+
# Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
|
254
|
+
# including the desired state, the path to the PowerShell module containing the resources, the invoke
|
255
|
+
# method, and metadata about the DSC Resource and Puppet Type.
|
256
|
+
#
|
257
|
+
# @param should [Hash] A hash representing the desired state of the DSC resource as defined in Puppet
|
258
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
259
|
+
# @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
|
260
|
+
# @return [Hash] a hash with the information needed to run `Invoke-DscResource`
|
261
|
+
def should_to_resource(should, context, dsc_invoke_method)
|
262
|
+
resource = {}
|
263
|
+
resource[:parameters] = {}
|
264
|
+
%i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
|
265
|
+
resource[k] = context.type.definition[k]
|
266
|
+
end
|
267
|
+
should.each do |k, v|
|
268
|
+
next if k == :ensure
|
269
|
+
# PSDscRunAsCredential is considered a namevar and will always be passed, even if nil
|
270
|
+
# To prevent flapping during runs, remove it from the resource definition unless specified
|
271
|
+
next if k == :dsc_psdscrunascredential && v.nil?
|
272
|
+
|
273
|
+
resource[:parameters][k] = {}
|
274
|
+
resource[:parameters][k][:value] = v
|
275
|
+
%i[mof_type mof_is_embedded].each do |ky|
|
276
|
+
resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
resource[:dsc_invoke_method] = dsc_invoke_method
|
280
|
+
|
281
|
+
# Because Puppet adds all of the modules to the LOAD_PATH we can be sure that the appropriate module lives here during an apply;
|
282
|
+
# PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
|
283
|
+
# During a Puppet agent run, the code lives in the cache so we can use the file expansion to discover the correct folder.
|
284
|
+
root_module_path = $LOAD_PATH.select { |path| path.match?(%r{#{resource[:dscmeta_module_name].downcase}/lib}) }.first
|
285
|
+
resource[:vendored_modules_path] = if root_module_path.nil?
|
286
|
+
File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources')
|
287
|
+
else
|
288
|
+
File.expand_path(root_module_path + '/puppet_x/dsc_resources')
|
289
|
+
end
|
290
|
+
resource[:attributes] = nil
|
291
|
+
context.debug("should_to_resource: #{resource.inspect}")
|
292
|
+
resource
|
293
|
+
end
|
294
|
+
|
295
|
+
# Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique
|
296
|
+
# variables in the PowerShell script.
|
297
|
+
#
|
298
|
+
# @return [String] a uuid with underscores instead of dashes.
|
299
|
+
def random_variable_name
|
300
|
+
# PowerShell variables can't include dashes
|
301
|
+
SecureRandom.uuid.gsub('-', '_')
|
302
|
+
end
|
303
|
+
|
304
|
+
# Return a Hash containing all of the variables defined for instantiation as well as the Ruby hash for their
|
305
|
+
# properties so they can be matched and replaced as needed.
|
306
|
+
#
|
307
|
+
# @return [Hash] containing all instantiated variables and the properties that they define
|
308
|
+
def instantiated_variables
|
309
|
+
@@instantiated_variables ||= {}
|
310
|
+
end
|
311
|
+
|
312
|
+
# Clear the instantiated variables hash to be ready for the next run
|
313
|
+
def clear_instantiated_variables!
|
314
|
+
@@instantiated_variables = {}
|
315
|
+
end
|
316
|
+
|
317
|
+
# Return true if the specified credential hash has already failed to execute a DSC resource due to
|
318
|
+
# a logon error, as when the account is not an administrator on the machine; otherwise returns false.
|
319
|
+
#
|
320
|
+
# @param [Hash] a credential hash with a user and password keys where the password is a sensitive string
|
321
|
+
# @return [Bool] true if the credential_hash has already failed logon, false otherwise
|
322
|
+
def logon_failed_already?(credential_hash)
|
323
|
+
@@logon_failures.any? do |failure_hash|
|
324
|
+
failure_hash['user'] == credential_hash['user'] && failure_hash['password'].unwrap == credential_hash['password'].unwrap
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Recursively transforms any enumerable, camelCasing any hash keys it finds
|
329
|
+
#
|
330
|
+
# @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
|
331
|
+
# @return [Enumerable] returns the input object with hash keys recursively camelCased
|
332
|
+
def camelcase_hash_keys!(enumerable)
|
333
|
+
if enumerable.is_a?(Hash)
|
334
|
+
enumerable.keys.each do |key|
|
335
|
+
name = key.dup
|
336
|
+
name[0] = name[0].downcase
|
337
|
+
enumerable[name] = enumerable.delete(key)
|
338
|
+
camelcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
|
339
|
+
end
|
340
|
+
else
|
341
|
+
enumerable.each { |item| camelcase_hash_keys!(item) if item.is_a?(Enumerable) }
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Recursively transforms any object, downcasing it to enable case insensitive comparisons
|
346
|
+
#
|
347
|
+
# @param object [Object] a string, array, hash, or other object to attempt to recursively downcase
|
348
|
+
# @return [Object] returns the input object recursively downcased
|
349
|
+
def recursively_downcase(object)
|
350
|
+
case object
|
351
|
+
when String
|
352
|
+
object.downcase
|
353
|
+
when Array
|
354
|
+
object.map { |item| recursively_downcase(item) }
|
355
|
+
when Hash
|
356
|
+
transformed = {}
|
357
|
+
object.transform_keys(&:downcase).each do |key, value|
|
358
|
+
transformed[key] = recursively_downcase(value)
|
359
|
+
end
|
360
|
+
transformed
|
361
|
+
else
|
362
|
+
object
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# Checks to see whether the DSC resource being managed is defined as ensurable
|
367
|
+
#
|
368
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
369
|
+
# @return [Bool] returns true if the DSC Resource is ensurable, otherwise false.
|
370
|
+
def ensurable?(context)
|
371
|
+
context.type.attributes.keys.include?(:ensure)
|
372
|
+
end
|
373
|
+
|
374
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
|
375
|
+
#
|
376
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
377
|
+
# @return [Array] returns an array of attribute names as symbols which are mandatory for get operations
|
378
|
+
def mandatory_get_attributes(context)
|
379
|
+
context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_get] }.keys
|
380
|
+
end
|
381
|
+
|
382
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for set operations
|
383
|
+
#
|
384
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
385
|
+
# @return [Array] returns an array of attribute names as symbols which are mandatory for set operations
|
386
|
+
def mandatory_set_attributes(context)
|
387
|
+
context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_set] }.keys
|
388
|
+
end
|
389
|
+
|
390
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as namevars
|
391
|
+
#
|
392
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
393
|
+
# @return [Array] returns an array of attribute names as symbols which are namevars
|
394
|
+
def namevar_attributes(context)
|
395
|
+
context.type.attributes.select { |_attribute, properties| properties[:behaviour] == :namevar }.keys
|
396
|
+
end
|
397
|
+
|
398
|
+
# Look through a fully formatted string, replacing all instances where a value matches the formatted properties
|
399
|
+
# of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
|
400
|
+
# CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
|
401
|
+
#
|
402
|
+
# @param string [String] the string of text to search through for places an instantiated variable can be referenced
|
403
|
+
# @return [String] the string with references to instantiated variables instead of their properties
|
404
|
+
def interpolate_variables(string)
|
405
|
+
modified_string = string
|
406
|
+
# Always replace later-created variables first as they sometimes were built from earlier ones
|
407
|
+
instantiated_variables.reverse_each do |variable_name, ruby_definition|
|
408
|
+
modified_string = modified_string.gsub(format(ruby_definition), "$#{variable_name}")
|
409
|
+
end
|
410
|
+
modified_string
|
411
|
+
end
|
412
|
+
|
413
|
+
# Parses a resource definition (as from `should_to_resource`) for any properties which are PowerShell
|
414
|
+
# Credentials. As these values need to be serialized into PSCredential objects, return an array of
|
415
|
+
# PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
|
416
|
+
# These credential variables can then be simply assigned in the parameter hash where needed.
|
417
|
+
#
|
418
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
419
|
+
# @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables
|
420
|
+
def prepare_credentials(resource)
|
421
|
+
credentials_block = []
|
422
|
+
resource[:parameters].each do |_property_name, property_hash|
|
423
|
+
next unless property_hash[:mof_type] == 'PSCredential'
|
424
|
+
next if property_hash[:value].nil?
|
425
|
+
|
426
|
+
variable_name = random_variable_name
|
427
|
+
credential_hash = {
|
428
|
+
'user' => property_hash[:value]['user'],
|
429
|
+
'password' => escape_quotes(property_hash[:value]['password'].unwrap)
|
430
|
+
}
|
431
|
+
instantiated_variables.merge!(variable_name => credential_hash)
|
432
|
+
credentials_block << format_pscredential(variable_name, credential_hash)
|
433
|
+
end
|
434
|
+
credentials_block.join("\n")
|
435
|
+
credentials_block == [] ? '' : credentials_block
|
436
|
+
end
|
437
|
+
|
438
|
+
# Write a line of PowerShell which creates a PSCredential object and assigns it to a variable
|
439
|
+
#
|
440
|
+
# @param variable_name [String] the name of the Variable to assign the PSCredential object to
|
441
|
+
# @param credential_hash [Hash] the Properties which define the PSCredential Object
|
442
|
+
# @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
|
443
|
+
def format_pscredential(variable_name, credential_hash)
|
444
|
+
definition = "$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}' # PuppetSensitive"
|
445
|
+
definition
|
446
|
+
end
|
447
|
+
|
448
|
+
# Parses a resource definition (as from `should_to_resource`) for any properties which are CIM instances
|
449
|
+
# whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
|
450
|
+
# those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
|
451
|
+
# will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
|
452
|
+
# by variable reference.
|
453
|
+
#
|
454
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
455
|
+
# @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables
|
456
|
+
def prepare_cim_instances(resource)
|
457
|
+
cim_instances_block = []
|
458
|
+
resource[:parameters].each do |_property_name, property_hash|
|
459
|
+
next unless property_hash[:mof_is_embedded]
|
460
|
+
|
461
|
+
# strip dsc_ from the beginning of the property name declaration
|
462
|
+
# name = property_name.to_s.gsub(/^dsc_/, '').to_sym
|
463
|
+
# Process nested CIM instances first as those neeed to be passed to higher-order
|
464
|
+
# instances and must therefore be declared before they must be referenced
|
465
|
+
cim_instance_hashes = nested_cim_instances(property_hash[:value]).flatten.reject(&:nil?)
|
466
|
+
# Sometimes the instances are an empty array
|
467
|
+
unless cim_instance_hashes.count.zero?
|
468
|
+
cim_instance_hashes.each do |instance|
|
469
|
+
variable_name = random_variable_name
|
470
|
+
instantiated_variables.merge!(variable_name => instance)
|
471
|
+
class_name = instance['cim_instance_type']
|
472
|
+
properties = instance.reject { |k, _v| k == 'cim_instance_type' }
|
473
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, properties)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
# We have to handle arrays of CIM instances slightly differently
|
477
|
+
if property_hash[:mof_type] =~ /\[\]$/
|
478
|
+
class_name = property_hash[:mof_type].gsub('[]', '')
|
479
|
+
property_hash[:value].each do |hash|
|
480
|
+
variable_name = random_variable_name
|
481
|
+
instantiated_variables.merge!(variable_name => hash)
|
482
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, hash)
|
483
|
+
end
|
484
|
+
else
|
485
|
+
variable_name = random_variable_name
|
486
|
+
instantiated_variables.merge!(variable_name => property_hash[:value])
|
487
|
+
class_name = property_hash[:mof_type]
|
488
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, property_hash[:value])
|
489
|
+
end
|
490
|
+
end
|
491
|
+
cim_instances_block == [] ? '' : cim_instances_block.join("\n")
|
492
|
+
end
|
493
|
+
|
494
|
+
# Recursively search for and return CIM instances nested in an enumerable
|
495
|
+
#
|
496
|
+
# @param enumerable [Enumerable] a hash or array which may contain CIM Instances
|
497
|
+
# @return [Hash] every discovered hash which does define a CIM Instance
|
498
|
+
def nested_cim_instances(enumerable)
|
499
|
+
enumerable.collect do |key, value|
|
500
|
+
if key.is_a?(Hash) && key.key?('cim_instance_type')
|
501
|
+
key
|
502
|
+
# TODO: Are there any cim instancees 3 levels deep, or only 2?
|
503
|
+
# if so, we should *also* keep searching and processing...
|
504
|
+
elsif key.is_a?(Enumerable)
|
505
|
+
nested_cim_instances(key)
|
506
|
+
elsif value.is_a?(Enumerable)
|
507
|
+
nested_cim_instances(value)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Write a line of PowerShell which creates a CIM Instance and assigns it to a variable
|
513
|
+
#
|
514
|
+
# @param variable_name [String] the name of the Variable to assign the CIM Instance to
|
515
|
+
# @param class_name [String] the CIM Class to instantiate
|
516
|
+
# @param property_hash [Hash] the Properties which define the CIM Instance
|
517
|
+
# @return [String] A line of PowerShell which defines the CIM Instance and stores it to a variable
|
518
|
+
def format_ciminstance(variable_name, class_name, property_hash)
|
519
|
+
definition = "$#{variable_name} = New-CimInstance -ClientOnly -ClassName '#{class_name}' -Property #{format(property_hash)}"
|
520
|
+
# AWFUL HACK to make New-CimInstance happy ; it can't parse an array unless it's an array of Cim Instances
|
521
|
+
# definition = definition.gsub("@(@{'cim_instance_type'","[CimInstance[]]@(@{'cim_instance_type'")
|
522
|
+
# EVEN WORSE HACK - this one we can't even be sure it's a cim instance...
|
523
|
+
# but I don't _think_ anything but nested cim instances show up as hashes inside an array
|
524
|
+
definition = definition.gsub('@(@{', '[CimInstance[]]@(@{')
|
525
|
+
definition = interpolate_variables(definition)
|
526
|
+
definition
|
527
|
+
end
|
528
|
+
|
529
|
+
# Munge a resource definition (as from `should_to_resource`) into valid PowerShell which represents
|
530
|
+
# the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
|
531
|
+
# defined variables into the hash.
|
532
|
+
#
|
533
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
534
|
+
# @return [String] A string representing the PowerShell definition of the InvokeParams hash
|
535
|
+
def invoke_params(resource)
|
536
|
+
params = {
|
537
|
+
Name: resource[:dscmeta_resource_friendly_name],
|
538
|
+
Method: resource[:dsc_invoke_method],
|
539
|
+
Property: {}
|
540
|
+
}
|
541
|
+
if resource.key?(:dscmeta_module_version)
|
542
|
+
params[:ModuleName] = {}
|
543
|
+
params[:ModuleName][:ModuleName] = "#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
|
544
|
+
params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
|
545
|
+
else
|
546
|
+
params[:ModuleName] = resource[:dscmeta_module_name]
|
547
|
+
end
|
548
|
+
resource[:parameters].each do |property_name, property_hash|
|
549
|
+
# strip dsc_ from the beginning of the property name declaration
|
550
|
+
name = property_name.to_s.gsub(/^dsc_/, '').to_sym
|
551
|
+
params[:Property][name] = if property_hash[:mof_type] == 'PSCredential'
|
552
|
+
# format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make
|
553
|
+
# the Credential hash interpolable as it will be replaced by a variable reference.
|
554
|
+
{
|
555
|
+
'user' => property_hash[:value]['user'],
|
556
|
+
'password' => escape_quotes(property_hash[:value]['password'].unwrap)
|
557
|
+
}
|
558
|
+
else
|
559
|
+
property_hash[:value]
|
560
|
+
end
|
561
|
+
end
|
562
|
+
params_block = interpolate_variables("$InvokeParams = #{format(params)}")
|
563
|
+
# HACK: make CIM instances work:
|
564
|
+
resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type] =~ /\[\]/ }.each do |_property_name, property_hash|
|
565
|
+
formatted_property_hash = interpolate_variables(format(property_hash[:value]))
|
566
|
+
params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}")
|
567
|
+
end
|
568
|
+
params_block
|
569
|
+
end
|
570
|
+
|
571
|
+
# Given a resource definition (as from `should_to_resource`), return a PowerShell script which has
|
572
|
+
# all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
|
573
|
+
# will correct munge the results for returning to Puppet as a JSON object.
|
574
|
+
#
|
575
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
576
|
+
# @return [String] A string representing the PowerShell script which will invoke the DSC Resource.
|
577
|
+
def ps_script_content(resource)
|
578
|
+
template_path = File.expand_path('../', __FILE__)
|
579
|
+
# Defines the helper functions
|
580
|
+
functions = File.new(template_path + '/invoke_dsc_resource_functions.ps1').read
|
581
|
+
# Defines the response hash and the runtime settings
|
582
|
+
preamble = File.new(template_path + '/invoke_dsc_resource_preamble.ps1').read
|
583
|
+
# The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
|
584
|
+
postscript = File.new(template_path + '/invoke_dsc_resource_postscript.ps1').read
|
585
|
+
# The blocks define the variables to define for the postscript.
|
586
|
+
credential_block = prepare_credentials(resource)
|
587
|
+
cim_instances_block = prepare_cim_instances(resource)
|
588
|
+
parameters_block = invoke_params(resource)
|
589
|
+
# clean them out of the temporary cache now that they're not needed; failure to do so can goof up future executions in this run
|
590
|
+
clear_instantiated_variables!
|
591
|
+
|
592
|
+
content = [functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
|
593
|
+
content
|
594
|
+
end
|
595
|
+
|
596
|
+
# Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
|
597
|
+
# munging over what is provided in the ruby-pwsh library, as it does not handle unwrapping Sensitive
|
598
|
+
# data types or interpolating Credentials.
|
599
|
+
#
|
600
|
+
# @param value [Object] The object to format into valid PowerShell
|
601
|
+
# @return [String] A string representation of the input value as valid PowerShell
|
602
|
+
def format(value)
|
603
|
+
Pwsh::Util.format_powershell_value(value)
|
604
|
+
rescue RuntimeError => e
|
605
|
+
raise unless e.message =~ /Sensitive \[value redacted\]/
|
606
|
+
|
607
|
+
string = Pwsh::Util.format_powershell_value(unwrap(value))
|
608
|
+
string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
|
609
|
+
end
|
610
|
+
|
611
|
+
# Unwrap sensitive strings for formatting, even inside an enumerable, appending '#PuppetSensitive'
|
612
|
+
# to the end of the string in preparation for gsub cleanup.
|
613
|
+
#
|
614
|
+
# @param value [Object] The object to unwrap sensitive data inside of
|
615
|
+
# @return [Object] The object with any sensitive strings unwrapped and annotated
|
616
|
+
def unwrap(value)
|
617
|
+
if value.class.name == 'Puppet::Pops::Types::PSensitiveType::Sensitive'
|
618
|
+
"#{value.unwrap}#PuppetSensitive"
|
619
|
+
elsif value.class.name == 'Hash'
|
620
|
+
unwrapped = {}
|
621
|
+
value.each do |k, v|
|
622
|
+
unwrapped[k] = unwrap(v)
|
623
|
+
end
|
624
|
+
unwrapped
|
625
|
+
elsif value.class.name == 'Array'
|
626
|
+
unwrapped = []
|
627
|
+
value.each do |v|
|
628
|
+
unwrapped << unwrap(v)
|
629
|
+
end
|
630
|
+
unwrapped
|
631
|
+
else
|
632
|
+
value
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
# Escape any nested single quotes in a Sensitive string
|
637
|
+
#
|
638
|
+
# @param text [String] the text to escape
|
639
|
+
# @return [String] the escaped text
|
640
|
+
def escape_quotes(text)
|
641
|
+
text.gsub("'", "''")
|
642
|
+
end
|
643
|
+
|
644
|
+
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
645
|
+
# and so for debugging purposes must be redacted before being sent to debug
|
646
|
+
# output but must *not* be redacted when sent to the PowerShell code manager.
|
647
|
+
#
|
648
|
+
# @param text [String] the text to redact
|
649
|
+
# @return [String] the redacted text
|
650
|
+
def redact_secrets(text)
|
651
|
+
# Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
|
652
|
+
# no known resources specify a SecureString instead of a PSCredential object. We therefore only
|
653
|
+
# need to redact strings which look like password declarations.
|
654
|
+
modified_text = text.gsub(/(?<=-Password )'.+' # PuppetSensitive/, "'#<Sensitive [value redacted]>'")
|
655
|
+
if modified_text =~ /'.+' # PuppetSensitive/
|
656
|
+
# Something has gone wrong, error loudly?
|
657
|
+
else
|
658
|
+
modified_text
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
# Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
|
663
|
+
# Definiing it here allows re-use of a single instance instead of continually instantiating and
|
664
|
+
# tearing a new instance down for every call.
|
665
|
+
def ps_manager
|
666
|
+
debug_output = Puppet::Util::Log.level == :debug
|
667
|
+
# TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
|
668
|
+
Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
|
669
|
+
end
|
670
|
+
end
|