ruby-pwsh 0.1.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ $script:ErrorActionPreference = 'Stop'
2
+ $script:WarningPreference = 'SilentlyContinue'
3
+
4
+ $response = @{
5
+ indesiredstate = $false
6
+ rebootrequired = $false
7
+ errormessage = ''
8
+ }
@@ -54,7 +54,7 @@ module Pwsh
54
54
  if manager.nil? || !manager.alive?
55
55
  # ignore any errors trying to tear down this unusable instance
56
56
  begin
57
- manager&.exit
57
+ manager.exit unless manager.nil? # rubocop:disable Style/SafeNavigation
58
58
  rescue
59
59
  nil
60
60
  end
@@ -117,6 +117,7 @@ module Pwsh
117
117
  # This named pipe path is Windows specific.
118
118
  pipe_path = "\\\\.\\pipe\\#{named_pipe_name}"
119
119
  else
120
+ require 'tmpdir'
120
121
  # .Net implements named pipes under Linux etc. as Unix Sockets in the filesystem
121
122
  # Paths that are rooted are not munged within C# Core.
122
123
  # https://github.com/dotnet/corefx/blob/94e9d02ad70b2224d012ac4a66eaa1f913ae4f29/src/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs#L49-L60
@@ -470,7 +471,7 @@ Invoke-PowerShellUserCode @params
470
471
  # @return [String] The UTF-8 encoded string containing the payload
471
472
  def self.read_length_prefixed_string!(bytes)
472
473
  # 32 bit integer in Little Endian format
473
- length = bytes.slice!(0, 4).unpack('V').first
474
+ length = bytes.slice!(0, 4).unpack1('V')
474
475
  return nil if length.zero?
475
476
 
476
477
  bytes.slice!(0, length).force_encoding(Encoding::UTF_8)
@@ -585,7 +586,7 @@ Invoke-PowerShellUserCode @params
585
586
 
586
587
  pipe_reader = Thread.new(@pipe) do |pipe|
587
588
  # Read a Little Endian 32-bit integer for length of response
588
- expected_response_length = pipe.sysread(4).unpack('V').first
589
+ expected_response_length = pipe.sysread(4).unpack1('V')
589
590
 
590
591
  next nil if expected_response_length.zero?
591
592
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Manage PowerShell and Windows PowerShell via ruby
3
4
  module Pwsh
4
5
  # Various helper methods
5
6
  module Util
@@ -11,7 +12,7 @@ module Pwsh
11
12
  def on_windows?
12
13
  # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
13
14
  # library uses that to test what platform it's on.
14
- !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation
15
+ !!File::ALT_SEPARATOR
15
16
  end
16
17
 
17
18
  # Verify paths specified are valid directories which exist.
@@ -29,6 +30,116 @@ module Pwsh
29
30
 
30
31
  invalid_paths
31
32
  end
33
+
34
+ # Return a string or symbol converted to snake_case
35
+ #
36
+ # @return [String] snake_cased string
37
+ def snake_case(object)
38
+ # Implementation copied from: https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb
39
+ # gsub(/::/, '/').
40
+ should_symbolize = object.is_a?(Symbol)
41
+ raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String)
42
+
43
+ text = object.to_s
44
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
45
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
46
+ .tr('-', '_')
47
+ .gsub(/\s/, '_')
48
+ .gsub(/__+/, '_')
49
+ .downcase
50
+ should_symbolize ? text.to_sym : text
51
+ end
52
+
53
+ # Iterate through a hashes keys, snake_casing them
54
+ #
55
+ # @return [Hash] Hash with all keys snake_cased
56
+ def snake_case_hash_keys(object)
57
+ snake_case_proc = proc { |key| snake_case(key) }
58
+ apply_key_mutator(object, snake_case_proc)
59
+ end
60
+
61
+ # Return a string or symbol converted to PascalCase
62
+ #
63
+ # @return [String] PascalCased string
64
+ def pascal_case(object)
65
+ should_symbolize = object.is_a?(Symbol)
66
+ raise "snake_case method only handles strings and symbols, passed a #{object.class}: #{object}" unless should_symbolize || object.is_a?(String)
67
+
68
+ # Break word boundaries to snake case first
69
+ text = snake_case(object.to_s).split('_').collect(&:capitalize).join
70
+ should_symbolize ? text.to_sym : text
71
+ end
72
+
73
+ # Iterate through a hashes keys, PascalCasing them
74
+ #
75
+ # @return [Hash] Hash with all keys PascalCased
76
+ def pascal_case_hash_keys(object)
77
+ pascal_case_proc = proc { |key| pascal_case(key) }
78
+ apply_key_mutator(object, pascal_case_proc)
79
+ end
80
+
81
+ # Ensure that quotes inside a passed string will continue to be passed
82
+ #
83
+ # @return [String] the string with quotes escaped
84
+ def escape_quotes(text)
85
+ text.gsub("'", "''")
86
+ end
87
+
88
+ # Ensure that all keys in a hash are symbols, not strings.
89
+ #
90
+ # @return [Hash] a hash whose keys have been converted to symbols.
91
+ def symbolize_hash_keys(object)
92
+ symbolize_proc = proc(&:to_sym)
93
+ apply_key_mutator(object, symbolize_proc)
94
+ end
95
+
96
+ def apply_key_mutator(object, proc)
97
+ return object.map { |item| apply_key_mutator(item, proc) } if object.is_a?(Array)
98
+ return object unless object.is_a?(Hash)
99
+
100
+ modified_hash = {}
101
+ object.each do |key, value|
102
+ modified_hash[proc.call(key)] = apply_key_mutator(value, proc)
103
+ end
104
+ modified_hash
105
+ end
106
+
107
+ private_class_method :apply_key_mutator
108
+
109
+ # Convert a ruby value into a string to be passed along to PowerShell for interpolation in a command
110
+ # Handles:
111
+ # - Strings
112
+ # - Numbers
113
+ # - Booleans
114
+ # - Symbols
115
+ # - Arrays
116
+ # - Hashes
117
+ #
118
+ # @return [String] representation of the value for interpolation
119
+ def format_powershell_value(object)
120
+ if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase) # rubocop:disable Lint/BooleanSymbol
121
+ "$#{object}"
122
+ elsif object.class.name == 'Symbol' || object.class.ancestors.include?(Numeric)
123
+ object.to_s
124
+ elsif object.class.name == 'String'
125
+ "'#{escape_quotes(object)}'"
126
+ elsif object.class.name == 'Array'
127
+ '@(' + object.collect { |item| format_powershell_value(item) }.join(', ') + ')'
128
+ elsif object.class.name == 'Hash'
129
+ '@{' + object.collect { |k, v| format_powershell_value(k) + ' = ' + format_powershell_value(v) }.join('; ') + '}'
130
+ else
131
+ raise "unsupported type #{object.class} of value '#{object}'"
132
+ end
133
+ end
134
+
135
+ # Return the representative string of a PowerShell hash for a custom object property to be used in selecting or filtering.
136
+ # The script block for the expression must be passed as the string you want interpolated into the hash; this method does
137
+ # not do any of the additional work of interpolation for you as the type sits inside a code block inside a hash.
138
+ #
139
+ # @return [String] representation of a PowerShell hash with the keys 'Name' and 'Expression'
140
+ def custom_powershell_property(name, expression)
141
+ "@{Name = '#{name}'; Expression = {#{expression}}}"
142
+ end
32
143
  end
33
144
  end
34
145
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Pwsh
4
4
  # The version of the ruby-pwsh gem
5
- VERSION = '0.1.0'
5
+ VERSION = '0.5.0'
6
6
  end
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "puppetlabs-pwshlib",
3
+ "version": "0.4.1",
4
+ "author": "puppetlabs",
5
+ "summary": "Provide library code for interoperating with PowerShell.",
6
+ "license": "MIT",
7
+ "source": "https://github.com/puppetlabs/ruby-pwsh",
8
+ "project_page": "https://github.com/puppetlabs/ruby-pwsh/blob/master/pwshlib.md",
9
+ "issues_url": "https://github.com/puppetlabs/ruby-pwsh/issues",
10
+ "dependencies": [
11
+
12
+ ],
13
+ "operatingsystem_support": [
14
+ {
15
+ "operatingsystem": "CentOS",
16
+ "operatingsystemrelease": [
17
+ "7"
18
+ ]
19
+ },
20
+ {
21
+ "operatingsystem": "OracleLinux",
22
+ "operatingsystemrelease": [
23
+ "7"
24
+ ]
25
+ },
26
+ {
27
+ "operatingsystem": "RedHat",
28
+ "operatingsystemrelease": [
29
+ "8"
30
+ ]
31
+ },
32
+ {
33
+ "operatingsystem": "Scientific",
34
+ "operatingsystemrelease": [
35
+ "7"
36
+ ]
37
+ },
38
+ {
39
+ "operatingsystem": "Debian",
40
+ "operatingsystemrelease": [
41
+ "9"
42
+ ]
43
+ },
44
+ {
45
+ "operatingsystem": "Ubuntu",
46
+ "operatingsystemrelease": [
47
+ "18.04"
48
+ ]
49
+ },
50
+ {
51
+ "operatingsystem": "windows",
52
+ "operatingsystemrelease": [
53
+ "2019",
54
+ "10"
55
+ ]
56
+ },
57
+ {
58
+ "operatingsystem": "SLES",
59
+ "operatingsystemrelease": [
60
+ "15"
61
+ ]
62
+ },
63
+ {
64
+ "operatingsystem": "Darwin",
65
+ "operatingsystemrelease": [
66
+ "16"
67
+ ]
68
+ },
69
+ {
70
+ "operatingsystem": "Fedora",
71
+ "operatingsystemrelease": [
72
+ "29"
73
+ ]
74
+ }
75
+ ],
76
+ "requirements": [
77
+ {
78
+ "name": "puppet",
79
+ "version_requirement": ">= 5.5.0 < 7.0.0"
80
+ }
81
+ ],
82
+ "pdk-version": "1.13.0",
83
+ "template-url": "pdk-default#1.13.0",
84
+ "template-ref": "1.13.0-0-g66e1443"
85
+ }
@@ -0,0 +1,92 @@
1
+ # pwshlib
2
+
3
+ This module enables you to leverage the `ruby-pwsh` gem to execute PowerShell from within your Puppet providers without having to instantiate and tear down a PowerShell process for each command called.
4
+ It supports Windows PowerShell as well as PowerShell Core - if you're running **PowerShell v3+**, this gem supports you.
5
+
6
+ The `Manager` class enables you to execute and interoperate with PowerShell from within ruby, leveraging the strengths of both languages as needed.
7
+
8
+ ## Prerequisites
9
+
10
+ Include `puppetlabs-pwshlib` as a dependency in your module and you can leverage it in your providers by using a requires statement, such as in this example:
11
+
12
+ ```ruby
13
+ require 'puppet/resource_api/simple_provider'
14
+ begin
15
+ require 'ruby-pwsh'
16
+ rescue LoadError
17
+ raise 'Could not load the "ruby-pwsh" library; is the dependency module puppetlabs-pwshlib installed in this environment?'
18
+ end
19
+
20
+ # Implementation for the foo type using the Resource API.
21
+ class Puppet::Provider::Foo::Foo < Puppet::ResourceApi::SimpleProvider
22
+ def get(context)
23
+ context.debug("PowerShell Path: #{Pwsh::Manager.powershell_path}")
24
+ context.debug('Returning pre-canned example data')
25
+ [
26
+ {
27
+ name: 'foo',
28
+ ensure: 'present',
29
+ },
30
+ {
31
+ name: 'bar',
32
+ ensure: 'present',
33
+ },
34
+ ]
35
+ end
36
+
37
+ def create(context, name, should)
38
+ context.notice("Creating '#{name}' with #{should.inspect}")
39
+ end
40
+
41
+ def update(context, name, should)
42
+ context.notice("Updating '#{name}' with #{should.inspect}")
43
+ end
44
+
45
+ def delete(context, name)
46
+ context.notice("Deleting '#{name}'")
47
+ end
48
+ end
49
+ ```
50
+
51
+ Aside from adding it as a dependency to your module metadata, you will probably also want to include it in your `.fixtures.yml` file:
52
+
53
+ ```yaml
54
+ fixtures:
55
+ forge_modules:
56
+ pwshlib: "puppetlabs/pwshlib"
57
+ ```
58
+
59
+ ## Using the Library
60
+
61
+ Instantiating the manager can be done using some defaults:
62
+
63
+ ```ruby
64
+ # Instantiate the manager for Windows PowerShell, using the default path and arguments
65
+ # Note that this takes a few seconds to instantiate.
66
+ posh = Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args)
67
+ # If you try to create another manager with the same arguments it will reuse the existing one.
68
+ ps = Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args)
69
+ # Note that this time the return is very fast.
70
+ # We can also use the defaults for PowerShell Core, though these only work if PowerShell is
71
+ # installed to the default paths - if it is installed anywhere else, you'll need to specify
72
+ # the full path to the pwsh executable.
73
+ pwsh = Pwsh::Manager.instance(Pwsh::Manager.pwsh_path, Pwsh::Manager.pwsh_args)
74
+ ```
75
+
76
+ Execution can be done with relatively little additional work - pass the command string you want executed:
77
+
78
+ ```ruby
79
+ # Instantiate the Manager:
80
+ posh = Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args)
81
+ # Pretty print the output of `$PSVersionTable` to validate the version of PowerShell running
82
+ # Note that the output is a hash with a few different keys, including stdout.
83
+ Puppet.debug(posh.execute('$PSVersionTable'))
84
+ # Lets reduce the noise a little and retrieve just the version number:
85
+ # Note: We cast to a string because PSVersion is actually a Version object.
86
+ Puppet.debug(posh.execute('[String]$PSVersionTable.PSVersion'))
87
+ # We could store this output to a ruby variable if we wanted, for further use:
88
+ ps_version = posh.execute('[String]$PSVersionTable.PSVersion')[:stdout].strip
89
+ Puppet.debug("The PowerShell version of the currently running Manager is #{ps_version}")
90
+ ```
91
+
92
+ For more information, please review the [online reference documentation for the gem](https://rubydoc.info/gems/ruby-pwsh).