From b004a6373a743c950bf8afc9fc4625451a7eeb9e Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 24 Oct 2017 07:21:30 +1000 Subject: [PATCH] windows: add #AnsibleRequires for Windows modules (#31683) * windows: add #AnsibleRequires to set whether a module requires module or a specific version * fix up pep8 issues * changed psversion to use the actual ps Requires -Version syntax * missed the check on #Requires -Version * fix #Requires module extensions --- lib/ansible/executor/module_common.py | 49 +++++++-- lib/ansible/plugins/shell/powershell.py | 20 ++++ .../targets/win_exec_wrapper/aliases | 1 + .../library/test_all_options.ps1 | 12 ++ .../library/test_invalid_requires.ps1 | 9 ++ .../library/test_min_os_version.ps1 | 8 ++ .../library/test_min_ps_version.ps1 | 8 ++ .../targets/win_exec_wrapper/tasks/main.yml | 103 ++++++++++++++++++ 8 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 test/integration/targets/win_exec_wrapper/aliases create mode 100644 test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/tasks/main.yml diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py index f67948ac7be..d23c95acda6 100644 --- a/lib/ansible/executor/module_common.py +++ b/lib/ansible/executor/module_common.py @@ -623,7 +623,13 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas elif b'from ansible.module_utils.' in b_module_data: module_style = 'new' module_substyle = 'python' - elif REPLACER_WINDOWS in b_module_data or re.search(b'#Requires \-Module', b_module_data, re.IGNORECASE): + elif REPLACER_WINDOWS in b_module_data: + module_style = 'new' + module_substyle = 'powershell' + 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): module_style = 'new' module_substyle = 'powershell' elif REPLACER_JSONARGS in b_module_data: @@ -785,16 +791,37 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas lines = b_module_data.split(b'\n') module_names = set() + become_required = False + min_os_version = None + min_ps_version = None requires_module_list = re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)')) + requires_ps_version = re.compile(to_bytes('(?i)^#requires\s+\-version\s+([0-9]+(\.[0-9]+){0,3})$')) + requires_os_version = re.compile(to_bytes('(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$')) + requires_become = re.compile(to_bytes('(?i)^#ansiblerequires\s+\-become$')) for line in lines: - # legacy, equivalent to #Requires -Modules powershell - if REPLACER_WINDOWS in line: - module_names.add(b'Ansible.ModuleUtils.Legacy') - line_match = requires_module_list.match(line) - if line_match: - module_names.add(line_match.group(1)) + module_util_line_match = requires_module_list.match(line) + if module_util_line_match: + module_names.add(module_util_line_match.group(1)) + + requires_ps_version_match = requires_ps_version.match(line) + if requires_ps_version_match: + min_ps_version = to_text(requires_ps_version_match.group(1)) + # Powershell cannot cast a string of "1" to version, it must + # have at least the major.minor for it to work so we append 0 + if requires_ps_version_match.group(2) is None: + min_ps_version = "%s.0" % min_ps_version + + requires_os_version_match = requires_os_version.match(line) + if requires_os_version_match: + min_os_version = to_text(requires_os_version_match.group(1)) + if requires_os_version_match.group(2) is None: + min_os_version = "%s.0" % min_os_version + + requires_become_match = requires_become.match(line) + if requires_become_match: + become_required = True for m in set(module_names): m = to_text(m) @@ -809,6 +836,14 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas ) ) + exec_manifest['min_ps_version'] = min_ps_version + exec_manifest['min_os_version'] = min_os_version + if become_required and 'become' not in exec_manifest["actions"]: + exec_manifest["actions"].insert(0, 'become') + exec_manifest["become_user"] = "SYSTEM" + exec_manifest["become_password"] = None + exec_manifest["become"] = to_text(base64.b64encode(to_bytes(become_wrapper))) + # FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it module_json = json.dumps(exec_manifest) diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index 23b670080f3..ed9985845ea 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -71,6 +71,26 @@ end { # TODO: handle binary modules # TODO: handle persistence + $min_os_version = [version]$payload.min_os_version + if ($min_os_version -ne $null) { + $actual_os_version = [System.Environment]::OSVersion.Version + if ($actual_os_version -lt $min_os_version) { + $msg = "This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version" + Write-Output (ConvertTo-Json @{failed=$true;msg=$msg}) + exit 1 + } + } + + $min_ps_version = [version]$payload.min_ps_version + if ($min_ps_version -ne $null) { + $actual_ps_version = $PSVersionTable.PSVersion + if ($actual_ps_version -lt $min_ps_version) { + $msg = "This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version" + Write-Output (ConvertTo-Json @{failed=$true;msg=$msg}) + exit 1 + } + } + $actions = $payload.actions # pop 0th action as entrypoint diff --git a/test/integration/targets/win_exec_wrapper/aliases b/test/integration/targets/win_exec_wrapper/aliases new file mode 100644 index 00000000000..c6d61981670 --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/aliases @@ -0,0 +1 @@ +windows/ci/group3 diff --git a/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 b/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 new file mode 100644 index 00000000000..7c2c9c7b0d8 --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 @@ -0,0 +1,12 @@ +#!powershell + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.SID +#Requires -Version 3.0 +#AnsibleRequires -OSVersion 6 +#AnsibleRequires -Become + +$output = &whoami.exe +$sid = Convert-ToSID -account_name $output.Trim() + +Exit-Json -obj @{ output = $sid; changed = $false } diff --git a/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 b/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 new file mode 100644 index 00000000000..89727ef1a18 --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 @@ -0,0 +1,9 @@ +#!powershell + +#Requires -Module Ansible.ModuleUtils.Legacy +# Requires -Version 20 +# AnsibleRequires -OSVersion 20 + +# requires statement must be straight after the original # with now space, this module won't fail + +Exit-Json -obj @{ output = "output"; changed = $false } diff --git a/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 b/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 new file mode 100644 index 00000000000..39b1ded1606 --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 @@ -0,0 +1,8 @@ +#!powershell + +#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -OSVersion 20.0 + +# this shouldn't run as no Windows OS will meet the version of 20.0 + +Exit-Json -obj @{ output = "output"; changed = $false } diff --git a/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 b/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 new file mode 100644 index 00000000000..bb5fd0f2a57 --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 @@ -0,0 +1,8 @@ +#!powershell + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Version 20.0.0.0 + +# this shouldn't run as no PS Version will be at 20 in the near future + +Exit-Json -obj @{ output = "output"; changed = $false } diff --git a/test/integration/targets/win_exec_wrapper/tasks/main.yml b/test/integration/targets/win_exec_wrapper/tasks/main.yml new file mode 100644 index 00000000000..d082d56325c --- /dev/null +++ b/test/integration/targets/win_exec_wrapper/tasks/main.yml @@ -0,0 +1,103 @@ +--- +- name: test out invalid options + test_invalid_requires: + register: invalid_options + +- name: assert test out invalid options + assert: + that: + - invalid_options|success + - invalid_options.output == "output" + +- name: test out invalid os version + test_min_os_version: + register: invalid_os_version + ignore_errors: yes + +- name: assert test out invalid os version + assert: + that: + - invalid_os_version|failed + - '"This module cannot run on this OS as it requires a minimum version of 20.0, actual was " in invalid_os_version.msg' + +- name: test out invalid powershell version + test_min_ps_version: + register: invalid_ps_version + ignore_errors: yes + +- name: assert test out invalid powershell version + assert: + that: + - invalid_ps_version|failed + - '"This module cannot run as it requires a minimum PowerShell version of 20.0.0.0, actual was " in invalid_ps_version.msg' + +- name: test out become requires without become_user set + test_all_options: + register: become_system + +- name: assert become requires without become_user set + assert: + that: + - become_system|success + - become_system.output == "S-1-5-18" + +- set_fact: + become_test_username: ansible_become_test + gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }} + +- name: create unprivileged user + win_user: + name: "{{ become_test_username }}" + password: "{{ gen_pw }}" + update_password: always + groups: Users + register: become_test_user_result + +- name: execute tests and ensure that test user is deleted regardless of success/failure + block: + - name: ensure current user is not the become user + win_shell: whoami + register: whoami_out + + - name: verify output + assert: + that: + - not whoami_out.stdout_lines[0].endswith(become_test_username) + + - name: get become user profile dir so we can clean it up later + vars: &become_vars + ansible_become_user: "{{ become_test_username }}" + ansible_become_password: "{{ gen_pw }}" + ansible_become_method: runas + ansible_become: yes + win_shell: $env:USERPROFILE + register: profile_dir_out + + - name: ensure profile dir contains test username (eg, if become fails silently, prevent deletion of real user profile) + assert: + that: + - become_test_username in profile_dir_out.stdout_lines[0] + + - name: test out become requires when become_user set + test_all_options: + vars: *become_vars + register: become_system + + - name: assert become requires when become_user set + assert: + that: + - become_system|success + - become_system.output == become_test_user_result.sid + + always: + - name: ensure test user is deleted + win_user: + name: "{{ become_test_username }}" + state: absent + + - name: ensure test user profile is deleted + # NB: have to work around powershell limitation of long filenames until win_file fixes it + win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }} + args: + executable: cmd.exe + when: become_test_username in profile_dir_out.stdout_lines[0]