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
This commit is contained in:
parent
0c34cdd0f7
commit
b004a6373a
8 changed files with 203 additions and 7 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
1
test/integration/targets/win_exec_wrapper/aliases
Normal file
1
test/integration/targets/win_exec_wrapper/aliases
Normal file
|
@ -0,0 +1 @@
|
|||
windows/ci/group3
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
103
test/integration/targets/win_exec_wrapper/tasks/main.yml
Normal file
103
test/integration/targets/win_exec_wrapper/tasks/main.yml
Normal file
|
@ -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]
|
Loading…
Reference in a new issue