ruby-pwsh 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -1
- data/.travis.yml +1 -0
- data/CHANGELOG.md +11 -3
- data/Gemfile +3 -8
- data/README.md +46 -0
- data/appveyor.yml +38 -0
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +666 -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 +2 -2
- data/lib/pwsh/util.rb +1 -1
- data/lib/pwsh/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83a03013a6f1fd8f72b90d29d76f211cf1fbba6f32a8e5275bc7b9ce29d7a52a
|
4
|
+
data.tar.gz: 8ce907ddb3d833eec4a193ce994e05a990ccc2f20e46518674896efa14bd6328
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71abf1b0cc94d5f742ae6240d08fff15f86e588157a4a482501f2383ac11e5f3644fb43702af5071720193303fa803debeed362bb20f2efec205b0137acb115e
|
7
|
+
data.tar.gz: b9c9ce3518e6cbc27c6b0f8126519a1c1ea53158424f72162b75e23ecd28415e80cf5df4e9dc762aadd93a99e30e16a5a91dc405cd9d8571a53184bbe04e4373
|
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
|
@@ -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,15 @@
|
|
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.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.0) (2020-08-20)
|
6
|
+
|
7
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.1...0.5.0)
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
12
|
+
|
13
|
+
## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
|
6
14
|
|
7
15
|
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.0...0.4.1)
|
8
16
|
|
@@ -16,7 +24,7 @@ All notable changes to this project will be documented in this file.The format i
|
|
16
24
|
|
17
25
|
### Added
|
18
26
|
|
19
|
-
- \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([
|
27
|
+
- \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
|
20
28
|
|
21
29
|
## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
|
22
30
|
|
@@ -41,4 +49,4 @@ All notable changes to this project will be documented in this file.The format i
|
|
41
49
|
|
42
50
|
|
43
51
|
|
44
|
-
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/
|
52
|
+
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
data/Gemfile
CHANGED
@@ -17,25 +17,20 @@ group :test do
|
|
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
@@ -70,4 +70,50 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
70
70
|
|
71
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). -->
|
72
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
|
+
|
73
119
|
## Known Issues
|
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,666 @@
|
|
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
|
282
|
+
# PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
|
283
|
+
root_module_path = $LOAD_PATH.select { |path| path.match?(%r{#{resource[:dscmeta_module_name].downcase}/lib}) }.first
|
284
|
+
resource[:vendored_modules_path] = File.expand_path(root_module_path + '/puppet_x/dsc_resources')
|
285
|
+
# resource[:vendored_modules_path] = File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources')
|
286
|
+
resource[:attributes] = nil
|
287
|
+
context.debug("should_to_resource: #{resource.inspect}")
|
288
|
+
resource
|
289
|
+
end
|
290
|
+
|
291
|
+
# Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique
|
292
|
+
# variables in the PowerShell script.
|
293
|
+
#
|
294
|
+
# @return [String] a uuid with underscores instead of dashes.
|
295
|
+
def random_variable_name
|
296
|
+
# PowerShell variables can't include dashes
|
297
|
+
SecureRandom.uuid.gsub('-', '_')
|
298
|
+
end
|
299
|
+
|
300
|
+
# Return a Hash containing all of the variables defined for instantiation as well as the Ruby hash for their
|
301
|
+
# properties so they can be matched and replaced as needed.
|
302
|
+
#
|
303
|
+
# @return [Hash] containing all instantiated variables and the properties that they define
|
304
|
+
def instantiated_variables
|
305
|
+
@@instantiated_variables ||= {}
|
306
|
+
end
|
307
|
+
|
308
|
+
# Clear the instantiated variables hash to be ready for the next run
|
309
|
+
def clear_instantiated_variables!
|
310
|
+
@@instantiated_variables = {}
|
311
|
+
end
|
312
|
+
|
313
|
+
# Return true if the specified credential hash has already failed to execute a DSC resource due to
|
314
|
+
# a logon error, as when the account is not an administrator on the machine; otherwise returns false.
|
315
|
+
#
|
316
|
+
# @param [Hash] a credential hash with a user and password keys where the password is a sensitive string
|
317
|
+
# @return [Bool] true if the credential_hash has already failed logon, false otherwise
|
318
|
+
def logon_failed_already?(credential_hash)
|
319
|
+
@@logon_failures.any? do |failure_hash|
|
320
|
+
failure_hash['user'] == credential_hash['user'] && failure_hash['password'].unwrap == credential_hash['password'].unwrap
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Recursively transforms any enumerable, camelCasing any hash keys it finds
|
325
|
+
#
|
326
|
+
# @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
|
327
|
+
# @return [Enumerable] returns the input object with hash keys recursively camelCased
|
328
|
+
def camelcase_hash_keys!(enumerable)
|
329
|
+
if enumerable.is_a?(Hash)
|
330
|
+
enumerable.keys.each do |key|
|
331
|
+
name = key.dup
|
332
|
+
name[0] = name[0].downcase
|
333
|
+
enumerable[name] = enumerable.delete(key)
|
334
|
+
camelcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
|
335
|
+
end
|
336
|
+
else
|
337
|
+
enumerable.each { |item| camelcase_hash_keys!(item) if item.is_a?(Enumerable) }
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Recursively transforms any object, downcasing it to enable case insensitive comparisons
|
342
|
+
#
|
343
|
+
# @param object [Object] a string, array, hash, or other object to attempt to recursively downcase
|
344
|
+
# @return [Object] returns the input object recursively downcased
|
345
|
+
def recursively_downcase(object)
|
346
|
+
case object
|
347
|
+
when String
|
348
|
+
object.downcase
|
349
|
+
when Array
|
350
|
+
object.map { |item| recursively_downcase(item) }
|
351
|
+
when Hash
|
352
|
+
transformed = {}
|
353
|
+
object.transform_keys(&:downcase).each do |key, value|
|
354
|
+
transformed[key] = recursively_downcase(value)
|
355
|
+
end
|
356
|
+
transformed
|
357
|
+
else
|
358
|
+
object
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Checks to see whether the DSC resource being managed is defined as ensurable
|
363
|
+
#
|
364
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
365
|
+
# @return [Bool] returns true if the DSC Resource is ensurable, otherwise false.
|
366
|
+
def ensurable?(context)
|
367
|
+
context.type.attributes.keys.include?(:ensure)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
|
371
|
+
#
|
372
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
373
|
+
# @return [Array] returns an array of attribute names as symbols which are mandatory for get operations
|
374
|
+
def mandatory_get_attributes(context)
|
375
|
+
context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_get] }.keys
|
376
|
+
end
|
377
|
+
|
378
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for set operations
|
379
|
+
#
|
380
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
381
|
+
# @return [Array] returns an array of attribute names as symbols which are mandatory for set operations
|
382
|
+
def mandatory_set_attributes(context)
|
383
|
+
context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_set] }.keys
|
384
|
+
end
|
385
|
+
|
386
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as namevars
|
387
|
+
#
|
388
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
389
|
+
# @return [Array] returns an array of attribute names as symbols which are namevars
|
390
|
+
def namevar_attributes(context)
|
391
|
+
context.type.attributes.select { |_attribute, properties| properties[:behaviour] == :namevar }.keys
|
392
|
+
end
|
393
|
+
|
394
|
+
# Look through a fully formatted string, replacing all instances where a value matches the formatted properties
|
395
|
+
# of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
|
396
|
+
# CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
|
397
|
+
#
|
398
|
+
# @param string [String] the string of text to search through for places an instantiated variable can be referenced
|
399
|
+
# @return [String] the string with references to instantiated variables instead of their properties
|
400
|
+
def interpolate_variables(string)
|
401
|
+
modified_string = string
|
402
|
+
# Always replace later-created variables first as they sometimes were built from earlier ones
|
403
|
+
instantiated_variables.reverse_each do |variable_name, ruby_definition|
|
404
|
+
modified_string = modified_string.gsub(format(ruby_definition), "$#{variable_name}")
|
405
|
+
end
|
406
|
+
modified_string
|
407
|
+
end
|
408
|
+
|
409
|
+
# Parses a resource definition (as from `should_to_resource`) for any properties which are PowerShell
|
410
|
+
# Credentials. As these values need to be serialized into PSCredential objects, return an array of
|
411
|
+
# PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
|
412
|
+
# These credential variables can then be simply assigned in the parameter hash where needed.
|
413
|
+
#
|
414
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
415
|
+
# @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables
|
416
|
+
def prepare_credentials(resource)
|
417
|
+
credentials_block = []
|
418
|
+
resource[:parameters].each do |_property_name, property_hash|
|
419
|
+
next unless property_hash[:mof_type] == 'PSCredential'
|
420
|
+
next if property_hash[:value].nil?
|
421
|
+
|
422
|
+
variable_name = random_variable_name
|
423
|
+
credential_hash = {
|
424
|
+
'user' => property_hash[:value]['user'],
|
425
|
+
'password' => escape_quotes(property_hash[:value]['password'].unwrap)
|
426
|
+
}
|
427
|
+
instantiated_variables.merge!(variable_name => credential_hash)
|
428
|
+
credentials_block << format_pscredential(variable_name, credential_hash)
|
429
|
+
end
|
430
|
+
credentials_block.join("\n")
|
431
|
+
credentials_block == [] ? '' : credentials_block
|
432
|
+
end
|
433
|
+
|
434
|
+
# Write a line of PowerShell which creates a PSCredential object and assigns it to a variable
|
435
|
+
#
|
436
|
+
# @param variable_name [String] the name of the Variable to assign the PSCredential object to
|
437
|
+
# @param credential_hash [Hash] the Properties which define the PSCredential Object
|
438
|
+
# @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
|
439
|
+
def format_pscredential(variable_name, credential_hash)
|
440
|
+
definition = "$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}' # PuppetSensitive"
|
441
|
+
definition
|
442
|
+
end
|
443
|
+
|
444
|
+
# Parses a resource definition (as from `should_to_resource`) for any properties which are CIM instances
|
445
|
+
# whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
|
446
|
+
# those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
|
447
|
+
# will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
|
448
|
+
# by variable reference.
|
449
|
+
#
|
450
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
451
|
+
# @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables
|
452
|
+
def prepare_cim_instances(resource)
|
453
|
+
cim_instances_block = []
|
454
|
+
resource[:parameters].each do |_property_name, property_hash|
|
455
|
+
next unless property_hash[:mof_is_embedded]
|
456
|
+
|
457
|
+
# strip dsc_ from the beginning of the property name declaration
|
458
|
+
# name = property_name.to_s.gsub(/^dsc_/, '').to_sym
|
459
|
+
# Process nested CIM instances first as those neeed to be passed to higher-order
|
460
|
+
# instances and must therefore be declared before they must be referenced
|
461
|
+
cim_instance_hashes = nested_cim_instances(property_hash[:value]).flatten.reject(&:nil?)
|
462
|
+
# Sometimes the instances are an empty array
|
463
|
+
unless cim_instance_hashes.count.zero?
|
464
|
+
cim_instance_hashes.each do |instance|
|
465
|
+
variable_name = random_variable_name
|
466
|
+
instantiated_variables.merge!(variable_name => instance)
|
467
|
+
class_name = instance['cim_instance_type']
|
468
|
+
properties = instance.reject { |k, _v| k == 'cim_instance_type' }
|
469
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, properties)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
# We have to handle arrays of CIM instances slightly differently
|
473
|
+
if property_hash[:mof_type] =~ /\[\]$/
|
474
|
+
class_name = property_hash[:mof_type].gsub('[]', '')
|
475
|
+
property_hash[:value].each do |hash|
|
476
|
+
variable_name = random_variable_name
|
477
|
+
instantiated_variables.merge!(variable_name => hash)
|
478
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, hash)
|
479
|
+
end
|
480
|
+
else
|
481
|
+
variable_name = random_variable_name
|
482
|
+
instantiated_variables.merge!(variable_name => property_hash[:value])
|
483
|
+
class_name = property_hash[:mof_type]
|
484
|
+
cim_instances_block << format_ciminstance(variable_name, class_name, property_hash[:value])
|
485
|
+
end
|
486
|
+
end
|
487
|
+
cim_instances_block == [] ? '' : cim_instances_block.join("\n")
|
488
|
+
end
|
489
|
+
|
490
|
+
# Recursively search for and return CIM instances nested in an enumerable
|
491
|
+
#
|
492
|
+
# @param enumerable [Enumerable] a hash or array which may contain CIM Instances
|
493
|
+
# @return [Hash] every discovered hash which does define a CIM Instance
|
494
|
+
def nested_cim_instances(enumerable)
|
495
|
+
enumerable.collect do |key, value|
|
496
|
+
if key.is_a?(Hash) && key.key?('cim_instance_type')
|
497
|
+
key
|
498
|
+
# TODO: Are there any cim instancees 3 levels deep, or only 2?
|
499
|
+
# if so, we should *also* keep searching and processing...
|
500
|
+
elsif key.is_a?(Enumerable)
|
501
|
+
nested_cim_instances(key)
|
502
|
+
elsif value.is_a?(Enumerable)
|
503
|
+
nested_cim_instances(value)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# Write a line of PowerShell which creates a CIM Instance and assigns it to a variable
|
509
|
+
#
|
510
|
+
# @param variable_name [String] the name of the Variable to assign the CIM Instance to
|
511
|
+
# @param class_name [String] the CIM Class to instantiate
|
512
|
+
# @param property_hash [Hash] the Properties which define the CIM Instance
|
513
|
+
# @return [String] A line of PowerShell which defines the CIM Instance and stores it to a variable
|
514
|
+
def format_ciminstance(variable_name, class_name, property_hash)
|
515
|
+
definition = "$#{variable_name} = New-CimInstance -ClientOnly -ClassName '#{class_name}' -Property #{format(property_hash)}"
|
516
|
+
# AWFUL HACK to make New-CimInstance happy ; it can't parse an array unless it's an array of Cim Instances
|
517
|
+
# definition = definition.gsub("@(@{'cim_instance_type'","[CimInstance[]]@(@{'cim_instance_type'")
|
518
|
+
# EVEN WORSE HACK - this one we can't even be sure it's a cim instance...
|
519
|
+
# but I don't _think_ anything but nested cim instances show up as hashes inside an array
|
520
|
+
definition = definition.gsub('@(@{', '[CimInstance[]]@(@{')
|
521
|
+
definition = interpolate_variables(definition)
|
522
|
+
definition
|
523
|
+
end
|
524
|
+
|
525
|
+
# Munge a resource definition (as from `should_to_resource`) into valid PowerShell which represents
|
526
|
+
# the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
|
527
|
+
# defined variables into the hash.
|
528
|
+
#
|
529
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
530
|
+
# @return [String] A string representing the PowerShell definition of the InvokeParams hash
|
531
|
+
def invoke_params(resource)
|
532
|
+
params = {
|
533
|
+
Name: resource[:dscmeta_resource_friendly_name],
|
534
|
+
Method: resource[:dsc_invoke_method],
|
535
|
+
Property: {}
|
536
|
+
}
|
537
|
+
if resource.key?(:dscmeta_module_version)
|
538
|
+
params[:ModuleName] = {}
|
539
|
+
params[:ModuleName][:ModuleName] = "#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
|
540
|
+
params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
|
541
|
+
else
|
542
|
+
params[:ModuleName] = resource[:dscmeta_module_name]
|
543
|
+
end
|
544
|
+
resource[:parameters].each do |property_name, property_hash|
|
545
|
+
# strip dsc_ from the beginning of the property name declaration
|
546
|
+
name = property_name.to_s.gsub(/^dsc_/, '').to_sym
|
547
|
+
params[:Property][name] = if property_hash[:mof_type] == 'PSCredential'
|
548
|
+
# format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make
|
549
|
+
# the Credential hash interpolable as it will be replaced by a variable reference.
|
550
|
+
{
|
551
|
+
'user' => property_hash[:value]['user'],
|
552
|
+
'password' => escape_quotes(property_hash[:value]['password'].unwrap)
|
553
|
+
}
|
554
|
+
else
|
555
|
+
property_hash[:value]
|
556
|
+
end
|
557
|
+
end
|
558
|
+
params_block = interpolate_variables("$InvokeParams = #{format(params)}")
|
559
|
+
# HACK: make CIM instances work:
|
560
|
+
resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type] =~ /\[\]/ }.each do |_property_name, property_hash|
|
561
|
+
formatted_property_hash = interpolate_variables(format(property_hash[:value]))
|
562
|
+
params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}")
|
563
|
+
end
|
564
|
+
params_block
|
565
|
+
end
|
566
|
+
|
567
|
+
# Given a resource definition (as from `should_to_resource`), return a PowerShell script which has
|
568
|
+
# all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
|
569
|
+
# will correct munge the results for returning to Puppet as a JSON object.
|
570
|
+
#
|
571
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
572
|
+
# @return [String] A string representing the PowerShell script which will invoke the DSC Resource.
|
573
|
+
def ps_script_content(resource)
|
574
|
+
template_path = File.expand_path('../', __FILE__)
|
575
|
+
# Defines the helper functions
|
576
|
+
functions = File.new(template_path + '/invoke_dsc_resource_functions.ps1').read
|
577
|
+
# Defines the response hash and the runtime settings
|
578
|
+
preamble = File.new(template_path + '/invoke_dsc_resource_preamble.ps1').read
|
579
|
+
# The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
|
580
|
+
postscript = File.new(template_path + '/invoke_dsc_resource_postscript.ps1').read
|
581
|
+
# The blocks define the variables to define for the postscript.
|
582
|
+
credential_block = prepare_credentials(resource)
|
583
|
+
cim_instances_block = prepare_cim_instances(resource)
|
584
|
+
parameters_block = invoke_params(resource)
|
585
|
+
# 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
|
586
|
+
clear_instantiated_variables!
|
587
|
+
|
588
|
+
content = [functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
|
589
|
+
content
|
590
|
+
end
|
591
|
+
|
592
|
+
# Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
|
593
|
+
# munging over what is provided in the ruby-pwsh library, as it does not handle unwrapping Sensitive
|
594
|
+
# data types or interpolating Credentials.
|
595
|
+
#
|
596
|
+
# @param value [Object] The object to format into valid PowerShell
|
597
|
+
# @return [String] A string representation of the input value as valid PowerShell
|
598
|
+
def format(value)
|
599
|
+
Pwsh::Util.format_powershell_value(value)
|
600
|
+
rescue RuntimeError => e
|
601
|
+
raise unless e.message =~ /Sensitive \[value redacted\]/
|
602
|
+
|
603
|
+
string = Pwsh::Util.format_powershell_value(unwrap(value))
|
604
|
+
string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
|
605
|
+
end
|
606
|
+
|
607
|
+
# Unwrap sensitive strings for formatting, even inside an enumerable, appending '#PuppetSensitive'
|
608
|
+
# to the end of the string in preparation for gsub cleanup.
|
609
|
+
#
|
610
|
+
# @param value [Object] The object to unwrap sensitive data inside of
|
611
|
+
# @return [Object] The object with any sensitive strings unwrapped and annotated
|
612
|
+
def unwrap(value)
|
613
|
+
if value.class.name == 'Puppet::Pops::Types::PSensitiveType::Sensitive'
|
614
|
+
"#{value.unwrap}#PuppetSensitive"
|
615
|
+
elsif value.class.name == 'Hash'
|
616
|
+
unwrapped = {}
|
617
|
+
value.each do |k, v|
|
618
|
+
unwrapped[k] = unwrap(v)
|
619
|
+
end
|
620
|
+
unwrapped
|
621
|
+
elsif value.class.name == 'Array'
|
622
|
+
unwrapped = []
|
623
|
+
value.each do |v|
|
624
|
+
unwrapped << unwrap(v)
|
625
|
+
end
|
626
|
+
unwrapped
|
627
|
+
else
|
628
|
+
value
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Escape any nested single quotes in a Sensitive string
|
633
|
+
#
|
634
|
+
# @param text [String] the text to escape
|
635
|
+
# @return [String] the escaped text
|
636
|
+
def escape_quotes(text)
|
637
|
+
text.gsub("'", "''")
|
638
|
+
end
|
639
|
+
|
640
|
+
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
641
|
+
# and so for debugging purposes must be redacted before being sent to debug
|
642
|
+
# output but must *not* be redacted when sent to the PowerShell code manager.
|
643
|
+
#
|
644
|
+
# @param text [String] the text to redact
|
645
|
+
# @return [String] the redacted text
|
646
|
+
def redact_secrets(text)
|
647
|
+
# Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
|
648
|
+
# no known resources specify a SecureString instead of a PSCredential object. We therefore only
|
649
|
+
# need to redact strings which look like password declarations.
|
650
|
+
modified_text = text.gsub(/(?<=-Password )'.+' # PuppetSensitive/, "'#<Sensitive [value redacted]>'")
|
651
|
+
if modified_text =~ /'.+' # PuppetSensitive/
|
652
|
+
# Something has gone wrong, error loudly?
|
653
|
+
else
|
654
|
+
modified_text
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
# Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
|
659
|
+
# Definiing it here allows re-use of a single instance instead of continually instantiating and
|
660
|
+
# tearing a new instance down for every call.
|
661
|
+
def ps_manager
|
662
|
+
debug_output = Puppet::Util::Log.level == :debug
|
663
|
+
# TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
|
664
|
+
Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
|
665
|
+
end
|
666
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
function new-pscredential {
|
2
|
+
[CmdletBinding()]
|
3
|
+
param (
|
4
|
+
[parameter(Mandatory = $true,
|
5
|
+
ValueFromPipelineByPropertyName = $true)]
|
6
|
+
[string]
|
7
|
+
$user,
|
8
|
+
|
9
|
+
[parameter(Mandatory = $true,
|
10
|
+
ValueFromPipelineByPropertyName = $true)]
|
11
|
+
[string]
|
12
|
+
$password
|
13
|
+
)
|
14
|
+
|
15
|
+
$secpasswd = ConvertTo-SecureString $password -AsPlainText -Force
|
16
|
+
$credentials = New-Object System.Management.Automation.PSCredential ($user, $secpasswd)
|
17
|
+
return $credentials
|
18
|
+
}
|
19
|
+
|
20
|
+
Function ConvertTo-CanonicalResult {
|
21
|
+
[CmdletBinding()]
|
22
|
+
param(
|
23
|
+
[Parameter(Mandatory, Position = 1)]
|
24
|
+
[psobject]
|
25
|
+
$Result,
|
26
|
+
|
27
|
+
[Parameter(DontShow)]
|
28
|
+
[string]
|
29
|
+
$PropertyPath,
|
30
|
+
|
31
|
+
[Parameter(DontShow)]
|
32
|
+
[int]
|
33
|
+
$RecursionLevel = 0
|
34
|
+
)
|
35
|
+
|
36
|
+
$MaxDepth = 5
|
37
|
+
$CimInstancePropertyFilter = { $_.Definition -match 'CimInstance' -and $_.Name -ne 'PSDscRunAsCredential' }
|
38
|
+
|
39
|
+
# Get the properties which are/aren't Cim instances
|
40
|
+
$ResultObject = @{ }
|
41
|
+
$ResultPropertyList = $Result | Get-Member -MemberType Property | Where-Object { $_.Name -ne 'PSComputerName' }
|
42
|
+
$CimInstanceProperties = $ResultPropertyList | Where-Object -FilterScript $CimInstancePropertyFilter
|
43
|
+
|
44
|
+
foreach ($Property in $ResultPropertyList) {
|
45
|
+
$PropertyName = $Property.Name
|
46
|
+
if ($Property -notin $CimInstanceProperties) {
|
47
|
+
$Value = $Result.$PropertyName
|
48
|
+
if ($PropertyName -eq 'Ensure' -and [string]::IsNullOrEmpty($Result.$PropertyName)) {
|
49
|
+
# Just set 'Present' since it was found /shrug
|
50
|
+
# If the value IS listed as absent, don't update it unless you want flapping
|
51
|
+
$Value = 'Present'
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
if ($Value -is [string] -or $value -is [string[]]) {
|
55
|
+
$Value = $Value
|
56
|
+
}
|
57
|
+
|
58
|
+
if ($Value.Count -eq 1 -and $Property.Definition -match '\\[\\]') {
|
59
|
+
$Value = @($Value)
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
elseif ($null -eq $Result.$PropertyName) {
|
64
|
+
if ($Property -match 'InstanceArray') {
|
65
|
+
$Value = @()
|
66
|
+
}
|
67
|
+
else {
|
68
|
+
$Value = $null
|
69
|
+
}
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
# Looks like a nested CIM instance, recurse if we're not too deep in already.
|
73
|
+
$RecursionLevel++
|
74
|
+
|
75
|
+
if ($PropertyPath -eq [string]::Empty) {
|
76
|
+
$PropertyPath = $PropertyName
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
$PropertyPath = "$PropertyPath.$PropertyName"
|
80
|
+
}
|
81
|
+
|
82
|
+
if ($RecursionLevel -gt $MaxDepth) {
|
83
|
+
# Give up recursing more than this
|
84
|
+
return $Result.ToString()
|
85
|
+
}
|
86
|
+
|
87
|
+
$Value = foreach ($item in $Result.$PropertyName) {
|
88
|
+
ConvertTo-CanonicalResult -Result $item -PropertyPath $PropertyPath -RecursionLevel ($RecursionLevel + 1) -WarningAction Continue
|
89
|
+
}
|
90
|
+
|
91
|
+
# The cim instance type is the last component of the type Name
|
92
|
+
# We need to return this for ruby to compare the result hashes
|
93
|
+
# We do NOT need it for the top-level properties as those are defined in the type
|
94
|
+
If ($RecursionLevel -gt 1 -and ![string]::IsNullOrEmpty($Value) ) {
|
95
|
+
# If there's multiple instances, you need to add the type to each one, but you
|
96
|
+
# need to specify only *one* name, otherwise things end up *very* broken.
|
97
|
+
if ($Value.GetType().Name -match '\[\]') {
|
98
|
+
$Value | ForEach-Object -Process {
|
99
|
+
$_.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName[0]
|
100
|
+
}
|
101
|
+
} else {
|
102
|
+
$Value.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName
|
103
|
+
# Ensure that, if it should be an array, it is
|
104
|
+
if ($Result.$PropertyName.GetType().Name -match '\[\]') {
|
105
|
+
$Value = @($Value)
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
if ($Property.Definition -match 'InstanceArray') {
|
112
|
+
if ($Value.Count -lt 2) { $Value = @($Value) }
|
113
|
+
}
|
114
|
+
|
115
|
+
$ResultObject.$PropertyName = $Value
|
116
|
+
}
|
117
|
+
|
118
|
+
# Output the final result
|
119
|
+
$ResultObject
|
120
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Try {
|
2
|
+
$Result = Invoke-DscResource @InvokeParams
|
3
|
+
} catch {
|
4
|
+
$Response.errormessage = $_.Exception.Message
|
5
|
+
return ($Response | ConvertTo-Json -Compress)
|
6
|
+
}
|
7
|
+
|
8
|
+
# keep the switch for when Test passes back changed properties
|
9
|
+
Switch ($invokeParams.Method) {
|
10
|
+
'Test' {
|
11
|
+
$Response.indesiredstate = $Result.InDesiredState
|
12
|
+
return ($Response | ConvertTo-Json -Compress)
|
13
|
+
}
|
14
|
+
'Set' {
|
15
|
+
$Response.indesiredstate = $true
|
16
|
+
$Response.rebootrequired = $Result.RebootRequired
|
17
|
+
return ($Response | ConvertTo-Json -Compress)
|
18
|
+
}
|
19
|
+
'Get' {
|
20
|
+
$CanonicalizedResult = ConvertTo-CanonicalResult -Result $Result
|
21
|
+
return ($CanonicalizedResult | ConvertTo-Json -Compress -Depth 10)
|
22
|
+
}
|
23
|
+
}
|
data/lib/pwsh.rb
CHANGED
@@ -471,7 +471,7 @@ Invoke-PowerShellUserCode @params
|
|
471
471
|
# @return [String] The UTF-8 encoded string containing the payload
|
472
472
|
def self.read_length_prefixed_string!(bytes)
|
473
473
|
# 32 bit integer in Little Endian format
|
474
|
-
length = bytes.slice!(0, 4).
|
474
|
+
length = bytes.slice!(0, 4).unpack1('V')
|
475
475
|
return nil if length.zero?
|
476
476
|
|
477
477
|
bytes.slice!(0, length).force_encoding(Encoding::UTF_8)
|
@@ -586,7 +586,7 @@ Invoke-PowerShellUserCode @params
|
|
586
586
|
|
587
587
|
pipe_reader = Thread.new(@pipe) do |pipe|
|
588
588
|
# Read a Little Endian 32-bit integer for length of response
|
589
|
-
expected_response_length = pipe.sysread(4).
|
589
|
+
expected_response_length = pipe.sysread(4).unpack1('V')
|
590
590
|
|
591
591
|
next nil if expected_response_length.zero?
|
592
592
|
|
data/lib/pwsh/util.rb
CHANGED
@@ -12,7 +12,7 @@ module Pwsh
|
|
12
12
|
def on_windows?
|
13
13
|
# Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
|
14
14
|
# library uses that to test what platform it's on.
|
15
|
-
!!File::ALT_SEPARATOR
|
15
|
+
!!File::ALT_SEPARATOR
|
16
16
|
end
|
17
17
|
|
18
18
|
# Verify paths specified are valid directories which exist.
|
data/lib/pwsh/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-pwsh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: PowerShell code manager for ruby.
|
14
14
|
email:
|
@@ -31,8 +31,13 @@ files:
|
|
31
31
|
- LICENSE.txt
|
32
32
|
- README.md
|
33
33
|
- Rakefile
|
34
|
+
- appveyor.yml
|
34
35
|
- design-comms.png
|
35
36
|
- lib/puppet/feature/pwshlib.rb
|
37
|
+
- lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
|
38
|
+
- lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_functions.ps1
|
39
|
+
- lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_postscript.ps1
|
40
|
+
- lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_preamble.ps1
|
36
41
|
- lib/pwsh.rb
|
37
42
|
- lib/pwsh/util.rb
|
38
43
|
- lib/pwsh/version.rb
|