diff options
author | Jordan Borean <jborean93@gmail.com> | 2021-09-24 09:47:10 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-23 16:47:10 -0700 |
commit | a2d4a8a9f36ee30a4f1fd7a2fedda91fb3860d96 (patch) | |
tree | c72487b41dec748585b55680051ce36a91375daf | |
parent | e3fd9b0769ec5ae85e0e698b5f9e0777d57c7c21 (diff) | |
download | ansible-a2d4a8a9f36ee30a4f1fd7a2fedda91fb3860d96.tar.gz |
PowerShell - Support optional module util imports (#75187)
9 files changed, 141 insertions, 14 deletions
diff --git a/changelogs/fragments/pwsh-optional-imp.yml b/changelogs/fragments/pwsh-optional-imp.yml new file mode 100644 index 0000000000..f09bd374f1 --- /dev/null +++ b/changelogs/fragments/pwsh-optional-imp.yml @@ -0,0 +1,2 @@ +minor_changes: +- PowerShell - Added support for optional module_util imports by scanning for ``-Optional`` at the end of the import declaration diff --git a/lib/ansible/executor/powershell/module_manifest.py b/lib/ansible/executor/powershell/module_manifest.py index b8aaeb7b81..970e8489e4 100644 --- a/lib/ansible/executor/powershell/module_manifest.py +++ b/lib/ansible/executor/powershell/module_manifest.py @@ -51,9 +51,10 @@ class PSModuleDepFinder(object): # '#AnsibleRequires -CSharpUtil Ansible.{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\..+)|' + # Can have '-Optional' at the end to denote the util is optional + re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+((Ansible\.[\w\.]+)|' r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+)|' - r'(\.[\w\.]+))')), + r'(\.[\w\.]+))(?P<optional>\s+-Optional){0,1}')), ] self._re_ps_module = [ @@ -64,9 +65,10 @@ class PSModuleDepFinder(object): # '#AnsibleRequires -PowerShell Ansible.ModuleUtils.{name}' # '#AnsibleRequires -PowerShell ansible_collections.{namespace}.{collection}.plugins.module_utils.{name}' # '#AnsibleRequires -PowerShell ..module_utils.{name}' - re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-powershell\s+((Ansible\.ModuleUtils\..+)|' + # Can have '-Optional' at the end to denote the util is optional + re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-powershell\s+((Ansible\.ModuleUtils\.[\w\.]+)|' r'(ansible_collections\.\w+\.\w+\.plugins\.module_utils\.[\w\.]+)|' - r'(\.[\w\.]+))')), + r'(\.[\w\.]+))(?P<optional>\s+-Optional){0,1}')), ] self._re_wrapper = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-wrapper\s+(\w*)')) @@ -104,9 +106,11 @@ class PSModuleDepFinder(object): # tolerate windows line endings by stripping any remaining # newline chars module_util_name = to_text(match.group(1).rstrip()) + match_dict = match.groupdict() + optional = match_dict.get('optional', None) is not None if module_util_name not in check[1].keys(): - module_utils.add((module_util_name, check[2], fqn)) + module_utils.add((module_util_name, check[2], fqn, optional)) break @@ -133,7 +137,7 @@ class PSModuleDepFinder(object): # recursively drill into each Requires to see if there are any more # requirements for m in set(module_utils): - self._add_module(m, wrapper=wrapper) + self._add_module(*m, wrapper=wrapper) def scan_exec_script(self, name): # scans lib/ansible/executor/powershell for scripts used in the module @@ -157,9 +161,8 @@ class PSModuleDepFinder(object): self.exec_scripts[name] = to_bytes(exec_script) self.scan_module(b_data, wrapper=True, powershell=True) - def _add_module(self, name, wrapper=False): - m, ext, fqn = name - m = to_text(m) + def _add_module(self, name, ext, fqn, optional, wrapper=False): + m = to_text(name) util_fqn = None @@ -168,6 +171,9 @@ class PSModuleDepFinder(object): mu_path = ps_module_utils_loader.find_plugin(m, ext) if not mu_path: + if optional: + return + raise AnsibleError('Could not find imported module support code ' 'for \'%s\'' % m) @@ -190,8 +196,11 @@ class PSModuleDepFinder(object): try: module_util = import_module(n_package_name) - module_util_data = to_bytes(pkgutil.get_data(n_package_name, n_resource_name), - errors='surrogate_or_strict') + pkg_data = pkgutil.get_data(n_package_name, n_resource_name) + if pkg_data is None: + raise ImportError("No package data found") + + module_util_data = to_bytes(pkg_data, 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. @@ -201,10 +210,14 @@ class PSModuleDepFinder(object): raise AnsibleError("Internal error: Referenced module_util package '%s' contains 0 or multiple " "import locations when we only expect 1." % n_package_name) mu_path = os.path.join(resource_paths[0], n_resource_name) - except OSError as err: - if err.errno == errno.ENOENT: + except (ImportError, OSError) as err: + if getattr(err, "errno", errno.ENOENT) == errno.ENOENT: + if optional: + return + raise AnsibleError('Could not find collection imported module support code for \'%s\'' % to_native(m)) + else: raise @@ -345,7 +358,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 if len(finder.cs_utils_wrapper) > 0 or len(finder.cs_utils_module) > 0: - finder._add_module((b"Ansible.ModuleUtils.AddType", ".psm1", None), + finder._add_module(b"Ansible.ModuleUtils.AddType", ".psm1", None, False, wrapper=False) # exec_wrapper is only required to be part of the payload if using diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs new file mode 100644 index 0000000000..0a3e758c53 --- /dev/null +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs @@ -0,0 +1,19 @@ +using System; + +using ansible_collections.testns.testcoll.plugins.module_utils.AnotherCSMU; +using ansible_collections.testns.testcoll.plugins.module_utils.subpkg.subcs; + +//TypeAccelerator -Name MyCSMU -TypeName CustomThing + +namespace ansible_collections.testns.testcoll.plugins.module_utils.MyCSMU +{ + public class CustomThing + { + public static string HelloWorld() + { + string res1 = AnotherThing.CallMe(); + string res2 = NestedUtil.HelloWorld(); + return String.Format("Hello from user_mu collection-hosted MyCSMUOptional, also {0} and {1}", res1, res2); + } + } +} diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1 new file mode 100644 index 0000000000..1e361598d9 --- /dev/null +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1 @@ -0,0 +1,16 @@ +#AnsibleRequires -CSharpUtil Ansible.Invalid -Optional +#AnsibleRequires -Powershell Ansible.ModuleUtils.Invalid -Optional +#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional +#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional +#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional +#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional + +Function Invoke-FromUserPSMU { + <# + .SYNOPSIS + Test function + #> + return "from optional user_mu" +} + +Export-ModuleMember -Function Invoke-FromUserPSMU diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1 b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1 new file mode 100644 index 0000000000..c44dcfea47 --- /dev/null +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1 @@ -0,0 +1,33 @@ +#!powershell + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Test builtin C# still works with -Optional +#AnsibleRequires -CSharpUtil Ansible.Basic -Optional + +# Test no failure when importing an invalid builtin C# and pwsh util with -Optional +#AnsibleRequires -CSharpUtil Ansible.Invalid -Optional +#AnsibleRequires -PowerShell Ansible.ModuleUtils.Invalid -Optional + +# Test valid module_util still works with -Optional +#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.MyCSMUOptional -Optional +#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.MyPSMUOptional -Optional + +# Test no failure when importing an invalid collection C# and pwsh util with -Optional +#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional +#AnsibleRequires -CSharpUtil ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional +#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid -Optional +#AnsibleRequires -Powershell ansible_collections.testns.testcoll.plugins.module_utils.invalid.invalid -Optional + +$spec = @{ + options = @{ + data = @{ type = "str"; default = "called $(Invoke-FromUserPSMU)" } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$module.Result.data = $module.Params.data +$module.Result.csharp = [MyCSMU]::HelloWorld() + +$module.ExitJson() diff --git a/test/integration/targets/collections/windows.yml b/test/integration/targets/collections/windows.yml index 4bdfb0edf4..cf98ca1e36 100644 --- a/test/integration/targets/collections/windows.yml +++ b/test/integration/targets/collections/windows.yml @@ -12,6 +12,9 @@ - testns.testcoll.win_uses_coll_csmu: register: uses_coll_csmu + - testns.testcoll.win_uses_optional: + register: uses_coll_optional + - assert: that: - selfcontained_out.source == 'user' @@ -26,3 +29,6 @@ - "'Hello from subpkg.subcs' in uses_coll_csmu.ping" - uses_coll_csmu.subpkg == 'Hello from subpkg.subcs' - uses_coll_csmu.type_accelerator == uses_coll_csmu.ping + # win_uses_optional + - uses_coll_optional.data == "called from optional user_mu" + - uses_coll_optional.csharp == "Hello from user_mu collection-hosted MyCSMUOptional, also Hello from nested user-collection-hosted AnotherCSMU and Hello from subpkg.subcs" diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1 new file mode 100644 index 0000000000..bcb5ec1948 --- /dev/null +++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1 @@ -0,0 +1,12 @@ +#AnsibleRequires -CSharpUtil .sub_pkg.CSRel5 -Optional +#AnsibleRequires -PowerShell .sub_pkg.PSRelInvalid -Optional + +Function Invoke-FromPSRel4 { + <# + .SYNOPSIS + Test function + #> + return "Invoke-FromPSRel4" +} + +Export-ModuleMember -Function Invoke-FromPSRel4 diff --git a/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1 b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1 new file mode 100644 index 0000000000..9086ca42a4 --- /dev/null +++ b/test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1 @@ -0,0 +1,17 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic -Optional +#AnsibleRequires -PowerShell ..module_utils.PSRel4 -optional + +# These do not exist +#AnsibleRequires -CSharpUtil ..invalid_package.name -Optional +#AnsibleRequires -CSharpUtil ..module_utils.InvalidName -optional +#AnsibleRequires -PowerShell ..invalid_package.pwsh_name -optional +#AnsibleRequires -PowerShell ..module_utils.InvalidPwshName -Optional + + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +$module.Result.data = Invoke-FromPSRel4 + +$module.ExitJson() diff --git a/test/integration/targets/collections_relative_imports/windows.yml b/test/integration/targets/collections_relative_imports/windows.yml index aa6badfa81..3a3c5488dc 100644 --- a/test/integration/targets/collections_relative_imports/windows.yml +++ b/test/integration/targets/collections_relative_imports/windows.yml @@ -9,3 +9,12 @@ assert: that: - win_relative.data == 'CSRel4.Invoke() -> Invoke-FromPSRel3 -> Invoke-FromPSRel2 -> Invoke-FromPSRel1' + + - name: test out relative imports on Windows modules with optional import + my_ns.my_col.win_relative_optional: + register: win_relative_optional + + - name: assert relative imports on Windows modules with optional import + assert: + that: + - win_relative_optional.data == 'Invoke-FromPSRel4' |