Added basic equivalent to PowerShell modules (#44705)

* Added basic equivalent to PowerShell modules

* changes based on latest review

* Added tests

* ignore sanity test due to how tests are set up

* Changes to work with PSCore

* Added documentation and change updated more modules

* Add some speed optimisations to AddType

* fix some issues in the doc changes

* doc changes
This commit is contained in:
Jordan Borean
2018-11-07 10:53:17 +10:00
committed by GitHub
parent 74619c2036
commit 501acae5ab
17 changed files with 3644 additions and 314 deletions

View File

@@ -649,7 +649,8 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
b_module_data = b_module_data.replace(REPLACER_WINDOWS, b'#Requires -Module Ansible.ModuleUtils.Legacy')
elif re.search(b'#Requires -Module', b_module_data, re.IGNORECASE) \
or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE)\
or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE):
or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE) \
or re.search(b'#AnsibleRequires -CSharpUtil', b_module_data, re.IGNORECASE):
module_style = 'new'
module_substyle = 'powershell'
elif REPLACER_JSONARGS in b_module_data:

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,8 @@ Function Add-CSharpType {
[Switch] Whether to return the loaded Assembly
.PARAMETER AnsibleModule
TODO - This is an AnsibleModule object that is used to derive the
TempPath and Debug values.
TempPath is set to the TmpDir property of the class
[Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values.
TempPath is set to the Tmpdir property of the class
IncludeDebugInfo is set when the Ansible verbosity is >= 3
.PARAMETER TempPath
@@ -69,8 +68,8 @@ Function Add-CSharpType {
}
# pattern used to find referenced assemblies in the code
$assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
$no_warn_pattern = "^//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
$assembly_pattern = [Regex]"//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
$no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
# PSCore vs PSDesktop use different methods to compile the code,
# PSCore uses Roslyn and can compile the code purely in memory
@@ -99,29 +98,26 @@ Function Add-CSharpType {
foreach ($reference in $References) {
# scan through code and add any assemblies that match
# //AssemblyReference -Name ... [-CLR Core]
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
try {
while ($null -ne ($line = $sr.ReadLine())) {
if ($line -imatch $assembly_pattern) {
# verify the reference is not for .NET Framework
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
continue
}
$assembly_path = $Matches.Name
if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) {
$assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path
}
$assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null
}
if ($line -imatch $no_warn_pattern) {
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
continue
}
$ignore_warnings.Add($Matches.Name, [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress)
}
# //NoWarn -Name ... [-CLR Core]
$assembly_matches = $assembly_pattern.Matches($reference)
foreach ($match in $assembly_matches) {
$clr = $match.Groups["CLR"].Value
if ($clr -and $clr -ne "Core") {
continue
}
} finally {
$sr.Close()
$assembly_path = $match.Groups["Name"]
if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) {
$assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path
}
$assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null
}
$warn_matches = $no_warn_pattern.Matches($reference)
foreach ($match in $warn_matches) {
$clr = $match.Groups["CLR"].Value
if ($clr -and $clr -ne "Core") {
continue
}
$ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress)
}
$syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
}
@@ -203,7 +199,7 @@ Function Add-CSharpType {
# configure compile options based on input
if ($PSCmdlet.ParameterSetName -eq "Module") {
$temp_path = $AnsibleModule.TmpDir
$temp_path = $AnsibleModule.Tmpdir
$include_debug = $AnsibleModule.Verbosity -ge 3
} else {
$temp_path = $TempPath
@@ -231,34 +227,32 @@ Function Add-CSharpType {
# create a code snippet for each reference and check if we need
# to reference any extra assemblies
# //AssemblyReference -Name ... [-CLR Framework]
$ignore_warnings = [System.Collections.ArrayList]@()
$compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
foreach ($reference in $References) {
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
try {
while ($null -ne ($line = $sr.ReadLine())) {
if ($line -imatch $assembly_pattern) {
# verify the reference is not for .NET Core
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
continue
}
$assemblies.Add($Matches.Name) > $null
}
if ($line -imatch $no_warn_pattern) {
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
continue
}
$warning_id = $Matches.Name
# /nowarn should only contain the numeric part
if ($warning_id.StartsWith("CS")) {
$warning_id = $warning_id.Substring(2)
}
$ignore_warnings.Add($warning_id) > $null
}
# scan through code and add any assemblies that match
# //AssemblyReference -Name ... [-CLR Framework]
# //NoWarn -Name ... [-CLR Framework]
$assembly_matches = $assembly_pattern.Matches($reference)
foreach ($match in $assembly_matches) {
$clr = $match.Groups["CLR"].Value
if ($clr -and $clr -ne "Framework") {
continue
}
} finally {
$sr.Close()
$assemblies.Add($match.Groups["Name"].Value) > $null
}
$warn_matches = $no_warn_pattern.Matches($reference)
foreach ($match in $warn_matches) {
$clr = $match.Groups["CLR"].Value
if ($clr -and $clr -ne "Framework") {
continue
}
$warning_id = $match.Groups["Name"].Value
# /nowarn should only contain the numeric part
if ($warning_id.StartsWith("CS")) {
$warning_id = $warning_id.Substring(2)
}
$ignore_warnings.Add($warning_id) > $null
}
$compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
}
@@ -270,7 +264,7 @@ Function Add-CSharpType {
# compile the code together and check for errors
$provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
$compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units.ToArray())
$compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units)
if ($compile.Errors.HasErrors) {
$msg = "Failed to compile C# code: "
foreach ($e in $compile.Errors) {

View File

@@ -3,35 +3,36 @@
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy
#AnsibleRequires -CSharpUtil Ansible.Basic
$ErrorActionPreference = "Stop"
$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() | ForEach-Object { $_.ToString() }
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() }
$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues()
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues()
$params = Parse-Args $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "exported", "present"
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty ($state -eq "present" -or $state -eq "exported")
$thumbprint = Get-AnsibleParam -obj $params -name "thumbprint" -type "str" -failifempty ($state -eq "exported")
$store_name = Get-AnsibleParam -obj $params -name "store_name" -type "str" -default "My" -validateset $store_name_values
$store_location = Get-AnsibleParam -obj $params -name "store_location" -type "str" -default "LocalMachine" -validateset $store_location_values
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
$key_exportable = Get-AnsibleParam -obj $params -name "key_exportable" -type "bool" -default $true
$key_storage = Get-AnsibleParam -obj $params -name "key_storage" -type "str" -default "default" -validateset "default", "machine", "user"
$file_type = Get-AnsibleParam -obj $params -name "file_type" -type "str" -default "der" -validateset "der", "pem", "pkcs12"
$result = @{
changed = $false
thumbprints = @()
$spec = @{
options = @{
state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" }
path = @{ type = "path" }
thumbprint = @{ type = "str" }
store_name = @{ type = "str"; default = "My"; choices = $store_name_values }
store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values }
password = @{ type = "str"; no_log = $true }
key_exportable = @{ type = "bool"; default = $true }
key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" }
file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" }
}
required_if = @(
@("state", "absent", @("path", "thumbprint"), $true),
@("state", "exported", @("path", "thumbprint")),
@("state", "present", @("path"))
)
supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
Function Get-CertFile($path, $password, $key_exportable, $key_storage) {
Function Get-CertFile($module, $path, $password, $key_exportable, $key_storage) {
# parses a certificate file and returns X509Certificate2Collection
if (-not (Test-Path -Path $path -PathType Leaf)) {
Fail-Json -obj $result -message "File at '$path' either does not exist or is not a file"
$module.FailJson("File at '$path' either does not exist or is not a file")
}
# must set at least the PersistKeySet flag so that the PrivateKey
@@ -52,13 +53,13 @@ Function Get-CertFile($path, $password, $key_exportable, $key_storage) {
try {
$certs.Import($path, $password, $store_flags)
} catch {
Fail-Json -obj $result -message "Failed to load cert from file: $($_.Exception.Message)"
$module.FailJson("Failed to load cert from file: $($_.Exception.Message)", $_)
}
return $certs
}
Function New-CertFile($cert, $path, $type, $password) {
Function New-CertFile($module, $cert, $path, $type, $password) {
$content_type = switch ($type) {
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
@@ -72,20 +73,20 @@ Function New-CertFile($cert, $path, $type, $password) {
$missing_key = $true
}
if ($missing_key) {
Fail-Json -obj $result -message "Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accesible by the current user"
$module.FailJson("Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accesible by the current user")
}
}
if (Test-Path -Path $path) {
Remove-Item -Path $path -Force
$result.changed = $true
$module.Result.changed = $true
}
try {
$cert_bytes = $cert.Export($content_type, $password)
} catch {
Fail-Json -obj $result -message "Failed to export certificate as bytes: $($_.Exception.Message)"
$module.FailJson("Failed to export certificate as bytes: $($_.Exception.Message)", $_)
}
# Need to manually handle a PEM file
if ($type -eq "pem") {
$cert_content = "-----BEGIN CERTIFICATE-----`r`n"
@@ -95,26 +96,26 @@ Function New-CertFile($cert, $path, $type, $password) {
$file_encoding = [System.Text.Encoding]::ASCII
$cert_bytes = $file_encoding.GetBytes($cert_content)
} elseif ($type -eq "pkcs12") {
$result.key_exported = $false
$module.Result.key_exported = $false
if ($cert.PrivateKey -ne $null) {
$result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
$module.Result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
}
}
if (-not $check_mode) {
if (-not $module.CheckMode) {
try {
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
} catch [System.ArgumentNullException] {
Fail-Json -obj $result -message "Failed to write cert to file, cert was null: $($_.Exception.Message)"
$module.FailJson("Failed to write cert to file, cert was null: $($_.Exception.Message)", $_)
} catch [System.IO.IOException] {
Fail-Json -obj $result -message "Failed to write cert to file due to IO exception: $($_.Exception.Message)"
$module.FailJson("Failed to write cert to file due to IO Exception: $($_.Exception.Message)", $_)
} catch [System.UnauthorizedAccessException, System>Security.SecurityException] {
Fail-Json -obj $result -message "Failed to write cert to file due to permission: $($_.Exception.Message)"
$module.FailJson("Failed to write cert to file due to permissions: $($_.Exception.Message)", $_)
} catch {
Fail-Json -obj $result -message "Failed to write cert to file: $($_.Exception.Message)"
$module.FailJson("Failed to write cert to file: $($_.Exception.Message)", $_)
}
}
$result.changed = $true
$module.Result.changed = $true
}
Function Get-CertFileType($path, $password) {
@@ -147,70 +148,78 @@ Function Get-CertFileType($path, $password) {
}
}
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::$store_name
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]::$store_location
$state = $module.Params.state
$path = $module.Params.path
$thumbprint = $module.Params.thumbprint
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$($module.Params.store_name)"
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)"
$password = $module.Params.password
$key_exportable = $module.Params.key_exportable
$key_storage = $module.Params.key_storage
$file_type = $module.Params.file_type
$module.Result.thumbprints = @()
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
try {
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
} catch [System.Security.Cryptography.CryptographicException] {
Fail-Json -obj $result -message "Unable to open the store as it is not readable: $($_.Exception.Message)"
$module.FailJson("Unable to open the store as it is not readable: $($_.Exception.Message)", $_)
} catch [System.Security.SecurityException] {
Fail-Json -obj $result -message "Unable to open the store with the current permissions: $($_.Exception.Message)"
$module.FailJson("Unable to open the store with the current permissions: $($_.Exception.Message)", $_)
} catch {
Fail-Json -obj $result -message "Unable to open the store: $($_.Exception.Message)"
$module.FailJson("Unable to open the store: $($_.Exception.Message)", $_)
}
$store_certificates = $store.Certificates
try {
if ($state -eq "absent") {
$cert_thumbprints = @()
if ($path -ne $null) {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
foreach ($cert in $certs) {
$cert_thumbprints += $cert.Thumbprint
}
} elseif ($thumbprint -ne $null) {
$cert_thumbprints += $thumbprint
} else {
Fail-Json -obj $result -message "Either path or thumbprint must be set when state=absent"
}
foreach ($cert_thumbprint in $cert_thumbprints) {
$result.thumbprints += $cert_thumbprint
$module.Result.thumbprints += $cert_thumbprint
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
if ($found_certs.Count -gt 0) {
foreach ($found_cert in $found_certs) {
try {
if (-not $check_mode) {
if (-not $module.CheckMode) {
$store.Remove($found_cert)
}
} catch [System.Security.SecurityException] {
Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint' with the current permissions: $($_.Exception.Message)"
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint' with current permissions: $($_.Exception.Message)", $_)
} catch {
Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)"
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)", $_)
}
$result.changed = $true
$module.Result.changed = $true
}
}
}
} elseif ($state -eq "exported") {
# TODO: Add support for PKCS7 and exporting a cert chain
$result.thumbprints += $thumbprint
$module.Result.thumbprints += $thumbprint
$export = $true
if (Test-Path -Path $path -PathType Container) {
Fail-Json -obj $result -message "Cannot export cert to path '$path' as it is a directory"
$module.FailJson("Cannot export cert to path '$path' as it is a directory")
} elseif (Test-Path -Path $path -PathType Leaf) {
$actual_cert_type = Get-CertFileType -path $path -password $password
if ($actual_cert_type -eq $file_type) {
try {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
} catch {
# failed to load the file so we set the thumbprint to something
# that will fail validation
$certs = @{Thumbprint = $null}
}
if ($certs.Thumbprint -eq $thumbprint) {
$export = $false
}
@@ -220,27 +229,27 @@ try {
if ($export) {
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
if ($found_certs.Count -ne 1) {
Fail-Json -obj $result -message "Found $($found_certs.Count) certs when only expecting 1"
$module.FailJson("Found $($found_certs.Count) certs when only expecting 1")
}
New-CertFile -cert $found_certs -path $path -type $file_type -password $password
New-CertFile -module $module -cert $found_certs -path $path -type $file_type -password $password
}
} else {
$certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
foreach ($cert in $certs) {
$result.thumbprints += $cert.Thumbprint
$module.Result.thumbprints += $cert.Thumbprint
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
if ($found_certs.Count -eq 0) {
try {
if (-not $check_mode) {
if (-not $module.CheckMode) {
$store.Add($cert)
}
} catch [System.Security.Cryptography.CryptographicException] {
Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)"
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)", $_)
} catch {
Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)"
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)", $_)
}
$result.changed = $true
$module.Result.changed = $true
}
}
}
@@ -248,4 +257,4 @@ try {
$store.Close()
}
Exit-Json -obj $result
$module.ExitJson()

View File

@@ -3,67 +3,59 @@
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy
#AnsibleRequires -CSharpUtil Ansible.Basic
$ErrorActionPreference = "Stop"
$spec = @{
options = @{
name = @{ type = "str"; required = $true }
level = @{ type = "str"; choices = "machine", "process", "user"; required = $true }
state = @{ type = "str"; choices = "absent", "present"; default = "present" }
value = @{ type = "str" }
}
required_if = @(,@("state", "present", @("value")))
supports_check_mode = $true
}
$params = Parse-Args -arguments $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
$value = Get-AnsibleParam -obj $params -name "value" -type "str"
$level = Get-AnsibleParam -obj $params -name "level" -type "str" -validateSet "machine","user","process" -failifempty $true
$name = $module.Params.name
$level = $module.Params.level
$state = $module.Params.state
$value = $module.Params.value
$before_value = [Environment]::GetEnvironmentVariable($name, $level)
$result = @{
before_value = $before_value
changed = $false
value = $value
}
$module.Result.before_value = $before_value
$module.Result.value = $value
# When removing environment, set value to $null if set
if ($state -eq "absent" -and $value) {
Add-Warning -obj $result -message "When removing environment variable '$name' it should not have a value '$value' set"
$module.Warn("When removing environment variable '$name' it should not have a value '$value' set")
$value = $null
} elseif ($state -eq "present" -and (-not $value)) {
Fail-Json -obj $result -message "When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent"
$module.FailJson("When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent")
}
$module.Diff.before = @{ $level = @{} }
if ($before_value) {
$module.Diff.before.$level.$name = $before_value
}
$module.Diff.after = @{ $level = @{} }
if ($value) {
$module.Diff.after.$level.$name = $value
}
if ($state -eq "present" -and $before_value -ne $value) {
if (-not $check_mode) {
if (-not $module.CheckMode) {
[Environment]::SetEnvironmentVariable($name, $value, $level)
}
$result.changed = $true
if ($diff_mode) {
if ($before_value -eq $null) {
$result.diff = @{
prepared = " [$level]`n+$name = $value`n"
}
} else {
$result.diff = @{
prepared = " [$level]`n-$name = $before_value`n+$name = $value`n"
}
}
}
$module.Result.changed = $true
} elseif ($state -eq "absent" -and $before_value -ne $null) {
if (-not $check_mode) {
if (-not $module.CheckMode) {
[Environment]::SetEnvironmentVariable($name, $null, $level)
}
$result.changed = $true
if ($diff_mode) {
$result.diff = @{
prepared = " [$level]`n-$name = $before_value`n"
}
}
$module.Result.changed = $true
}
Exit-Json -obj $result
$module.ExitJson()

View File

@@ -2,21 +2,20 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy
#AnsibleRequires -CSharpUtil Ansible.Basic
$ErrorActionPreference = "Stop"
$params = Parse-Args $args -supports_check_mode $true
$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "pong"
$spec = @{
options = @{
data = @{ type = "str"; default = "pong" }
}
supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$data = $module.Params.data
if ($data -eq "crash") {
throw "boom"
}
$result = @{
changed = $false
ping = $data
}
Exit-Json $result
$module.Result.ping = $data
$module.ExitJson()

View File

@@ -4,65 +4,80 @@
# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy
#AnsibleRequires -CSharpUtil Ansible.Basic
#Requires -Module Ansible.ModuleUtils.CamelConversion
#Requires -Module Ansible.ModuleUtils.FileUtil
#Requires -Module Ansible.ModuleUtils.Legacy
$ErrorActionPreference = "Stop"
$spec = @{
options = @{
url = @{ type = "str"; required = $true }
method = @{
type = "str"
default = "GET"
choices = "CONNECT", "DELETE", "GET", "HEAD", "MERGE", "OPTIONS", "PATCH", "POST", "PUT", "REFRESH", "TRACE"
}
content_type = @{ type = "str" }
headers = @{ type = "dict" }
body = @{ type = "raw" }
dest = @{ type = "path" }
user = @{ type = "str" }
password = @{ type = "str"; no_log = $true }
force_basic_auth = @{ type = "bool"; default = $false }
creates = @{ type = "path" }
removes = @{ type = "path" }
follow_redirects = @{
type = "str"
default = "safe"
choices = "all", "none", "safe"
}
maximum_redirection = @{ type = "int"; default = 50 }
return_content = @{ type = "bool"; default = $false }
status_code = @{ type = "list"; elements = "int"; default = @(200) }
timeout = @{ type = "int"; default = 30 }
validate_certs = @{ type = "bool"; default = $true }
client_cert = @{ type = "path" }
client_cert_password = @{ type = "str"; no_log = $true }
}
supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$params = Parse-Args -arguments $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$url = Get-AnsibleParam -obj $params -name "url" -type "str" -failifempty $true
$method = Get-AnsibleParam -obj $params "method" -type "str" -default "GET" -validateset "CONNECT","DELETE","GET","HEAD","MERGE","OPTIONS","PATCH","POST","PUT","REFRESH","TRACE"
$content_type = Get-AnsibleParam -obj $params -name "content_type" -type "str"
$headers = Get-AnsibleParam -obj $params -name "headers"
$body = Get-AnsibleParam -obj $params -name "body"
$dest = Get-AnsibleParam -obj $params -name "dest" -type "path"
$user = Get-AnsibleParam -obj $params -name "user" -type "str"
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
$force_basic_auth = Get-AnsibleParam -obj $params -name "force_basic_auth" -type "bool" -default $false
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
$follow_redirects = Get-AnsibleParam -obj $params -name "follow_redirects" -type "str" -default "safe" -validateset "all","none","safe"
$maximum_redirection = Get-AnsibleParam -obj $params -name "maximum_redirection" -type "int" -default 50
$return_content = Get-AnsibleParam -obj $params -name "return_content" -type "bool" -default $false
$status_code = Get-AnsibleParam -obj $params -name "status_code" -type "list" -default @(200)
$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 30
$validate_certs = Get-AnsibleParam -obj $params -name "validate_certs" -type "bool" -default $true
$client_cert = Get-AnsibleParam -obj $params -name "client_cert" -type "path"
$client_cert_password = Get-AnsibleParam -obj $params -name "client_cert_password" -type "str"
$url = $module.Params.url
$method = $module.Params.method
$content_type = $module.Params.content_type
$headers = $module.Params.headers
$body = $module.Params.body
$dest = $module.Params.dest
$user = $module.Params.user
$password = $module.Params.password
$force_basic_auth = $module.Params.force_basic_auth
$creates = $module.Params.creates
$removes = $module.Params.removes
$follow_redirects = $module.Params.follow_redirects
$maximum_redirection = $module.Params.maximum_redirection
$return_content = $module.Params.return_content
$status_code = $module.Params.status_code
$timeout = $module.Params.timeout
$validate_certs = $module.Params.validate_certs
$client_cert = $module.Params.client_cert
$client_cert_password = $module.Params.client_cert_password
$JSON_CANDIDATES = @('text', 'json', 'javascript')
$result = @{
changed = $false
elapsed = 0
url = $url
}
$module.Result.elapsed = 0
$module.Result.url = $url
if ($creates -and (Test-AnsiblePath -Path $creates)) {
$result.skipped = $true
Exit-Json -obj $result -message "The 'creates' file or directory ($creates) already exists."
$module.Result.skipped = $true
$module.Result.msg = "The 'creates' file or directory ($creates) already exists."
$module.ExitJson()
}
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
$result.skipped = $true
Exit-Json -obj $result -message "The 'removes' file or directory ($removes) does not exist."
}
if ($status_code) {
$status_code = foreach ($code in $status_code) {
try {
[int]$code
}
catch [System.InvalidCastException] {
Fail-Json -obj $result -message "Failed to convert '$code' to an integer. Status codes must be provided in numeric format."
}
}
$module.Result.skipped = $true
$module.Result.msg = "The 'removes' file or directory ($removes) does not exist."
$module.ExitJson()
}
# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
@@ -114,7 +129,7 @@ if ($headers) {
$req_headers = New-Object -TypeName System.Net.WebHeaderCollection
foreach ($header in $headers.GetEnumerator()) {
# some headers need to be set on the property itself
switch ($header.Name) {
switch ($header.Key) {
Accept { $client.Accept = $header.Value }
Connection { $client.Connection = $header.Value }
Content-Length { $client.ContentLength = $header.Value }
@@ -130,7 +145,7 @@ if ($headers) {
$client.TransferEncoding = $header.Value
}
User-Agent { $client.UserAgent = $header.Value }
default { $req_headers.Add($header.Name, $header.Value) }
default { $req_headers.Add($header.Key, $header.Value) }
}
}
$client.Headers.Add($req_headers)
@@ -138,15 +153,15 @@ if ($headers) {
if ($client_cert) {
if (-not (Test-AnsiblePath -Path $client_cert)) {
Fail-Json -obj $result -message "Client certificate '$client_cert' does not exist"
$module.FailJson("Client certificate '$client_cert' does not exit")
}
try {
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection -ArgumentList $client_cert, $client_cert_password
$client.ClientCertificates = $certs
} catch [System.Security.Cryptography.CryptographicException] {
Fail-Json -obj $result -message "Failed to read client certificate '$client_cert': $($_.Exception.Message)"
$module.FailJson("Failed to read client certificate '$client_cert': $($_.Exception.Message)", $_)
} catch {
Fail-Json -obj $result -message "Unhandled exception when reading client certificate at '$client_cert': $($_.Exception.Message)"
$module.FailJson("Unhandled exception when reading client certificate at '$client_cert': $($_.Exception.Message)", $_)
}
}
@@ -160,7 +175,7 @@ if ($user -and $password) {
$client.Credentials = $credential
}
} elseif ($user -or $password) {
Add-Warning -obj $result -message "Both 'user' and 'password' parameters are required together, skipping authentication"
$module.Warn("Both 'user' and 'password' parameters are required together, skipping authentication")
}
if ($null -ne $body) {
@@ -187,7 +202,7 @@ $module_start = Get-Date
try {
$response = $client.GetResponse()
} catch [System.Net.WebException] {
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$response = $null
if ($_.Exception.PSObject.Properties.Name -match "Response") {
# was a non-successful response but we at least have a response and
@@ -198,17 +213,17 @@ try {
# in the case a response (or empty response) was on the exception like in
# a timeout scenario, we should still fail
if ($null -eq $response) {
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
Fail-Json -obj $result -message "WebException occurred when sending web request: $($_.Exception.Message)"
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$module.FailJson("WebException occurred when sending web request: $($_.Exception.Message)", $_)
}
} catch [System.Net.ProtocolViolationException] {
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
Fail-Json -obj $result -message "ProtocolViolationException when sending web request: $($_.Exception.Message)"
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$module.FailJson("ProtocolViolationException when sending web request: $($_.Exception.Message)", $_)
} catch {
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
Fail-Json -obj $result -message "Unhandled exception occured when sending web request. Exception: $($_.Exception.Message)"
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$module.FailJson("Unhandled exception occured when sending web request. Exception: $($_.Exception.Message)", $_)
}
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
ForEach ($prop in $response.psobject.properties) {
$result_key = Convert-StringToSnakeCase -string $prop.Name
@@ -217,7 +232,7 @@ ForEach ($prop in $response.psobject.properties) {
if ($prop_value -is [System.DateTime]) {
$prop_value = $prop_value.ToString("o", [System.Globalization.CultureInfo]::InvariantCulture)
}
$result.$result_key = $prop_value
$module.Result.$result_key = $prop_value
}
# manually get the headers as not all of them are in the response properties
@@ -225,7 +240,7 @@ foreach ($header_key in $response.Headers.GetEnumerator()) {
$header_value = $response.Headers[$header_key]
$header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion
$header_key = Convert-StringToSnakeCase -string $header_key
$result.$header_key = $header_value
$module.Result.$header_key = $header_value
}
# we only care about the return body if we need to return the content or create a file
@@ -241,10 +256,10 @@ if ($return_content -or $dest) {
if ($return_content) {
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
$content_bytes = $memory_st.ToArray()
$result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes)
if ($result.ContainsKey("content_type") -and $result.content_type -Match ($JSON_CANDIDATES -join '|')) {
$module.Result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes)
if ($module.Result.ContainsKey("content_type") -and $module.Result.content_type -Match ($JSON_CANDIDATES -join '|')) {
try {
$result.json = ConvertFrom-Json -InputObject $result.content
$module.Result.json = ([Ansible.Basic.AnsibleModule]::FromJson($module.Result.content))
} catch [System.ArgumentException] {
# Simply continue, since 'text' might be anything
}
@@ -266,8 +281,8 @@ if ($return_content -or $dest) {
}
}
$result.changed = $changed
if ($changed -and (-not $check_mode)) {
$module.Result.changed = $changed
if ($changed -and (-not $module.CheckMode)) {
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
$file_stream = [System.IO.File]::Create($dest)
try {
@@ -284,7 +299,7 @@ if ($return_content -or $dest) {
}
if ($status_code -notcontains $response.StatusCode) {
Fail-Json -obj $result -message "Status code of request '$([int]$response.StatusCode)' is not in list of valid status codes $status_code : '$($response.StatusCode)'."
$module.FailJson("Status code of request '$([int]$response.StatusCode)' is not in list of valid status codes $status_code : $($response.StatusCode)'.")
}
Exit-Json -obj $result
$module.ExitJson()