win_exec: refactor PS exec runner (#45334)

* win_exec: refactor PS exec runner

* more changes for PSCore compatibility

* made some changes based on the recent review

* split up module exec scripts for smaller payload

* removed C# module support to focus on just error msg improvement

* cleaned up c# test classifier code
This commit is contained in:
Jordan Borean
2018-10-03 08:55:53 +10:00
committed by Matt Davis
parent aa2f3edb49
commit e972287c35
34 changed files with 2751 additions and 1676 deletions

View File

@@ -148,8 +148,7 @@
- asyncresult is finished
- asyncresult is not changed
- asyncresult is failed
# TODO: re-enable after catastrophic failure behavior is cleaned up
# - asyncresult.msg is search('failing via exception')
- 'asyncresult.msg == "Unhandled exception while executing module: failing via exception"'
- name: echo some non ascii characters
win_command: cmd.exe /c echo über den Fußgängerübergang gehen

View File

@@ -136,7 +136,8 @@
register: become_invalid_pass
failed_when:
- '"Failed to become user " + become_test_username not in become_invalid_pass.msg'
- '"LogonUser failed (The user name or password is incorrect, Win32ErrorCode 1326)" not in become_invalid_pass.msg'
- '"LogonUser failed" not in become_invalid_pass.msg'
- '"Win32ErrorCode 1326)" not in become_invalid_pass.msg'
- name: test become with SYSTEM account
win_whoami:
@@ -206,21 +207,21 @@
become_flags: logon_type=batch invalid_flags=a
become_method: runas
register: failed_flags_invalid_key
failed_when: "failed_flags_invalid_key.msg != \"Failed to parse become_flags 'logon_type=batch invalid_flags=a': become_flags key 'invalid_flags' is not a valid runas flag, must be 'logon_type' or 'logon_flags'\""
failed_when: "failed_flags_invalid_key.msg != \"internal error: failed to parse become_flags 'logon_type=batch invalid_flags=a': become_flags key 'invalid_flags' is not a valid runas flag, must be 'logon_type' or 'logon_flags'\""
- name: test failure with invalid logon_type
vars: *become_vars
win_whoami:
become_flags: logon_type=invalid
register: failed_flags_invalid_type
failed_when: "failed_flags_invalid_type.msg != \"Failed to parse become_flags 'logon_type=invalid': become_flags logon_type value 'invalid' is not valid, valid values are: interactive, network, batch, service, unlock, network_cleartext, new_credentials\""
failed_when: "failed_flags_invalid_type.msg != \"internal error: failed to parse become_flags 'logon_type=invalid': become_flags logon_type value 'invalid' is not valid, valid values are: interactive, network, batch, service, unlock, network_cleartext, new_credentials\""
- name: test failure with invalid logon_flag
vars: *become_vars
win_whoami:
become_flags: logon_flags=with_profile,invalid
register: failed_flags_invalid_flag
failed_when: "failed_flags_invalid_flag.msg != \"Failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
failed_when: "failed_flags_invalid_flag.msg != \"internal error: failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
# Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway
- name: check if we are running on a dinosaur, neanderthal or an OS of the modern age

View File

@@ -0,0 +1,40 @@
#!powershell
#Requires -Module Ansible.ModuleUtils.Legacy
$ErrorActionPreference = "Stop"
Function Assert-Equals($actual, $expected) {
if ($actual -cne $expected) {
$call_stack = (Get-PSCallStack)[1]
$error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
Fail-Json -obj $result -message $error_msg
}
}
$result = @{
changed = $false
}
#ConvertFrom-AnsibleJso
$input_json = '{"string":"string","float":3.1415926,"dict":{"string":"string","int":1},"list":["entry 1","entry 2"],"null":null,"int":1}'
$actual = ConvertFrom-AnsibleJson -InputObject $input_json
Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
Assert-Equals -actual $actual.string.GetType() -expected ([String])
Assert-Equals -actual $actual.string -expected "string"
Assert-Equals -actual $actual.int.GetType() -expected ([Int32])
Assert-Equals -actual $actual.int -expected 1
Assert-Equals -actual $actual.null -expected $null
Assert-Equals -actual $actual.float.GetType() -expected ([Decimal])
Assert-Equals -actual $actual.float -expected 3.1415926
Assert-Equals -actual $actual.list.GetType() -expected ([Object[]])
Assert-Equals -actual $actual.list.Count -expected 2
Assert-Equals -actual $actual.list[0] -expected "entry 1"
Assert-Equals -actual $actual.list[1] -expected "entry 2"
Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
Assert-Equals -actual $actual.dict.string -expected "string"
Assert-Equals -actual $actual.dict.int -expected 1
$result.msg = "good"
Exit-Json -obj $result

View File

@@ -0,0 +1,58 @@
#!powershell
#Requires -Module Ansible.ModuleUtils.Legacy
$params = Parse-Args $args -supports_check_mode $true
$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "normal"
$result = @{
changed = $false
}
<#
This module tests various error events in PowerShell to verify our hidden trap
catches them all and outputs a pretty error message with a traceback to help
users debug the actual issue
normal - normal execution, no errors
fail - Calls Fail-Json like normal
throw - throws an exception
error - Write-Error with ErrorActionPreferenceStop
cmdlet_error - Calls a Cmdlet with an invalid error
dotnet_exception - Calls a .NET function that will throw an error
function_throw - Throws an exception in a function
proc_exit_fine - calls an executable with a non-zero exit code with Exit-Json
proc_exit_fail - calls an executable with a non-zero exit code with Fail-Json
#>
Function Test-ThrowException {
throw "exception in function"
}
if ($data -eq "normal") {
Exit-Json -obj $result
} elseif ($data -eq "fail") {
Fail-Json -obj $result -message "fail message"
} elseif ($data -eq "throw") {
throw [ArgumentException]"module is thrown"
} elseif ($data -eq "error") {
Write-Error -Message $data
} elseif ($data -eq "cmdlet_error") {
Get-Item -Path "fake:\path"
} elseif ($data -eq "dotnet_exception") {
[System.IO.Path]::GetFullPath($null)
} elseif ($data -eq "function_throw") {
Test-ThrowException
} elseif ($data -eq "proc_exit_fine") {
# verifies that if no error was actually fired and we have an output, we
# don't use the RC to validate if the module failed
&cmd.exe /c exit 2
Exit-Json -obj $result
} elseif ($data -eq "proc_exit_fail") {
&cmd.exe /c exit 2
Fail-Json -obj $result -message "proc_exit_fail"
}
# verify no exception were silently caught during our tests
Fail-Json -obj $result -message "end of module"

View File

@@ -1,4 +1,115 @@
---
- name: test normal module execution
test_fail:
register: normal
- name: assert test normal module execution
assert:
that:
- not normal is failed
- name: test fail module execution
test_fail:
data: fail
register: fail_module
ignore_errors: yes
- name: assert test fail module execution
assert:
that:
- fail_module is failed
- fail_module.msg == "fail message"
- not fail_module.exception is defined
- name: test module with exception thrown
test_fail:
data: throw
register: throw_module
ignore_errors: yes
- name: assert test module with exception thrown
assert:
that:
- throw_module is failed
- 'throw_module.msg == "Unhandled exception while executing module: module is thrown"'
- '"throw [ArgumentException]\"module is thrown\"" in throw_module.exception'
- name: test module with error msg
test_fail:
data: error
register: error_module
ignore_errors: yes
- name: assert test module with error msg
assert:
that:
- error_module is failed
- 'error_module.msg == "Unhandled exception while executing module: error"'
- '"Write-Error -Message $data" in error_module.exception'
- name: test module with cmdlet error
test_fail:
data: cmdlet_error
register: cmdlet_error
ignore_errors: yes
- name: assert test module with cmdlet error
assert:
that:
- cmdlet_error is failed
- 'cmdlet_error.msg == "Unhandled exception while executing module: Cannot find drive. A drive with the name ''fake'' does not exist."'
- '"Get-Item -Path \"fake:\\path\"" in cmdlet_error.exception'
- name: test module with .NET exception
test_fail:
data: dotnet_exception
register: dotnet_exception
ignore_errors: yes
- name: assert test module with .NET exception
assert:
that:
- dotnet_exception is failed
- 'dotnet_exception.msg == "Unhandled exception while executing module: Exception calling \"GetFullPath\" with \"1\" argument(s): \"The path is not of a legal form.\""'
- '"[System.IO.Path]::GetFullPath($null)" in dotnet_exception.exception'
- name: test module with function exception
test_fail:
data: function_throw
register: function_exception
ignore_errors: yes
- name: assert test module with function exception
assert:
that:
- function_exception is failed
- 'function_exception.msg == "Unhandled exception while executing module: exception in function"'
- '"throw \"exception in function\"" in function_exception.exception'
- '"at Test-ThrowException, <No file>: line" in function_exception.exception'
- name: test module with fail process but Exit-Json
test_fail:
data: proc_exit_fine
register: proc_exit_fine
- name: assert test module with fail process but Exit-Json
assert:
that:
- not proc_exit_fine is failed
- name: test module with fail process but Fail-Json
test_fail:
data: proc_exit_fail
register: proc_exit_fail
ignore_errors: yes
- name: assert test module with fail process but Fail-Json
assert:
that:
- proc_exit_fail is failed
- proc_exit_fail.msg == "proc_exit_fail"
- not proc_exit_fail.exception is defined
- name: test out invalid options
test_invalid_requires:
register: invalid_options
@@ -127,3 +238,13 @@
args:
executable: cmd.exe
when: become_test_username in profile_dir_out.stdout_lines[0]
- name: test common functions in exec
test_common_functions:
register: common_functions_res
- name: assert test common functions in exec
assert:
that:
- not common_functions_res is failed
- common_functions_res.msg == "good"

View File

@@ -0,0 +1,187 @@
#!powershell
#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.AddType
$ErrorActionPreference = "Stop"
$result = @{
changed = $false
}
Function Assert-Equals($actual, $expected) {
if ($actual -cne $expected) {
$call_stack = (Get-PSCallStack)[1]
$error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
Fail-Json -obj $result -message $error_msg
}
}
$code = @'
using System;
namespace Namespace1
{
public class Class1
{
public static string GetString(bool error)
{
if (error)
throw new Exception("error");
return "Hello World";
}
}
}
'@
$res = Add-CSharpType -References $code
Assert-Equals -actual $res -expected $null
$actual = [Namespace1.Class1]::GetString($false)
Assert-Equals $actual -expected "Hello World"
try {
[Namespace1.Class1]::GetString($true)
} catch {
Assert-Equals ($_.Exception.ToString().Contains("at Namespace1.Class1.GetString(Boolean error)`r`n")) -expected $true
}
$code_debug = @'
using System;
namespace Namespace2
{
public class Class2
{
public static string GetString(bool error)
{
if (error)
throw new Exception("error");
return "Hello World";
}
}
}
'@
$res = Add-CSharpType -References $code_debug -IncludeDebugInfo
Assert-Equals -actual $res -expected $null
$actual = [Namespace2.Class2]::GetString($false)
Assert-Equals $actual -expected "Hello World"
try {
[Namespace2.Class2]::GetString($true)
} catch {
$tmp_path = [System.IO.Path]::GetFullPath($env:TMP).ToLower()
Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace2.class2.getstring(boolean error) in $tmp_path")) -expected $true
Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
}
$code_tmp = @'
using System;
namespace Namespace3
{
public class Class3
{
public static string GetString(bool error)
{
if (error)
throw new Exception("error");
return "Hello World";
}
}
}
'@
$tmp_path = $env:USERPROFILE
$res = Add-CSharpType -References $code_tmp -IncludeDebugInfo -TempPath $tmp_path -PassThru
Assert-Equals -actual $res.GetType().Name -expected "RuntimeAssembly"
Assert-Equals -actual $res.Location -expected ""
Assert-Equals -actual $res.GetTypes().Length -expected 1
Assert-Equals -actual $res.GetTypes()[0].Name -expected "Class3"
$actual = [Namespace3.Class3]::GetString($false)
Assert-Equals $actual -expected "Hello World"
try {
[Namespace3.Class3]::GetString($true)
} catch {
Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace3.class3.getstring(boolean error) in $($tmp_path.ToLower())")) -expected $true
Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
}
$warning_code = @'
using System;
namespace Namespace4
{
public class Class4
{
public static string GetString(bool test)
{
if (test)
{
string a = "";
}
return "Hello World";
}
}
}
'@
$failed = $false
try {
Add-CSharpType -References $warning_code
} catch {
$failed = $true
Assert-Equals -actual ($_.Exception.Message.Contains("error CS0219: Warning as Error: The variable 'a' is assigned but its value is never used")) -expected $true
}
Assert-Equals -actual $failed -expected $true
Add-CSharpType -References $warning_code -IgnoreWarnings
$actual = [Namespace4.Class4]::GetString($true)
Assert-Equals -actual $actual -expected "Hello World"
$reference_1 = @'
using System;
using System.Web.Script.Serialization;
//AssemblyReference -Name System.Web.Extensions.dll
namespace Namespace5
{
public class Class5
{
public static string GetString()
{
return "Hello World";
}
}
}
'@
$reference_2 = @'
using System;
using Namespace5;
using System.Management.Automation;
using System.Collections;
using System.Collections.Generic;
namespace Namespace6
{
public class Class6
{
public static string GetString()
{
Hashtable hash = new Hashtable();
hash["test"] = "abc";
return Class5.GetString();
}
}
}
'@
Add-CSharpType -References $reference_1, $reference_2
$actual = [Namespace6.Class6]::GetString()
Assert-Equals -actual $actual -expected "Hello World"
$result.res = "success"
Exit-Json -obj $result

View File

@@ -0,0 +1,12 @@
#1powershell
#Requires -Module Ansible.ModuleUtils.Legacy
#AnsibleRequires -CSharpUtil Ansible.Test
$result = @{
res = [Ansible.Test.OutputTest]::GetString()
changed = $false
}
Exit-Json -obj $result

View File

@@ -0,0 +1,26 @@
//AssemblyReference -Name System.Web.Extensions.dll
using System;
using System.Collections.Generic;
using System.Web.Script.Serialization;
namespace Ansible.Test
{
public class OutputTest
{
public static string GetString()
{
Dictionary<string, object> obj = new Dictionary<string, object>();
obj["a"] = "a";
obj["b"] = 1;
return ToJson(obj);
}
private static string ToJson(object obj)
{
JavaScriptSerializer jss = new JavaScriptSerializer();
return jss.Serialize(obj);
}
}
}

View File

@@ -143,3 +143,23 @@
- assert:
that:
- privilege_util_test.data == 'success'
- name: call module with C# reference
csharp_util:
register: csharp_res
- name: assert call module with C# reference
assert:
that:
- not csharp_res is failed
- csharp_res.res == '{"a":"a","b":1}'
- name: call module with AddType tests
add_type_test:
register: add_type_test
- name: assert call module with AddType tests
assert:
that:
- not add_type_test is failed
- add_type_test.res == 'success'

View File

@@ -51,6 +51,7 @@
- win_ping_ps1_result is not changed
- win_ping_ps1_result.ping == 'bleep'
# TODO: this will have to be removed once PS basic is implemented
- name: test win_ping with extra args to verify that v2 module replacer escaping works as expected
win_ping:
data: bloop
@@ -92,71 +93,5 @@
that:
- win_ping_crash_result is failed
- win_ping_crash_result is not changed
- "'FullyQualifiedErrorId : boom' in win_ping_crash_result.module_stderr"
# TODO: fix code or tests? discrete error returns from PS are strange...
#- name: test modified win_ping that throws an exception
# action: win_ping_throw
# register: win_ping_throw_result
# ignore_errors: true
#
#- name: check win_ping_throw result
# assert:
# that:
# - win_ping_throw_result is failed
# - win_ping_throw_result is not changed
# - win_ping_throw_result.msg == 'MODULE FAILURE'
# - win_ping_throw_result.exception
# - win_ping_throw_result.error_record
#
#- name: test modified win_ping that throws a string exception
# action: win_ping_throw_string
# register: win_ping_throw_string_result
# ignore_errors: true
#
#- name: check win_ping_throw_string result
# assert:
# that:
# - win_ping_throw_string_result is failed
# - win_ping_throw_string_result is not changed
# - win_ping_throw_string_result.msg == 'no ping for you'
# - win_ping_throw_string_result.exception
# - win_ping_throw_string_result.error_record
#
#- name: test modified win_ping that has a syntax error
# action: win_ping_syntax_error
# register: win_ping_syntax_error_result
# ignore_errors: true
#
#- name: check win_ping_syntax_error result
# assert:
# that:
# - win_ping_syntax_error_result is failed
# - win_ping_syntax_error_result is not changed
# - win_ping_syntax_error_result.msg
# - win_ping_syntax_error_result.exception
#
#- name: test modified win_ping that has an error that only surfaces when strict mode is on
# action: win_ping_strict_mode_error
# register: win_ping_strict_mode_error_result
# ignore_errors: true
#
#- name: check win_ping_strict_mode_error result
# assert:
# that:
# - win_ping_strict_mode_error_result is failed
# - win_ping_strict_mode_error_result is not changed
# - win_ping_strict_mode_error_result.msg
# - win_ping_strict_mode_error_result.exception
#
#- name: test modified win_ping to verify a Set-Attr fix
# action: win_ping_set_attr data="fixed"
# register: win_ping_set_attr_result
#
#- name: check win_ping_set_attr_result result
# assert:
# that:
# - win_ping_set_attr_result is not failed
# - win_ping_set_attr_result is not changed
# - win_ping_set_attr_result.ping == 'fixed'
- 'win_ping_crash_result.msg == "Unhandled exception while executing module: boom"'
- '"throw \"boom\"" in win_ping_crash_result.exception'

View File

@@ -25,6 +25,10 @@ from lib.import_analysis import (
get_python_module_utils_imports,
)
from lib.csharp_import_analysis import (
get_csharp_module_utils_imports,
)
from lib.powershell_import_analysis import (
get_powershell_module_utils_imports,
)
@@ -168,6 +172,7 @@ class PathMapper(object):
self.units_targets = list(walk_units_targets())
self.sanity_targets = list(walk_sanity_targets())
self.powershell_targets = [t for t in self.sanity_targets if os.path.splitext(t.path)[1] == '.ps1']
self.csharp_targets = [t for t in self.sanity_targets if os.path.splitext(t.path)[1] == '.cs']
self.units_modules = set(t.module for t in self.units_targets if t.module)
self.units_paths = set(a for t in self.units_targets for a in t.aliases)
@@ -189,6 +194,7 @@ class PathMapper(object):
self.python_module_utils_imports = {} # populated on first use to reduce overhead when not needed
self.powershell_module_utils_imports = {} # populated on first use to reduce overhead when not needed
self.csharp_module_utils_imports = {} # populated on first use to reduce overhead when not needed
def get_dependent_paths(self, path):
"""
@@ -204,6 +210,9 @@ class PathMapper(object):
if ext == '.psm1':
return self.get_powershell_module_utils_usage(path)
if ext == '.cs':
return self.get_csharp_module_utils_usage(path)
if path.startswith('test/integration/targets/'):
return self.get_integration_target_usage(path)
@@ -247,6 +256,22 @@ class PathMapper(object):
return sorted(self.powershell_module_utils_imports[name])
def get_csharp_module_utils_usage(self, path):
"""
:type path: str
:rtype: list[str]
"""
if not self.csharp_module_utils_imports:
display.info('Analyzing C# module_utils imports...')
before = time.time()
self.csharp_module_utils_imports = get_csharp_module_utils_imports(self.powershell_targets, self.csharp_targets)
after = time.time()
display.info('Processed %d C# module_utils in %d second(s).' % (len(self.csharp_module_utils_imports), after - before))
name = os.path.splitext(os.path.basename(path))[0]
return sorted(self.csharp_module_utils_imports[name])
def get_integration_target_usage(self, path):
"""
:type path: str
@@ -320,7 +345,7 @@ class PathMapper(object):
return {
'units': module_name if module_name in self.units_modules else None,
'integration': self.posix_integration_by_module.get(module_name) if ext == '.py' else None,
'windows-integration': self.windows_integration_by_module.get(module_name) if ext == '.ps1' else None,
'windows-integration': self.windows_integration_by_module.get(module_name) if ext in ['.cs', '.ps1'] else None,
'network-integration': self.network_integration_by_module.get(module_name),
FOCUSED_TARGET: True,
}
@@ -328,6 +353,9 @@ class PathMapper(object):
return minimal
if path.startswith('lib/ansible/module_utils/'):
if ext == '.cs':
return minimal # already expanded using get_dependent_paths
if ext == '.psm1':
return minimal # already expanded using get_dependent_paths

View File

@@ -0,0 +1,76 @@
"""Analyze C# import statements."""
from __future__ import absolute_import, print_function
import os
import re
from lib.util import (
display,
)
def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
"""Return a dictionary of module_utils names mapped to sets of powershell file paths.
:type powershell_targets: list[TestTarget] - C# files
:type csharp_targets: list[TestTarget] - PS files
:rtype: dict[str, set[str]]
"""
module_utils = enumerate_module_utils()
imports_by_target_path = {}
for target in powershell_targets:
imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
for target in csharp_targets:
imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
imports = dict([(module_util, set()) for module_util in module_utils])
for target_path in imports_by_target_path:
for module_util in imports_by_target_path[target_path]:
imports[module_util].add(target_path)
for module_util in sorted(imports):
if not imports[module_util]:
display.warning('No imports found which use the "%s" module_util.' % module_util)
return imports
def enumerate_module_utils():
"""Return a list of available module_utils imports.
:rtype: set[str]
"""
return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
"""Return a list of module_utils imports found in the specified source file.
:type path: str
:type module_utils: set[str]
:rtype: set[str]
"""
imports = set()
if is_pure_csharp:
pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
else:
pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
with open(path, 'r') as module_file:
for line_number, line in enumerate(module_file, 1):
match = re.search(pattern, line)
if not match:
continue
import_name = match.group(1)
if import_name not in module_utils:
display.warning('%s:%d Invalid module_utils import: %s' % (path, line_number, import_name))
continue
imports.add(import_name)
return imports

View File

@@ -489,6 +489,7 @@ def is_binary_file(path):
'.cfg',
'.conf',
'.crt',
'.cs',
'.css',
'.html',
'.ini',