Add relative module_util support for powershell (#68321)

* Add relative module_util support for powershell

* Added ansible-test classification support
This commit is contained in:
Jordan Borean 2020-04-01 09:01:18 +10:00 committed by GitHub
parent a20a527014
commit 6db66bcadd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 164 additions and 37 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- windows collections - Support relative module util imports in PowerShell modules and module_utils

View file

@ -411,8 +411,8 @@ else:
# Do this instead of getting site-packages from distutils.sysconfig so we work when we # Do this instead of getting site-packages from distutils.sysconfig so we work when we
# haven't been installed # haven't been installed
site_packages = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) site_packages = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
CORE_LIBRARY_PATH_RE = re.compile(r'%s/(?P<path>ansible/modules/.*)\.py$' % site_packages) CORE_LIBRARY_PATH_RE = re.compile(r'%s/(?P<path>ansible/modules/.*)\.(py|ps1)$' % site_packages)
COLLECTION_PATH_RE = re.compile(r'/(?P<path>ansible_collections/[^/]+/[^/]+/plugins/modules/.*)\.py$') COLLECTION_PATH_RE = re.compile(r'/(?P<path>ansible_collections/[^/]+/[^/]+/plugins/modules/.*)\.(py|ps1)$')
# Detect new-style Python modules by looking for required imports: # Detect new-style Python modules by looking for required imports:
# import ansible_collections.[my_ns.my_col.plugins.module_utils.my_module_util] # import ansible_collections.[my_ns.my_col.plugins.module_utils.my_module_util]
@ -1000,6 +1000,17 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
output = BytesIO() output = BytesIO()
py_module_names = set() py_module_names = set()
try:
remote_module_fqn = _get_ansible_module_fqn(module_path)
except ValueError:
# Modules in roles currently are not found by the fqn heuristic so we
# fallback to this. This means that relative imports inside a module from
# a role may fail. Absolute imports should be used for future-proofness.
# People should start writing collections instead of modules in roles so we
# may never fix this
display.debug('ANSIBALLZ: Could not determine module FQN')
remote_module_fqn = 'ansible.modules.%s' % module_name
if module_substyle == 'python': if module_substyle == 'python':
params = dict(ANSIBLE_MODULE_ARGS=module_args,) params = dict(ANSIBLE_MODULE_ARGS=module_args,)
try: try:
@ -1013,17 +1024,6 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
display.warning(u'Bad module compression string specified: %s. Using ZIP_STORED (no compression)' % module_compression) display.warning(u'Bad module compression string specified: %s. Using ZIP_STORED (no compression)' % module_compression)
compression_method = zipfile.ZIP_STORED compression_method = zipfile.ZIP_STORED
try:
remote_module_fqn = _get_ansible_module_fqn(module_path)
except ValueError:
# Modules in roles currently are not found by the fqn heuristic so we
# fallback to this. This means that relative imports inside a module from
# a role may fail. Absolute imports should be used for future-proofness.
# People should start writing collections instead of modules in roles so we
# may never fix this
display.debug('ANSIBALLZ: Could not determine module FQN')
remote_module_fqn = 'ansible.modules.%s' % module_name
lookup_path = os.path.join(C.DEFAULT_LOCAL_TMP, 'ansiballz_cache') lookup_path = os.path.join(C.DEFAULT_LOCAL_TMP, 'ansiballz_cache')
cached_module_filename = os.path.join(lookup_path, "%s-%s" % (module_name, module_compression)) cached_module_filename = os.path.join(lookup_path, "%s-%s" % (module_name, module_compression))
@ -1191,7 +1191,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
b_module_data = ps_manifest._create_powershell_wrapper( b_module_data = ps_manifest._create_powershell_wrapper(
b_module_data, module_path, module_args, environment, b_module_data, module_path, module_args, environment,
async_timeout, become, become_method, become_user, become_password, async_timeout, become, become_method, become_user, become_password,
become_flags, module_substyle, task_vars become_flags, module_substyle, task_vars, remote_module_fqn
) )
elif module_substyle == 'jsonargs': elif module_substyle == 'jsonargs':

View file

@ -39,7 +39,7 @@ class PSModuleDepFinder(object):
self.become = False self.become = False
self._re_cs_module = [ self._re_cs_module = [
# Reference C# module_util in another C# util # Reference C# module_util in another C# util, this must always be the fully qualified name.
# 'using ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}' # 'using ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}'
re.compile(to_bytes(r'(?i)^using\s((Ansible\..+)|' re.compile(to_bytes(r'(?i)^using\s((Ansible\..+)|'
r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+));\s*$')), r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+));\s*$')),
@ -49,8 +49,10 @@ class PSModuleDepFinder(object):
# Reference C# module_util in a PowerShell module # Reference C# module_util in a PowerShell module
# '#AnsibleRequires -CSharpUtil Ansible.{name}' # '#AnsibleRequires -CSharpUtil Ansible.{name}'
# '#AnsibleRequires -CSharpUtil ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}' # '#AnsibleRequires -CSharpUtil ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}'
# '#AnsibleRequires -CSharpUtil ..module_utils.{name}'
re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+((Ansible\..+)|' re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+((Ansible\..+)|'
r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+))')), r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+)|'
r'(\.[\w\.]+))')),
] ]
self._re_ps_module = [ self._re_ps_module = [
@ -58,10 +60,12 @@ class PSModuleDepFinder(object):
# '#Requires -Module Ansible.ModuleUtils.{name} # '#Requires -Module Ansible.ModuleUtils.{name}
re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)')), re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)')),
# New way of referencing a builtin and collection module_util # New way of referencing a builtin and collection module_util
# '#AnsibleRequires -PowerShell ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}'
# '#AnsibleRequires -PowerShell Ansible.ModuleUtils.{name}' # '#AnsibleRequires -PowerShell Ansible.ModuleUtils.{name}'
re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-powershell\s+(((Ansible\.ModuleUtils\..+))|' # '#AnsibleRequires -PowerShell ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}'
r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+))')), # '#AnsibleRequires -PowerShell ..module_utils.{name}'
re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-powershell\s+((Ansible\.ModuleUtils\..+)|'
r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+)|'
r'(\.[\w\.]+))')),
] ]
self._re_wrapper = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-wrapper\s+(\w*)')) self._re_wrapper = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-wrapper\s+(\w*)'))
@ -69,7 +73,7 @@ class PSModuleDepFinder(object):
self._re_os_version = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$')) self._re_os_version = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$'))
self._re_become = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-become$')) self._re_become = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-become$'))
def scan_module(self, module_data, wrapper=False, powershell=True): def scan_module(self, module_data, fqn=None, wrapper=False, powershell=True):
lines = module_data.split(b'\n') lines = module_data.split(b'\n')
module_utils = set() module_utils = set()
if wrapper: if wrapper:
@ -80,9 +84,9 @@ class PSModuleDepFinder(object):
if powershell: if powershell:
checks = [ checks = [
# PS module contains '#Requires -Module Ansible.ModuleUtils.*' # PS module contains '#Requires -Module Ansible.ModuleUtils.*'
# PS module contains '#AnsibleRequires -Powershell Ansible.*' (or FQ collections module_utils ref) # PS module contains '#AnsibleRequires -Powershell Ansible.*' (or collections module_utils ref)
(self._re_ps_module, self.ps_modules, ".psm1"), (self._re_ps_module, self.ps_modules, ".psm1"),
# PS module contains '#AnsibleRequires -CSharpUtil Ansible.*' # PS module contains '#AnsibleRequires -CSharpUtil Ansible.*' (or collections module_utils ref)
(self._re_cs_in_ps_module, cs_utils, ".cs"), (self._re_cs_in_ps_module, cs_utils, ".cs"),
] ]
else: else:
@ -101,7 +105,7 @@ class PSModuleDepFinder(object):
module_util_name = to_text(match.group(1).rstrip()) module_util_name = to_text(match.group(1).rstrip())
if module_util_name not in check[1].keys(): if module_util_name not in check[1].keys():
module_utils.add((module_util_name, check[2])) module_utils.add((module_util_name, check[2], fqn))
break break
@ -153,8 +157,11 @@ class PSModuleDepFinder(object):
self.scan_module(b_data, wrapper=True, powershell=True) self.scan_module(b_data, wrapper=True, powershell=True)
def _add_module(self, name, wrapper=False): def _add_module(self, name, wrapper=False):
m, ext = name m, ext, fqn = name
m = to_text(m) m = to_text(m)
util_fqn = None
if m.startswith("Ansible."): if m.startswith("Ansible."):
# Builtin util, use plugin loader to get the data # Builtin util, use plugin loader to get the data
mu_path = ps_module_utils_loader.find_plugin(m, ext) mu_path = ps_module_utils_loader.find_plugin(m, ext)
@ -166,14 +173,25 @@ class PSModuleDepFinder(object):
module_util_data = to_bytes(_slurp(mu_path)) module_util_data = to_bytes(_slurp(mu_path))
else: else:
# Collection util, load the package data based on the util import. # Collection util, load the package data based on the util import.
submodules = tuple(m.split("."))
submodules = m.split(".")
if m.startswith('.'):
fqn_submodules = fqn.split('.')
for submodule in submodules:
if submodule:
break
del fqn_submodules[-1]
submodules = fqn_submodules + [s for s in submodules if s]
n_package_name = to_native('.'.join(submodules[:-1]), errors='surrogate_or_strict') n_package_name = to_native('.'.join(submodules[:-1]), errors='surrogate_or_strict')
n_resource_name = to_native(submodules[-1] + ext, errors='surrogate_or_strict') n_resource_name = to_native(submodules[-1] + ext, errors='surrogate_or_strict')
try: try:
module_util = import_module(to_native(n_package_name)) module_util = import_module(n_package_name)
module_util_data = to_bytes(pkgutil.get_data(n_package_name, n_resource_name), module_util_data = to_bytes(pkgutil.get_data(n_package_name, n_resource_name),
errors='surrogate_or_strict') errors='surrogate_or_strict')
util_fqn = to_text("%s.%s " % (n_package_name, submodules[-1]), errors='surrogate_or_strict')
# Get the path of the util which is required for coverage collection. # Get the path of the util which is required for coverage collection.
resource_paths = list(module_util.__path__) resource_paths = list(module_util.__path__)
@ -200,8 +218,7 @@ class PSModuleDepFinder(object):
self.cs_utils_wrapper[m] = util_info self.cs_utils_wrapper[m] = util_info
else: else:
self.cs_utils_module[m] = util_info self.cs_utils_module[m] = util_info
self.scan_module(module_util_data, wrapper=wrapper, self.scan_module(module_util_data, fqn=util_fqn, wrapper=wrapper, powershell=(ext == ".psm1"))
powershell=(ext == ".psm1"))
def _parse_version_match(self, match, attribute): def _parse_version_match(self, match, attribute):
new_version = to_text(match.group(1)).rstrip() new_version = to_text(match.group(1)).rstrip()
@ -255,7 +272,7 @@ def _strip_comments(source):
def _create_powershell_wrapper(b_module_data, module_path, module_args, def _create_powershell_wrapper(b_module_data, module_path, module_args,
environment, async_timeout, become, environment, async_timeout, become,
become_method, become_user, become_password, become_method, become_user, become_password,
become_flags, substyle, task_vars): become_flags, substyle, task_vars, module_fqn):
# creates the manifest/wrapper used in PowerShell/C# modules to enable # creates the manifest/wrapper used in PowerShell/C# modules to enable
# things like become and async - this is also called in action/script.py # things like become and async - this is also called in action/script.py
@ -266,7 +283,7 @@ def _create_powershell_wrapper(b_module_data, module_path, module_args,
if substyle != 'script': if substyle != 'script':
# don't scan the module for util dependencies and other Ansible related # don't scan the module for util dependencies and other Ansible related
# flags if the substyle is 'script' which is set by action/script # flags if the substyle is 'script' which is set by action/script
finder.scan_module(b_module_data, powershell=(substyle == "powershell")) finder.scan_module(b_module_data, fqn=module_fqn, powershell=(substyle == "powershell"))
module_wrapper = "module_%s_wrapper" % substyle module_wrapper = "module_%s_wrapper" % substyle
exec_manifest = dict( exec_manifest = dict(
@ -327,7 +344,7 @@ def _create_powershell_wrapper(b_module_data, module_path, module_args,
# make sure Ansible.ModuleUtils.AddType is added if any C# utils are used # make sure Ansible.ModuleUtils.AddType is added if any C# utils are used
if len(finder.cs_utils_wrapper) > 0 or len(finder.cs_utils_module) > 0: if len(finder.cs_utils_wrapper) > 0 or len(finder.cs_utils_module) > 0:
finder._add_module((b"Ansible.ModuleUtils.AddType", ".psm1"), finder._add_module((b"Ansible.ModuleUtils.AddType", ".psm1", None),
wrapper=False) wrapper=False)
# exec_wrapper is only required to be part of the payload if using # exec_wrapper is only required to be part of the payload if using

View file

@ -132,7 +132,7 @@ class ActionModule(ActionBase):
exec_data = ps_manifest._create_powershell_wrapper( exec_data = ps_manifest._create_powershell_wrapper(
to_bytes(script_cmd), source, {}, env_dict, self._task.async_val, to_bytes(script_cmd), source, {}, env_dict, self._task.async_val,
pc.become, pc.become_method, pc.become_user, pc.become, pc.become_method, pc.become_user,
pc.become_pass, pc.become_flags, "script", task_vars pc.become_pass, pc.become_flags, "script", task_vars, None
) )
# build the necessary exec wrapper command # build the necessary exec wrapper command
# FUTURE: this still doesn't let script work on Windows with non-pipelined connections or # FUTURE: this still doesn't let script work on Windows with non-pipelined connections or

View file

@ -1 +1,4 @@
posix
shippable/posix/group1 shippable/posix/group1
shippable/windows/group1
windows

View file

@ -0,0 +1,11 @@
#AnsibleRequires -PowerShell .sub_pkg.PSRel2
Function Invoke-FromPSRel1 {
<#
.SYNOPSIS
Test function
#>
return "$(Invoke-FromPSRel2) -> Invoke-FromPSRel1"
}
Export-ModuleMember -Function Invoke-FromPSRel1

View file

@ -0,0 +1,11 @@
#AnsibleRequires -PowerShell ansible_collections.my_ns.my_col2.plugins.module_utils.PSRel3
Function Invoke-FromPSRel2 {
<#
.SYNOPSIS
Test function
#>
return "$(Invoke-FromPSRel3) -> Invoke-FromPSRel2"
}
Export-ModuleMember -Function Invoke-FromPSRel2

View file

@ -0,0 +1,10 @@
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ..module_utils.PSRel1
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
$module.Result.data = Invoke-FromPSRel1
$module.ExitJson()

View file

@ -0,0 +1,11 @@
#AnsibleRequires -CSharpUtil .sub_pkg.CSRel4
Function Invoke-FromPSRel3 {
<#
.SYNOPSIS
Test function
#>
return "$([CSRel4]::Invoke()) -> Invoke-FromPSRel3"
}
Export-ModuleMember -Function Invoke-FromPSRel3

View file

@ -0,0 +1,14 @@
using System;
//TypeAccelerator -Name CSRel4 -TypeName TestClass
namespace ansible_collections.my_ns.my_col.plugins.module_utils.sub_pkg.CSRel4
{
public class TestClass
{
public static string Invoke()
{
return "CSRel4.Invoke()";
}
}
}

View file

@ -2,4 +2,12 @@
set -eux set -eux
ANSIBLE_COLLECTIONS_PATHS="${PWD}/collection_root" ansible-playbook test.yml -i ../../inventory "$@" # we need multiple plays, and conditional import_playbook is noisy and causes problems, so choose here which one to use...
if [[ ${INVENTORY_PATH} == *.winrm ]]; then
export TEST_PLAYBOOK=windows.yml
else
export TEST_PLAYBOOK=test.yml
fi
ANSIBLE_COLLECTIONS_PATHS="${PWD}/collection_root" ansible-playbook "${TEST_PLAYBOOK}" -i "${INVENTORY_PATH}" "$@"

View file

@ -0,0 +1,11 @@
- hosts: windows
gather_facts: no
tasks:
- name: test out relative imports on Windows modules
my_ns.my_col.win_relative:
register: win_relative
- name: assert relative imports on Windows modules
assert:
that:
- win_relative.data == 'CSRel4.Invoke() -> Invoke-FromPSRel3 -> Invoke-FromPSRel2 -> Invoke-FromPSRel1'

View file

@ -13,6 +13,10 @@ from .util import (
display, display,
) )
from .util_common import (
resolve_csharp_ps_util,
)
from .data import ( from .data import (
data_context, data_context,
) )
@ -82,7 +86,7 @@ def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
if is_pure_csharp: if is_pure_csharp:
pattern = re.compile(r'(?i)^using\s((?:Ansible|AnsibleCollections)\..+);$') pattern = re.compile(r'(?i)^using\s((?:Ansible|AnsibleCollections)\..+);$')
else: else:
pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+((?:Ansible|ansible.collections)\..+)') pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+((?:Ansible|ansible.collections|\.)\..+)')
with open_text_file(path) as module_file: with open_text_file(path) as module_file:
for line_number, line in enumerate(module_file, 1): for line_number, line in enumerate(module_file, 1):
@ -91,7 +95,7 @@ def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
if not match: if not match:
continue continue
import_name = match.group(1) import_name = resolve_csharp_ps_util(match.group(1), path)
if import_name in module_utils: if import_name in module_utils:
imports.add(import_name) imports.add(import_name)

View file

@ -13,6 +13,10 @@ from .util import (
display, display,
) )
from .util_common import (
resolve_csharp_ps_util,
)
from .data import ( from .data import (
data_context, data_context,
) )
@ -85,12 +89,12 @@ def extract_powershell_module_utils_imports(path, module_utils):
for line in lines: for line in lines:
line_number += 1 line_number += 1
match = re.search(r'(?i)^#\s*(?:requires\s+-module(?:s?)|ansiblerequires\s+-powershell)\s*((?:Ansible|ansible_collections)\..+)', line) match = re.search(r'(?i)^#\s*(?:requires\s+-module(?:s?)|ansiblerequires\s+-powershell)\s*((?:Ansible|ansible_collections|\.)\..+)', line)
if not match: if not match:
continue continue
import_name = match.group(1) import_name = resolve_csharp_ps_util(match.group(1), path)
if import_name in module_utils: if import_name in module_utils:
imports.add(import_name) imports.add(import_name)

View file

@ -353,6 +353,27 @@ def intercept_command(args, cmd, target_name, env, capture=False, data=None, cwd
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd) return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd)
def resolve_csharp_ps_util(import_name, path):
"""
:type import_name: str
:type path: str
"""
if data_context().content.is_ansible or not import_name.startswith('.'):
# We don't support relative paths for builtin utils, there's no point.
return import_name
packages = import_name.split('.')
module_packages = path.split(os.path.sep)
for package in packages:
if not module_packages or package:
break
del module_packages[-1]
return 'ansible_collections.%s%s' % (data_context().content.prefix,
'.'.join(module_packages + [p for p in packages if p]))
def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None, def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None,
cmd_verbosity=1, str_errors='strict'): cmd_verbosity=1, str_errors='strict'):
""" """