Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows: add #AnsibleRequires for Windows modules #31683

Merged
merged 5 commits into from
Oct 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 42 additions & 7 deletions lib/ansible/executor/module_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
20 changes: 20 additions & 0 deletions lib/ansible/plugins/shell/powershell.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@
# 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
Expand Down
1 change: 1 addition & 0 deletions test/integration/targets/win_exec_wrapper/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
windows/ci/group3
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
@@ -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 changes: 103 additions & 0 deletions test/integration/targets/win_exec_wrapper/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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]