mirror of https://github.com/ansible/ansible.git
Third batch of incidental integration tests. (#67830)
* Copy in incidental windows tests. * Update incidental test aliases. * Add support plugins. * Update target references. * Update sanity ignores. * Update integration-aliases test. * Add to CI.pull/67852/head
parent
da30e6d2e1
commit
f735fd672a
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1 @@
|
|||||||
|
test_win_copy_path: C:\ansible\win_copy .ÅÑŚÌβŁÈ [$!@^&test(;)]
|
@ -0,0 +1,6 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
65653164323866373138353632323531393664393563633665373635623763353561386431373366
|
||||||
|
3232353263363034313136663062623336663463373966320a333763323032646463386432626161
|
||||||
|
36386330356637666362396661653935653064623038333031653335626164376465353235303636
|
||||||
|
3335616231663838620a303632343938326538656233393562303162343261383465623261646664
|
||||||
|
33613932343461626339333832363930303962633364303736376634396364643861
|
@ -0,0 +1,5 @@
|
|||||||
|
This directory contains some files that have been encrypted with ansible-vault.
|
||||||
|
|
||||||
|
This is to test out the decrypt parameter in win_copy.
|
||||||
|
|
||||||
|
The password is: password
|
@ -0,0 +1,6 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
30353665333635633433356261616636356130386330363962386533303566313463383734373532
|
||||||
|
3933643234323638623939613462346361313431363939370a303532656338353035346661353965
|
||||||
|
34656231633238396361393131623834316262306533663838336362366137306562646561383766
|
||||||
|
6363373965633337640a373666336461613337346131353564383134326139616561393664663563
|
||||||
|
3431
|
@ -0,0 +1 @@
|
|||||||
|
foo.txt
|
@ -0,0 +1 @@
|
|||||||
|
baz
|
@ -0,0 +1 @@
|
|||||||
|
baz
|
@ -0,0 +1 @@
|
|||||||
|
qux
|
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
- name: create empty folder
|
||||||
|
file:
|
||||||
|
path: '{{role_path}}/files/subdir/empty'
|
||||||
|
state: directory
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
# removes the cached zip module from the previous task so we can replicate
|
||||||
|
# the below issue where win_copy would delete DEFAULT_LOCAL_TMP if it
|
||||||
|
# had permission to
|
||||||
|
# https://github.com/ansible/ansible/issues/35613
|
||||||
|
- name: clear the local ansiballz cache
|
||||||
|
file:
|
||||||
|
path: "{{lookup('config', 'DEFAULT_LOCAL_TMP')}}/ansiballz_cache"
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: create test folder
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}'
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: run tests for local to remote
|
||||||
|
include_tasks: tests.yml
|
||||||
|
|
||||||
|
- name: run tests for remote to remote
|
||||||
|
include_tasks: remote_tests.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: remove test folder
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}'
|
||||||
|
state: absent
|
@ -0,0 +1,471 @@
|
|||||||
|
---
|
||||||
|
- name: fail when source does not exist remote
|
||||||
|
win_copy:
|
||||||
|
src: fakesource
|
||||||
|
dest: fakedest
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_invalid_source
|
||||||
|
failed_when: "'it does not exist' not in fail_remote_invalid_source.msg"
|
||||||
|
|
||||||
|
- name: setup source folder for remote tests
|
||||||
|
win_copy:
|
||||||
|
src: files/
|
||||||
|
dest: '{{test_win_copy_path}}\source\'
|
||||||
|
|
||||||
|
- name: setup remote failure tests
|
||||||
|
win_file:
|
||||||
|
path: '{{item.path}}'
|
||||||
|
state: '{{item.state}}'
|
||||||
|
with_items:
|
||||||
|
- { 'path': '{{test_win_copy_path}}\target\folder', 'state': 'directory' }
|
||||||
|
- { 'path': '{{test_win_copy_path}}\target\file', 'state': 'touch' }
|
||||||
|
- { 'path': '{{test_win_copy_path}}\target\subdir', 'state': 'touch' }
|
||||||
|
|
||||||
|
- name: fail source is a file but dest is a folder
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\folder'
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_file_to_folder
|
||||||
|
failed_when: "'dest is already a folder' not in fail_remote_file_to_folder.msg"
|
||||||
|
|
||||||
|
- name: fail source is a file but dest is a folder
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\'
|
||||||
|
dest: '{{test_win_copy_path}}\target\'
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_folder_to_file
|
||||||
|
failed_when: "'dest is already a file' not in fail_remote_folder_to_file.msg"
|
||||||
|
|
||||||
|
- name: fail source is a file dest parent dir is also a file
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\file\foo.txt'
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_file_parent_dir_file
|
||||||
|
failed_when: "'is currently a file' not in fail_remote_file_parent_dir_file.msg"
|
||||||
|
|
||||||
|
- name: fail source is a folder dest parent dir is also a file
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\subdir'
|
||||||
|
dest: '{{test_win_copy_path}}\target\file'
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_folder_parent_dir_file
|
||||||
|
failed_when: "'object at dest parent dir is not a folder' not in fail_remote_folder_parent_dir_file.msg"
|
||||||
|
|
||||||
|
- name: fail to copy a remote file with parent dir that doesn't exist and filename is set
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\missing-dir\foo.txt'
|
||||||
|
remote_src: yes
|
||||||
|
register: fail_remote_missing_parent_dir
|
||||||
|
failed_when: "'does not exist' not in fail_remote_missing_parent_dir.msg"
|
||||||
|
|
||||||
|
- name: remove target after remote failure tests
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: create remote target after cleaning
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: copy single file remote (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\foo-target.txt'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file remote (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\foo-target.txt'
|
||||||
|
register: remote_copy_file_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file remote (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_check is changed
|
||||||
|
- remote_copy_file_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file remote
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\foo-target.txt'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file
|
||||||
|
|
||||||
|
- name: get result of copy single file remote
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\foo-target.txt'
|
||||||
|
register: remote_copy_file_actual
|
||||||
|
|
||||||
|
- name: assert copy single file remote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file is changed
|
||||||
|
- remote_copy_file.operation == 'file_copy'
|
||||||
|
- remote_copy_file.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- remote_copy_file.size == 8
|
||||||
|
- remote_copy_file.original_basename == 'foo.txt'
|
||||||
|
- remote_copy_file_actual.stat.exists == True
|
||||||
|
- remote_copy_file_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: copy single file remote (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\foo-target.txt'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_again
|
||||||
|
|
||||||
|
- name: assert copy single file remote (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_again is not changed
|
||||||
|
|
||||||
|
- name: copy single file into folder remote (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_to_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file into folder remote (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\foo.txt'
|
||||||
|
register: remote_copy_file_to_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file into folder remote (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_to_folder_check is changed
|
||||||
|
- remote_copy_file_to_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file into folder remote
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_to_folder
|
||||||
|
|
||||||
|
- name: get result of copy single file into folder remote
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\foo.txt'
|
||||||
|
register: remote_copy_file_to_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy single file into folder remote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_to_folder is changed
|
||||||
|
- remote_copy_file_to_folder.operation == 'file_copy'
|
||||||
|
- remote_copy_file_to_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- remote_copy_file_to_folder.size == 8
|
||||||
|
- remote_copy_file_to_folder.original_basename == 'foo.txt'
|
||||||
|
- remote_copy_file_to_folder_actual.stat.exists == True
|
||||||
|
- remote_copy_file_to_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: copy single file into folder remote (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_to_folder_again
|
||||||
|
|
||||||
|
- name: assert copy single file into folder remote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_to_folder_again is not changed
|
||||||
|
|
||||||
|
- name: copy single file to missing folder (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\missing\'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_to_missing_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file to missing folder remote (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\missing\foo.txt'
|
||||||
|
register: remote_copy_file_to_missing_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file to missing folder remote (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_to_missing_folder_check is changed
|
||||||
|
- remote_copy_file_to_missing_folder_check.operation == 'file_copy'
|
||||||
|
- remote_copy_file_to_missing_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file to missing folder remote
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\foo.txt'
|
||||||
|
dest: '{{test_win_copy_path}}\target\missing\'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_file_to_missing_folder
|
||||||
|
|
||||||
|
- name: get result of copy single file to missing folder remote
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target\missing\foo.txt'
|
||||||
|
register: remote_copy_file_to_missing_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy single file to missing folder remote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_file_to_missing_folder is changed
|
||||||
|
- remote_copy_file_to_missing_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- remote_copy_file_to_missing_folder.operation == 'file_copy'
|
||||||
|
- remote_copy_file_to_missing_folder.size == 8
|
||||||
|
- remote_copy_file_to_missing_folder_actual.stat.exists == True
|
||||||
|
- remote_copy_file_to_missing_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: clear target for folder to folder test
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: copy folder to folder remote (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_to_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy folder to folder remote (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
register: remote_copy_folder_to_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy folder to folder remote (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_to_folder_check is changed
|
||||||
|
- remote_copy_folder_to_folder_check.operation == 'folder_copy'
|
||||||
|
- remote_copy_folder_to_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy folder to folder remote
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_to_folder
|
||||||
|
|
||||||
|
- name: get result of copy folder to folder remote
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\target'
|
||||||
|
recurse: yes
|
||||||
|
file_type: directory
|
||||||
|
register: remote_copy_folder_to_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy folder to folder remote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_to_folder is changed
|
||||||
|
- remote_copy_folder_to_folder.operation == 'folder_copy'
|
||||||
|
- remote_copy_folder_to_folder_actual.examined == 11
|
||||||
|
- remote_copy_folder_to_folder_actual.matched == 6
|
||||||
|
- remote_copy_folder_to_folder_actual.files[0].filename == 'source'
|
||||||
|
- remote_copy_folder_to_folder_actual.files[1].filename == 'subdir'
|
||||||
|
- remote_copy_folder_to_folder_actual.files[2].filename == 'empty'
|
||||||
|
- remote_copy_folder_to_folder_actual.files[3].filename == 'subdir2'
|
||||||
|
- remote_copy_folder_to_folder_actual.files[4].filename == 'subdir3'
|
||||||
|
- remote_copy_folder_to_folder_actual.files[5].filename == 'subdir4'
|
||||||
|
|
||||||
|
- name: copy folder to folder remote (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_to_folder_again
|
||||||
|
|
||||||
|
- name: assert copy folder to folder remote (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_to_folder_again is not changed
|
||||||
|
|
||||||
|
- name: change remote file after folder to folder test
|
||||||
|
win_copy:
|
||||||
|
content: bar.txt
|
||||||
|
dest: '{{test_win_copy_path}}\target\source\foo.txt'
|
||||||
|
|
||||||
|
- name: remote remote folder after folder to folder test
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target\source\subdir\subdir2\subdir3\subdir4'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: copy folder to folder remote after change
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_to_folder_after_change
|
||||||
|
|
||||||
|
- name: get result of copy folder to folder remote after change
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\target\source'
|
||||||
|
recurse: yes
|
||||||
|
patterns: ['foo.txt', 'qux.txt']
|
||||||
|
register: remote_copy_folder_to_folder_after_change_actual
|
||||||
|
|
||||||
|
- name: assert copy folder after changes
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_to_folder_after_change is changed
|
||||||
|
- remote_copy_folder_to_folder_after_change_actual.matched == 2
|
||||||
|
- remote_copy_folder_to_folder_after_change_actual.files[0].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- remote_copy_folder_to_folder_after_change_actual.files[1].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43'
|
||||||
|
|
||||||
|
- name: clear target folder before folder contents to remote test
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: copy folder contents to folder remote with backslash (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_content_backslash_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy folder contents to folder remote with backslash (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\target'
|
||||||
|
register: remote_copy_folder_content_backslash_actual_check
|
||||||
|
|
||||||
|
- name: assert copy folder content to folder remote with backslash (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_content_backslash_check is changed
|
||||||
|
- remote_copy_folder_content_backslash_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy folder contents to folder remote with backslash
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_content_backslash
|
||||||
|
|
||||||
|
- name: get result of copy folder contents to folder remote with backslash
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\target'
|
||||||
|
recurse: yes
|
||||||
|
file_type: directory
|
||||||
|
register: remote_copy_folder_content_backslash_actual
|
||||||
|
|
||||||
|
- name: assert copy folder content to folder remote with backslash
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_content_backslash is changed
|
||||||
|
- remote_copy_folder_content_backslash.operation == 'folder_copy'
|
||||||
|
- remote_copy_folder_content_backslash_actual.examined == 10
|
||||||
|
- remote_copy_folder_content_backslash_actual.matched == 5
|
||||||
|
- remote_copy_folder_content_backslash_actual.files[0].filename == 'subdir'
|
||||||
|
- remote_copy_folder_content_backslash_actual.files[1].filename == 'empty'
|
||||||
|
- remote_copy_folder_content_backslash_actual.files[2].filename == 'subdir2'
|
||||||
|
- remote_copy_folder_content_backslash_actual.files[3].filename == 'subdir3'
|
||||||
|
- remote_copy_folder_content_backslash_actual.files[4].filename == 'subdir4'
|
||||||
|
|
||||||
|
- name: copy folder contents to folder remote with backslash (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}\source\'
|
||||||
|
dest: '{{test_win_copy_path}}\target'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_content_backslash_again
|
||||||
|
|
||||||
|
- name: assert copy folder content to folder remote with backslash (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_content_backslash_again is not changed
|
||||||
|
|
||||||
|
- name: change remote file after folder content to folder test
|
||||||
|
win_copy:
|
||||||
|
content: bar.txt
|
||||||
|
dest: '{{test_win_copy_path}}\target\foo.txt'
|
||||||
|
|
||||||
|
- name: remote remote folder after folder content to folder test
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\target\subdir\subdir2\subdir3\subdir4'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: copy folder content to folder remote after change
|
||||||
|
win_copy:
|
||||||
|
src: '{{test_win_copy_path}}/source/'
|
||||||
|
dest: '{{test_win_copy_path}}/target/'
|
||||||
|
remote_src: yes
|
||||||
|
register: remote_copy_folder_content_to_folder_after_change
|
||||||
|
|
||||||
|
- name: get result of copy folder content to folder remote after change
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\target'
|
||||||
|
recurse: yes
|
||||||
|
patterns: ['foo.txt', 'qux.txt']
|
||||||
|
register: remote_copy_folder_content_to_folder_after_change_actual
|
||||||
|
|
||||||
|
- name: assert copy folder content to folder after changes
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remote_copy_folder_content_to_folder_after_change is changed
|
||||||
|
- remote_copy_folder_content_to_folder_after_change_actual.matched == 2
|
||||||
|
- remote_copy_folder_content_to_folder_after_change_actual.files[0].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- remote_copy_folder_content_to_folder_after_change_actual.files[1].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43'
|
||||||
|
|
||||||
|
# https://github.com/ansible/ansible/issues/50077
|
||||||
|
- name: create empty nested directory
|
||||||
|
win_file:
|
||||||
|
path: '{{ test_win_copy_path }}\source\empty-nested\nested-dir'
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: copy empty nested directory (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: '{{ test_win_copy_path }}\source\empty-nested'
|
||||||
|
dest: '{{ test_win_copy_path }}\target'
|
||||||
|
remote_src: True
|
||||||
|
check_mode: True
|
||||||
|
register: copy_empty_dir_check
|
||||||
|
|
||||||
|
- name: get result of copy empty nested directory (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{ test_win_copy_path }}\target\empty-nested'
|
||||||
|
register: copy_empty_dir_actual_check
|
||||||
|
|
||||||
|
- name: assert copy empty nested directory (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_empty_dir_check is changed
|
||||||
|
- copy_empty_dir_check.operation == "folder_copy"
|
||||||
|
- not copy_empty_dir_actual_check.stat.exists
|
||||||
|
|
||||||
|
- name: copy empty nested directory
|
||||||
|
win_copy:
|
||||||
|
src: '{{ test_win_copy_path }}\source\empty-nested'
|
||||||
|
dest: '{{ test_win_copy_path }}\target'
|
||||||
|
remote_src: True
|
||||||
|
register: copy_empty_dir
|
||||||
|
|
||||||
|
- name: get result of copy empty nested directory
|
||||||
|
win_stat:
|
||||||
|
path: '{{ test_win_copy_path }}\target\empty-nested\nested-dir'
|
||||||
|
register: copy_empty_dir_actual
|
||||||
|
|
||||||
|
- name: assert copy empty nested directory
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_empty_dir is changed
|
||||||
|
- copy_empty_dir.operation == "folder_copy"
|
||||||
|
- copy_empty_dir_actual.stat.exists
|
||||||
|
|
||||||
|
- name: copy empty nested directory (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{ test_win_copy_path }}\source\empty-nested'
|
||||||
|
dest: '{{ test_win_copy_path }}\target'
|
||||||
|
remote_src: True
|
||||||
|
register: copy_empty_dir_again
|
||||||
|
|
||||||
|
- name: assert copy empty nested directory (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not copy_empty_dir_again is changed
|
@ -0,0 +1,535 @@
|
|||||||
|
---
|
||||||
|
- name: fail no source or content
|
||||||
|
win_copy:
|
||||||
|
dest: dest
|
||||||
|
register: fail_no_source_content
|
||||||
|
failed_when: fail_no_source_content.msg != 'src (or content) and dest are required'
|
||||||
|
|
||||||
|
- name: fail content but dest isn't a file, unix ending
|
||||||
|
win_copy:
|
||||||
|
content: a
|
||||||
|
dest: a/
|
||||||
|
register: fail_dest_not_file_unix
|
||||||
|
failed_when: fail_dest_not_file_unix.msg != 'dest must be a file if content is defined'
|
||||||
|
|
||||||
|
- name: fail content but dest isn't a file, windows ending
|
||||||
|
win_copy:
|
||||||
|
content: a
|
||||||
|
dest: a\
|
||||||
|
register: fail_dest_not_file_windows
|
||||||
|
failed_when: fail_dest_not_file_windows.msg != 'dest must be a file if content is defined'
|
||||||
|
|
||||||
|
- name: fail to copy a file with parent dir that doesn't exist and filename is set
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\missing-dir\foo.txt'
|
||||||
|
register: fail_missing_parent_dir
|
||||||
|
failed_when: "'does not exist' not in fail_missing_parent_dir.msg"
|
||||||
|
|
||||||
|
- name: fail to copy an encrypted file without the password set
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault/vault-file'
|
||||||
|
dest: '{{test_win_copy_path}}\file'
|
||||||
|
register: fail_copy_encrypted_file
|
||||||
|
ignore_errors: yes # weird failed_when doesn't work in this case
|
||||||
|
|
||||||
|
- name: assert failure message when copying an encrypted file without the password set
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- fail_copy_encrypted_file is failed
|
||||||
|
- fail_copy_encrypted_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file'
|
||||||
|
|
||||||
|
- name: fail to copy a directory with an encrypted file without the password
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault'
|
||||||
|
dest: '{{test_win_copy_path}}'
|
||||||
|
register: fail_copy_directory_with_enc_file
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: assert failure message when copying a directory that contains an encrypted file without the password set
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- fail_copy_directory_with_enc_file is failed
|
||||||
|
- fail_copy_directory_with_enc_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file'
|
||||||
|
|
||||||
|
- name: copy with content (check mode)
|
||||||
|
win_copy:
|
||||||
|
content: a
|
||||||
|
dest: '{{test_win_copy_path}}\file'
|
||||||
|
register: copy_content_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy with content (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\file'
|
||||||
|
register: copy_content_actual_check
|
||||||
|
|
||||||
|
- name: assert copy with content (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_content_check is changed
|
||||||
|
- copy_content_check.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8'
|
||||||
|
- copy_content_check.operation == 'file_copy'
|
||||||
|
- copy_content_check.size == 1
|
||||||
|
- copy_content_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy with content
|
||||||
|
win_copy:
|
||||||
|
content: a
|
||||||
|
dest: '{{test_win_copy_path}}\file'
|
||||||
|
register: copy_content
|
||||||
|
|
||||||
|
- name: get result of copy with content
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\file'
|
||||||
|
register: copy_content_actual
|
||||||
|
|
||||||
|
- name: assert copy with content
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_content is changed
|
||||||
|
- copy_content.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8'
|
||||||
|
- copy_content.operation == 'file_copy'
|
||||||
|
- copy_content.size == 1
|
||||||
|
- copy_content_actual.stat.exists == True
|
||||||
|
- copy_content_actual.stat.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8'
|
||||||
|
|
||||||
|
- name: copy with content (idempotent)
|
||||||
|
win_copy:
|
||||||
|
content: a
|
||||||
|
dest: '{{test_win_copy_path}}\file'
|
||||||
|
register: copy_content_again
|
||||||
|
|
||||||
|
- name: assert copy with content (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_content_again is not changed
|
||||||
|
|
||||||
|
- name: copy with content change when missing
|
||||||
|
win_copy:
|
||||||
|
content: b
|
||||||
|
dest: '{{test_win_copy_path}}\file'
|
||||||
|
force: no
|
||||||
|
register: copy_content_when_missing
|
||||||
|
|
||||||
|
- name: assert copy with content change when missing
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_content_when_missing is not changed
|
||||||
|
|
||||||
|
- name: copy single file (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
register: copy_file_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
register: copy_file_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_check is changed
|
||||||
|
- copy_file_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file_check.dest == test_win_copy_path + '\\foo-target.txt'
|
||||||
|
- copy_file_check.operation == 'file_copy'
|
||||||
|
- copy_file_check.size == 8
|
||||||
|
- copy_file_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
register: copy_file
|
||||||
|
|
||||||
|
- name: get result of copy single file
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
register: copy_file_actual
|
||||||
|
|
||||||
|
- name: assert copy single file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file is changed
|
||||||
|
- copy_file.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file.dest == test_win_copy_path + '\\foo-target.txt'
|
||||||
|
- copy_file.operation == 'file_copy'
|
||||||
|
- copy_file.size == 8
|
||||||
|
- copy_file_actual.stat.exists == True
|
||||||
|
- copy_file_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: copy single file (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
register: copy_file_again
|
||||||
|
|
||||||
|
- name: assert copy single file (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_again is not changed
|
||||||
|
|
||||||
|
- name: copy single file (backup)
|
||||||
|
win_copy:
|
||||||
|
content: "{{ lookup('file', 'foo.txt') }}\nfoo bar"
|
||||||
|
dest: '{{test_win_copy_path}}\foo-target.txt'
|
||||||
|
backup: yes
|
||||||
|
register: copy_file_backup
|
||||||
|
|
||||||
|
- name: check backup_file
|
||||||
|
win_stat:
|
||||||
|
path: '{{ copy_file_backup.backup_file }}'
|
||||||
|
register: backup_file
|
||||||
|
|
||||||
|
- name: assert copy single file (backup)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_backup is changed
|
||||||
|
- backup_file.stat.exists == true
|
||||||
|
|
||||||
|
- name: copy single file to folder (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\'
|
||||||
|
register: copy_file_to_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file to folder (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\foo.txt'
|
||||||
|
register: copy_file_to_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file to folder (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_to_folder_check is changed
|
||||||
|
- copy_file_to_folder_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file_to_folder_check.dest == test_win_copy_path + '\\foo.txt'
|
||||||
|
- copy_file_to_folder_check.operation == 'file_copy'
|
||||||
|
- copy_file_to_folder_check.size == 8
|
||||||
|
- copy_file_to_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file to folder
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\'
|
||||||
|
register: copy_file_to_folder
|
||||||
|
|
||||||
|
- name: get result of copy single file to folder
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\foo.txt'
|
||||||
|
register: copy_file_to_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy single file to folder
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_to_folder is changed
|
||||||
|
- copy_file_to_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file_to_folder.dest == test_win_copy_path + '\\foo.txt'
|
||||||
|
- copy_file_to_folder.operation == 'file_copy'
|
||||||
|
- copy_file_to_folder.size == 8
|
||||||
|
- copy_file_to_folder_actual.stat.exists == True
|
||||||
|
- copy_file_to_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: copy single file to folder (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\'
|
||||||
|
register: copy_file_to_folder_again
|
||||||
|
|
||||||
|
- name: assert copy single file to folder (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_to_folder_again is not changed
|
||||||
|
|
||||||
|
- name: copy single file to missing folder (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\missing\'
|
||||||
|
register: copy_file_to_missing_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy single file to missing folder (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\missing\foo.txt'
|
||||||
|
register: copy_file_to_missing_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy single file to missing folder (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_to_missing_folder_check is changed
|
||||||
|
- copy_file_to_missing_folder_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file_to_missing_folder_check.operation == 'file_copy'
|
||||||
|
- copy_file_to_missing_folder_check.size == 8
|
||||||
|
- copy_file_to_missing_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy single file to missing folder
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\missing\'
|
||||||
|
register: copy_file_to_missing_folder
|
||||||
|
|
||||||
|
- name: get result of copy single file to missing folder
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\missing\foo.txt'
|
||||||
|
register: copy_file_to_missing_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy single file to missing folder
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_to_missing_folder is changed
|
||||||
|
- copy_file_to_missing_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_file_to_missing_folder.operation == 'file_copy'
|
||||||
|
- copy_file_to_missing_folder.size == 8
|
||||||
|
- copy_file_to_missing_folder_actual.stat.exists == True
|
||||||
|
- copy_file_to_missing_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
|
||||||
|
- name: copy folder (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: files
|
||||||
|
dest: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
register: copy_folder_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy folder (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
register: copy_folder_actual_check
|
||||||
|
|
||||||
|
- name: assert copy folder (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder_check is changed
|
||||||
|
- copy_folder_check.operation == 'folder_copy'
|
||||||
|
- copy_folder_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy folder
|
||||||
|
win_copy:
|
||||||
|
src: files
|
||||||
|
dest: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
register: copy_folder
|
||||||
|
|
||||||
|
- name: get result of copy folder
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
recurse: yes
|
||||||
|
file_type: directory
|
||||||
|
register: copy_folder_actual
|
||||||
|
|
||||||
|
- name: assert copy folder
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder is changed
|
||||||
|
- copy_folder.operation == 'folder_copy'
|
||||||
|
- copy_folder_actual.examined == 11 # includes files and folders, the below is the nested order
|
||||||
|
- copy_folder_actual.matched == 6
|
||||||
|
- copy_folder_actual.files[0].filename == 'files'
|
||||||
|
- copy_folder_actual.files[1].filename == 'subdir'
|
||||||
|
- copy_folder_actual.files[2].filename == 'empty'
|
||||||
|
- copy_folder_actual.files[3].filename == 'subdir2'
|
||||||
|
- copy_folder_actual.files[4].filename == 'subdir3'
|
||||||
|
- copy_folder_actual.files[5].filename == 'subdir4'
|
||||||
|
|
||||||
|
- name: copy folder (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: files
|
||||||
|
dest: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
register: copy_folder_again
|
||||||
|
|
||||||
|
- name: assert copy folder (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder_again is not changed
|
||||||
|
|
||||||
|
- name: change the text of a file in the remote source
|
||||||
|
win_copy:
|
||||||
|
content: bar.txt
|
||||||
|
dest: '{{test_win_copy_path}}\recursive\folder\files\foo.txt'
|
||||||
|
|
||||||
|
- name: remove folder for test of recursive copy
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}\recursive\folder\files\subdir\subdir2\subdir3\subdir4'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: copy folder after changes
|
||||||
|
win_copy:
|
||||||
|
src: files
|
||||||
|
dest: '{{test_win_copy_path}}\recursive\folder'
|
||||||
|
register: copy_folder_after_change
|
||||||
|
|
||||||
|
- name: get result of copy folder after changes
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\recursive\folder\files'
|
||||||
|
recurse: yes
|
||||||
|
patterns: ['foo.txt', 'qux.txt']
|
||||||
|
register: copy_folder_after_changes_actual
|
||||||
|
|
||||||
|
- name: assert copy folder after changes
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder_after_change is changed
|
||||||
|
- copy_folder_after_changes_actual.matched == 2
|
||||||
|
- copy_folder_after_changes_actual.files[0].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6'
|
||||||
|
- copy_folder_after_changes_actual.files[1].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43'
|
||||||
|
|
||||||
|
- name: copy folder's contents (check mode)
|
||||||
|
win_copy:
|
||||||
|
src: files/
|
||||||
|
dest: '{{test_win_copy_path}}\recursive-contents\'
|
||||||
|
register: copy_folder_contents_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of copy folder'scontents (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\recursive-contents'
|
||||||
|
register: copy_folder_contents_actual_check
|
||||||
|
|
||||||
|
- name: assert copy folder's contents (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder_contents_check is changed
|
||||||
|
- copy_folder_contents_check.operation == 'folder_copy'
|
||||||
|
- copy_folder_contents_actual_check.stat.exists == False
|
||||||
|
|
||||||
|
- name: copy folder's contents
|
||||||
|
win_copy:
|
||||||
|
src: files/
|
||||||
|
dest: '{{test_win_copy_path}}\recursive-contents\'
|
||||||
|
register: copy_folder_contents
|
||||||
|
|
||||||
|
- name: get result of copy folder
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\recursive-contents'
|
||||||
|
recurse: yes
|
||||||
|
file_type: directory
|
||||||
|
register: copy_folder_contents_actual
|
||||||
|
|
||||||
|
- name: assert copy folder
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_folder_contents is changed
|
||||||
|
- copy_folder_contents.operation == 'folder_copy'
|
||||||
|
- copy_folder_contents_actual.examined == 10 # includes files and folders, the below is the nested order
|
||||||
|
- copy_folder_contents_actual.matched == 5
|
||||||
|
- copy_folder_contents_actual.files[0].filename == 'subdir'
|
||||||
|
- copy_folder_contents_actual.files[1].filename == 'empty'
|
||||||
|
- copy_folder_contents_actual.files[2].filename == 'subdir2'
|
||||||
|
- copy_folder_contents_actual.files[3].filename == 'subdir3'
|
||||||
|
- copy_folder_contents_actual.files[4].filename == 'subdir4'
|
||||||
|
|
||||||
|
- name: fail to copy file to a folder
|
||||||
|
win_copy:
|
||||||
|
src: foo.txt
|
||||||
|
dest: '{{test_win_copy_path}}\recursive-contents'
|
||||||
|
register: fail_file_to_folder
|
||||||
|
failed_when: "'object at path is already a directory' not in fail_file_to_folder.msg"
|
||||||
|
|
||||||
|
- name: fail to copy folder to a file
|
||||||
|
win_copy:
|
||||||
|
src: subdir/
|
||||||
|
dest: '{{test_win_copy_path}}\recursive-contents\foo.txt'
|
||||||
|
register: fail_folder_to_file
|
||||||
|
failed_when: "'object at parent directory path is already a file' not in fail_folder_to_file.msg"
|
||||||
|
|
||||||
|
# https://github.com/ansible/ansible/issues/31336
|
||||||
|
- name: create file with colon in the name
|
||||||
|
copy:
|
||||||
|
dest: '{{role_path}}/files-different/colon:file'
|
||||||
|
content: test
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: copy a file with colon as a source
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/colon:file'
|
||||||
|
dest: '{{test_win_copy_path}}\colon.file'
|
||||||
|
register: copy_file_with_colon
|
||||||
|
|
||||||
|
- name: get result of file with colon as a source
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\colon.file'
|
||||||
|
register: copy_file_with_colon_result
|
||||||
|
|
||||||
|
- name: assert results of copy a file with colon as a source
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_file_with_colon is changed
|
||||||
|
- copy_file_with_colon_result.stat.exists == True
|
||||||
|
- copy_file_with_colon_result.stat.checksum == "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
|
||||||
|
|
||||||
|
- name: remove file with colon in the name
|
||||||
|
file:
|
||||||
|
path: '{{role_path}}/files-different/colon:file'
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: copy an encrypted file without decrypting
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault/vault-file'
|
||||||
|
dest: '{{test_win_copy_path}}\vault-file'
|
||||||
|
decrypt: no
|
||||||
|
register: copy_encrypted_file
|
||||||
|
|
||||||
|
- name: get stat of copied encrypted file without decrypting
|
||||||
|
win_stat:
|
||||||
|
path: '{{test_win_copy_path}}\vault-file'
|
||||||
|
register: copy_encrypted_file_result
|
||||||
|
|
||||||
|
- name: assert result of copy an encrypted file without decrypting
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_encrypted_file is changed
|
||||||
|
- copy_encrypted_file_result.stat.checksum == "74a89620002d253f38834ee5b06cddd28956a43d"
|
||||||
|
|
||||||
|
- name: copy an encrypted file without decrypting (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault/vault-file'
|
||||||
|
dest: '{{test_win_copy_path}}\vault-file'
|
||||||
|
decrypt: no
|
||||||
|
register: copy_encrypted_file_again
|
||||||
|
|
||||||
|
- name: assert result of copy an encrypted file without decrypting (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_encrypted_file_again is not changed
|
||||||
|
|
||||||
|
- name: copy folder with encrypted files without decrypting
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault/'
|
||||||
|
dest: '{{test_win_copy_path}}\encrypted-test'
|
||||||
|
decrypt: no
|
||||||
|
register: copy_encrypted_file
|
||||||
|
|
||||||
|
- name: get result of copy folder with encrypted files without decrypting
|
||||||
|
win_find:
|
||||||
|
paths: '{{test_win_copy_path}}\encrypted-test'
|
||||||
|
recurse: yes
|
||||||
|
patterns: '*vault*'
|
||||||
|
register: copy_encrypted_file_result
|
||||||
|
|
||||||
|
- name: assert result of copy folder with encrypted files without decrypting
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_encrypted_file is changed
|
||||||
|
- copy_encrypted_file_result.files|count == 2
|
||||||
|
- copy_encrypted_file_result.files[0].checksum == "834563c94127730ecfa42dfc1e1821bbda2e51da"
|
||||||
|
- copy_encrypted_file_result.files[1].checksum == "74a89620002d253f38834ee5b06cddd28956a43d"
|
||||||
|
|
||||||
|
- name: copy folder with encrypted files without decrypting (idempotent)
|
||||||
|
win_copy:
|
||||||
|
src: '{{role_path}}/files-different/vault/'
|
||||||
|
dest: '{{test_win_copy_path}}\encrypted-test'
|
||||||
|
decrypt: no
|
||||||
|
register: copy_encrypted_file_again
|
||||||
|
|
||||||
|
- name: assert result of copy folder with encrypted files without decrypting (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_encrypted_file_again is not changed
|
||||||
|
|
||||||
|
- name: remove test folder after local to remote tests
|
||||||
|
win_file:
|
||||||
|
path: '{{test_win_copy_path}}'
|
||||||
|
state: absent
|
@ -0,0 +1,5 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
||||||
|
skip/windows/2008
|
||||||
|
skip/windows/2008-R2
|
||||||
|
skip/windows/2012
|
@ -0,0 +1,2 @@
|
|||||||
|
dependencies:
|
||||||
|
- setup_remote_tmp_dir
|
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
- include: pre_test.yml
|
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
- set_fact:
|
||||||
|
AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx'
|
||||||
|
|
||||||
|
- name: Install FS-Data-Deduplication
|
||||||
|
win_feature:
|
||||||
|
name: FS-Data-Deduplication
|
||||||
|
include_sub_features: true
|
||||||
|
state: present
|
||||||
|
register: data_dedup_feat_reg
|
||||||
|
|
||||||
|
- name: Reboot windows after the feature has been installed
|
||||||
|
win_reboot:
|
||||||
|
reboot_timeout: 3600
|
||||||
|
when:
|
||||||
|
- data_dedup_feat_reg.success
|
||||||
|
- data_dedup_feat_reg.reboot_required
|
||||||
|
|
||||||
|
- name: Copy VHDX scripts
|
||||||
|
win_template:
|
||||||
|
src: "{{ item.src }}"
|
||||||
|
dest: '{{ remote_tmp_dir }}\{{ item.dest }}'
|
||||||
|
loop:
|
||||||
|
- { src: partition_creation_script.j2, dest: partition_creation_script.txt }
|
||||||
|
- { src: partition_deletion_script.j2, dest: partition_deletion_script.txt }
|
||||||
|
|
||||||
|
- name: Create partition
|
||||||
|
win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_creation_script.txt
|
||||||
|
|
||||||
|
- name: Format T with NTFS
|
||||||
|
win_format:
|
||||||
|
drive_letter: T
|
||||||
|
file_system: ntfs
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
block:
|
||||||
|
- include: tests.yml
|
||||||
|
always:
|
||||||
|
- name: Detach disk
|
||||||
|
win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_deletion_script.txt
|
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- name: Enable Data Deduplication on the T drive - check mode
|
||||||
|
win_data_deduplication:
|
||||||
|
drive_letter: "T"
|
||||||
|
state: present
|
||||||
|
settings:
|
||||||
|
no_compress: true
|
||||||
|
minimum_file_age_days: 2
|
||||||
|
minimum_file_size: 0
|
||||||
|
check_mode: yes
|
||||||
|
register: win_data_deduplication_enable_check_mode
|
||||||
|
|
||||||
|
- name: Check that it was successful with a change - check mode
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_data_deduplication_enable_check_mode is changed
|
||||||
|
|
||||||
|
- name: Enable Data Deduplication on the T drive
|
||||||
|
win_data_deduplication:
|
||||||
|
drive_letter: "T"
|
||||||
|
state: present
|
||||||
|
settings:
|
||||||
|
no_compress: true
|
||||||
|
minimum_file_age_days: 2
|
||||||
|
minimum_file_size: 0
|
||||||
|
register: win_data_deduplication_enable
|
||||||
|
|
||||||
|
- name: Check that it was successful with a change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_data_deduplication_enable is changed
|
||||||
|
|
||||||
|
- name: Enable Data Deduplication on the T drive
|
||||||
|
win_data_deduplication:
|
||||||
|
drive_letter: "T"
|
||||||
|
state: present
|
||||||
|
settings:
|
||||||
|
no_compress: true
|
||||||
|
minimum_file_age_days: 2
|
||||||
|
minimum_file_size: 0
|
||||||
|
register: win_data_deduplication_enable_again
|
||||||
|
|
||||||
|
- name: Check that it was successful without a change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_data_deduplication_enable_again is not changed
|
@ -0,0 +1,11 @@
|
|||||||
|
create vdisk file="{{ AnsibleVhdx }}" maximum=2000 type=fixed
|
||||||
|
|
||||||
|
select vdisk file="{{ AnsibleVhdx }}"
|
||||||
|
|
||||||
|
attach vdisk
|
||||||
|
|
||||||
|
convert mbr
|
||||||
|
|
||||||
|
create partition primary
|
||||||
|
|
||||||
|
assign letter="T"
|
@ -0,0 +1,3 @@
|
|||||||
|
select vdisk file="{{ AnsibleVhdx }}"
|
||||||
|
|
||||||
|
detach vdisk
|
@ -0,0 +1,6 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
||||||
|
skip/windows/2008
|
||||||
|
skip/windows/2008-R2
|
||||||
|
skip/windows/2012
|
||||||
|
skip/windows/2012-R2
|
@ -0,0 +1,41 @@
|
|||||||
|
#Requires -Version 5.0 -Modules CimCmdlets
|
||||||
|
|
||||||
|
Function Get-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Hashtable])]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String]$KeyParam
|
||||||
|
)
|
||||||
|
return @{Value = [bool]$global:DSCMachineStatus}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Set-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String]$KeyParam,
|
||||||
|
[Bool]$Value = $true
|
||||||
|
)
|
||||||
|
$global:DSCMachineStatus = [int]$Value
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Test-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Boolean])]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String]$KeyParam,
|
||||||
|
[Bool]$Value = $true
|
||||||
|
)
|
||||||
|
$false
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function *-TargetResource
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
[ClassVersion("1.0.0"), FriendlyName("xSetReboot")]
|
||||||
|
class ANSIBLE_xSetReboot : OMI_BaseResource
|
||||||
|
{
|
||||||
|
[Key] String KeyParam;
|
||||||
|
[Write] Boolean Value;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,214 @@
|
|||||||
|
#Requires -Version 5.0 -Modules CimCmdlets
|
||||||
|
|
||||||
|
Function ConvertFrom-CimInstance {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][CimInstance]$Instance
|
||||||
|
)
|
||||||
|
$hashtable = @{
|
||||||
|
_cim_instance = $Instance.CimSystemProperties.ClassName
|
||||||
|
}
|
||||||
|
foreach ($prop in $Instance.CimInstanceProperties) {
|
||||||
|
$hashtable."$($prop.Name)" = ConvertTo-OutputValue -Value $prop.Value
|
||||||
|
}
|
||||||
|
return $hashtable
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-OutputValue {
|
||||||
|
param($Value)
|
||||||
|
|
||||||
|
if ($Value -is [DateTime[]]) {
|
||||||
|
$Value = $Value | ForEach-Object { $_.ToString("o") }
|
||||||
|
} elseif ($Value -is [DateTime]) {
|
||||||
|
$Value = $Value.ToString("o")
|
||||||
|
} elseif ($Value -is [Double]) {
|
||||||
|
$Value = $Value.ToString() # To avoid Python 2 double parsing issues on test validation
|
||||||
|
} elseif ($Value -is [Double[]]) {
|
||||||
|
$Value = $Value | ForEach-Object { $_.ToString() }
|
||||||
|
} elseif ($Value -is [PSCredential]) {
|
||||||
|
$password = $null
|
||||||
|
$password_ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($Value.Password)
|
||||||
|
try {
|
||||||
|
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($password_ptr)
|
||||||
|
} finally {
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($password_ptr)
|
||||||
|
}
|
||||||
|
$Value = @{
|
||||||
|
username = $Value.Username
|
||||||
|
password = $password
|
||||||
|
}
|
||||||
|
} elseif ($Value -is [CimInstance[]]) {
|
||||||
|
$value_list = [System.Collections.Generic.List`1[Hashtable]]@()
|
||||||
|
foreach ($cim_instance in $Value) {
|
||||||
|
$value_list.Add((ConvertFrom-CimInstance -Instance $cim_instance))
|
||||||
|
}
|
||||||
|
$Value = $value_list.ToArray()
|
||||||
|
} elseif ($Value -is [CimInstance]) {
|
||||||
|
$Value = ConvertFrom-CimInstance -Instance $Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return ,$Value
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Hashtable])]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
Ensure = $Ensure
|
||||||
|
Path = $Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Set-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path,
|
||||||
|
|
||||||
|
[String] $DefaultParam = "Default",
|
||||||
|
[String] $StringParam,
|
||||||
|
[String[]] $StringArrayParam,
|
||||||
|
[SByte] $Int8Param,
|
||||||
|
[SByte[]] $Int8ArrayParam,
|
||||||
|
[Byte] $UInt8Param,
|
||||||
|
[Byte[]] $UInt8ArrayParam,
|
||||||
|
[Int16] $Int16Param,
|
||||||
|
[Int16[]] $Int16ArrayParam,
|
||||||
|
[UInt16] $UInt16Param,
|
||||||
|
[UInt16[]] $UInt16ArrayParam,
|
||||||
|
[Int32] $Int32Param,
|
||||||
|
[Int32[]] $Int32ArrayParam,
|
||||||
|
[UInt32] $UInt32Param,
|
||||||
|
[UInt32[]] $UInt32ArrayParam,
|
||||||
|
[Int64] $Int64Param,
|
||||||
|
[Int64[]] $Int64ArrayParam,
|
||||||
|
[UInt64] $UInt64Param,
|
||||||
|
[UInt64[]] $UInt64ArrayParam,
|
||||||
|
[Bool] $BooleanParam,
|
||||||
|
[Bool[]] $BooleanArrayParam,
|
||||||
|
[Char] $CharParam,
|
||||||
|
[Char[]] $CharArrayParam,
|
||||||
|
[Single] $SingleParam,
|
||||||
|
[Single[]] $SingleArrayParam,
|
||||||
|
[Double] $DoubleParam,
|
||||||
|
[Double[]] $DoubleArrayParam,
|
||||||
|
[DateTime] $DateTimeParam,
|
||||||
|
[DateTime[]] $DateTimeArrayParam,
|
||||||
|
[PSCredential] $PSCredentialParam,
|
||||||
|
[CimInstance[]] $HashtableParam,
|
||||||
|
[CimInstance] $CimInstanceParam,
|
||||||
|
[CimInstance[]] $CimInstanceArrayParam,
|
||||||
|
[CimInstance] $NestedCimInstanceParam,
|
||||||
|
[CimInstance[]] $NestedCimInstanceArrayParam
|
||||||
|
)
|
||||||
|
|
||||||
|
$info = @{
|
||||||
|
Version = "1.0.0"
|
||||||
|
Ensure = @{
|
||||||
|
Type = $Ensure.GetType().FullName
|
||||||
|
Value = $Ensure
|
||||||
|
}
|
||||||
|
Path = @{
|
||||||
|
Type = $Path.GetType().FullName
|
||||||
|
Value = $Path
|
||||||
|
}
|
||||||
|
DefaultParam = @{
|
||||||
|
Type = $DefaultParam.GetType().FullName
|
||||||
|
Value = $DefaultParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($kvp in $PSCmdlet.MyInvocation.BoundParameters.GetEnumerator()) {
|
||||||
|
$info."$($kvp.Key)" = @{
|
||||||
|
Type = $kvp.Value.GetType().FullName
|
||||||
|
Value = (ConvertTo-OutputValue -Value $kvp.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path $Path) {
|
||||||
|
Remove-Item -Path $Path -Force > $null
|
||||||
|
}
|
||||||
|
New-Item -Path $Path -ItemType File > $null
|
||||||
|
Set-Content -Path $Path -Value (ConvertTo-Json -InputObject $info -Depth 10) > $null
|
||||||
|
Write-Verbose -Message "set verbose"
|
||||||
|
Write-Warning -Message "set warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Test-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Boolean])]
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path,
|
||||||
|
|
||||||
|
[String] $DefaultParam = "Default",
|
||||||
|
[String] $StringParam,
|
||||||
|
[String[]] $StringArrayParam,
|
||||||
|
[SByte] $Int8Param,
|
||||||
|
[SByte[]] $Int8ArrayParam,
|
||||||
|
[Byte] $UInt8Param,
|
||||||
|
[Byte[]] $UInt8ArrayParam,
|
||||||
|
[Int16] $Int16Param,
|
||||||
|
[Int16[]] $Int16ArrayParam,
|
||||||
|
[UInt16] $UInt16Param,
|
||||||
|
[UInt16[]] $UInt16ArrayParam,
|
||||||
|
[Int32] $Int32Param,
|
||||||
|
[Int32[]] $Int32ArrayParam,
|
||||||
|
[UInt32] $UInt32Param,
|
||||||
|
[UInt32[]] $UInt32ArrayParam,
|
||||||
|
[Int64] $Int64Param,
|
||||||
|
[Int64[]] $Int64ArrayParam,
|
||||||
|
[UInt64] $UInt64Param,
|
||||||
|
[UInt64[]] $UInt64ArrayParam,
|
||||||
|
[Bool] $BooleanParam,
|
||||||
|
[Bool[]] $BooleanArrayParam,
|
||||||
|
[Char] $CharParam,
|
||||||
|
[Char[]] $CharArrayParam,
|
||||||
|
[Single] $SingleParam,
|
||||||
|
[Single[]] $SingleArrayParam,
|
||||||
|
[Double] $DoubleParam,
|
||||||
|
[Double[]] $DoubleArrayParam,
|
||||||
|
[DateTime] $DateTimeParam,
|
||||||
|
[DateTime[]] $DateTimeArrayParam,
|
||||||
|
[PSCredential] $PSCredentialParam,
|
||||||
|
[CimInstance[]] $HashtableParam,
|
||||||
|
[CimInstance] $CimInstanceParam,
|
||||||
|
[CimInstance[]] $CimInstanceArrayParam,
|
||||||
|
[CimInstance] $NestedCimInstanceParam,
|
||||||
|
[CimInstance[]] $NestedCimInstanceArrayParam
|
||||||
|
)
|
||||||
|
Write-Verbose -Message "test verbose"
|
||||||
|
Write-Warning -Message "test warning"
|
||||||
|
$exists = Test-Path -LiteralPath $Path -PathType Leaf
|
||||||
|
if ($Ensure -eq "Present") {
|
||||||
|
$exists
|
||||||
|
} else {
|
||||||
|
-not $exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function *-TargetResource
|
||||||
|
|
@ -0,0 +1,60 @@
|
|||||||
|
[ClassVersion("1.0.0")]
|
||||||
|
class ANSIBLE_xTestClass
|
||||||
|
{
|
||||||
|
[Key] String Key;
|
||||||
|
[Write] String StringValue;
|
||||||
|
[Write] SInt32 IntValue;
|
||||||
|
[Write] String StringArrayValue[];
|
||||||
|
};
|
||||||
|
|
||||||
|
[ClassVersion("1.0.0")]
|
||||||
|
class ANSIBLE_xNestedClass
|
||||||
|
{
|
||||||
|
[Key] String KeyValue;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimValue;
|
||||||
|
[Write, EmbeddedInstance("MSFT_KeyValuePair")] String HashValue[];
|
||||||
|
[Write] SInt16 IntValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
[ClassVersion("1.0.0"), FriendlyName("xTestResource")]
|
||||||
|
class ANSIBLE_xTestResource : OMI_BaseResource
|
||||||
|
{
|
||||||
|
[Key] String Path;
|
||||||
|
[Required, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure;
|
||||||
|
[Read] String ReadParam;
|
||||||
|
[Write] String DefaultParam;
|
||||||
|
[Write] String StringParam;
|
||||||
|
[Write] String StringArrayParam[];
|
||||||
|
[Write] SInt8 Int8Param;
|
||||||
|
[Write] SInt8 Int8ArrayParam[];
|
||||||
|
[Write] UInt8 UInt8Param;
|
||||||
|
[Write] UInt8 UInt8ArrayParam[];
|
||||||
|
[Write] SInt16 Int16Param;
|
||||||
|
[Write] SInt16 Int16ArrayParam[];
|
||||||
|
[Write] UInt16 UInt16Param;
|
||||||
|
[Write] UInt16 UInt16ArrayParam[];
|
||||||
|
[Write] SInt32 Int32Param;
|
||||||
|
[Write] SInt32 Int32ArrayParam[];
|
||||||
|
[Write] UInt32 UInt32Param;
|
||||||
|
[Write] UInt32 UInt32ArrayParam[];
|
||||||
|
[Write] SInt64 Int64Param;
|
||||||
|
[Write] SInt64 Int64ArrayParam[];
|
||||||
|
[Write] UInt64 UInt64Param;
|
||||||
|
[Write] UInt64 UInt64ArrayParam[];
|
||||||
|
[Write] Boolean BooleanParam;
|
||||||
|
[Write] Boolean BooleanArrayParam[];
|
||||||
|
[Write] Char16 CharParam;
|
||||||
|
[Write] Char16 CharArrayParam[];
|
||||||
|
[Write] Real32 SingleParam;
|
||||||
|
[Write] Real32 SingleArrayParam[];
|
||||||
|
[Write] Real64 DoubleParam;
|
||||||
|
[Write] Real64 DoubleArrayParam[];
|
||||||
|
[Write] DateTime DateTimeParam;
|
||||||
|
[Write] DateTime DateTimeArrayParam[];
|
||||||
|
[Write, EmbeddedInstance("MSFT_Credential")] String PSCredentialParam;
|
||||||
|
[Write, EmbeddedInstance("MSFT_KeyValuePair")] String HashtableParam[];
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimInstanceArrayParam[];
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xNestedClass")] String NestedCimInstanceParam;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xNestedClass")] String NestedCimInstanceArrayParam[];
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
@{
|
||||||
|
ModuleVersion = '1.0.0'
|
||||||
|
GUID = '80c895c4-de3f-4d6d-8fa4-c504c96b6f22'
|
||||||
|
Author = 'Ansible'
|
||||||
|
CompanyName = 'Ansible'
|
||||||
|
Copyright = '(c) 2019'
|
||||||
|
Description = 'Test DSC Resource for Ansible integration tests'
|
||||||
|
PowerShellVersion = '5.0'
|
||||||
|
CLRVersion = '4.0'
|
||||||
|
FunctionsToExport = '*'
|
||||||
|
CmdletsToExport = '*'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
|||||||
|
#Requires -Version 5.0 -Modules CimCmdlets
|
||||||
|
|
||||||
|
Function ConvertFrom-CimInstance {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][CimInstance]$Instance
|
||||||
|
)
|
||||||
|
$hashtable = @{
|
||||||
|
_cim_instance = $Instance.CimSystemProperties.ClassName
|
||||||
|
}
|
||||||
|
foreach ($prop in $Instance.CimInstanceProperties) {
|
||||||
|
$hashtable."$($prop.Name)" = ConvertTo-OutputValue -Value $prop.Value
|
||||||
|
}
|
||||||
|
return $hashtable
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-OutputValue {
|
||||||
|
param($Value)
|
||||||
|
|
||||||
|
if ($Value -is [DateTime[]]) {
|
||||||
|
$Value = $Value | ForEach-Object { $_.ToString("o") }
|
||||||
|
} elseif ($Value -is [DateTime]) {
|
||||||
|
$Value = $Value.ToString("o")
|
||||||
|
} elseif ($Value -is [Double]) {
|
||||||
|
$Value = $Value.ToString() # To avoid Python 2 double parsing issues on test validation
|
||||||
|
} elseif ($Value -is [Double[]]) {
|
||||||
|
$Value = $Value | ForEach-Object { $_.ToString() }
|
||||||
|
} elseif ($Value -is [PSCredential]) {
|
||||||
|
$password = $null
|
||||||
|
$password_ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($Value.Password)
|
||||||
|
try {
|
||||||
|
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($password_ptr)
|
||||||
|
} finally {
|
||||||
|
[System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($password_ptr)
|
||||||
|
}
|
||||||
|
$Value = @{
|
||||||
|
username = $Value.Username
|
||||||
|
password = $password
|
||||||
|
}
|
||||||
|
} elseif ($Value -is [CimInstance[]]) {
|
||||||
|
$value_list = [System.Collections.Generic.List`1[Hashtable]]@()
|
||||||
|
foreach ($cim_instance in $Value) {
|
||||||
|
$value_list.Add((ConvertFrom-CimInstance -Instance $cim_instance))
|
||||||
|
}
|
||||||
|
$Value = $value_list.ToArray()
|
||||||
|
} elseif ($Value -is [CimInstance]) {
|
||||||
|
$Value = ConvertFrom-CimInstance -Instance $Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return ,$Value
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Hashtable])]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
Ensure = $Ensure
|
||||||
|
Path = $Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Set-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path,
|
||||||
|
|
||||||
|
[String] $DefaultParam = "Default",
|
||||||
|
[String] $StringParam,
|
||||||
|
[String[]] $StringArrayParam,
|
||||||
|
[SByte] $Int8Param,
|
||||||
|
[SByte[]] $Int8ArrayParam,
|
||||||
|
[Byte] $UInt8Param,
|
||||||
|
[Byte[]] $UInt8ArrayParam,
|
||||||
|
[Int16] $Int16Param,
|
||||||
|
[Int16[]] $Int16ArrayParam,
|
||||||
|
[UInt16] $UInt16Param,
|
||||||
|
[UInt16[]] $UInt16ArrayParam,
|
||||||
|
[Int32] $Int32Param,
|
||||||
|
[Int32[]] $Int32ArrayParam,
|
||||||
|
[UInt32] $UInt32Param,
|
||||||
|
[UInt32[]] $UInt32ArrayParam,
|
||||||
|
[Int64] $Int64Param,
|
||||||
|
[Int64[]] $Int64ArrayParam,
|
||||||
|
[UInt64] $UInt64Param,
|
||||||
|
[UInt64[]] $UInt64ArrayParam,
|
||||||
|
[Bool] $BooleanParam,
|
||||||
|
[Bool[]] $BooleanArrayParam,
|
||||||
|
[Char] $CharParam,
|
||||||
|
[Char[]] $CharArrayParam,
|
||||||
|
[Single] $SingleParam,
|
||||||
|
[Single[]] $SingleArrayParam,
|
||||||
|
[Double] $DoubleParam,
|
||||||
|
[Double[]] $DoubleArrayParam,
|
||||||
|
[DateTime] $DateTimeParam,
|
||||||
|
[DateTime[]] $DateTimeArrayParam,
|
||||||
|
[PSCredential] $PSCredentialParam,
|
||||||
|
[CimInstance[]] $HashtableParam,
|
||||||
|
[CimInstance] $CimInstanceParam,
|
||||||
|
[CimInstance[]] $CimInstanceArrayParam,
|
||||||
|
[CimInstance] $NestedCimInstanceParam,
|
||||||
|
[CimInstance[]] $NestedCimInstanceArrayParam
|
||||||
|
)
|
||||||
|
|
||||||
|
$info = @{
|
||||||
|
Version = "1.0.1"
|
||||||
|
Ensure = @{
|
||||||
|
Type = $Ensure.GetType().FullName
|
||||||
|
Value = $Ensure
|
||||||
|
}
|
||||||
|
Path = @{
|
||||||
|
Type = $Path.GetType().FullName
|
||||||
|
Value = $Path
|
||||||
|
}
|
||||||
|
DefaultParam = @{
|
||||||
|
Type = $DefaultParam.GetType().FullName
|
||||||
|
Value = $DefaultParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($kvp in $PSCmdlet.MyInvocation.BoundParameters.GetEnumerator()) {
|
||||||
|
$info."$($kvp.Key)" = @{
|
||||||
|
Type = $kvp.Value.GetType().FullName
|
||||||
|
Value = (ConvertTo-OutputValue -Value $kvp.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path $Path) {
|
||||||
|
Remove-Item -Path $Path -Force > $null
|
||||||
|
}
|
||||||
|
New-Item -Path $Path -ItemType File > $null
|
||||||
|
Set-Content -Path $Path -Value (ConvertTo-Json -InputObject $info -Depth 10) > $null
|
||||||
|
Write-Verbose -Message "set verbose"
|
||||||
|
Write-Warning -Message "set warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Test-TargetResource
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([Boolean])]
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet("Present", "Absent")]
|
||||||
|
[String] $Ensure = "Present",
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[String] $Path,
|
||||||
|
|
||||||
|
[String] $DefaultParam = "Default",
|
||||||
|
[String] $StringParam,
|
||||||
|
[String[]] $StringArrayParam,
|
||||||
|
[SByte] $Int8Param,
|
||||||
|
[SByte[]] $Int8ArrayParam,
|
||||||
|
[Byte] $UInt8Param,
|
||||||
|
[Byte[]] $UInt8ArrayParam,
|
||||||
|
[Int16] $Int16Param,
|
||||||
|
[Int16[]] $Int16ArrayParam,
|
||||||
|
[UInt16] $UInt16Param,
|
||||||
|
[UInt16[]] $UInt16ArrayParam,
|
||||||
|
[Int32] $Int32Param,
|
||||||
|
[Int32[]] $Int32ArrayParam,
|
||||||
|
[UInt32] $UInt32Param,
|
||||||
|
[UInt32[]] $UInt32ArrayParam,
|
||||||
|
[Int64] $Int64Param,
|
||||||
|
[Int64[]] $Int64ArrayParam,
|
||||||
|
[UInt64] $UInt64Param,
|
||||||
|
[UInt64[]] $UInt64ArrayParam,
|
||||||
|
[Bool] $BooleanParam,
|
||||||
|
[Bool[]] $BooleanArrayParam,
|
||||||
|
[Char] $CharParam,
|
||||||
|
[Char[]] $CharArrayParam,
|
||||||
|
[Single] $SingleParam,
|
||||||
|
[Single[]] $SingleArrayParam,
|
||||||
|
[Double] $DoubleParam,
|
||||||
|
[Double[]] $DoubleArrayParam,
|
||||||
|
[DateTime] $DateTimeParam,
|
||||||
|
[DateTime[]] $DateTimeArrayParam,
|
||||||
|
[PSCredential] $PSCredentialParam,
|
||||||
|
[CimInstance[]] $HashtableParam,
|
||||||
|
[CimInstance] $CimInstanceParam,
|
||||||
|
[CimInstance[]] $CimInstanceArrayParam,
|
||||||
|
[CimInstance] $NestedCimInstanceParam,
|
||||||
|
[CimInstance[]] $NestedCimInstanceArrayParam
|
||||||
|
)
|
||||||
|
Write-Verbose -Message "test verbose"
|
||||||
|
Write-Warning -Message "test warning"
|
||||||
|
$exists = Test-Path -LiteralPath $Path -PathType Leaf
|
||||||
|
if ($Ensure -eq "Present") {
|
||||||
|
$exists
|
||||||
|
} else {
|
||||||
|
-not $exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function *-TargetResource
|
||||||
|
|
@ -0,0 +1,63 @@
|
|||||||
|
[ClassVersion("1.0.1")]
|
||||||
|
class ANSIBLE_xTestClass
|
||||||
|
{
|
||||||
|
[Key] String KeyValue;
|
||||||
|
[Write, ValueMap{"Choice1", "Choice2"}, Values{"Choice1", "Choice2"}] String Choice;
|
||||||
|
[Write] String StringValue;
|
||||||
|
[Write] SInt32 IntValue;
|
||||||
|
[Write] String StringArrayValue[];
|
||||||
|
};
|
||||||
|
|
||||||
|
[ClassVersion("1.0.1")]
|
||||||
|
class ANSIBLE_xNestedClass
|
||||||
|
{
|
||||||
|
[Key] String KeyValue;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimValue;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimArrayValue[];
|
||||||
|
[Write, EmbeddedInstance("MSFT_KeyValuePair")] String HashValue[];
|
||||||
|
[Write] SInt16 IntValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
[ClassVersion("1.0.1"), FriendlyName("xTestResource")]
|
||||||
|
class ANSIBLE_xTestResource : OMI_BaseResource
|
||||||
|
{
|
||||||
|
[Key] String Path;
|
||||||
|
[Required, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure;
|
||||||
|
[Read] String ReadParam;
|
||||||
|
[Write] String DefaultParam;
|
||||||
|
[Write] String StringParam;
|
||||||
|
[Write] String StringArrayParam[];
|
||||||
|
[Write] SInt8 Int8Param;
|
||||||
|
[Write] SInt8 Int8ArrayParam[];
|
||||||
|
[Write] UInt8 UInt8Param;
|
||||||
|
[Write] UInt8 UInt8ArrayParam[];
|
||||||
|
[Write] SInt16 Int16Param;
|
||||||
|
[Write] SInt16 Int16ArrayParam[];
|
||||||
|
[Write] UInt16 UInt16Param;
|
||||||
|
[Write] UInt16 UInt16ArrayParam[];
|
||||||
|
[Write] SInt32 Int32Param;
|
||||||
|
[Write] SInt32 Int32ArrayParam[];
|
||||||
|
[Write] UInt32 UInt32Param;
|
||||||
|
[Write] UInt32 UInt32ArrayParam[];
|
||||||
|
[Write] SInt64 Int64Param;
|
||||||
|
[Write] SInt64 Int64ArrayParam[];
|
||||||
|
[Write] UInt64 UInt64Param;
|
||||||
|
[Write] UInt64 UInt64ArrayParam[];
|
||||||
|
[Write] Boolean BooleanParam;
|
||||||
|
[Write] Boolean BooleanArrayParam[];
|
||||||
|
[Write] Char16 CharParam;
|
||||||
|
[Write] Char16 CharArrayParam[];
|
||||||
|
[Write] Real32 SingleParam;
|
||||||
|
[Write] Real32 SingleArrayParam[];
|
||||||
|
[Write] Real64 DoubleParam;
|
||||||
|
[Write] Real64 DoubleArrayParam[];
|
||||||
|
[Write] DateTime DateTimeParam;
|
||||||
|
[Write] DateTime DateTimeArrayParam[];
|
||||||
|
[Write, EmbeddedInstance("MSFT_Credential")] String PSCredentialParam;
|
||||||
|
[Write, EmbeddedInstance("MSFT_KeyValuePair")] String HashtableParam[];
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimInstanceParam;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xTestClass")] String CimInstanceArrayParam[];
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xNestedClass")] String NestedCimInstanceParam;
|
||||||
|
[Write, EmbeddedInstance("ANSIBLE_xNestedClass")] String NestedCimInstanceArrayParam[];
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
@{
|
||||||
|
ModuleVersion = '1.0.1'
|
||||||
|
GUID = '80c895c4-de3f-4d6d-8fa4-c504c96b6f22'
|
||||||
|
Author = 'Ansible'
|
||||||
|
CompanyName = 'Ansible'
|
||||||
|
Copyright = '(c) 2019'
|
||||||
|
Description = 'Test DSC Resource for Ansible integration tests'
|
||||||
|
PowerShellVersion = '5.0'
|
||||||
|
CLRVersion = '4.0'
|
||||||
|
FunctionsToExport = '*'
|
||||||
|
CmdletsToExport = '*'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
dependencies:
|
||||||
|
- setup_remote_tmp_dir
|
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
- name: get powershell version
|
||||||
|
win_shell: $PSVersionTable.PSVersion.Major
|
||||||
|
register: powershell_version
|
||||||
|
|
||||||
|
- name: expect failure when running on old PS hosts
|
||||||
|
win_dsc:
|
||||||
|
resource_name: File
|
||||||
|
register: fail_dsc_old
|
||||||
|
failed_when: '"This module cannot run as it requires a minimum PowerShell version of 5.0" not in fail_dsc_old.msg'
|
||||||
|
when: powershell_version.stdout_lines[0]|int < 5
|
||||||
|
|
||||||
|
- name: run tests when PSv5+
|
||||||
|
when: powershell_version.stdout_lines[0]|int >= 5
|
||||||
|
block:
|
||||||
|
- name: add remote temp dir to PSModulePath
|
||||||
|
win_path:
|
||||||
|
name: PSModulePath
|
||||||
|
state: present
|
||||||
|
scope: machine
|
||||||
|
elements:
|
||||||
|
- '{{ remote_tmp_dir }}'
|
||||||
|
|
||||||
|
- name: copy custom DSC resources to remote temp dir
|
||||||
|
win_copy:
|
||||||
|
src: xTestDsc
|
||||||
|
dest: '{{ remote_tmp_dir }}'
|
||||||
|
|
||||||
|
- name: run tests
|
||||||
|
include_tasks: tests.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: remove remote tmp dir from PSModulePath
|
||||||
|
win_path:
|
||||||
|
name: PSModulePath
|
||||||
|
state: absent
|
||||||
|
scope: machine
|
||||||
|
elements:
|
||||||
|
- '{{ remote_tmp_dir }}'
|
@ -0,0 +1,544 @@
|
|||||||
|
---
|
||||||
|
- name: fail with incorrect DSC resource name
|
||||||
|
win_dsc:
|
||||||
|
resource_name: FakeResource
|
||||||
|
register: fail_invalid_resource
|
||||||
|
failed_when: fail_invalid_resource.msg != "Resource 'FakeResource' not found."
|
||||||
|
|
||||||
|
- name: fail with invalid DSC version
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
module_version: 0.0.1
|
||||||
|
register: fail_invalid_version
|
||||||
|
failed_when: 'fail_invalid_version.msg != "Resource ''xTestResource'' with version ''0.0.1'' not found. Versions installed: ''1.0.0'', ''1.0.1''."'
|
||||||
|
|
||||||
|
- name: fail with mandatory option not set
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xSetReboot
|
||||||
|
Value: yes
|
||||||
|
register: fail_man_key
|
||||||
|
failed_when: 'fail_man_key.msg != "missing required arguments: KeyParam"'
|
||||||
|
|
||||||
|
- name: fail with mandatory option not set in sub dict
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
CimInstanceParam: # Missing KeyValue in dict
|
||||||
|
Choice: Choice1
|
||||||
|
register: fail_man_key_sub_dict
|
||||||
|
failed_when: 'fail_man_key_sub_dict.msg != "missing required arguments: KeyValue found in CimInstanceParam"'
|
||||||
|
|
||||||
|
- name: fail invalid option
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xSetReboot
|
||||||
|
KeyParam: key
|
||||||
|
OtherParam: invalid
|
||||||
|
register: fail_invalid_option
|
||||||
|
failed_when: 'fail_invalid_option.msg != "Unsupported parameters for (win_dsc) module: OtherParam. Supported parameters include: KeyParam, PsDscRunAsCredential_username, module_version, Value, PsDscRunAsCredential_password, resource_name, DependsOn"'
|
||||||
|
|
||||||
|
- name: fail invalid option in sub dict
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
NestedCimInstanceParam:
|
||||||
|
KeyValue: key
|
||||||
|
CimValue:
|
||||||
|
KeyValue: other key
|
||||||
|
InvalidKey: invalid
|
||||||
|
register: fail_invalid_option_sub_dict
|
||||||
|
failed_when: 'fail_invalid_option_sub_dict.msg != "Unsupported parameters for (win_dsc) module: InvalidKey found in NestedCimInstanceParam -> CimValue. Supported parameters include: IntValue, KeyValue, StringArrayValue, Choice, StringValue"'
|
||||||
|
|
||||||
|
- name: fail invalid read only option
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
ReadParam: abc
|
||||||
|
register: fail_invalid_option_read_only
|
||||||
|
failed_when: '"Unsupported parameters for (win_dsc) module: ReadParam" not in fail_invalid_option_read_only.msg'
|
||||||
|
|
||||||
|
- name: fail invalid choice
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: invalid
|
||||||
|
register: fail_invalid_choice
|
||||||
|
failed_when: 'fail_invalid_choice.msg != "value of Ensure must be one of: Present, Absent. Got no match for: invalid"'
|
||||||
|
|
||||||
|
- name: fail invalid choice in sub dict
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
CimInstanceArrayParam:
|
||||||
|
- KeyValue: key
|
||||||
|
- KeyValue: key2
|
||||||
|
Choice: Choice3
|
||||||
|
register: fail_invalid_choice_sub_dict
|
||||||
|
failed_when: 'fail_invalid_choice_sub_dict.msg != "value of Choice must be one of: Choice1, Choice2. Got no match for: Choice3 found in CimInstanceArrayParam"'
|
||||||
|
|
||||||
|
- name: fail old version missing new option
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
module_version: 1.0.0
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
CimInstanceParam: # CimInstanceParam does not exist in the 1.0.0 version
|
||||||
|
Key: key
|
||||||
|
register: fail_invalid_option_old
|
||||||
|
failed_when: '"Unsupported parameters for (win_dsc) module: CimInstanceParam" not in fail_invalid_option_old.msg'
|
||||||
|
|
||||||
|
- name: fail old version missing new option sub dict
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
module_version: 1.0.0
|
||||||
|
Path: C:\path
|
||||||
|
Ensure: Present
|
||||||
|
CimInstanceArrayParam:
|
||||||
|
- Key: key
|
||||||
|
Choice: Choice1
|
||||||
|
register: fail_invalid_option_old_sub_dict
|
||||||
|
failed_when: 'fail_invalid_option_old_sub_dict.msg != "Unsupported parameters for (win_dsc) module: Choice found in CimInstanceArrayParam. Supported parameters include: Key, IntValue, StringArrayValue, StringValue"'
|
||||||
|
|
||||||
|
- name: create test file (check mode)
|
||||||
|
win_dsc:
|
||||||
|
resource_name: File
|
||||||
|
DestinationPath: '{{ remote_tmp_dir }}\dsc-file'
|
||||||
|
Contents: file contents
|
||||||
|
Attributes:
|
||||||
|
- Hidden
|
||||||
|
- ReadOnly
|
||||||
|
Ensure: Present
|
||||||
|
Type: File
|
||||||
|
register: create_file_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get result of create test file (check mode)
|
||||||
|
win_stat:
|
||||||
|
path: '{{ remote_tmp_dir }}\dsc-file'
|
||||||
|
register: create_file_actual_check
|
||||||
|
|
||||||
|
- name: assert create test file (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- create_file_check is changed
|
||||||
|
- create_file_check.module_version == None # Some built in modules don't have a version set
|
||||||
|
- not create_file_check.reboot_required
|
||||||
|
- not create_file_actual_check.stat.exists
|
||||||
|
|
||||||
|
- name: assert create test file verbosity (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- create_file_check.verbose_test is defined
|
||||||
|
- not create_file_check.verbose_set is defined
|
||||||
|
when: ansible_verbosity >= 3
|
||||||
|
|
||||||
|
- name: create test file
|
||||||
|
win_dsc:
|
||||||
|
resource_name: File
|
||||||
|
DestinationPath: '{{ remote_tmp_dir }}\dsc-file'
|
||||||
|
Contents: file contents
|
||||||
|
Attributes:
|
||||||
|
- Hidden
|
||||||
|
- ReadOnly
|
||||||
|
Ensure: Present
|
||||||
|
Type: File
|
||||||
|
register: create_file
|
||||||
|
|
||||||
|
- name: get result of create test file
|
||||||
|
win_stat:
|
||||||
|
path: '{{ remote_tmp_dir }}\dsc-file'
|
||||||
|
register: create_file_actual
|
||||||
|
|
||||||
|
- name: assert create test file verbosity
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- create_file.verbose_test is defined
|
||||||
|
- create_file.verbose_set is defined
|
||||||
|
when: ansible_verbosity >= 3
|
||||||
|
|
||||||
|
- name: assert create test file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- create_file is changed
|
||||||
|
- create_file.module_version == None
|
||||||
|
- not create_file.reboot_required
|
||||||
|
- create_file_actual.stat.exists
|
||||||
|
- create_file_actual.stat.attributes == "ReadOnly, Hidden, Archive"
|
||||||
|
- create_file_actual.stat.checksum == 'd48daab51112b49ecabd917adc345b8ba257055e'
|
||||||
|
|
||||||
|
- name: create test file (idempotent)
|
||||||
|
win_dsc:
|
||||||
|
resource_name: File
|
||||||
|
DestinationPath: '{{ remote_tmp_dir }}\dsc-file'
|
||||||
|
Contents: file contents
|
||||||
|
Attributes:
|
||||||
|
- Hidden
|
||||||
|
- ReadOnly
|
||||||
|
Ensure: Present
|
||||||
|
Type: File
|
||||||
|
register: create_file_again
|
||||||
|
|
||||||
|
- name: assert create test file (idempotent)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not create_file_again is changed
|
||||||
|
- create_file.module_version == None
|
||||||
|
- not create_file.reboot_required
|
||||||
|
|
||||||
|
- name: get SID of the current Ansible user
|
||||||
|
win_shell: |
|
||||||
|
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||||
|
[System.DirectoryServices.AccountManagement.UserPrincipal]::Current.Sid.Value
|
||||||
|
register: actual_sid
|
||||||
|
|
||||||
|
- name: run DSC process as another user
|
||||||
|
win_dsc:
|
||||||
|
resource_name: Script
|
||||||
|
GetScript: '@{ Result= "" }'
|
||||||
|
SetScript: |
|
||||||
|
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||||
|
$sid = [System.DirectoryServices.AccountManagement.UserPrincipal]::Current.Sid.Value
|
||||||
|
Set-Content -Path "{{ remote_tmp_dir }}\runas.txt" -Value $sid
|
||||||
|
TestScript: $false
|
||||||
|
PsDscRunAsCredential_username: '{{ ansible_user }}'
|
||||||
|
PsDscRunAsCredential_password: '{{ ansible_password }}'
|
||||||
|
register: runas_user
|
||||||
|
|
||||||
|
- name: get result of run DSC process as another user
|
||||||
|
slurp:
|
||||||
|
path: '{{ remote_tmp_dir }}\runas.txt'
|
||||||
|
register: runas_user_result
|
||||||
|
|
||||||
|
- name: assert run DSC process as another user
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- runas_user is changed
|
||||||
|
- runas_user.module_version != None # Can't reliably set the version but we can test it is set
|
||||||
|
- not runas_user.reboot_required
|
||||||
|
- runas_user_result.content|b64decode == actual_sid.stdout
|
||||||
|
|
||||||
|
- name: run DSC that sets reboot_required with defaults
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xSetReboot
|
||||||
|
KeyParam: value # Just to satisfy the Resource with key validation
|
||||||
|
register: set_reboot_defaults
|
||||||
|
|
||||||
|
- name: assert run DSC that sets reboot_required with defaults
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- set_reboot_defaults.reboot_required
|
||||||
|
|
||||||
|
- name: run DSC that sets reboot_required with False
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xSetReboot
|
||||||
|
KeyParam: value
|
||||||
|
Value: no
|
||||||
|
register: set_reboot_false
|
||||||
|
|
||||||
|
- name: assert run DSC that sets reboot_required with False
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not set_reboot_false.reboot_required
|
||||||
|
|
||||||
|
- name: run DSC that sets reboot_required with True
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xSetReboot
|
||||||
|
KeyParam: value
|
||||||
|
Value: yes
|
||||||
|
register: set_reboot_true
|
||||||
|
|
||||||
|
- name: assert run DSC that sets reboot_required with True
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- set_reboot_true.reboot_required
|
||||||
|
|
||||||
|
- name: test DSC with all types
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
Path: '{{ remote_tmp_dir }}\test-types.json'
|
||||||
|
Ensure: Present
|
||||||
|
StringParam: string param
|
||||||
|
StringArrayParam:
|
||||||
|
- string 1
|
||||||
|
- string 2
|
||||||
|
Int8Param: 127 # [SByte]::MaxValue
|
||||||
|
Int8ArrayParam:
|
||||||
|
- 127
|
||||||
|
- '127'
|
||||||
|
UInt8Param: 255 # [Byte]::MaxValue
|
||||||
|
UInt8ArrayParam:
|
||||||
|
- 255
|
||||||
|
- '255'
|
||||||
|
Int16Param: 32767 # [Int16]::MaxValue
|
||||||
|
Int16ArrayParam: 32767, 32767
|
||||||
|
UInt16Param: '65535' # [UInt16]::MaxValue
|
||||||
|
UInt16ArrayParam: 65535
|
||||||
|
Int32Param: 2147483647 # [Int32]::MaxValue
|
||||||
|
Int32ArrayParam: '2147483647'
|
||||||
|
UInt32Param: '4294967295' # [UInt32]::MaxValue
|
||||||
|
UInt32ArrayParam:
|
||||||
|
- '4294967295'
|
||||||
|
- 4294967295
|
||||||
|
Int64Param: 9223372036854775807 # [Int64]::MaxValue
|
||||||
|
Int64ArrayParam:
|
||||||
|
- -9223372036854775808 # [Int64]::MinValue
|
||||||
|
- 9223372036854775807
|
||||||
|
UInt64Param: 18446744073709551615 # [UInt64]::MaxValue
|
||||||
|
UInt64ArrayParam:
|
||||||
|
- 0 # [UInt64]::MinValue
|
||||||
|
- 18446744073709551615
|
||||||
|
BooleanParam: True
|
||||||
|
BooleanArrayParam:
|
||||||
|
- True
|
||||||
|
- 'True'
|
||||||
|
- 'true'
|
||||||
|
- 'y'
|
||||||
|
- 'yes'
|
||||||
|
- 1
|
||||||
|
- False
|
||||||
|
- 'False'
|
||||||
|
- 'false'
|
||||||
|
- 'n'
|
||||||
|
- 'no'
|
||||||
|
- 0
|
||||||
|
CharParam: c
|
||||||
|
CharArrayParam:
|
||||||
|
- c
|
||||||
|
- h
|
||||||
|
- a
|
||||||
|
- r
|
||||||
|
SingleParam: 3.402823E+38
|
||||||
|
SingleArrayParam:
|
||||||
|
- '3.402823E+38'
|
||||||
|
- 1.2393494
|
||||||
|
DoubleParam: 1.79769313486232E+300
|
||||||
|
DoubleArrayParam:
|
||||||
|
- '1.79769313486232E+300'
|
||||||
|
- 3.56821831681516
|
||||||
|
DateTimeParam: '2019-02-22T13:57:31.2311892-04:00'
|
||||||
|
DateTimeArrayParam:
|
||||||
|
- '2019-02-22T13:57:31.2311892+00:00'
|
||||||
|
- '2019-02-22T13:57:31.2311892+04:00'
|
||||||
|
PSCredentialParam_username: username1
|
||||||
|
PSCredentialParam_password: password1
|
||||||
|
HashtableParam:
|
||||||
|
key1: string 1
|
||||||
|
key2: ''
|
||||||
|
key3: 1
|
||||||
|
CimInstanceParam:
|
||||||
|
KeyValue: a
|
||||||
|
CimInstanceArrayParam:
|
||||||
|
- KeyValue: b
|
||||||
|
Choice: Choice1
|
||||||
|
StringValue: string 1
|
||||||
|
IntValue: 1
|
||||||
|
StringArrayValue:
|
||||||
|
- abc
|
||||||
|
- def
|
||||||
|
- KeyValue: c
|
||||||
|
Choice: Choice2
|
||||||
|
StringValue: string 2
|
||||||
|
IntValue: '2'
|
||||||
|
StringArrayValue:
|
||||||
|
- ghi
|
||||||
|
- jkl
|
||||||
|
NestedCimInstanceParam:
|
||||||
|
KeyValue: key value
|
||||||
|
CimValue:
|
||||||
|
KeyValue: d
|
||||||
|
CimArrayValue:
|
||||||
|
- KeyValue: e
|
||||||
|
Choice: Choice2
|
||||||
|
HashValue:
|
||||||
|
a: a
|
||||||
|
IntValue: '300'
|
||||||
|
register: dsc_types
|
||||||
|
|
||||||
|
- name: get result of test DSC with all types
|
||||||
|
slurp:
|
||||||
|
path: '{{ remote_tmp_dir }}\test-types.json'
|
||||||
|
register: dsc_types_raw
|
||||||
|
|
||||||
|
- name: convert result of test DSC with all types to dict
|
||||||
|
set_fact:
|
||||||
|
dsc_types_actual: '{{ dsc_types_raw.content | b64decode | from_json }}'
|
||||||
|
|
||||||
|
- name: assert test DSC with all types
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- dsc_types is changed
|
||||||
|
- dsc_types.module_version == '1.0.1'
|
||||||
|
- not dsc_types.reboot_required
|
||||||
|
- dsc_types_actual.Version == '1.0.1'
|
||||||
|
- dsc_types_actual.Verbose.Value.IsPresent
|
||||||
|
- dsc_types_actual.DefaultParam.Value == 'Default' # ensures that the default is set in the engine if we don't set it outselves
|
||||||
|
- dsc_types_actual.Ensure.Value == 'Present'
|
||||||
|
- dsc_types_actual.Path.Value == remote_tmp_dir + "\\test-types.json"
|
||||||
|
- dsc_types_actual.StringParam.Type == 'System.String'
|
||||||
|
- dsc_types_actual.StringParam.Value == 'string param'
|
||||||
|
- dsc_types_actual.StringArrayParam.Type == 'System.String[]'
|
||||||
|
- dsc_types_actual.StringArrayParam.Value == ['string 1', 'string 2']
|
||||||
|
- dsc_types_actual.Int8Param.Type == 'System.SByte'
|
||||||
|
- dsc_types_actual.Int8Param.Value == 127
|
||||||
|
- dsc_types_actual.Int8ArrayParam.Type == 'System.SByte[]'
|
||||||
|
- dsc_types_actual.Int8ArrayParam.Value == [127, 127]
|
||||||
|
- dsc_types_actual.UInt8Param.Type == 'System.Byte'
|
||||||
|
- dsc_types_actual.UInt8Param.Value == 255
|
||||||
|
- dsc_types_actual.UInt8ArrayParam.Type == 'System.Byte[]'
|
||||||
|
- dsc_types_actual.UInt8ArrayParam.Value == [255, 255]
|
||||||
|
- dsc_types_actual.Int16Param.Type == 'System.Int16'
|
||||||
|
- dsc_types_actual.Int16Param.Value == 32767
|
||||||
|
- dsc_types_actual.Int16ArrayParam.Type == 'System.Int16[]'
|
||||||
|
- dsc_types_actual.Int16ArrayParam.Value == [32767, 32767]
|
||||||
|
- dsc_types_actual.UInt16Param.Type == 'System.UInt16'
|
||||||
|
- dsc_types_actual.UInt16Param.Value == 65535
|
||||||
|
- dsc_types_actual.UInt16ArrayParam.Type == 'System.UInt16[]'
|
||||||
|
- dsc_types_actual.UInt16ArrayParam.Value == [65535]
|
||||||
|
- dsc_types_actual.Int32Param.Type == 'System.Int32'
|
||||||
|
- dsc_types_actual.Int32Param.Value == 2147483647
|
||||||
|
- dsc_types_actual.Int32ArrayParam.Type == 'System.Int32[]'
|
||||||
|
- dsc_types_actual.Int32ArrayParam.Value == [2147483647]
|
||||||
|
- dsc_types_actual.UInt32Param.Type == 'System.UInt32'
|
||||||
|
- dsc_types_actual.UInt32Param.Value == 4294967295
|
||||||
|
- dsc_types_actual.UInt32ArrayParam.Type == 'System.UInt32[]'
|
||||||
|
- dsc_types_actual.UInt32ArrayParam.Value == [4294967295, 4294967295]
|
||||||
|
- dsc_types_actual.Int64Param.Type == 'System.Int64'
|
||||||
|
- dsc_types_actual.Int64Param.Value == 9223372036854775807
|
||||||
|
- dsc_types_actual.Int64ArrayParam.Type == 'System.Int64[]'
|
||||||
|
- dsc_types_actual.Int64ArrayParam.Value == [-9223372036854775808, 9223372036854775807]
|
||||||
|
- dsc_types_actual.UInt64Param.Type == 'System.UInt64'
|
||||||
|
- dsc_types_actual.UInt64Param.Value == 18446744073709551615
|
||||||
|
- dsc_types_actual.UInt64ArrayParam.Type == 'System.UInt64[]'
|
||||||
|
- dsc_types_actual.UInt64ArrayParam.Value == [0, 18446744073709551615]
|
||||||
|
- dsc_types_actual.BooleanParam.Type == 'System.Boolean'
|
||||||
|
- dsc_types_actual.BooleanParam.Value == True
|
||||||
|
- dsc_types_actual.BooleanArrayParam.Type == 'System.Boolean[]'
|
||||||
|
- dsc_types_actual.BooleanArrayParam.Value == [True, True, True, True, True, True, False, False, False, False, False, False]
|
||||||
|
- dsc_types_actual.CharParam.Type == 'System.Char'
|
||||||
|
- dsc_types_actual.CharParam.Value == 'c'
|
||||||
|
- dsc_types_actual.CharArrayParam.Type == 'System.Char[]'
|
||||||
|
- dsc_types_actual.CharArrayParam.Value == ['c', 'h', 'a', 'r']
|
||||||
|
- dsc_types_actual.SingleParam.Type == 'System.Single'
|
||||||
|
- dsc_types_actual.SingleParam.Value|string == '3.402823e+38'
|
||||||
|
- dsc_types_actual.SingleArrayParam.Type == 'System.Single[]'
|
||||||
|
- dsc_types_actual.SingleArrayParam.Value|length == 2
|
||||||
|
- dsc_types_actual.SingleArrayParam.Value[0]|string == '3.402823e+38'
|
||||||
|
- dsc_types_actual.SingleArrayParam.Value[1]|string == '1.23934937'
|
||||||
|
- dsc_types_actual.DoubleParam.Type == 'System.Double'
|
||||||
|
- dsc_types_actual.DoubleParam.Value == '1.79769313486232E+300'
|
||||||
|
- dsc_types_actual.DoubleArrayParam.Type == 'System.Double[]'
|
||||||
|
- dsc_types_actual.DoubleArrayParam.Value|length == 2
|
||||||
|
- dsc_types_actual.DoubleArrayParam.Value[0] == '1.79769313486232E+300'
|
||||||
|
- dsc_types_actual.DoubleArrayParam.Value[1] == '3.56821831681516'
|
||||||
|
- dsc_types_actual.DateTimeParam.Type == 'System.DateTime'
|
||||||
|
- dsc_types_actual.DateTimeParam.Value == '2019-02-22T17:57:31.2311890+00:00'
|
||||||
|
- dsc_types_actual.DateTimeArrayParam.Type == 'System.DateTime[]'
|
||||||
|
- dsc_types_actual.DateTimeArrayParam.Value == ['2019-02-22T13:57:31.2311890+00:00', '2019-02-22T09:57:31.2311890+00:00']
|
||||||
|
- dsc_types_actual.PSCredentialParam.Type == 'System.Management.Automation.PSCredential'
|
||||||
|
- dsc_types_actual.PSCredentialParam.Value.username == 'username1'
|
||||||
|
- dsc_types_actual.PSCredentialParam.Value.password == 'password1'
|
||||||
|
# Hashtable is actually a CimInstance[] of MSFT_KeyValuePairs
|
||||||
|
- dsc_types_actual.HashtableParam.Type == 'Microsoft.Management.Infrastructure.CimInstance[]'
|
||||||
|
- dsc_types_actual.HashtableParam.Value|length == 3
|
||||||
|
# Can't guarantee the order of the keys so just check they are the values they could be
|
||||||
|
- dsc_types_actual.HashtableParam.Value[0].Key in ["key1", "key2", "key3"]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[0].Value in ["string 1", "1", ""]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[0]._cim_instance == 'MSFT_KeyValuePair'
|
||||||
|
- dsc_types_actual.HashtableParam.Value[1].Key in ["key1", "key2", "key3"]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[1].Value in ["string 1", "1", ""]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[1]._cim_instance == 'MSFT_KeyValuePair'
|
||||||
|
- dsc_types_actual.HashtableParam.Value[2].Key in ["key1", "key2", "key3"]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[2].Value in ["string 1", "1", ""]
|
||||||
|
- dsc_types_actual.HashtableParam.Value[2]._cim_instance == 'MSFT_KeyValuePair'
|
||||||
|
- dsc_types_actual.CimInstanceParam.Type == 'Microsoft.Management.Infrastructure.CimInstance'
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value.Choice == None
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value.IntValue == None
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value.KeyValue == 'a'
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value.StringArrayValue == None
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value.StringValue == None
|
||||||
|
- dsc_types_actual.CimInstanceParam.Value._cim_instance == "ANSIBLE_xTestClass"
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Type == 'Microsoft.Management.Infrastructure.CimInstance[]'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value|length == 2
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0].Choice == 'Choice1'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0].IntValue == 1
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0].KeyValue == 'b'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0].StringArrayValue == ['abc', 'def']
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0].StringValue == 'string 1'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[0]._cim_instance == 'ANSIBLE_xTestClass'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1].Choice == 'Choice2'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1].IntValue == 2
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1].KeyValue == 'c'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1].StringArrayValue == ['ghi', 'jkl']
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1].StringValue == 'string 2'
|
||||||
|
- dsc_types_actual.CimInstanceArrayParam.Value[1]._cim_instance == 'ANSIBLE_xTestClass'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Type == 'Microsoft.Management.Infrastructure.CimInstance'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue|length == 1
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0].Choice == 'Choice2'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0].IntValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0].KeyValue == 'e'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0].StringArrayValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0].StringValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimArrayValue[0]._cim_instance == 'ANSIBLE_xTestClass'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue.Choice == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue.IntValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue.KeyValue == 'd'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue.StringArrayValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue.StringValue == None
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.CimValue._cim_instance == 'ANSIBLE_xTestClass'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.HashValue|length == 1
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.HashValue[0].Key == 'a'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.HashValue[0].Value == 'a'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.HashValue[0]._cim_instance == 'MSFT_KeyValuePair'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.IntValue == 300
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value.KeyValue == 'key value'
|
||||||
|
- dsc_types_actual.NestedCimInstanceParam.Value._cim_instance == 'ANSIBLE_xNestedClass'
|
||||||
|
|
||||||
|
- name: test DSC with all types older version
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xTestResource
|
||||||
|
module_version: 1.0.0
|
||||||
|
Path: '{{ remote_tmp_dir }}\test-types.json'
|
||||||
|
Ensure: Absent
|
||||||
|
StringParam: string param old
|
||||||
|
CimInstanceArrayParam:
|
||||||
|
- Key: old key
|
||||||
|
StringValue: string old 1
|
||||||
|
IntValue: 0
|
||||||
|
StringArrayValue:
|
||||||
|
- zyx
|
||||||
|
- wvu
|
||||||
|
register: dsc_types_old
|
||||||
|
|
||||||
|
- name: get result of test DSC with all types older version
|
||||||
|
slurp:
|
||||||
|
path: '{{ remote_tmp_dir }}\test-types.json'
|
||||||
|
register: dsc_types_old_raw
|
||||||
|
|
||||||
|
- name: convert result of test DSC with all types to dict
|
||||||
|
set_fact:
|
||||||
|
dsc_types_old_actual: '{{ dsc_types_old_raw.content | b64decode | from_json }}'
|
||||||
|
|
||||||
|
- name: assert test DSC with all types older version
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- dsc_types_old is changed
|
||||||
|
- dsc_types_old.module_version == '1.0.0'
|
||||||
|
- not dsc_types_old.reboot_required
|
||||||
|
- dsc_types_old_actual.Version == '1.0.0'
|
||||||
|
- dsc_types_old_actual.Verbose.Value.IsPresent
|
||||||
|
- dsc_types_old_actual.DefaultParam.Value == 'Default'
|
||||||
|
- dsc_types_old_actual.Ensure.Value == 'Absent'
|
||||||
|
- dsc_types_old_actual.Path.Value == remote_tmp_dir + "\\test-types.json"
|
||||||
|
- dsc_types_old_actual.StringParam.Type == 'System.String'
|
||||||
|
- dsc_types_old_actual.StringParam.Value == 'string param old'
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Type == 'Microsoft.Management.Infrastructure.CimInstance[]'
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value|length == 1
|
||||||
|
- not dsc_types_old_actual.CimInstanceArrayParam.Value[0].Choice is defined # 1.0.0 does not have a Choice option
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value[0].IntValue == 0
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value[0].Key == 'old key'
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value[0].StringArrayValue == ['zyx', 'wvu']
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value[0].StringValue == 'string old 1'
|
||||||
|
- dsc_types_old_actual.CimInstanceArrayParam.Value[0]._cim_instance == 'ANSIBLE_xTestClass'
|
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
test_win_hosts_cname: testhost
|
||||||
|
test_win_hosts_ip: 192.168.168.1
|
||||||
|
|
||||||
|
test_win_hosts_aliases_set:
|
||||||
|
- alias1
|
||||||
|
- alias2
|
||||||
|
- alias3
|
||||||
|
- alias4
|
||||||
|
|
||||||
|
test_win_hosts_aliases_remove:
|
||||||
|
- alias3
|
||||||
|
- alias4
|
@ -0,0 +1,2 @@
|
|||||||
|
dependencies:
|
||||||
|
- setup_remote_tmp_dir
|
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
- name: take a copy of the original hosts file
|
||||||
|
win_copy:
|
||||||
|
src: C:\Windows\System32\drivers\etc\hosts
|
||||||
|
dest: '{{ remote_tmp_dir }}\hosts'
|
||||||
|
remote_src: yes
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: run tests
|
||||||
|
include_tasks: tests.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: restore hosts file
|
||||||
|
win_copy:
|
||||||
|
src: '{{ remote_tmp_dir }}\hosts'
|
||||||
|
dest: C:\Windows\System32\drivers\etc\hosts
|
||||||
|
remote_src: yes
|
@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- name: add a simple host with address
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
register: add_ip
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "add_ip.changed == true"
|
||||||
|
|
||||||
|
- name: get actual dns result
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ test_win_hosts_cname }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: add_ip_actual
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "add_ip_actual.stdout_lines[0]|lower == 'true'"
|
||||||
|
|
||||||
|
- name: add a simple host with ipv4 address (idempotent)
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
register: add_ip
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "add_ip.changed == false"
|
||||||
|
|
||||||
|
- name: remove simple host
|
||||||
|
win_hosts:
|
||||||
|
state: absent
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
register: remove_ip
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "remove_ip.changed == true"
|
||||||
|
|
||||||
|
- name: get actual dns result
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ test_win_hosts_cname}}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: remove_ip_actual
|
||||||
|
failed_when: "remove_ip_actual.rc == 0"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "remove_ip_actual.stdout_lines[0]|lower == 'false'"
|
||||||
|
|
||||||
|
- name: remove simple host (idempotent)
|
||||||
|
win_hosts:
|
||||||
|
state: absent
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
register: remove_ip
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "remove_ip.changed == false"
|
||||||
|
|
||||||
|
- name: add host and set aliases
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_set | union(test_win_hosts_aliases_remove) }}"
|
||||||
|
action: set
|
||||||
|
register: set_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "set_aliases.changed == true"
|
||||||
|
|
||||||
|
- name: get actual dns result for host
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ test_win_hosts_cname }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: set_aliases_actual_host
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "set_aliases_actual_host.stdout_lines[0]|lower == 'true'"
|
||||||
|
|
||||||
|
- name: get actual dns results for aliases
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ item }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: set_aliases_actual
|
||||||
|
with_items: "{{ test_win_hosts_aliases_set | union(test_win_hosts_aliases_remove) }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "item.stdout_lines[0]|lower == 'true'"
|
||||||
|
with_items: "{{ set_aliases_actual.results }}"
|
||||||
|
|
||||||
|
- name: add host and set aliases (idempotent)
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_set | union(test_win_hosts_aliases_remove) }}"
|
||||||
|
action: set
|
||||||
|
register: set_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "set_aliases.changed == false"
|
||||||
|
|
||||||
|
- name: remove aliases from the list
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_remove }}"
|
||||||
|
action: remove
|
||||||
|
register: remove_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "remove_aliases.changed == true"
|
||||||
|
|
||||||
|
- name: get actual dns result for removed aliases
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ item }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: remove_aliases_removed_actual
|
||||||
|
failed_when: "remove_aliases_removed_actual.rc == 0"
|
||||||
|
with_items: "{{ test_win_hosts_aliases_remove }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "item.stdout_lines[0]|lower == 'false'"
|
||||||
|
with_items: "{{ remove_aliases_removed_actual.results }}"
|
||||||
|
|
||||||
|
- name: get actual dns result for remaining aliases
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ item }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: remove_aliases_remain_actual
|
||||||
|
with_items: "{{ test_win_hosts_aliases_set | difference(test_win_hosts_aliases_remove) }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "item.stdout_lines[0]|lower == 'true'"
|
||||||
|
with_items: "{{ remove_aliases_remain_actual.results }}"
|
||||||
|
|
||||||
|
- name: remove aliases from the list (idempotent)
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_remove }}"
|
||||||
|
action: remove
|
||||||
|
register: remove_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "remove_aliases.changed == false"
|
||||||
|
|
||||||
|
- name: add aliases back
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_remove }}"
|
||||||
|
action: add
|
||||||
|
register: add_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "add_aliases.changed == true"
|
||||||
|
|
||||||
|
- name: get actual dns results for aliases
|
||||||
|
win_shell: "try{ [array]$t = [Net.DNS]::GetHostEntry('{{ item }}') } catch { return 'false' } if ($t[0].HostName -eq '{{ test_win_hosts_cname }}' -and $t[0].AddressList[0].toString() -eq '{{ test_win_hosts_ip }}'){ return 'true' } else { return 'false' }"
|
||||||
|
register: add_aliases_actual
|
||||||
|
with_items: "{{ test_win_hosts_aliases_set | union(test_win_hosts_aliases_remove) }}"
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "item.stdout_lines[0]|lower == 'true'"
|
||||||
|
with_items: "{{ add_aliases_actual.results }}"
|
||||||
|
|
||||||
|
- name: add aliases back (idempotent)
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
ip_address: "{{ test_win_hosts_ip }}"
|
||||||
|
canonical_name: "{{ test_win_hosts_cname }}"
|
||||||
|
aliases: "{{ test_win_hosts_aliases_remove }}"
|
||||||
|
action: add
|
||||||
|
register: add_aliases
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "add_aliases.changed == false"
|
@ -0,0 +1,3 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
||||||
|
skip/windows/2016 # Host takes a while to run and module isn't OS dependent
|
@ -0,0 +1,5 @@
|
|||||||
|
This is line 1
|
||||||
|
This is line 2
|
||||||
|
REF this is a line for backrefs REF
|
||||||
|
This is line 4
|
||||||
|
This is line 5
|
@ -0,0 +1,2 @@
|
|||||||
|
This is line 1
|
||||||
|
This is line 2
|
@ -0,0 +1,2 @@
|
|||||||
|
dependencies:
|
||||||
|
- incidental_win_prepare_tests
|
@ -0,0 +1,708 @@
|
|||||||
|
# Test code for the win_lineinfile module, adapted from the standard lineinfile module tests
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
- name: deploy the test file for lineinfile
|
||||||
|
win_copy: src=test.txt dest={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the test file was deployed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
|
||||||
|
- name: stat the test file
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: check win_stat file result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.exists"
|
||||||
|
- "not result.stat.isdir"
|
||||||
|
- "result.stat.checksum == '5feac65e442c91f557fc90069ce6efc4d346ab51'"
|
||||||
|
- "result is not failed"
|
||||||
|
- "result is not changed"
|
||||||
|
|
||||||
|
|
||||||
|
- name: insert a line at the beginning of the file, and back it up
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line at the beginning" insertbefore="BOF" backup=yes
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: check backup_file
|
||||||
|
win_stat:
|
||||||
|
path: '{{ result.backup_file }}'
|
||||||
|
register: backup_file
|
||||||
|
|
||||||
|
- name: assert that the line was inserted at the head of the file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == true
|
||||||
|
- result.msg == 'line added'
|
||||||
|
- backup_file.stat.exists == true
|
||||||
|
|
||||||
|
- name: stat the backup file
|
||||||
|
win_stat: path={{result.backup}}
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the backup file matches the previous hash
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '5feac65e442c91f557fc90069ce6efc4d346ab51'"
|
||||||
|
|
||||||
|
- name: stat the test after the insert at the head
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test hash is what we expect for the file with the insert at the head
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'b526e2e044defc64dfb0fad2f56e105178f317d8'"
|
||||||
|
|
||||||
|
- name: insert a line at the end of the file
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line at the end" insertafter="EOF"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line was inserted at the end of the file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat the test after the insert at the end
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after the insert at the end
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'dd5e207e28ce694ab18e41c2b16deb74fde93b14'"
|
||||||
|
|
||||||
|
- name: insert a line after the first line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line after line 1" insertafter="^This is line 1$"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line was inserted after the first line
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat the test after insert after the first line
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after the insert after the first line
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '604b17405f2088e6868af9680b7834087acdc8f4'"
|
||||||
|
|
||||||
|
- name: insert a line before the last line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line before line 5" insertbefore="^This is line 5$"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line was inserted before the last line
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat the test after the insert before the last line
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after the insert before the last line
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '8f5b30e8f01578043d782e5a68d4c327e75a6e34'"
|
||||||
|
|
||||||
|
- name: replace a line with backrefs
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="This is line 3" backrefs=yes regexp="^(REF).*$"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line with backrefs was changed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line replaced'"
|
||||||
|
|
||||||
|
- name: stat the test after the backref line was replaced
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after backref line was replaced
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'ef6b02645908511a2cfd2df29d50dd008897c580'"
|
||||||
|
|
||||||
|
- name: remove the middle line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=absent regexp="^This is line 3$"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line was removed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == '1 line(s) removed'"
|
||||||
|
|
||||||
|
- name: stat the test after the middle line was removed
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after the middle line was removed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '11695efa472be5c31c736bc43e055f8ac90eabdf'"
|
||||||
|
|
||||||
|
- name: run a validation script that succeeds
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=absent regexp="^This is line 5$" validate="sort.exe %s"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the file validated after removing a line
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == '1 line(s) removed'"
|
||||||
|
|
||||||
|
- name: stat the test after the validation succeeded
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after the validation succeeded
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '39c38a30aa6ac6af9ec41f54c7ed7683f1249347'"
|
||||||
|
|
||||||
|
- name: run a validation script that fails
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=absent regexp="^This is line 1$" validate="sort.exe %s.foo"
|
||||||
|
register: result
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: assert that the validate failed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.failed == true"
|
||||||
|
|
||||||
|
- name: stat the test after the validation failed
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches the previous after the validation failed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '39c38a30aa6ac6af9ec41f54c7ed7683f1249347'"
|
||||||
|
|
||||||
|
- name: use create=yes
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/new_test.txt create=yes insertbefore=BOF state=present line="This is a new file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/new_test.txt
|
||||||
|
register: result
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: assert the newly created test checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '84faac1183841c57434693752fc3debc91b9195d'"
|
||||||
|
|
||||||
|
# Test EOF in cases where file has no newline at EOF
|
||||||
|
- name: testnoeof deploy the file for lineinfile
|
||||||
|
win_copy: src=testnoeof.txt dest={{win_output_dir}}/testnoeof.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testnoeof insert a line at the end of the file
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/testnoeof.txt state=present line="New line at the end" insertafter="EOF"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testempty assert that the line was inserted at the end of the file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: testnoeof stat the no newline EOF test after the insert at the end
|
||||||
|
win_stat: path={{win_output_dir}}/testnoeof.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testnoeof assert test checksum matches after the insert at the end
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '229852b09f7e9921fbcbb0ee0166ba78f7f7f261'"
|
||||||
|
|
||||||
|
- name: add multiple lines at the end of the file
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="This is a line\r\nwith newline character" insertafter="EOF"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the multiple lines was inserted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat file after adding multiple lines
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after inserting multiple lines
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '1401413cd4eac732be66cd6aceddd334c4240f86'"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Test EOF with empty file to make sure no unnecessary newline is added
|
||||||
|
- name: testempty deploy the testempty file for lineinfile
|
||||||
|
win_copy: src=testempty.txt dest={{win_output_dir}}/testempty.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testempty insert a line at the end of the file
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/testempty.txt state=present line="New line at the end" insertafter="EOF"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testempty assert that the line was inserted at the end of the file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: testempty stat the test after the insert at the end
|
||||||
|
win_stat: path={{win_output_dir}}/testempty.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: testempty assert test checksum matches after the insert at the end
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'd3d34f11edda51be7ca5dcb0757cf3e1257c0bfe'"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: replace a line with backrefs included in the line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New $1 created with the backref" backrefs=yes regexp="^This is (line 4)$"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the line with backrefs was changed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line replaced'"
|
||||||
|
|
||||||
|
- name: stat the test after the backref line was replaced
|
||||||
|
win_stat: path={{win_output_dir}}/test.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches after backref line was replaced
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'e6ff42e926dac2274c93dff0b8a323e07ae09149'"
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
# issue 8535
|
||||||
|
|
||||||
|
- name: create a new file for testing quoting issues
|
||||||
|
win_copy: src=test_quoting.txt dest={{win_output_dir}}/test_quoting.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed
|
||||||
|
|
||||||
|
- name: use with_items to add code-like strings to the quoting txt file
|
||||||
|
win_lineinfile: >
|
||||||
|
dest={{win_output_dir}}/test_quoting.txt
|
||||||
|
line="{{ item }}"
|
||||||
|
insertbefore="BOF"
|
||||||
|
with_items:
|
||||||
|
- "'foo'"
|
||||||
|
- "dotenv.load();"
|
||||||
|
- "var dotenv = require('dotenv');"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the quote test file was modified correctly
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.results|length == 3
|
||||||
|
- result.results[0].changed
|
||||||
|
- result.results[0].item == "'foo'"
|
||||||
|
- result.results[1].changed
|
||||||
|
- result.results[1].item == "dotenv.load();"
|
||||||
|
- result.results[2].changed
|
||||||
|
- result.results[2].item == "var dotenv = require('dotenv');"
|
||||||
|
|
||||||
|
- name: stat the quote test file
|
||||||
|
win_stat: path={{win_output_dir}}/test_quoting.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches for quote test file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'f3bccdbdfa1d7176c497ef87d04957af40ab48d2'"
|
||||||
|
|
||||||
|
- name: append a line into the quoted file with a single quote
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_quoting.txt line="import g'"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the quoted file was changed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed
|
||||||
|
|
||||||
|
- name: stat the quote test file
|
||||||
|
win_stat: path={{win_output_dir}}/test_quoting.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches adding line with single quote
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'dabf4cbe471e1797d8dcfc773b6b638c524d5237'"
|
||||||
|
|
||||||
|
- name: insert a line into the quoted file with many double quotation strings
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_quoting.txt line='"quote" and "unquote"'
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the quoted file was changed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed
|
||||||
|
|
||||||
|
- name: stat the quote test file
|
||||||
|
win_stat: path={{win_output_dir}}/test_quoting.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert test checksum matches quoted line added
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '9dc1fc1ff19942e2936564102ad37134fa83b91d'"
|
||||||
|
|
||||||
|
|
||||||
|
# Windows vs. Unix line separator test cases
|
||||||
|
|
||||||
|
- name: Create windows test file with initial line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_windows_sep.txt create=yes insertbefore=BOF state=present line="This is a new file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_windows_sep.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '84faac1183841c57434693752fc3debc91b9195d'"
|
||||||
|
|
||||||
|
- name: Test appending to the file using the default (windows) line separator
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_windows_sep.txt insertbefore=EOF state=present line="This is the last line"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_windows_sep.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches expected checksum
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '71a17ddd1d57ed7c7912e4fd11ecb2ead0b27033'"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Create unix test file with initial line
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_unix_sep.txt create=yes insertbefore=BOF state=present line="This is a new file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_unix_sep.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '84faac1183841c57434693752fc3debc91b9195d'"
|
||||||
|
|
||||||
|
- name: Test appending to the file using unix line separator
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_unix_sep.txt insertbefore=EOF state=present line="This is the last line" newline="unix"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_unix_sep.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches expected checksum
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'f1f634a37ab1c73efb77a71a5ad2cc87b61b17ae'"
|
||||||
|
|
||||||
|
|
||||||
|
# Encoding management test cases
|
||||||
|
|
||||||
|
# Default (auto) encoding should use utf-8 with no BOM
|
||||||
|
- name: Test create file without explicit encoding results in utf-8 without BOM
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_auto_utf8.txt create=yes insertbefore=BOF state=present line="This is a new utf-8 file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-8'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_auto_utf8.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'b69fcbacca8291a4668f57fba91d7c022f1c3dc7'"
|
||||||
|
|
||||||
|
- name: Test appending to the utf-8 without BOM file - should autodetect UTF-8 no BOM
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_auto_utf8.txt insertbefore=EOF state=present line="This is the last line"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added and encoding did not change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-8'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_auto_utf8.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '64d747f1ebf8c9d793dbfd27126e4152d39a3848'"
|
||||||
|
|
||||||
|
|
||||||
|
# UTF-8 explicit (with BOM)
|
||||||
|
- name: Test create file with explicit utf-8 encoding results in utf-8 with a BOM
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf8.txt create=yes encoding="utf-8" insertbefore=BOF state=present line="This is a new utf-8 file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-8'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf8.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == 'd45344b2b3bf1cf90eae851b40612f5f37a88bbb'"
|
||||||
|
|
||||||
|
- name: Test appending to the utf-8 with BOM file - should autodetect utf-8 with BOM encoding
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf8.txt insertbefore=EOF state=present line="This is the last line"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added and encoding did not change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-8'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf8.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '9b84254489f40f258871a4c6573cacc65895ee1a'"
|
||||||
|
|
||||||
|
|
||||||
|
# UTF-16 explicit
|
||||||
|
- name: Test create file with explicit utf-16 encoding
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf16.txt create=yes encoding="utf-16" insertbefore=BOF state=present line="This is a new utf-16 file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-16'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf16.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '785b0693cec13b60e2c232782adeda2f8a967434'"
|
||||||
|
|
||||||
|
- name: Test appending to the utf-16 file - should autodetect utf-16 encoding
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf16.txt insertbefore=EOF state=present line="This is the last line"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added and encoding did not change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-16'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf16.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '70e4eb3ba795e1ba94d262db47e4fd17c64b2e73'"
|
||||||
|
|
||||||
|
# UTF-32 explicit
|
||||||
|
- name: Test create file with explicit utf-32 encoding
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf32.txt create=yes encoding="utf-32" insertbefore=BOF state=present line="This is a new utf-32 file"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new file was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-32'"
|
||||||
|
|
||||||
|
- name: validate that the newly created file exists
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf32.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the newly created file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '7a6e3f3604c0def431aaa813173a4ddaa10fd1fb'"
|
||||||
|
|
||||||
|
- name: Test appending to the utf-32 file - should autodetect utf-32 encoding
|
||||||
|
win_lineinfile: dest={{win_output_dir}}/test_utf32.txt insertbefore=EOF state=present line="This is the last line"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert that the new line was added and encoding did not change
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.changed == true"
|
||||||
|
- "result.msg == 'line added'"
|
||||||
|
- "result.encoding == 'utf-32'"
|
||||||
|
|
||||||
|
- name: stat the file
|
||||||
|
win_stat: path={{win_output_dir}}/test_utf32.txt
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file checksum matches
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "result.stat.checksum == '66a72e71f42c4775f4326da95cfe82c8830e5022'"
|
||||||
|
|
||||||
|
#########################################################################
|
||||||
|
# issue #33858
|
||||||
|
# \r\n causes line break instead of printing literally which breaks paths.
|
||||||
|
|
||||||
|
- name: create testing file
|
||||||
|
win_copy:
|
||||||
|
src: test_linebreak.txt
|
||||||
|
dest: "{{win_output_dir}}/test_linebreak.txt"
|
||||||
|
|
||||||
|
- name: stat the test file
|
||||||
|
win_stat:
|
||||||
|
path: "{{win_output_dir}}/test_linebreak.txt"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
# (Get-FileHash -path C:\ansible\test\integration\targets\win_lineinfile\files\test_linebreak.txt -Algorithm sha1).hash.tolower()
|
||||||
|
- name: check win_stat file result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.stat.exists
|
||||||
|
- not result.stat.isdir
|
||||||
|
- result.stat.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
|
||||||
|
- result is not failed
|
||||||
|
- result is not changed
|
||||||
|
|
||||||
|
- name: insert path c:\return\new to test file
|
||||||
|
win_lineinfile:
|
||||||
|
dest: "{{win_output_dir}}/test_linebreak.txt"
|
||||||
|
line: c:\return\new
|
||||||
|
register: result_literal
|
||||||
|
|
||||||
|
- name: insert path "c:\return\new" to test file, will cause line breaks
|
||||||
|
win_lineinfile:
|
||||||
|
dest: "{{win_output_dir}}/test_linebreak.txt"
|
||||||
|
line: "c:\return\new"
|
||||||
|
register: result_expand
|
||||||
|
|
||||||
|
- name: assert that the lines were inserted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result_literal.changed == true
|
||||||
|
- result_literal.msg == 'line added'
|
||||||
|
- result_expand.changed == true
|
||||||
|
- result_expand.msg == 'line added'
|
||||||
|
|
||||||
|
- name: stat the test file
|
||||||
|
win_stat:
|
||||||
|
path: "{{win_output_dir}}/test_linebreak.txt"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: result
|
||||||
|
verbosity: 1
|
||||||
|
|
||||||
|
# expect that the file looks like this:
|
||||||
|
# c:\return\new
|
||||||
|
# c:
|
||||||
|
# eturn
|
||||||
|
# ew #or c:eturnew on windows
|
||||||
|
- name: assert that one line is literal and the other has breaks
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.stat.checksum == 'd2dfd11bc70526ff13a91153c76a7ae5595a845b'
|
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1,31 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
$params = Parse-Args $args $true;
|
||||||
|
|
||||||
|
$data = Get-Attr $params "data" "pong";
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
ping = "pong"
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test that Set-Attr will replace an existing attribute.
|
||||||
|
Set-Attr $result "ping" $data
|
||||||
|
|
||||||
|
Exit-Json $result;
|
@ -0,0 +1,30 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
$params = Parse-Args $args $true;
|
||||||
|
|
||||||
|
$params.thisPropertyDoesNotExist
|
||||||
|
|
||||||
|
$data = Get-Attr $params "data" "pong";
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
ping = $data
|
||||||
|
};
|
||||||
|
|
||||||
|
Exit-Json $result;
|
@ -0,0 +1,30 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
$blah = 'I can't quote my strings correctly.'
|
||||||
|
|
||||||
|
$params = Parse-Args $args $true;
|
||||||
|
|
||||||
|
$data = Get-Attr $params "data" "pong";
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
ping = $data
|
||||||
|
};
|
||||||
|
|
||||||
|
Exit-Json $result;
|
@ -0,0 +1,30 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
throw
|
||||||
|
|
||||||
|
$params = Parse-Args $args $true;
|
||||||
|
|
||||||
|
$data = Get-Attr $params "data" "pong";
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
ping = $data
|
||||||
|
};
|
||||||
|
|
||||||
|
Exit-Json $result;
|
@ -0,0 +1,30 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
throw "no ping for you"
|
||||||
|
|
||||||
|
$params = Parse-Args $args $true;
|
||||||
|
|
||||||
|
$data = Get-Attr $params "data" "pong";
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
ping = $data
|
||||||
|
};
|
||||||
|
|
||||||
|
Exit-Json $result;
|
@ -0,0 +1,67 @@
|
|||||||
|
# test code for the win_ping module
|
||||||
|
# (c) 2014, Chris Church <chris@ninemoreminutes.com>
|
||||||
|
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
- name: test win_ping
|
||||||
|
action: win_ping
|
||||||
|
register: win_ping_result
|
||||||
|
|
||||||
|
- name: check win_ping result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_ping_result is not failed
|
||||||
|
- win_ping_result is not changed
|
||||||
|
- win_ping_result.ping == 'pong'
|
||||||
|
|
||||||
|
- name: test win_ping with data
|
||||||
|
win_ping:
|
||||||
|
data: ☠
|
||||||
|
register: win_ping_with_data_result
|
||||||
|
|
||||||
|
- name: check win_ping result with data
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_ping_with_data_result is not failed
|
||||||
|
- win_ping_with_data_result is not changed
|
||||||
|
- win_ping_with_data_result.ping == '☠'
|
||||||
|
|
||||||
|
- name: test win_ping.ps1 with data as complex args
|
||||||
|
# win_ping.ps1: # TODO: do we want to actually support this? no other tests that I can see...
|
||||||
|
win_ping:
|
||||||
|
data: bleep
|
||||||
|
register: win_ping_ps1_result
|
||||||
|
|
||||||
|
- name: check win_ping.ps1 result with data
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_ping_ps1_result is not failed
|
||||||
|
- win_ping_ps1_result is not changed
|
||||||
|
- win_ping_ps1_result.ping == 'bleep'
|
||||||
|
|
||||||
|
- name: test win_ping using data=crash so that it throws an exception
|
||||||
|
win_ping:
|
||||||
|
data: crash
|
||||||
|
register: win_ping_crash_result
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: check win_ping_crash result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- win_ping_crash_result is failed
|
||||||
|
- win_ping_crash_result is not changed
|
||||||
|
- 'win_ping_crash_result.msg == "Unhandled exception while executing module: boom"'
|
||||||
|
- '"throw \"boom\"" in win_ping_crash_result.exception'
|
@ -0,0 +1 @@
|
|||||||
|
hidden
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
allow_duplicates: yes
|
@ -0,0 +1,29 @@
|
|||||||
|
# test code for the windows versions of copy, file and template module
|
||||||
|
# originally
|
||||||
|
# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
|
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
- name: clean out the test directory
|
||||||
|
win_file: name={{win_output_dir|mandatory}} state=absent
|
||||||
|
tags:
|
||||||
|
- prepare
|
||||||
|
|
||||||
|
- name: create the test directory
|
||||||
|
win_file: name={{win_output_dir}} state=directory
|
||||||
|
tags:
|
||||||
|
- prepare
|
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1,2 @@
|
|||||||
|
dependencies:
|
||||||
|
- setup_remote_tmp_dir
|
@ -0,0 +1,80 @@
|
|||||||
|
# Would use [] but this has troubles with PATH and trying to find the executable so just resort to keeping a space
|
||||||
|
- name: record special path for tests
|
||||||
|
set_fact:
|
||||||
|
testing_dir: '{{ remote_tmp_dir }}\ansible win_psexec'
|
||||||
|
|
||||||
|
- name: create special path testing dir
|
||||||
|
win_file:
|
||||||
|
path: '{{ testing_dir }}'
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: Download PsExec
|
||||||
|
win_get_url:
|
||||||
|
url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_psexec/PsExec.exe
|
||||||
|
dest: '{{ testing_dir }}\PsExec.exe'
|
||||||
|
|
||||||
|
- name: Get the existing PATH env var
|
||||||
|
win_shell: '$env:PATH'
|
||||||
|
register: system_path
|
||||||
|
changed_when: False
|
||||||
|
|
||||||
|
- name: Run whoami
|
||||||
|
win_psexec:
|
||||||
|
command: whoami.exe
|
||||||
|
nobanner: true
|
||||||
|
register: whoami
|
||||||
|
environment:
|
||||||
|
PATH: '{{ testing_dir }};{{ system_path.stdout | trim }}'
|
||||||
|
|
||||||
|
- name: Test whoami
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- whoami.rc == 0
|
||||||
|
- whoami.stdout == ''
|
||||||
|
# FIXME: Standard output does not work or is truncated
|
||||||
|
#- whoami.stdout == '{{ ansible_hostname|lower }}'
|
||||||
|
|
||||||
|
- name: Run whoami as SYSTEM
|
||||||
|
win_psexec:
|
||||||
|
command: whoami.exe
|
||||||
|
system: yes
|
||||||
|
nobanner: true
|
||||||
|
executable: '{{ testing_dir }}\PsExec.exe'
|
||||||
|
register: whoami_as_system
|
||||||
|
# Seems to be a bug with PsExec where the stdout can be empty, just retry the task to make this test a bit more stable
|
||||||
|
until: whoami_as_system.rc == 0 and whoami_as_system.stdout == 'nt authority\system'
|
||||||
|
retries: 3
|
||||||
|
delay: 2
|
||||||
|
|
||||||
|
# FIXME: Behaviour is not consistent on all Windows systems
|
||||||
|
#- name: Run whoami as ELEVATED
|
||||||
|
# win_psexec:
|
||||||
|
# command: whoami.exe
|
||||||
|
# elevated: yes
|
||||||
|
# register: whoami_as_elevated
|
||||||
|
#
|
||||||
|
## Ensure we have basic facts
|
||||||
|
#- setup:
|
||||||
|
#
|
||||||
|
#- debug:
|
||||||
|
# msg: '{{ whoami_as_elevated.stdout|lower }} == {{ ansible_hostname|lower }}\{{ ansible_user_id|lower }}'
|
||||||
|
#
|
||||||
|
#- name: Test whoami
|
||||||
|
# assert:
|
||||||
|
# that:
|
||||||
|
# - whoami_as_elevated.rc == 0
|
||||||
|
# - whoami_as_elevated.stdout|lower == '{{ ansible_hostname|lower }}\{{ ansible_user_id|lower }}'
|
||||||
|
|
||||||
|
- name: Run command with multiple arguments
|
||||||
|
win_psexec:
|
||||||
|
command: powershell.exe -NonInteractive "exit 1"
|
||||||
|
ignore_errors: yes
|
||||||
|
register: whoami_multiple_args
|
||||||
|
environment:
|
||||||
|
PATH: '{{ testing_dir }};{{ system_path.stdout | trim }}'
|
||||||
|
|
||||||
|
- name: Test command with multiple argumetns
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- whoami_multiple_args.rc == 1
|
||||||
|
- whoami_multiple_args.psexec_command == "psexec.exe -accepteula powershell.exe -NonInteractive \"exit 1\""
|
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
- name: make sure win output dir exists
|
||||||
|
win_file:
|
||||||
|
path: "{{win_output_dir}}"
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: reboot with defaults
|
||||||
|
win_reboot:
|
||||||
|
|
||||||
|
- name: test with negative values for delays
|
||||||
|
win_reboot:
|
||||||
|
post_reboot_delay: -0.5
|
||||||
|
pre_reboot_delay: -61
|
||||||
|
|
||||||
|
- name: schedule a reboot for sometime in the future
|
||||||
|
win_command: shutdown.exe /r /t 599
|
||||||
|
|
||||||
|
- name: reboot with a shutdown already scheduled
|
||||||
|
win_reboot:
|
||||||
|
|
||||||
|
# test a reboot that reboots again during the test_command phase
|
||||||
|
- name: create test file
|
||||||
|
win_file:
|
||||||
|
path: '{{win_output_dir}}\win_reboot_test'
|
||||||
|
state: touch
|
||||||
|
|
||||||
|
- name: reboot with secondary reboot stage
|
||||||
|
win_reboot:
|
||||||
|
test_command: '{{ lookup("template", "post_reboot.ps1") }}'
|
||||||
|
|
||||||
|
- name: reboot with test command that fails
|
||||||
|
win_reboot:
|
||||||
|
test_command: 'FAIL'
|
||||||
|
reboot_timeout: 120
|
||||||
|
register: reboot_fail_test
|
||||||
|
failed_when: "reboot_fail_test.msg != 'Timed out waiting for post-reboot test command (timeout=120)'"
|
||||||
|
|
||||||
|
- name: remove SeRemoteShutdownPrivilege
|
||||||
|
win_user_right:
|
||||||
|
name: SeRemoteShutdownPrivilege
|
||||||
|
users: []
|
||||||
|
action: set
|
||||||
|
register: removed_shutdown_privilege
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: try and reboot without required privilege
|
||||||
|
win_reboot:
|
||||||
|
register: fail_privilege
|
||||||
|
failed_when:
|
||||||
|
- "'Reboot command failed, error was:' not in fail_privilege.msg"
|
||||||
|
- "'Access is denied.(5)' not in fail_privilege.msg"
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: reset the SeRemoteShutdownPrivilege
|
||||||
|
win_user_right:
|
||||||
|
name: SeRemoteShutdownPrivilege
|
||||||
|
users: '{{ removed_shutdown_privilege.removed }}'
|
||||||
|
action: add
|
||||||
|
|
||||||
|
- name: Use invalid parameter
|
||||||
|
reboot:
|
||||||
|
foo: bar
|
||||||
|
ignore_errors: true
|
||||||
|
register: invalid_parameter
|
||||||
|
|
||||||
|
- name: Ensure task fails with error
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- invalid_parameter is failed
|
||||||
|
- "invalid_parameter.msg == 'Invalid options for reboot: foo'"
|
@ -0,0 +1,8 @@
|
|||||||
|
if (Test-Path -Path '{{win_output_dir}}\win_reboot_test') {
|
||||||
|
New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' `
|
||||||
|
-Name PendingFileRenameOperations `
|
||||||
|
-Value @("\??\{{win_output_dir}}\win_reboot_test`0") `
|
||||||
|
-PropertyType MultiString
|
||||||
|
Restart-Computer -Force
|
||||||
|
exit 1
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
shippable/windows/incidental
|
||||||
|
windows
|
@ -0,0 +1,53 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# WANT_JSON
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
# basic script to get the lsit of users in a particular right
|
||||||
|
# this is quite complex to put as a simple script so this is
|
||||||
|
# just a simple module
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $false
|
||||||
|
$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true
|
||||||
|
$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertFrom-Ini($file_path) {
|
||||||
|
$ini = @{}
|
||||||
|
switch -Regex -File $file_path {
|
||||||
|
"^\[(.+)\]" {
|
||||||
|
$section = $matches[1]
|
||||||
|
$ini.$section = @{}
|
||||||
|
}
|
||||||
|
"(.+?)\s*=(.*)" {
|
||||||
|
$name = $matches[1].Trim()
|
||||||
|
$value = $matches[2].Trim()
|
||||||
|
if ($value -match "^\d+$") {
|
||||||
|
$value = [int]$value
|
||||||
|
} elseif ($value.StartsWith('"') -and $value.EndsWith('"')) {
|
||||||
|
$value = $value.Substring(1, $value.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$ini.$section.$name = $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ini
|
||||||
|
}
|
||||||
|
|
||||||
|
$secedit_ini_path = [IO.Path]::GetTempFileName()
|
||||||
|
&SecEdit.exe /export /cfg $secedit_ini_path /quiet
|
||||||
|
$secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path
|
||||||
|
|
||||||
|
if ($secedit_ini.ContainsKey($section)) {
|
||||||
|
$result.value = $secedit_ini.$section.$key
|
||||||
|
} else {
|
||||||
|
$result.value = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json $result
|
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
- name: get current entry for audit
|
||||||
|
test_win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
register: before_value_audit
|
||||||
|
|
||||||
|
- name: get current entry for guest
|
||||||
|
test_win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
register: before_value_guest
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: set AuditSystemEvents entry before tests
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: 0
|
||||||
|
|
||||||
|
- name: set NewGuestName entry before tests
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
value: Guest
|
||||||
|
|
||||||
|
- name: run tests
|
||||||
|
include_tasks: tests.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: reset entries for AuditSystemEvents
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: "{{before_value_audit.value}}"
|
||||||
|
|
||||||
|
- name: reset entries for NewGuestName
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
value: "{{before_value_guest.value}}"
|
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
- name: fail with invalid section name
|
||||||
|
win_security_policy:
|
||||||
|
section: This is not a valid section
|
||||||
|
key: KeyName
|
||||||
|
value: 0
|
||||||
|
register: fail_invalid_section
|
||||||
|
failed_when: fail_invalid_section.msg != "The section 'This is not a valid section' does not exist in SecEdit.exe output ini"
|
||||||
|
|
||||||
|
- name: fail with invalid key name
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: InvalidKey
|
||||||
|
value: 0
|
||||||
|
register: fail_invalid_key
|
||||||
|
failed_when: fail_invalid_key.msg != "The key 'InvalidKey' in section 'System Access' is not a valid key, cannot set this value"
|
||||||
|
|
||||||
|
- name: change existing key check
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: 1
|
||||||
|
register: change_existing_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get actual change existing key check
|
||||||
|
test_win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
register: change_existing_actual_check
|
||||||
|
|
||||||
|
- name: assert change existing key check
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_check is changed
|
||||||
|
- change_existing_actual_check.value == 0
|
||||||
|
|
||||||
|
- name: change existing key
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: 1
|
||||||
|
register: change_existing
|
||||||
|
|
||||||
|
- name: get actual change existing key
|
||||||
|
test_win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
register: change_existing_actual
|
||||||
|
|
||||||
|
- name: assert change existing key
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing is changed
|
||||||
|
- change_existing_actual.value == 1
|
||||||
|
|
||||||
|
- name: change existing key again
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: 1
|
||||||
|
register: change_existing_again
|
||||||
|
|
||||||
|
- name: assert change existing key again
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_again is not changed
|
||||||
|
- change_existing_again.value == 1
|
||||||
|
|
||||||
|
- name: change existing key with string type
|
||||||
|
win_security_policy:
|
||||||
|
section: Event Audit
|
||||||
|
key: AuditSystemEvents
|
||||||
|
value: "1"
|
||||||
|
register: change_existing_key_with_type
|
||||||
|
|
||||||
|
- name: assert change existing key with string type
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_key_with_type is not changed
|
||||||
|
- change_existing_key_with_type.value == "1"
|
||||||
|
|
||||||
|
- name: change existing string key check
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
value: New Guest
|
||||||
|
register: change_existing_string_check
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: get actual change existing string key check
|
||||||
|
test_win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
register: change_existing_string_actual_check
|
||||||
|
|
||||||
|
- name: assert change existing string key check
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_string_check is changed
|
||||||
|
- change_existing_string_actual_check.value == "Guest"
|
||||||
|
|
||||||
|
- name: change existing string key
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
value: New Guest
|
||||||
|
register: change_existing_string
|
||||||
|
|
||||||
|
- name: get actual change existing string key
|
||||||
|
test_win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
register: change_existing_string_actual
|
||||||
|
|
||||||
|
- name: assert change existing string key
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_string is changed
|
||||||
|
- change_existing_string_actual.value == "New Guest"
|
||||||
|
|
||||||
|
- name: change existing string key again
|
||||||
|
win_security_policy:
|
||||||
|
section: System Access
|
||||||
|
key: NewGuestName
|
||||||
|
value: New Guest
|
||||||
|
register: change_existing_string_again
|
||||||
|
|
||||||
|
- name: assert change existing string key again
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- change_existing_string_again is not changed
|
||||||
|
- change_existing_string_again.value == "New Guest"
|
||||||
|
|
||||||
|
- name: add policy setting
|
||||||
|
win_security_policy:
|
||||||
|
section: Privilege Rights
|
||||||
|
# following key is empty by default
|
||||||
|
key: SeCreateTokenPrivilege
|
||||||
|
# add Guests
|
||||||
|
value: '*S-1-5-32-546'
|
||||||
|
|
||||||
|
- name: get actual policy setting
|
||||||
|
test_win_security_policy:
|
||||||
|
section: Privilege Rights
|
||||||
|
key: SeCreateTokenPrivilege
|
||||||
|
register: add_policy_setting_actual
|
||||||
|
|
||||||
|
- name: assert add policy setting
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- add_policy_setting_actual.value == '*S-1-5-32-546'
|
||||||
|
|
||||||
|
- name: remove policy setting
|
||||||
|
win_security_policy:
|
||||||
|
section: Privilege Rights
|
||||||
|
key: SeCreateTokenPrivilege
|
||||||
|
value: ''
|
||||||
|
diff: yes
|
||||||
|
register: remove_policy_setting
|
||||||
|
|
||||||
|
- name: get actual policy setting
|
||||||
|
test_win_security_policy:
|
||||||
|
section: Privilege Rights
|
||||||
|
key: SeCreateTokenPrivilege
|
||||||
|
register: remove_policy_setting_actual
|
||||||
|
|
||||||
|
- name: assert remove policy setting
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remove_policy_setting is changed
|
||||||
|
- remove_policy_setting.diff.prepared == "[Privilege Rights]\n-SeCreateTokenPrivilege = *S-1-5-32-546\n+SeCreateTokenPrivilege = "
|
||||||
|
- remove_policy_setting_actual.value is none
|
||||||
|
|
||||||
|
- name: remove policy setting again
|
||||||
|
win_security_policy:
|
||||||
|
section: Privilege Rights
|
||||||
|
key: SeCreateTokenPrivilege
|
||||||
|
value: ''
|
||||||
|
register: remove_policy_setting_again
|
||||||
|
|
||||||
|
- name: assert remove policy setting again
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- remove_policy_setting_again is not changed
|
||||||
|
- remove_policy_setting_again.value == ''
|
@ -0,0 +1,522 @@
|
|||||||
|
# This file is part of Ansible
|
||||||
|
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# Make coding more python3-ish
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from ansible import constants as C
|
||||||
|
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||||
|
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||||
|
from ansible.module_utils.parsing.convert_bool import boolean
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.utils.hashing import checksum
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_dirs(topdir, loader, decrypt=True, base_path=None, local_follow=False, trailing_slash_detector=None, checksum_check=False):
|
||||||
|
"""
|
||||||
|
Walk a filesystem tree returning enough information to copy the files.
|
||||||
|
This is similar to the _walk_dirs function in ``copy.py`` but returns
|
||||||
|
a dict instead of a tuple for each entry and includes the checksum of
|
||||||
|
a local file if wanted.
|
||||||
|
|
||||||
|
:arg topdir: The directory that the filesystem tree is rooted at
|
||||||
|
:arg loader: The self._loader object from ActionBase
|
||||||
|
:kwarg decrypt: Whether to decrypt a file encrypted with ansible-vault
|
||||||
|
:kwarg base_path: The initial directory structure to strip off of the
|
||||||
|
files for the destination directory. If this is None (the default),
|
||||||
|
the base_path is set to ``top_dir``.
|
||||||
|
:kwarg local_follow: Whether to follow symlinks on the source. When set
|
||||||
|
to False, no symlinks are dereferenced. When set to True (the
|
||||||
|
default), the code will dereference most symlinks. However, symlinks
|
||||||
|
can still be present if needed to break a circular link.
|
||||||
|
:kwarg trailing_slash_detector: Function to determine if a path has
|
||||||
|
a trailing directory separator. Only needed when dealing with paths on
|
||||||
|
a remote machine (in which case, pass in a function that is aware of the
|
||||||
|
directory separator conventions on the remote machine).
|
||||||
|
:kawrg whether to get the checksum of the local file and add to the dict
|
||||||
|
:returns: dictionary of dictionaries. All of the path elements in the structure are text string.
|
||||||
|
This separates all the files, directories, and symlinks along with
|
||||||
|
import information about each::
|
||||||
|
|
||||||
|
{
|
||||||
|
'files'; [{
|
||||||
|
src: '/absolute/path/to/copy/from',
|
||||||
|
dest: 'relative/path/to/copy/to',
|
||||||
|
checksum: 'b54ba7f5621240d403f06815f7246006ef8c7d43'
|
||||||
|
}, ...],
|
||||||
|
'directories'; [{
|
||||||
|
src: '/absolute/path/to/copy/from',
|
||||||
|
dest: 'relative/path/to/copy/to'
|
||||||
|
}, ...],
|
||||||
|
'symlinks'; [{
|
||||||
|
src: '/symlink/target/path',
|
||||||
|
dest: 'relative/path/to/copy/to'
|
||||||
|
}, ...],
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
The ``symlinks`` field is only populated if ``local_follow`` is set to False
|
||||||
|
*or* a circular symlink cannot be dereferenced. The ``checksum`` entry is set
|
||||||
|
to None if checksum_check=False.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Convert the path segments into byte strings
|
||||||
|
|
||||||
|
r_files = {'files': [], 'directories': [], 'symlinks': []}
|
||||||
|
|
||||||
|
def _recurse(topdir, rel_offset, parent_dirs, rel_base=u'', checksum_check=False):
|
||||||
|
"""
|
||||||
|
This is a closure (function utilizing variables from it's parent
|
||||||
|
function's scope) so that we only need one copy of all the containers.
|
||||||
|
Note that this function uses side effects (See the Variables used from
|
||||||
|
outer scope).
|
||||||
|
|
||||||
|
:arg topdir: The directory we are walking for files
|
||||||
|
:arg rel_offset: Integer defining how many characters to strip off of
|
||||||
|
the beginning of a path
|
||||||
|
:arg parent_dirs: Directories that we're copying that this directory is in.
|
||||||
|
:kwarg rel_base: String to prepend to the path after ``rel_offset`` is
|
||||||
|
applied to form the relative path.
|
||||||
|
|
||||||
|
Variables used from the outer scope
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:r_files: Dictionary of files in the hierarchy. See the return value
|
||||||
|
for :func:`walk` for the structure of this dictionary.
|
||||||
|
:local_follow: Read-only inside of :func:`_recurse`. Whether to follow symlinks
|
||||||
|
"""
|
||||||
|
for base_path, sub_folders, files in os.walk(topdir):
|
||||||
|
for filename in files:
|
||||||
|
filepath = os.path.join(base_path, filename)
|
||||||
|
dest_filepath = os.path.join(rel_base, filepath[rel_offset:])
|
||||||
|
|
||||||
|
if os.path.islink(filepath):
|
||||||
|
# Dereference the symlnk
|
||||||
|
real_file = loader.get_real_file(os.path.realpath(filepath), decrypt=decrypt)
|
||||||
|
if local_follow and os.path.isfile(real_file):
|
||||||
|
# Add the file pointed to by the symlink
|
||||||
|
r_files['files'].append(
|
||||||
|
{
|
||||||
|
"src": real_file,
|
||||||
|
"dest": dest_filepath,
|
||||||
|
"checksum": _get_local_checksum(checksum_check, real_file)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Mark this file as a symlink to copy
|
||||||
|
r_files['symlinks'].append({"src": os.readlink(filepath), "dest": dest_filepath})
|
||||||
|
else:
|
||||||
|
# Just a normal file
|
||||||
|
real_file = loader.get_real_file(filepath, decrypt=decrypt)
|
||||||
|
r_files['files'].append(
|
||||||
|
{
|
||||||
|
"src": real_file,
|
||||||
|
"dest": dest_filepath,
|
||||||
|
"checksum": _get_local_checksum(checksum_check, real_file)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for dirname in sub_folders:
|
||||||
|
dirpath = os.path.join(base_path, dirname)
|
||||||
|
dest_dirpath = os.path.join(rel_base, dirpath[rel_offset:])
|
||||||
|
real_dir = os.path.realpath(dirpath)
|
||||||
|
dir_stats = os.stat(real_dir)
|
||||||
|
|
||||||
|
if os.path.islink(dirpath):
|
||||||
|
if local_follow:
|
||||||
|
if (dir_stats.st_dev, dir_stats.st_ino) in parent_dirs:
|
||||||
|
# Just insert the symlink if the target directory
|
||||||
|
# exists inside of the copy already
|
||||||
|
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||||
|
else:
|
||||||
|
# Walk the dirpath to find all parent directories.
|
||||||
|
new_parents = set()
|
||||||
|
parent_dir_list = os.path.dirname(dirpath).split(os.path.sep)
|
||||||
|
for parent in range(len(parent_dir_list), 0, -1):
|
||||||
|
parent_stat = os.stat(u'/'.join(parent_dir_list[:parent]))
|
||||||
|
if (parent_stat.st_dev, parent_stat.st_ino) in parent_dirs:
|
||||||
|
# Reached the point at which the directory
|
||||||
|
# tree is already known. Don't add any
|
||||||
|
# more or we might go to an ancestor that
|
||||||
|
# isn't being copied.
|
||||||
|
break
|
||||||
|
new_parents.add((parent_stat.st_dev, parent_stat.st_ino))
|
||||||
|
|
||||||
|
if (dir_stats.st_dev, dir_stats.st_ino) in new_parents:
|
||||||
|
# This was a a circular symlink. So add it as
|
||||||
|
# a symlink
|
||||||
|
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||||
|
else:
|
||||||
|
# Walk the directory pointed to by the symlink
|
||||||
|
r_files['directories'].append({"src": real_dir, "dest": dest_dirpath})
|
||||||
|
offset = len(real_dir) + 1
|
||||||
|
_recurse(real_dir, offset, parent_dirs.union(new_parents),
|
||||||
|
rel_base=dest_dirpath,
|
||||||
|
checksum_check=checksum_check)
|
||||||
|
else:
|
||||||
|
# Add the symlink to the destination
|
||||||
|
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||||
|
else:
|
||||||
|
# Just a normal directory
|
||||||
|
r_files['directories'].append({"src": dirpath, "dest": dest_dirpath})
|
||||||
|
|
||||||
|
# Check if the source ends with a "/" so that we know which directory
|
||||||
|
# level to work at (similar to rsync)
|
||||||
|
source_trailing_slash = False
|
||||||
|
if trailing_slash_detector:
|
||||||
|
source_trailing_slash = trailing_slash_detector(topdir)
|
||||||
|
else:
|
||||||
|
source_trailing_slash = topdir.endswith(os.path.sep)
|
||||||
|
|
||||||
|
# Calculate the offset needed to strip the base_path to make relative
|
||||||
|
# paths
|
||||||
|
if base_path is None:
|
||||||
|
base_path = topdir
|
||||||
|
if not source_trailing_slash:
|
||||||
|
base_path = os.path.dirname(base_path)
|
||||||
|
if topdir.startswith(base_path):
|
||||||
|
offset = len(base_path)
|
||||||
|
|
||||||
|
# Make sure we're making the new paths relative
|
||||||
|
if trailing_slash_detector and not trailing_slash_detector(base_path):
|
||||||
|
offset += 1
|
||||||
|
elif not base_path.endswith(os.path.sep):
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
if os.path.islink(topdir) and not local_follow:
|
||||||
|
r_files['symlinks'] = {"src": os.readlink(topdir), "dest": os.path.basename(topdir)}
|
||||||
|
return r_files
|
||||||
|
|
||||||
|
dir_stats = os.stat(topdir)
|
||||||
|
parents = frozenset(((dir_stats.st_dev, dir_stats.st_ino),))
|
||||||
|
# Actually walk the directory hierarchy
|
||||||
|
_recurse(topdir, offset, parents, checksum_check=checksum_check)
|
||||||
|
|
||||||
|
return r_files
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_checksum(get_checksum, local_path):
|
||||||
|
if get_checksum:
|
||||||
|
return checksum(local_path)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ActionModule(ActionBase):
|
||||||
|
|
||||||
|
WIN_PATH_SEPARATOR = "\\"
|
||||||
|
|
||||||
|
def _create_content_tempfile(self, content):
|
||||||
|
''' Create a tempfile containing defined content '''
|
||||||
|
fd, content_tempfile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP)
|
||||||
|
f = os.fdopen(fd, 'wb')
|
||||||
|
content = to_bytes(content)
|
||||||
|
try:
|
||||||
|
f.write(content)
|
||||||
|
except Exception as err:
|
||||||
|
os.remove(content_tempfile)
|
||||||
|
raise Exception(err)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
return content_tempfile
|
||||||
|
|
||||||
|
def _create_zip_tempfile(self, files, directories):
|
||||||
|
tmpdir = tempfile.mkdtemp(dir=C.DEFAULT_LOCAL_TMP)
|
||||||
|
zip_file_path = os.path.join(tmpdir, "win_copy.zip")
|
||||||
|
zip_file = zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_STORED, True)
|
||||||
|
|
||||||
|
# encoding the file/dir name with base64 so Windows can unzip a unicode
|
||||||
|
# filename and get the right name, Windows doesn't handle unicode names
|
||||||
|
# very well
|
||||||
|
for directory in directories:
|
||||||
|
directory_path = to_bytes(directory['src'], errors='surrogate_or_strict')
|
||||||
|
archive_path = to_bytes(directory['dest'], errors='surrogate_or_strict')
|
||||||
|
|
||||||
|
encoded_path = to_text(base64.b64encode(archive_path), errors='surrogate_or_strict')
|
||||||
|
zip_file.write(directory_path, encoded_path, zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_path = to_bytes(file['src'], errors='surrogate_or_strict')
|
||||||
|
archive_path = to_bytes(file['dest'], errors='surrogate_or_strict')
|
||||||
|
|
||||||
|
encoded_path = to_text(base64.b64encode(archive_path), errors='surrogate_or_strict')
|
||||||
|
zip_file.write(file_path, encoded_path, zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
|
return zip_file_path
|
||||||
|
|
||||||
|
def _remove_tempfile_if_content_defined(self, content, content_tempfile):
|
||||||
|
if content is not None:
|
||||||
|
os.remove(content_tempfile)
|
||||||
|
|
||||||
|
def _copy_single_file(self, local_file, dest, source_rel, task_vars, tmp, backup):
|
||||||
|
if self._play_context.check_mode:
|
||||||
|
module_return = dict(changed=True)
|
||||||
|
return module_return
|
||||||
|
|
||||||
|
# copy the file across to the server
|
||||||
|
tmp_src = self._connection._shell.join_path(tmp, 'source')
|
||||||
|
self._transfer_file(local_file, tmp_src)
|
||||||
|
|
||||||
|
copy_args = self._task.args.copy()
|
||||||
|
copy_args.update(
|
||||||
|
dict(
|
||||||
|
dest=dest,
|
||||||
|
src=tmp_src,
|
||||||
|
_original_basename=source_rel,
|
||||||
|
_copy_mode="single",
|
||||||
|
backup=backup,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
copy_args.pop('content', None)
|
||||||
|
|
||||||
|
copy_result = self._execute_module(module_name="copy",
|
||||||
|
module_args=copy_args,
|
||||||
|
task_vars=task_vars)
|
||||||
|
|
||||||
|
return copy_result
|
||||||
|
|
||||||
|
def _copy_zip_file(self, dest, files, directories, task_vars, tmp, backup):
|
||||||
|
# create local zip file containing all the files and directories that
|
||||||
|
# need to be copied to the server
|
||||||
|
if self._play_context.check_mode:
|
||||||
|
module_return = dict(changed=True)
|
||||||
|
return module_return
|
||||||
|
|
||||||
|
try:
|
||||||
|
zip_file = self._create_zip_tempfile(files, directories)
|
||||||
|
except Exception as e:
|
||||||
|
module_return = dict(
|
||||||
|
changed=False,
|
||||||
|
failed=True,
|
||||||
|
msg="failed to create tmp zip file: %s" % to_text(e),
|
||||||
|
exception=traceback.format_exc()
|
||||||
|
)
|
||||||
|
return module_return
|
||||||
|
|
||||||
|
zip_path = self._loader.get_real_file(zip_file)
|
||||||
|
|
||||||
|
# send zip file to remote, file must end in .zip so
|
||||||
|
# Com Shell.Application works
|
||||||
|
tmp_src = self._connection._shell.join_path(tmp, 'source.zip')
|
||||||
|
self._transfer_file(zip_path, tmp_src)
|
||||||
|
|
||||||
|
# run the explode operation of win_copy on remote
|
||||||
|
copy_args = self._task.args.copy()
|
||||||
|
copy_args.update(
|
||||||
|
dict(
|
||||||
|
src=tmp_src,
|
||||||
|
dest=dest,
|
||||||
|
_copy_mode="explode",
|
||||||
|
backup=backup,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
copy_args.pop('content', None)
|
||||||
|
module_return = self._execute_module(module_name='copy',
|
||||||
|
module_args=copy_args,
|
||||||
|
task_vars=task_vars)
|
||||||
|
shutil.rmtree(os.path.dirname(zip_path))
|
||||||
|
return module_return
|
||||||
|
|
||||||
|
def run(self, tmp=None, task_vars=None):
|
||||||
|
''' handler for file transfer operations '''
|
||||||
|
if task_vars is None:
|
||||||
|
task_vars = dict()
|
||||||
|
|
||||||
|
result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
|
del tmp # tmp no longer has any effect
|
||||||
|
|
||||||
|
source = self._task.args.get('src', None)
|
||||||
|
content = self._task.args.get('content', None)
|
||||||
|
dest = self._task.args.get('dest', None)
|
||||||
|
remote_src = boolean(self._task.args.get('remote_src', False), strict=False)
|
||||||
|
local_follow = boolean(self._task.args.get('local_follow', False), strict=False)
|
||||||
|
force = boolean(self._task.args.get('force', True), strict=False)
|
||||||
|
decrypt = boolean(self._task.args.get('decrypt', True), strict=False)
|
||||||
|
backup = boolean(self._task.args.get('backup', False), strict=False)
|
||||||
|
|
||||||
|
result['src'] = source
|
||||||
|
result['dest'] = dest
|
||||||
|
|
||||||
|
result['failed'] = True
|
||||||
|
if (source is None and content is None) or dest is None:
|
||||||
|
result['msg'] = "src (or content) and dest are required"
|
||||||
|
elif source is not None and content is not None:
|
||||||
|
result['msg'] = "src and content are mutually exclusive"
|
||||||
|
elif content is not None and dest is not None and (
|
||||||
|
dest.endswith(os.path.sep) or dest.endswith(self.WIN_PATH_SEPARATOR)):
|
||||||
|
result['msg'] = "dest must be a file if content is defined"
|
||||||
|
else:
|
||||||
|
del result['failed']
|
||||||
|
|
||||||
|
if result.get('failed'):
|
||||||
|
return result
|
||||||
|
|
||||||
|
# If content is defined make a temp file and write the content into it
|
||||||
|
content_tempfile = None
|
||||||
|
if content is not None:
|
||||||
|
try:
|
||||||
|
# if content comes to us as a dict it should be decoded json.
|
||||||
|
# We need to encode it back into a string and write it out
|
||||||
|
if isinstance(content, dict) or isinstance(content, list):
|
||||||
|
content_tempfile = self._create_content_tempfile(json.dumps(content))
|
||||||
|
else:
|
||||||
|
content_tempfile = self._create_content_tempfile(content)
|
||||||
|
source = content_tempfile
|
||||||
|
except Exception as err:
|
||||||
|
result['failed'] = True
|
||||||
|
result['msg'] = "could not write content tmp file: %s" % to_native(err)
|
||||||
|
return result
|
||||||
|
# all actions should occur on the remote server, run win_copy module
|
||||||
|
elif remote_src:
|
||||||
|
new_module_args = self._task.args.copy()
|
||||||
|
new_module_args.update(
|
||||||
|
dict(
|
||||||
|
_copy_mode="remote",
|
||||||
|
dest=dest,
|
||||||
|
src=source,
|
||||||
|
force=force,
|
||||||
|
backup=backup,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
new_module_args.pop('content', None)
|
||||||
|
result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars))
|
||||||
|
return result
|
||||||
|
# find_needle returns a path that may not have a trailing slash on a
|
||||||
|
# directory so we need to find that out first and append at the end
|
||||||
|
else:
|
||||||
|
trailing_slash = source.endswith(os.path.sep)
|
||||||
|
try:
|
||||||
|
# find in expected paths
|
||||||
|
source = self._find_needle('files', source)
|
||||||
|
except AnsibleError as e:
|
||||||
|
result['failed'] = True
|
||||||
|
result['msg'] = to_text(e)
|
||||||
|
result['exception'] = traceback.format_exc()
|
||||||
|
return result
|
||||||
|
|
||||||
|
if trailing_slash != source.endswith(os.path.sep):
|
||||||
|
if source[-1] == os.path.sep:
|
||||||
|
source = source[:-1]
|
||||||
|
else:
|
||||||
|
source = source + os.path.sep
|
||||||
|
|
||||||
|
# A list of source file tuples (full_path, relative_path) which will try to copy to the destination
|
||||||
|
source_files = {'files': [], 'directories': [], 'symlinks': []}
|
||||||
|
|
||||||
|
# If source is a directory populate our list else source is a file and translate it to a tuple.
|
||||||
|
if os.path.isdir(to_bytes(source, errors='surrogate_or_strict')):
|
||||||
|
result['operation'] = 'folder_copy'
|
||||||
|
|
||||||
|
# Get a list of the files we want to replicate on the remote side
|
||||||
|
source_files = _walk_dirs(source, self._loader, decrypt=decrypt, local_follow=local_follow,
|
||||||
|
trailing_slash_detector=self._connection._shell.path_has_trailing_slash,
|
||||||
|
checksum_check=force)
|
||||||
|
|
||||||
|
# If it's recursive copy, destination is always a dir,
|
||||||
|
# explicitly mark it so (note - win_copy module relies on this).
|
||||||
|
if not self._connection._shell.path_has_trailing_slash(dest):
|
||||||
|
dest = "%s%s" % (dest, self.WIN_PATH_SEPARATOR)
|
||||||
|
|
||||||
|
check_dest = dest
|
||||||
|
# Source is a file, add details to source_files dict
|
||||||
|
else:
|
||||||
|
result['operation'] = 'file_copy'
|
||||||
|
|
||||||
|
# If the local file does not exist, get_real_file() raises AnsibleFileNotFound
|
||||||
|
try:
|
||||||
|
source_full = self._loader.get_real_file(source, decrypt=decrypt)
|
||||||
|
except AnsibleFileNotFound as e:
|
||||||
|
result['failed'] = True
|
||||||
|
result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e))
|
||||||
|
return result
|
||||||
|
|
||||||
|
original_basename = os.path.basename(source)
|
||||||
|
result['original_basename'] = original_basename
|
||||||
|
|
||||||
|
# check if dest ends with / or \ and append source filename to dest
|
||||||
|
if self._connection._shell.path_has_trailing_slash(dest):
|
||||||
|
check_dest = dest
|
||||||
|
filename = original_basename
|
||||||
|
result['dest'] = self._connection._shell.join_path(dest, filename)
|
||||||
|
else:
|
||||||
|
# replace \\ with / so we can use os.path to get the filename or dirname
|
||||||
|
unix_path = dest.replace(self.WIN_PATH_SEPARATOR, os.path.sep)
|
||||||
|
filename = os.path.basename(unix_path)
|
||||||
|
check_dest = os.path.dirname(unix_path)
|
||||||
|
|
||||||
|
file_checksum = _get_local_checksum(force, source_full)
|
||||||
|
source_files['files'].append(
|
||||||
|
dict(
|
||||||
|
src=source_full,
|
||||||
|
dest=filename,
|
||||||
|
checksum=file_checksum
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result['checksum'] = file_checksum
|
||||||
|
result['size'] = os.path.getsize(to_bytes(source_full, errors='surrogate_or_strict'))
|
||||||
|
|
||||||
|
# find out the files/directories/symlinks that we need to copy to the server
|
||||||
|
query_args = self._task.args.copy()
|
||||||
|
query_args.update(
|
||||||
|
dict(
|
||||||
|
_copy_mode="query",
|
||||||
|
dest=check_dest,
|
||||||
|
force=force,
|
||||||
|
files=source_files['files'],
|
||||||
|
directories=source_files['directories'],
|
||||||
|
symlinks=source_files['symlinks'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# src is not required for query, will fail path validation is src has unix allowed chars
|
||||||
|
query_args.pop('src', None)
|
||||||
|
|
||||||
|
query_args.pop('content', None)
|
||||||
|
query_return = self._execute_module(module_args=query_args,
|
||||||
|
task_vars=task_vars)
|
||||||
|
|
||||||
|
if query_return.get('failed') is True:
|
||||||
|
result.update(query_return)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if len(query_return['files']) > 0 or len(query_return['directories']) > 0 and self._connection._shell.tmpdir is None:
|
||||||
|
self._connection._shell.tmpdir = self._make_tmp_path()
|
||||||
|
|
||||||
|
if len(query_return['files']) == 1 and len(query_return['directories']) == 0:
|
||||||
|
# we only need to copy 1 file, don't mess around with zips
|
||||||
|
file_src = query_return['files'][0]['src']
|
||||||
|
file_dest = query_return['files'][0]['dest']
|
||||||
|
result.update(self._copy_single_file(file_src, dest, file_dest,
|
||||||
|
task_vars, self._connection._shell.tmpdir, backup))
|
||||||
|
if result.get('failed') is True:
|
||||||
|
result['msg'] = "failed to copy file %s: %s" % (file_src, result['msg'])
|
||||||
|
result['changed'] = True
|
||||||
|
|
||||||
|
elif len(query_return['files']) > 0 or len(query_return['directories']) > 0:
|
||||||
|
# either multiple files or directories need to be copied, compress
|
||||||
|
# to a zip and 'explode' the zip on the server
|
||||||
|
# TODO: handle symlinks
|
||||||
|
result.update(self._copy_zip_file(dest, source_files['files'],
|
||||||
|
source_files['directories'],
|
||||||
|
task_vars, self._connection._shell.tmpdir, backup))
|
||||||
|
result['changed'] = True
|
||||||
|
else:
|
||||||
|
# no operations need to occur
|
||||||
|
result['failed'] = False
|
||||||
|
result['changed'] = False
|
||||||
|
|
||||||
|
# remove the content tmp file and remote tmp file if it was created
|
||||||
|
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||||
|
self._remove_tmp_path(self._connection._shell.tmpdir)
|
||||||
|
return result
|
@ -0,0 +1,96 @@
|
|||||||
|
# Copyright: (c) 2018, Matt Davis <mdavis@ansible.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleError
|
||||||
|
from ansible.module_utils._text import to_native
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.plugins.action.reboot import ActionModule as RebootActionModule
|
||||||
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
|
class TimedOutException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ActionModule(RebootActionModule, ActionBase):
|
||||||
|
TRANSFERS_FILES = False
|
||||||
|
_VALID_ARGS = frozenset((
|
||||||
|
'connect_timeout', 'connect_timeout_sec', 'msg', 'post_reboot_delay', 'post_reboot_delay_sec', 'pre_reboot_delay', 'pre_reboot_delay_sec',
|
||||||
|
'reboot_timeout', 'reboot_timeout_sec', 'shutdown_timeout', 'shutdown_timeout_sec', 'test_command',
|
||||||
|
))
|
||||||
|
|
||||||
|
DEFAULT_BOOT_TIME_COMMAND = "(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime"
|
||||||
|
DEFAULT_CONNECT_TIMEOUT = 5
|
||||||
|
DEFAULT_PRE_REBOOT_DELAY = 2
|
||||||
|
DEFAULT_SUDOABLE = False
|
||||||
|
DEFAULT_SHUTDOWN_COMMAND_ARGS = '/r /t {delay_sec} /c "{message}"'
|
||||||
|
|
||||||
|
DEPRECATED_ARGS = {
|
||||||
|
'shutdown_timeout': '2.5',
|
||||||
|
'shutdown_timeout_sec': '2.5',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ActionModule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_distribution(self, task_vars):
|
||||||
|
return {'name': 'windows', 'version': '', 'family': ''}
|
||||||
|
|
||||||
|
def get_shutdown_command(self, task_vars, distribution):
|
||||||
|
return self.DEFAULT_SHUTDOWN_COMMAND
|
||||||
|
|
||||||
|
def run_test_command(self, distribution, **kwargs):
|
||||||
|
# Need to wrap the test_command in our PowerShell encoded wrapper. This is done to align the command input to a
|
||||||
|
# common shell and to allow the psrp connection plugin to report the correct exit code without manually setting
|
||||||
|
# $LASTEXITCODE for just that plugin.
|
||||||
|
test_command = self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND)
|
||||||
|
kwargs['test_command'] = self._connection._shell._encode_script(test_command)
|
||||||
|
super(ActionModule, self).run_test_command(distribution, **kwargs)
|
||||||
|
|
||||||
|
def perform_reboot(self, task_vars, distribution):
|
||||||
|
shutdown_command = self.get_shutdown_command(task_vars, distribution)
|
||||||
|
shutdown_command_args = self.get_shutdown_command_args(distribution)
|
||||||
|
reboot_command = self._connection._shell._encode_script('{0} {1}'.format(shutdown_command, shutdown_command_args))
|
||||||
|
|
||||||
|
display.vvv("{action}: rebooting server...".format(action=self._task.action))
|
||||||
|
display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
|
||||||
|
display.debug("{action}: rebooting server with command '{command}'".format(action=self._task.action, command=reboot_command))
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
reboot_result = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE)
|
||||||
|
result['start'] = datetime.utcnow()
|
||||||
|
|
||||||
|
# Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully
|
||||||
|
stdout = reboot_result['stdout']
|
||||||
|
stderr = reboot_result['stderr']
|
||||||
|
if reboot_result['rc'] == 1190 or (reboot_result['rc'] != 0 and "(1190)" in reboot_result['stderr']):
|
||||||
|
display.warning('A scheduled reboot was pre-empted by Ansible.')
|
||||||
|
|
||||||
|
# Try to abort (this may fail if it was already aborted)
|
||||||
|
result1 = self._low_level_execute_command(self._connection._shell._encode_script('shutdown /a'),
|
||||||
|
sudoable=self.DEFAULT_SUDOABLE)
|
||||||
|
|
||||||
|
# Initiate reboot again
|
||||||
|
result2 = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE)
|
||||||
|
|
||||||
|
reboot_result['rc'] = result2['rc']
|
||||||
|
stdout += result1['stdout'] + result2['stdout']
|
||||||
|
stderr += result1['stderr'] + result2['stderr']
|
||||||
|
|
||||||
|
if reboot_result['rc'] != 0:
|
||||||
|
result['failed'] = True
|
||||||
|
result['rebooted'] = False
|
||||||
|
result['msg'] = "Reboot command failed, error was: {stdout} {stderr}".format(
|
||||||
|
stdout=to_native(stdout.strip()),
|
||||||
|
stderr=to_native(stderr.strip()))
|
||||||
|
return result
|
||||||
|
|
||||||
|
result['failed'] = False
|
||||||
|
return result
|
@ -0,0 +1,29 @@
|
|||||||
|
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Make coding more python3-ish
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.plugins.action.template import ActionModule as TemplateActionModule
|
||||||
|
|
||||||
|
|
||||||
|
# Even though TemplateActionModule inherits from ActionBase, we still need to
|
||||||
|
# directly inherit from ActionBase to appease the plugin loader.
|
||||||
|
class ActionModule(TemplateActionModule, ActionBase):
|
||||||
|
DEFAULT_NEWLINE_SEQUENCE = '\r\n'
|
@ -0,0 +1,70 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright: (c) 2018, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
become: runas
|
||||||
|
short_description: Run As user
|
||||||
|
description:
|
||||||
|
- This become plugins allows your remote/login user to execute commands as another user via the windows runas facility.
|
||||||
|
author: ansible (@core)
|
||||||
|
version_added: "2.8"
|
||||||
|
options:
|
||||||
|
become_user:
|
||||||
|
description: User you 'become' to execute the task
|
||||||
|
ini:
|
||||||
|
- section: privilege_escalation
|
||||||
|
key: become_user
|
||||||
|
- section: runas_become_plugin
|
||||||
|
key: user
|
||||||
|
vars:
|
||||||
|
- name: ansible_become_user
|
||||||
|
- name: ansible_runas_user
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_BECOME_USER
|
||||||
|
- name: ANSIBLE_RUNAS_USER
|
||||||
|
required: True
|
||||||
|
become_flags:
|
||||||
|
description: Options to pass to runas, a space delimited list of k=v pairs
|
||||||
|
default: ''
|
||||||
|
ini:
|
||||||
|
- section: privilege_escalation
|
||||||
|
key: become_flags
|
||||||
|
- section: runas_become_plugin
|
||||||
|
key: flags
|
||||||
|
vars:
|
||||||
|
- name: ansible_become_flags
|
||||||
|
- name: ansible_runas_flags
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_BECOME_FLAGS
|
||||||
|
- name: ANSIBLE_RUNAS_FLAGS
|
||||||
|
become_pass:
|
||||||
|
description: password
|
||||||
|
ini:
|
||||||
|
- section: runas_become_plugin
|
||||||
|
key: password
|
||||||
|
vars:
|
||||||
|
- name: ansible_become_password
|
||||||
|
- name: ansible_become_pass
|
||||||
|
- name: ansible_runas_pass
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_BECOME_PASS
|
||||||
|
- name: ANSIBLE_RUNAS_PASS
|
||||||
|
notes:
|
||||||
|
- runas is really implemented in the powershell module handler and as such can only be used with winrm connections.
|
||||||
|
- This plugin ignores the 'become_exe' setting as it uses an API and not an executable.
|
||||||
|
- The Secondary Logon service (seclogon) must be running to use runas
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.plugins.become import BecomeBase
|
||||||
|
|
||||||
|
|
||||||
|
class BecomeModule(BecomeBase):
|
||||||
|
|
||||||
|
name = 'runas'
|
||||||
|
|
||||||
|
def build_become_command(self, cmd, shell):
|
||||||
|
# runas is implemented inside the winrm connection plugin
|
||||||
|
return cmd
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,58 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
|
$results = @{changed=$false}
|
||||||
|
|
||||||
|
$parsed_args = Parse-Args $args
|
||||||
|
$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results
|
||||||
|
$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup"
|
||||||
|
|
||||||
|
# parsed in from the async_status action plugin
|
||||||
|
$async_dir = Get-AnsibleParam $parsed_args "_async_dir" -type "path" -failifempty $true
|
||||||
|
|
||||||
|
$log_path = [System.IO.Path]::Combine($async_dir, $jid)
|
||||||
|
|
||||||
|
If(-not $(Test-Path $log_path))
|
||||||
|
{
|
||||||
|
Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job at '$async_dir'"
|
||||||
|
}
|
||||||
|
|
||||||
|
If($mode -eq "cleanup") {
|
||||||
|
Remove-Item $log_path -Recurse
|
||||||
|
Exit-Json @{ansible_job_id=$jid; erased=$log_path}
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOT in cleanup mode, assume regular status mode
|
||||||
|
# no remote kill mode currently exists, but probably should
|
||||||
|
# consider log_path + ".pid" file and also unlink that above
|
||||||
|
|
||||||
|
$data = $null
|
||||||
|
Try {
|
||||||
|
$data_raw = Get-Content $log_path
|
||||||
|
|
||||||
|
# TODO: move this into module_utils/powershell.ps1?
|
||||||
|
$jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer
|
||||||
|
$data = $jss.DeserializeObject($data_raw)
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
If(-not $data_raw) {
|
||||||
|
# file not written yet? That means it is running
|
||||||
|
Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0}
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If (-not $data.ContainsKey("started")) {
|
||||||
|
$data['finished'] = 1
|
||||||
|
$data['ansible_job_id'] = $jid
|
||||||
|
}
|
||||||
|
ElseIf (-not $data.ContainsKey("finished")) {
|
||||||
|
$data['finished'] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json $data
|
@ -0,0 +1,516 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2018, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
|
Function Get-CustomFacts {
|
||||||
|
[cmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(mandatory=$false)]
|
||||||
|
$factpath = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Test-Path -Path $factpath) {
|
||||||
|
$FactsFiles = Get-ChildItem -Path $factpath | Where-Object -FilterScript {($PSItem.PSIsContainer -eq $false) -and ($PSItem.Extension -eq '.ps1')}
|
||||||
|
|
||||||
|
foreach ($FactsFile in $FactsFiles) {
|
||||||
|
$out = & $($FactsFile.FullName)
|
||||||
|
$result.ansible_facts.Add("ansible_$(($FactsFile.Name).Split('.')[0])", $out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Add-Warning $result "Non existing path was set for local facts - $factpath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-MachineSid {
|
||||||
|
# The Machine SID is stored in HKLM:\SECURITY\SAM\Domains\Account and is
|
||||||
|
# only accessible by the Local System account. This method get's the local
|
||||||
|
# admin account (ends with -500) and lops it off to get the machine sid.
|
||||||
|
|
||||||
|
$machine_sid = $null
|
||||||
|
|
||||||
|
try {
|
||||||
|
$admins_sid = "S-1-5-32-544"
|
||||||
|
$admin_group = ([Security.Principal.SecurityIdentifier]$admins_sid).Translate([Security.Principal.NTAccount]).Value
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||||
|
$principal_context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
|
||||||
|
$group_principal = New-Object -TypeName System.DirectoryServices.AccountManagement.GroupPrincipal($principal_context, $admin_group)
|
||||||
|
$searcher = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalSearcher($group_principal)
|
||||||
|
$groups = $searcher.FindOne()
|
||||||
|
|
||||||
|
foreach ($user in $groups.Members) {
|
||||||
|
$user_sid = $user.Sid
|
||||||
|
if ($user_sid.Value.EndsWith("-500")) {
|
||||||
|
$machine_sid = $user_sid.AccountDomainSid.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
#can fail for any number of reasons, if it does just return the original null
|
||||||
|
Add-Warning -obj $result -message "Error during machine sid retrieval: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $machine_sid
|
||||||
|
}
|
||||||
|
|
||||||
|
$cim_instances = @{}
|
||||||
|
|
||||||
|
Function Get-LazyCimInstance([string]$instance_name, [string]$namespace="Root\CIMV2") {
|
||||||
|
if(-not $cim_instances.ContainsKey($instance_name)) {
|
||||||
|
$cim_instances[$instance_name] = $(Get-CimInstance -Namespace $namespace -ClassName $instance_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cim_instances[$instance_name]
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
ansible_facts = @{ }
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped_subsets = @{
|
||||||
|
min=[System.Collections.Generic.List[string]]@('date_time','distribution','dns','env','local','platform','powershell_version','user')
|
||||||
|
network=[System.Collections.Generic.List[string]]@('all_ipv4_addresses','all_ipv6_addresses','interfaces','windows_domain', 'winrm')
|
||||||
|
hardware=[System.Collections.Generic.List[string]]@('bios','memory','processor','uptime','virtual')
|
||||||
|
external=[System.Collections.Generic.List[string]]@('facter')
|
||||||
|
}
|
||||||
|
|
||||||
|
# build "all" set from everything mentioned in the group- this means every value must be in at least one subset to be considered legal
|
||||||
|
$all_set = [System.Collections.Generic.HashSet[string]]@()
|
||||||
|
|
||||||
|
foreach($kv in $grouped_subsets.GetEnumerator()) {
|
||||||
|
[void] $all_set.UnionWith($kv.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
# dynamically create an "all" subset now that we know what should be in it
|
||||||
|
$grouped_subsets['all'] = [System.Collections.Generic.List[string]]$all_set
|
||||||
|
|
||||||
|
# start with all, build up gather and exclude subsets
|
||||||
|
$gather_subset = [System.Collections.Generic.HashSet[string]]$grouped_subsets.all
|
||||||
|
$explicit_subset = [System.Collections.Generic.HashSet[string]]@()
|
||||||
|
$exclude_subset = [System.Collections.Generic.HashSet[string]]@()
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true
|
||||||
|
$factpath = Get-AnsibleParam -obj $params -name "fact_path" -type "path"
|
||||||
|
$gather_subset_source = Get-AnsibleParam -obj $params -name "gather_subset" -type "list" -default "all"
|
||||||
|
|
||||||
|
foreach($item in $gather_subset_source) {
|
||||||
|
if(([string]$item).StartsWith("!")) {
|
||||||
|
$item = ([string]$item).Substring(1)
|
||||||
|
if($item -eq "all") {
|
||||||
|
$all_minus_min = [System.Collections.Generic.HashSet[string]]@($all_set)
|
||||||
|
[void] $all_minus_min.ExceptWith($grouped_subsets.min)
|
||||||
|
[void] $exclude_subset.UnionWith($all_minus_min)
|
||||||
|
}
|
||||||
|
elseif($grouped_subsets.ContainsKey($item)) {
|
||||||
|
[void] $exclude_subset.UnionWith($grouped_subsets[$item])
|
||||||
|
}
|
||||||
|
elseif($all_set.Contains($item)) {
|
||||||
|
[void] $exclude_subset.Add($item)
|
||||||
|
}
|
||||||
|
# NB: invalid exclude values are ignored, since that's what posix setup does
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if($grouped_subsets.ContainsKey($item)) {
|
||||||
|
[void] $explicit_subset.UnionWith($grouped_subsets[$item])
|
||||||
|
}
|
||||||
|
elseif($all_set.Contains($item)) {
|
||||||
|
[void] $explicit_subset.Add($item)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# NB: POSIX setup fails on invalid value; we warn, because we don't implement the same set as POSIX
|
||||||
|
# and we don't have platform-specific config for this...
|
||||||
|
Add-Warning $result "invalid value $item specified in gather_subset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[void] $gather_subset.ExceptWith($exclude_subset)
|
||||||
|
[void] $gather_subset.UnionWith($explicit_subset)
|
||||||
|
|
||||||
|
$ansible_facts = @{
|
||||||
|
gather_subset=@($gather_subset_source)
|
||||||
|
module_setup=$true
|
||||||
|
}
|
||||||
|
|
||||||
|
$osversion = [Environment]::OSVersion
|
||||||
|
|
||||||
|
if ($osversion.Version -lt [version]"6.2") {
|
||||||
|
# Server 2008, 2008 R2, and Windows 7 are not tested in CI and we want to let customers know about it before
|
||||||
|
# removing support altogether.
|
||||||
|
$version_string = "{0}.{1}" -f ($osversion.Version.Major, $osversion.Version.Minor)
|
||||||
|
$msg = "Windows version '$version_string' will no longer be supported or tested in the next Ansible release"
|
||||||
|
Add-DeprecationWarning -obj $result -message $msg -version "2.11"
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('all_ipv4_addresses') -or $gather_subset.Contains('all_ipv6_addresses')) {
|
||||||
|
$netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
|
||||||
|
|
||||||
|
# TODO: split v4/v6 properly, return in separate keys
|
||||||
|
$ips = @()
|
||||||
|
Foreach ($ip in $netcfg.IPAddress) {
|
||||||
|
If ($ip) {
|
||||||
|
$ips += $ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_ip_addresses = $ips
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('bios')) {
|
||||||
|
$win32_bios = Get-LazyCimInstance Win32_Bios
|
||||||
|
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_bios_date = $win32_bios.ReleaseDate.ToString("MM/dd/yyyy")
|
||||||
|
ansible_bios_version = $win32_bios.SMBIOSBIOSVersion
|
||||||
|
ansible_product_name = $win32_cs.Model.Trim()
|
||||||
|
ansible_product_serial = $win32_bios.SerialNumber
|
||||||
|
# ansible_product_version = ([string] $win32_cs.SystemFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('date_time')) {
|
||||||
|
$datetime = (Get-Date)
|
||||||
|
$datetime_utc = $datetime.ToUniversalTime()
|
||||||
|
$date = @{
|
||||||
|
date = $datetime.ToString("yyyy-MM-dd")
|
||||||
|
day = $datetime.ToString("dd")
|
||||||
|
epoch = (Get-Date -UFormat "%s")
|
||||||
|
hour = $datetime.ToString("HH")
|
||||||
|
iso8601 = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||||
|
iso8601_basic = $datetime.ToString("yyyyMMddTHHmmssffffff")
|
||||||
|
iso8601_basic_short = $datetime.ToString("yyyyMMddTHHmmss")
|
||||||
|
iso8601_micro = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ")
|
||||||
|
minute = $datetime.ToString("mm")
|
||||||
|
month = $datetime.ToString("MM")
|
||||||
|
second = $datetime.ToString("ss")
|
||||||
|
time = $datetime.ToString("HH:mm:ss")
|
||||||
|
tz = ([System.TimeZoneInfo]::Local.Id)
|
||||||
|
tz_offset = $datetime.ToString("zzzz")
|
||||||
|
# Ensure that the weekday is in English
|
||||||
|
weekday = $datetime.ToString("dddd", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||||
|
weekday_number = (Get-Date -UFormat "%w")
|
||||||
|
weeknumber = (Get-Date -UFormat "%W")
|
||||||
|
year = $datetime.ToString("yyyy")
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_date_time = $date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('distribution')) {
|
||||||
|
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||||
|
$product_type = switch($win32_os.ProductType) {
|
||||||
|
1 { "workstation" }
|
||||||
|
2 { "domain_controller" }
|
||||||
|
3 { "server" }
|
||||||
|
default { "unknown" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$installation_type = $null
|
||||||
|
$current_version_path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
|
||||||
|
if (Test-Path -LiteralPath $current_version_path) {
|
||||||
|
$install_type_prop = Get-ItemProperty -LiteralPath $current_version_path -ErrorAction SilentlyContinue
|
||||||
|
$installation_type = [String]$install_type_prop.InstallationType
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_distribution = $win32_os.Caption
|
||||||
|
ansible_distribution_version = $osversion.Version.ToString()
|
||||||
|
ansible_distribution_major_version = $osversion.Version.Major.ToString()
|
||||||
|
ansible_os_family = "Windows"
|
||||||
|
ansible_os_name = ($win32_os.Name.Split('|')[0]).Trim()
|
||||||
|
ansible_os_product_type = $product_type
|
||||||
|
ansible_os_installation_type = $installation_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('env')) {
|
||||||
|
$env_vars = @{ }
|
||||||
|
foreach ($item in Get-ChildItem Env:) {
|
||||||
|
$name = $item | Select-Object -ExpandProperty Name
|
||||||
|
# Powershell ConvertTo-Json fails if string ends with \
|
||||||
|
$value = ($item | Select-Object -ExpandProperty Value).TrimEnd("\")
|
||||||
|
$env_vars.Add($name, $value)
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_env = $env_vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('facter')) {
|
||||||
|
# See if Facter is on the System Path
|
||||||
|
Try {
|
||||||
|
Get-Command facter -ErrorAction Stop > $null
|
||||||
|
$facter_installed = $true
|
||||||
|
} Catch {
|
||||||
|
$facter_installed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get JSON from Facter, and parse it out.
|
||||||
|
if ($facter_installed) {
|
||||||
|
&facter -j | Tee-Object -Variable facter_output > $null
|
||||||
|
$facts = "$facter_output" | ConvertFrom-Json
|
||||||
|
ForEach($fact in $facts.PSObject.Properties) {
|
||||||
|
$fact_name = $fact.Name
|
||||||
|
$ansible_facts.Add("facter_$fact_name", $fact.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('interfaces')) {
|
||||||
|
$netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
|
||||||
|
$ActiveNetcfg = @()
|
||||||
|
$ActiveNetcfg += $netcfg | Where-Object {$_.ipaddress -ne $null}
|
||||||
|
|
||||||
|
$namespaces = Get-LazyCimInstance __Namespace -namespace root
|
||||||
|
if ($namespaces | Where-Object { $_.Name -eq "StandardCimv" }) {
|
||||||
|
$net_adapters = Get-LazyCimInstance MSFT_NetAdapter -namespace Root\StandardCimv2
|
||||||
|
$guid_key = "InterfaceGUID"
|
||||||
|
$name_key = "Name"
|
||||||
|
} else {
|
||||||
|
$net_adapters = Get-LazyCimInstance Win32_NetworkAdapter
|
||||||
|
$guid_key = "GUID"
|
||||||
|
$name_key = "NetConnectionID"
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattednetcfg = @()
|
||||||
|
foreach ($adapter in $ActiveNetcfg)
|
||||||
|
{
|
||||||
|
$thisadapter = @{
|
||||||
|
default_gateway = $null
|
||||||
|
connection_name = $null
|
||||||
|
dns_domain = $adapter.dnsdomain
|
||||||
|
interface_index = $adapter.InterfaceIndex
|
||||||
|
interface_name = $adapter.description
|
||||||
|
macaddress = $adapter.macaddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($adapter.defaultIPGateway)
|
||||||
|
{
|
||||||
|
$thisadapter.default_gateway = $adapter.DefaultIPGateway[0].ToString()
|
||||||
|
}
|
||||||
|
$net_adapter = $net_adapters | Where-Object { $_.$guid_key -eq $adapter.SettingID }
|
||||||
|
if ($net_adapter) {
|
||||||
|
$thisadapter.connection_name = $net_adapter.$name_key
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattednetcfg += $thisadapter
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_interfaces = $formattednetcfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gather_subset.Contains("local") -and $null -ne $factpath) {
|
||||||
|
# Get any custom facts; results are updated in the
|
||||||
|
Get-CustomFacts -factpath $factpath
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('memory')) {
|
||||||
|
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||||
|
$ansible_facts += @{
|
||||||
|
# Win32_PhysicalMemory is empty on some virtual platforms
|
||||||
|
ansible_memtotal_mb = ([math]::ceiling($win32_cs.TotalPhysicalMemory / 1024 / 1024))
|
||||||
|
ansible_memfree_mb = ([math]::ceiling($win32_os.FreePhysicalMemory / 1024))
|
||||||
|
ansible_swaptotal_mb = ([math]::round($win32_os.TotalSwapSpaceSize / 1024))
|
||||||
|
ansible_pagefiletotal_mb = ([math]::round($win32_os.SizeStoredInPagingFiles / 1024))
|
||||||
|
ansible_pagefilefree_mb = ([math]::round($win32_os.FreeSpaceInPagingFiles / 1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if($gather_subset.Contains('platform')) {
|
||||||
|
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||||
|
$domain_suffix = $win32_cs.Domain.Substring($win32_cs.Workgroup.length)
|
||||||
|
$fqdn = $win32_cs.DNSHostname
|
||||||
|
|
||||||
|
if( $domain_suffix -ne "")
|
||||||
|
{
|
||||||
|
$fqdn = $win32_cs.DNSHostname + "." + $domain_suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ansible_reboot_pending = Get-PendingRebootStatus
|
||||||
|
} catch {
|
||||||
|
# fails for non-admin users, set to null in this case
|
||||||
|
$ansible_reboot_pending = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_architecture = $win32_os.OSArchitecture
|
||||||
|
ansible_domain = $domain_suffix
|
||||||
|
ansible_fqdn = $fqdn
|
||||||
|
ansible_hostname = $win32_cs.DNSHostname
|
||||||
|
ansible_netbios_name = $win32_cs.Name
|
||||||
|
ansible_kernel = $osversion.Version.ToString()
|
||||||
|
ansible_nodename = $fqdn
|
||||||
|
ansible_machine_id = Get-MachineSid
|
||||||
|
ansible_owner_contact = ([string] $win32_cs.PrimaryOwnerContact)
|
||||||
|
ansible_owner_name = ([string] $win32_cs.PrimaryOwnerName)
|
||||||
|
# FUTURE: should this live in its own subset?
|
||||||
|
ansible_reboot_pending = $ansible_reboot_pending
|
||||||
|
ansible_system = $osversion.Platform.ToString()
|
||||||
|
ansible_system_description = ([string] $win32_os.Description)
|
||||||
|
ansible_system_vendor = $win32_cs.Manufacturer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('powershell_version')) {
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_powershell_version = ($PSVersionTable.PSVersion.Major)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('processor')) {
|
||||||
|
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
$win32_cpu = Get-LazyCimInstance Win32_Processor
|
||||||
|
if ($win32_cpu -is [array]) {
|
||||||
|
# multi-socket, pick first
|
||||||
|
$win32_cpu = $win32_cpu[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
$cpu_list = @( )
|
||||||
|
for ($i=1; $i -le $win32_cs.NumberOfLogicalProcessors; $i++) {
|
||||||
|
$cpu_list += $win32_cpu.Manufacturer
|
||||||
|
$cpu_list += $win32_cpu.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_processor = $cpu_list
|
||||||
|
ansible_processor_cores = $win32_cpu.NumberOfCores
|
||||||
|
ansible_processor_count = $win32_cs.NumberOfProcessors
|
||||||
|
ansible_processor_threads_per_core = ($win32_cpu.NumberOfLogicalProcessors / $win32_cpu.NumberofCores)
|
||||||
|
ansible_processor_vcpus = $win32_cs.NumberOfLogicalProcessors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('uptime')) {
|
||||||
|
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_lastboot = $win32_os.lastbootuptime.ToString("u")
|
||||||
|
ansible_uptime_seconds = $([System.Convert]::ToInt64($(Get-Date).Subtract($win32_os.lastbootuptime).TotalSeconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('user')) {
|
||||||
|
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_user_dir = $env:userprofile
|
||||||
|
# Win32_UserAccount.FullName is probably the right thing here, but it can be expensive to get on large domains
|
||||||
|
ansible_user_gecos = ""
|
||||||
|
ansible_user_id = $env:username
|
||||||
|
ansible_user_sid = $user.User.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('windows_domain')) {
|
||||||
|
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
$domain_roles = @{
|
||||||
|
0 = "Stand-alone workstation"
|
||||||
|
1 = "Member workstation"
|
||||||
|
2 = "Stand-alone server"
|
||||||
|
3 = "Member server"
|
||||||
|
4 = "Backup domain controller"
|
||||||
|
5 = "Primary domain controller"
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain_role = $domain_roles.Get_Item([Int32]$win32_cs.DomainRole)
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_windows_domain = $win32_cs.Domain
|
||||||
|
ansible_windows_domain_member = $win32_cs.PartOfDomain
|
||||||
|
ansible_windows_domain_role = $domain_role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('winrm')) {
|
||||||
|
|
||||||
|
$winrm_https_listener_parent_paths = Get-ChildItem -Path WSMan:\localhost\Listener -Recurse -ErrorAction SilentlyContinue | `
|
||||||
|
Where-Object {$_.PSChildName -eq "Transport" -and $_.Value -eq "HTTPS"} | Select-Object PSParentPath
|
||||||
|
if ($winrm_https_listener_parent_paths -isnot [array]) {
|
||||||
|
$winrm_https_listener_parent_paths = @($winrm_https_listener_parent_paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
$winrm_https_listener_paths = @()
|
||||||
|
foreach ($winrm_https_listener_parent_path in $winrm_https_listener_parent_paths) {
|
||||||
|
$winrm_https_listener_paths += $winrm_https_listener_parent_path.PSParentPath.Substring($winrm_https_listener_parent_path.PSParentPath.LastIndexOf("\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
$https_listeners = @()
|
||||||
|
foreach ($winrm_https_listener_path in $winrm_https_listener_paths) {
|
||||||
|
$https_listeners += Get-ChildItem -Path "WSMan:\localhost\Listener$winrm_https_listener_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
$winrm_cert_thumbprints = @()
|
||||||
|
foreach ($https_listener in $https_listeners) {
|
||||||
|
$winrm_cert_thumbprints += $https_listener | Where-Object {$_.Name -EQ "CertificateThumbprint" } | Select-Object Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$winrm_cert_expiry = @()
|
||||||
|
foreach ($winrm_cert_thumbprint in $winrm_cert_thumbprints) {
|
||||||
|
Try {
|
||||||
|
$winrm_cert_expiry += Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object Thumbprint -EQ $winrm_cert_thumbprint.Value.ToString().ToUpper() | Select-Object NotAfter
|
||||||
|
} Catch {
|
||||||
|
Add-Warning -obj $result -message "Error during certificate expiration retrieval: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$winrm_cert_expirations = $winrm_cert_expiry | Sort-Object NotAfter
|
||||||
|
if ($winrm_cert_expirations) {
|
||||||
|
# this fact was renamed from ansible_winrm_certificate_expires due to collision with ansible_winrm_X connection var pattern
|
||||||
|
$ansible_facts.Add("ansible_win_rm_certificate_expires", $winrm_cert_expirations[0].NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($gather_subset.Contains('virtual')) {
|
||||||
|
$machine_info = Get-LazyCimInstance Win32_ComputerSystem
|
||||||
|
|
||||||
|
switch ($machine_info.model) {
|
||||||
|
"Virtual Machine" {
|
||||||
|
$machine_type="Hyper-V"
|
||||||
|
$machine_role="guest"
|
||||||
|
}
|
||||||
|
|
||||||
|
"VMware Virtual Platform" {
|
||||||
|
$machine_type="VMware"
|
||||||
|
$machine_role="guest"
|
||||||
|
}
|
||||||
|
|
||||||
|
"VirtualBox" {
|
||||||
|
$machine_type="VirtualBox"
|
||||||
|
$machine_role="guest"
|
||||||
|
}
|
||||||
|
|
||||||
|
"HVM domU" {
|
||||||
|
$machine_type="Xen"
|
||||||
|
$machine_role="guest"
|
||||||
|
}
|
||||||
|
|
||||||
|
default {
|
||||||
|
$machine_type="NA"
|
||||||
|
$machine_role="NA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_facts += @{
|
||||||
|
ansible_virtualization_role = $machine_role
|
||||||
|
ansible_virtualization_type = $machine_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result.ansible_facts += $ansible_facts
|
||||||
|
|
||||||
|
Exit-Json $result
|
@ -0,0 +1,28 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true;
|
||||||
|
$src = Get-AnsibleParam -obj $params -name "src" -type "path" -aliases "path" -failifempty $true;
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false;
|
||||||
|
}
|
||||||
|
|
||||||
|
If (Test-Path -LiteralPath $src -PathType Leaf)
|
||||||
|
{
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($src);
|
||||||
|
$result.content = [System.Convert]::ToBase64String($bytes);
|
||||||
|
$result.encoding = "base64";
|
||||||
|
Exit-Json $result;
|
||||||
|
}
|
||||||
|
ElseIf (Test-Path -LiteralPath $src -PathType Container)
|
||||||
|
{
|
||||||
|
Fail-Json $result "Path $src is a directory";
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
Fail-Json $result "Path $src is not found";
|
||||||
|
}
|
@ -0,0 +1,225 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
|
||||||
|
# Copyright: (c) 2015, Trond Hindenes
|
||||||
|
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
|
||||||
|
#Requires -Module Ansible.ModuleUtils.SID
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# win_acl module (File/Resources Permission Additions/Removal)
|
||||||
|
|
||||||
|
#Functions
|
||||||
|
function Get-UserSID {
|
||||||
|
param(
|
||||||
|
[String]$AccountName
|
||||||
|
)
|
||||||
|
|
||||||
|
$userSID = $null
|
||||||
|
$searchAppPools = $false
|
||||||
|
|
||||||
|
if ($AccountName.Split("\").Count -gt 1) {
|
||||||
|
if ($AccountName.Split("\")[0] -eq "IIS APPPOOL") {
|
||||||
|
$searchAppPools = $true
|
||||||
|
$AccountName = $AccountName.Split("\")[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchAppPools) {
|
||||||
|
Import-Module -Name WebAdministration
|
||||||
|
$testIISPath = Test-Path -LiteralPath "IIS:"
|
||||||
|
if ($testIISPath) {
|
||||||
|
$appPoolObj = Get-ItemProperty -LiteralPath "IIS:\AppPools\$AccountName"
|
||||||
|
$userSID = $appPoolObj.applicationPoolSid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$userSID = Convert-ToSID -account_name $AccountName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $userSID
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = Parse-Args $args
|
||||||
|
|
||||||
|
Function SetPrivilegeTokens() {
|
||||||
|
# Set privilege tokens only if admin.
|
||||||
|
# Admins would have these privs or be able to set these privs in the UI Anyway
|
||||||
|
|
||||||
|
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
|
||||||
|
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
|
||||||
|
|
||||||
|
|
||||||
|
if ($myWindowsPrincipal.IsInRole($adminRole)) {
|
||||||
|
# Need to adjust token privs when executing Set-ACL in certain cases.
|
||||||
|
# e.g. d:\testdir is owned by group in which current user is not a member and no perms are inherited from d:\
|
||||||
|
# This also sets us up for setting the owner as a feature.
|
||||||
|
# See the following for details of each privilege
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716(v=vs.85).aspx
|
||||||
|
$privileges = @(
|
||||||
|
"SeRestorePrivilege", # Grants all write access control to any file, regardless of ACL.
|
||||||
|
"SeBackupPrivilege", # Grants all read access control to any file, regardless of ACL.
|
||||||
|
"SeTakeOwnershipPrivilege" # Grants ability to take owernship of an object w/out being granted discretionary access
|
||||||
|
)
|
||||||
|
foreach ($privilege in $privileges) {
|
||||||
|
$state = Get-AnsiblePrivilege -Name $privilege
|
||||||
|
if ($state -eq $false) {
|
||||||
|
Set-AnsiblePrivilege -Name $privilege -Value $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true
|
||||||
|
$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true
|
||||||
|
$rights = Get-AnsibleParam -obj $params -name "rights" -type "str" -failifempty $true
|
||||||
|
|
||||||
|
$type = Get-AnsibleParam -obj $params -name "type" -type "str" -failifempty $true -validateset "allow","deny"
|
||||||
|
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
|
||||||
|
|
||||||
|
$inherit = Get-AnsibleParam -obj $params -name "inherit" -type "str"
|
||||||
|
$propagation = Get-AnsibleParam -obj $params -name "propagation" -type "str" -default "None" -validateset "InheritOnly","None","NoPropagateInherit"
|
||||||
|
|
||||||
|
# We mount the HKCR, HKU, and HKCC registry hives so PS can access them.
|
||||||
|
# Network paths have no qualifiers so we use -EA SilentlyContinue to ignore that
|
||||||
|
$path_qualifier = Split-Path -Path $path -Qualifier -ErrorAction SilentlyContinue
|
||||||
|
if ($path_qualifier -eq "HKCR:" -and (-not (Test-Path -LiteralPath HKCR:\))) {
|
||||||
|
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT > $null
|
||||||
|
}
|
||||||
|
if ($path_qualifier -eq "HKU:" -and (-not (Test-Path -LiteralPath HKU:\))) {
|
||||||
|
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS > $null
|
||||||
|
}
|
||||||
|
if ($path_qualifier -eq "HKCC:" -and (-not (Test-Path -LiteralPath HKCC:\))) {
|
||||||
|
New-PSDrive -Name HKCC -PSProvider Registry -Root HKEY_CURRENT_CONFIG > $null
|
||||||
|
}
|
||||||
|
|
||||||
|
If (-Not (Test-Path -LiteralPath $path)) {
|
||||||
|
Fail-Json -obj $result -message "$path file or directory does not exist on the host"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test that the user/group is resolvable on the local machine
|
||||||
|
$sid = Get-UserSID -AccountName $user
|
||||||
|
if (!$sid) {
|
||||||
|
Fail-Json -obj $result -message "$user is not a valid user or group on the host machine or domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
If (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||||
|
$inherit = "None"
|
||||||
|
}
|
||||||
|
ElseIf ($null -eq $inherit) {
|
||||||
|
$inherit = "ContainerInherit, ObjectInherit"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bug in Set-Acl, Get-Acl where -LiteralPath only works for the Registry provider if the location is in that root
|
||||||
|
# qualifier. We also don't have a qualifier for a network path so only change if not null
|
||||||
|
if ($null -ne $path_qualifier) {
|
||||||
|
Push-Location -LiteralPath $path_qualifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Try {
|
||||||
|
SetPrivilegeTokens
|
||||||
|
$path_item = Get-Item -LiteralPath $path -Force
|
||||||
|
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||||
|
$colRights = [System.Security.AccessControl.RegistryRights]$rights
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$colRights = [System.Security.AccessControl.FileSystemRights]$rights
|
||||||
|
}
|
||||||
|
|
||||||
|
$InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit
|
||||||
|
$PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation
|
||||||
|
|
||||||
|
If ($type -eq "allow") {
|
||||||
|
$objType =[System.Security.AccessControl.AccessControlType]::Allow
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$objType =[System.Security.AccessControl.AccessControlType]::Deny
|
||||||
|
}
|
||||||
|
|
||||||
|
$objUser = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||||
|
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||||
|
$objACE = New-Object System.Security.AccessControl.RegistryAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
|
||||||
|
}
|
||||||
|
$objACL = Get-ACL -LiteralPath $path
|
||||||
|
|
||||||
|
# Check if the ACE exists already in the objects ACL list
|
||||||
|
$match = $false
|
||||||
|
|
||||||
|
ForEach($rule in $objACL.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])){
|
||||||
|
|
||||||
|
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||||
|
If (($rule.RegistryRights -eq $objACE.RegistryRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
|
||||||
|
$match = $true
|
||||||
|
Break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
|
||||||
|
$match = $true
|
||||||
|
Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($state -eq "present" -And $match -eq $false) {
|
||||||
|
Try {
|
||||||
|
$objACL.AddAccessRule($objACE)
|
||||||
|
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||||
|
Set-ACL -LiteralPath $path -AclObject $objACL
|
||||||
|
} else {
|
||||||
|
(Get-Item -LiteralPath $path).SetAccessControl($objACL)
|
||||||
|
}
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json -obj $result -message "an exception occurred when adding the specified rule - $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElseIf ($state -eq "absent" -And $match -eq $true) {
|
||||||
|
Try {
|
||||||
|
$objACL.RemoveAccessRule($objACE)
|
||||||
|
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||||
|
Set-ACL -LiteralPath $path -AclObject $objACL
|
||||||
|
} else {
|
||||||
|
(Get-Item -LiteralPath $path).SetAccessControl($objACL)
|
||||||
|
}
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json -obj $result -message "an exception occurred when removing the specified rule - $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
# A rule was attempting to be added but already exists
|
||||||
|
If ($match -eq $true) {
|
||||||
|
Exit-Json -obj $result -message "the specified rule already exists"
|
||||||
|
}
|
||||||
|
# A rule didn't exist that was trying to be removed
|
||||||
|
Else {
|
||||||
|
Exit-Json -obj $result -message "the specified rule does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json -obj $result -message "an error occurred when attempting to $state $rights permission(s) on $path for $user - $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
Finally {
|
||||||
|
# Make sure we revert the location stack to the original path just for cleanups sake
|
||||||
|
if ($null -ne $path_qualifier) {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json -obj $result
|
@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
|
||||||
|
# Copyright: (c) 2015, Trond Hindenes
|
||||||
|
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'core'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_acl
|
||||||
|
version_added: "2.0"
|
||||||
|
short_description: Set file/directory/registry permissions for a system user or group
|
||||||
|
description:
|
||||||
|
- Add or remove rights/permissions for a given user or group for the specified
|
||||||
|
file, folder, registry key or AppPool identifies.
|
||||||
|
options:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The path to the file or directory.
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
user:
|
||||||
|
description:
|
||||||
|
- User or Group to add specified rights to act on src file/folder or
|
||||||
|
registry key.
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Specify whether to add C(present) or remove C(absent) the specified access rule.
|
||||||
|
type: str
|
||||||
|
choices: [ absent, present ]
|
||||||
|
default: present
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- Specify whether to allow or deny the rights specified.
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
choices: [ allow, deny ]
|
||||||
|
rights:
|
||||||
|
description:
|
||||||
|
- The rights/permissions that are to be allowed/denied for the specified
|
||||||
|
user or group for the item at C(path).
|
||||||
|
- If C(path) is a file or directory, rights can be any right under MSDN
|
||||||
|
FileSystemRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx).
|
||||||
|
- If C(path) is a registry key, rights can be any right under MSDN
|
||||||
|
RegistryRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.registryrights.aspx).
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
inherit:
|
||||||
|
description:
|
||||||
|
- Inherit flags on the ACL rules.
|
||||||
|
- Can be specified as a comma separated list, e.g. C(ContainerInherit),
|
||||||
|
C(ObjectInherit).
|
||||||
|
- For more information on the choices see MSDN InheritanceFlags enumeration
|
||||||
|
at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx).
|
||||||
|
- Defaults to C(ContainerInherit, ObjectInherit) for Directories.
|
||||||
|
type: str
|
||||||
|
choices: [ ContainerInherit, ObjectInherit ]
|
||||||
|
propagation:
|
||||||
|
description:
|
||||||
|
- Propagation flag on the ACL rules.
|
||||||
|
- For more information on the choices see MSDN PropagationFlags enumeration
|
||||||
|
at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx).
|
||||||
|
type: str
|
||||||
|
choices: [ InheritOnly, None, NoPropagateInherit ]
|
||||||
|
default: "None"
|
||||||
|
notes:
|
||||||
|
- If adding ACL's for AppPool identities (available since 2.3), the Windows
|
||||||
|
Feature "Web-Scripting-Tools" must be enabled.
|
||||||
|
seealso:
|
||||||
|
- module: win_acl_inheritance
|
||||||
|
- module: win_file
|
||||||
|
- module: win_owner
|
||||||
|
- module: win_stat
|
||||||
|
author:
|
||||||
|
- Phil Schwartz (@schwartzmx)
|
||||||
|
- Trond Hindenes (@trondhindenes)
|
||||||
|
- Hans-Joachim Kliemeck (@h0nIg)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Restrict write and execute access to User Fed-Phil
|
||||||
|
win_acl:
|
||||||
|
user: Fed-Phil
|
||||||
|
path: C:\Important\Executable.exe
|
||||||
|
type: deny
|
||||||
|
rights: ExecuteFile,Write
|
||||||
|
|
||||||
|
- name: Add IIS_IUSRS allow rights
|
||||||
|
win_acl:
|
||||||
|
path: C:\inetpub\wwwroot\MySite
|
||||||
|
user: IIS_IUSRS
|
||||||
|
rights: FullControl
|
||||||
|
type: allow
|
||||||
|
state: present
|
||||||
|
inherit: ContainerInherit, ObjectInherit
|
||||||
|
propagation: 'None'
|
||||||
|
|
||||||
|
- name: Set registry key right
|
||||||
|
win_acl:
|
||||||
|
path: HKCU:\Bovine\Key
|
||||||
|
user: BUILTIN\Users
|
||||||
|
rights: EnumerateSubKeys
|
||||||
|
type: allow
|
||||||
|
state: present
|
||||||
|
inherit: ContainerInherit, ObjectInherit
|
||||||
|
propagation: 'None'
|
||||||
|
|
||||||
|
- name: Remove FullControl AccessRule for IIS_IUSRS
|
||||||
|
win_acl:
|
||||||
|
path: C:\inetpub\wwwroot\MySite
|
||||||
|
user: IIS_IUSRS
|
||||||
|
rights: FullControl
|
||||||
|
type: allow
|
||||||
|
state: absent
|
||||||
|
inherit: ContainerInherit, ObjectInherit
|
||||||
|
propagation: 'None'
|
||||||
|
|
||||||
|
- name: Deny Intern
|
||||||
|
win_acl:
|
||||||
|
path: C:\Administrator\Documents
|
||||||
|
user: Intern
|
||||||
|
rights: Read,Write,Modify,FullControl,Delete
|
||||||
|
type: deny
|
||||||
|
state: present
|
||||||
|
'''
|
@ -0,0 +1,78 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||||
|
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||||
|
|
||||||
|
# TODO: add check mode support
|
||||||
|
|
||||||
|
Set-StrictMode -Version 2
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $false
|
||||||
|
|
||||||
|
$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
|
||||||
|
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
||||||
|
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||||
|
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||||
|
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||||
|
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||||
|
|
||||||
|
$raw_command_line = $raw_command_line.Trim()
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $true
|
||||||
|
cmd = $raw_command_line
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($creates -and $(Test-AnsiblePath -Path $creates)) {
|
||||||
|
Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
|
||||||
|
Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||||
|
}
|
||||||
|
|
||||||
|
$command_args = @{
|
||||||
|
command = $raw_command_line
|
||||||
|
}
|
||||||
|
if ($chdir) {
|
||||||
|
$command_args['working_directory'] = $chdir
|
||||||
|
}
|
||||||
|
if ($stdin) {
|
||||||
|
$command_args['stdin'] = $stdin
|
||||||
|
}
|
||||||
|
if ($output_encoding_override) {
|
||||||
|
$command_args['output_encoding_override'] = $output_encoding_override
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_datetime = [DateTime]::UtcNow
|
||||||
|
try {
|
||||||
|
$command_result = Run-Command @command_args
|
||||||
|
} catch {
|
||||||
|
$result.changed = $false
|
||||||
|
try {
|
||||||
|
$result.rc = $_.Exception.NativeErrorCode
|
||||||
|
} catch {
|
||||||
|
$result.rc = 2
|
||||||
|
}
|
||||||
|
Fail-Json -obj $result -message $_.Exception.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
$result.stdout = $command_result.stdout
|
||||||
|
$result.stderr = $command_result.stderr
|
||||||
|
$result.rc = $command_result.rc
|
||||||
|
|
||||||
|
$end_datetime = [DateTime]::UtcNow
|
||||||
|
$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||||
|
$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||||
|
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||||
|
|
||||||
|
If ($result.rc -ne 0) {
|
||||||
|
Fail-Json -obj $result -message "non-zero return code"
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json $result
|
@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2016, Ansible, inc
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'core'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_command
|
||||||
|
short_description: Executes a command on a remote Windows node
|
||||||
|
version_added: 2.2
|
||||||
|
description:
|
||||||
|
- The C(win_command) module takes the command name followed by a list of space-delimited arguments.
|
||||||
|
- The given command will be executed on all selected nodes. It will not be
|
||||||
|
processed through the shell, so variables like C($env:HOME) and operations
|
||||||
|
like C("<"), C(">"), C("|"), and C(";") will not work (use the M(win_shell)
|
||||||
|
module if you need these features).
|
||||||
|
- For non-Windows targets, use the M(command) module instead.
|
||||||
|
options:
|
||||||
|
free_form:
|
||||||
|
description:
|
||||||
|
- The C(win_command) module takes a free form command to run.
|
||||||
|
- There is no parameter actually named 'free form'. See the examples!
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
creates:
|
||||||
|
description:
|
||||||
|
- A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
|
||||||
|
type: path
|
||||||
|
removes:
|
||||||
|
description:
|
||||||
|
- A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped.
|
||||||
|
type: path
|
||||||
|
chdir:
|
||||||
|
description:
|
||||||
|
- Set the specified path as the current working directory before executing a command.
|
||||||
|
type: path
|
||||||
|
stdin:
|
||||||
|
description:
|
||||||
|
- Set the stdin of the command directly to the specified value.
|
||||||
|
type: str
|
||||||
|
version_added: '2.5'
|
||||||
|
output_encoding_override:
|
||||||
|
description:
|
||||||
|
- This option overrides the encoding of stdout/stderr output.
|
||||||
|
- You can use this option when you need to run a command which ignore the console's codepage.
|
||||||
|
- You should only need to use this option in very rare circumstances.
|
||||||
|
- This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
|
||||||
|
See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
|
||||||
|
type: str
|
||||||
|
version_added: '2.10'
|
||||||
|
notes:
|
||||||
|
- If you want to run a command through a shell (say you are using C(<),
|
||||||
|
C(>), C(|), etc), you actually want the M(win_shell) module instead. The
|
||||||
|
C(win_command) module is much more secure as it's not affected by the user's
|
||||||
|
environment.
|
||||||
|
- C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not
|
||||||
|
exist, use this.
|
||||||
|
seealso:
|
||||||
|
- module: command
|
||||||
|
- module: psexec
|
||||||
|
- module: raw
|
||||||
|
- module: win_psexec
|
||||||
|
- module: win_shell
|
||||||
|
author:
|
||||||
|
- Matt Davis (@nitzmahone)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Save the result of 'whoami' in 'whoami_out'
|
||||||
|
win_command: whoami
|
||||||
|
register: whoami_out
|
||||||
|
|
||||||
|
- name: Run command that only runs if folder exists and runs from a specific folder
|
||||||
|
win_command: wbadmin -backupTarget:C:\backup\
|
||||||
|
args:
|
||||||
|
chdir: C:\somedir\
|
||||||
|
creates: C:\backup\
|
||||||
|
|
||||||
|
- name: Run an executable and send data to the stdin for the executable
|
||||||
|
win_command: powershell.exe -
|
||||||
|
args:
|
||||||
|
stdin: Write-Host test
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
msg:
|
||||||
|
description: changed
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
start:
|
||||||
|
description: The command execution start time
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: '2016-02-25 09:18:26.429568'
|
||||||
|
end:
|
||||||
|
description: The command execution end time
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: '2016-02-25 09:18:26.755339'
|
||||||
|
delta:
|
||||||
|
description: The command execution delta time
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: '0:00:00.325771'
|
||||||
|
stdout:
|
||||||
|
description: The command standard output
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: 'Clustering node rabbit@slave1 with rabbit@master ...'
|
||||||
|
stderr:
|
||||||
|
description: The command standard error
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: 'ls: cannot access foo: No such file or directory'
|
||||||
|
cmd:
|
||||||
|
description: The command executed by the task
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: 'rabbitmqctl join_cluster rabbit@master'
|
||||||
|
rc:
|
||||||
|
description: The command return code (0 means success)
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 0
|
||||||
|
stdout_lines:
|
||||||
|
description: The command standard output split in lines
|
||||||
|
returned: always
|
||||||
|
type: list
|
||||||
|
sample: [u'Clustering node rabbit@slave1 with rabbit@master ...']
|
||||||
|
'''
|
@ -0,0 +1,403 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Backup
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||||
|
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||||
|
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
|
||||||
|
|
||||||
|
# there are 4 modes to win_copy which are driven by the action plugins:
|
||||||
|
# explode: src is a zip file which needs to be extracted to dest, for use with multiple files
|
||||||
|
# query: win_copy action plugin wants to get the state of remote files to check whether it needs to send them
|
||||||
|
# remote: all copy action is happening remotely (remote_src=True)
|
||||||
|
# single: a single file has been copied, also used with template
|
||||||
|
$copy_mode = Get-AnsibleParam -obj $params -name "_copy_mode" -type "str" -default "single" -validateset "explode","query","remote","single"
|
||||||
|
|
||||||
|
# used in explode, remote and single mode
|
||||||
|
$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_mode -in @("explode","process","single"))
|
||||||
|
$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
|
||||||
|
$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false
|
||||||
|
|
||||||
|
# used in single mode
|
||||||
|
$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
|
||||||
|
|
||||||
|
# used in query and remote mode
|
||||||
|
$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true
|
||||||
|
|
||||||
|
# used in query mode, contains the local files/directories/symlinks that are to be copied
|
||||||
|
$files = Get-AnsibleParam -obj $params -name "files" -type "list"
|
||||||
|
$directories = Get-AnsibleParam -obj $params -name "directories" -type "list"
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diff_mode) {
|
||||||
|
$result.diff = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Copy-File($source, $dest) {
|
||||||
|
$diff = ""
|
||||||
|
$copy_file = $false
|
||||||
|
$source_checksum = $null
|
||||||
|
if ($force) {
|
||||||
|
$source_checksum = Get-FileChecksum -path $source
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $dest -PathType Container) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': dest is already a folder"
|
||||||
|
} elseif (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||||
|
if ($force) {
|
||||||
|
$target_checksum = Get-FileChecksum -path $dest
|
||||||
|
if ($source_checksum -ne $target_checksum) {
|
||||||
|
$copy_file = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$copy_file = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($copy_file) {
|
||||||
|
$file_dir = [System.IO.Path]::GetDirectoryName($dest)
|
||||||
|
# validate the parent dir is not a file and that it exists
|
||||||
|
if (Test-Path -LiteralPath $file_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
|
||||||
|
} elseif (-not (Test-Path -LiteralPath $file_dir)) {
|
||||||
|
# directory doesn't exist, need to create
|
||||||
|
New-Item -Path $file_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||||
|
$diff += "+$file_dir\`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backup) {
|
||||||
|
$result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||||
|
Remove-Item -LiteralPath $dest -Force -Recurse -WhatIf:$check_mode | Out-Null
|
||||||
|
$diff += "-$dest`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $check_mode) {
|
||||||
|
# cannot run with -WhatIf:$check_mode as if the parent dir didn't
|
||||||
|
# exist and was created above would still not exist in check mode
|
||||||
|
Copy-Item -LiteralPath $source -Destination $dest -Force | Out-Null
|
||||||
|
}
|
||||||
|
$diff += "+$dest`n"
|
||||||
|
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ugly but to save us from running the checksum twice, let's return it for
|
||||||
|
# the main code to add it to $result
|
||||||
|
return ,@{ diff = $diff; checksum = $source_checksum }
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Copy-Folder($source, $dest) {
|
||||||
|
$diff = ""
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $dest -PathType Container)) {
|
||||||
|
$parent_dir = [System.IO.Path]::GetDirectoryName($dest)
|
||||||
|
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy folder from '$source' to '$dest': dest is already a file"
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Path $dest -ItemType Container -WhatIf:$check_mode | Out-Null
|
||||||
|
$diff += "+$dest\`n"
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$child_items = Get-ChildItem -LiteralPath $source -Force
|
||||||
|
foreach ($child_item in $child_items) {
|
||||||
|
$dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name
|
||||||
|
if ($child_item.PSIsContainer) {
|
||||||
|
$diff += (Copy-Folder -source $child_item.Fullname -dest $dest_child_path)
|
||||||
|
} else {
|
||||||
|
$diff += (Copy-File -source $child_item.Fullname -dest $dest_child_path).diff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-FileSize($path) {
|
||||||
|
$file = Get-Item -LiteralPath $path -Force
|
||||||
|
if ($file.PSIsContainer) {
|
||||||
|
$size = (Get-ChildItem -Literalpath $file.FullName -Recurse -Force | `
|
||||||
|
Where-Object { $_.PSObject.Properties.Name -contains 'Length' } | `
|
||||||
|
Measure-Object -Property Length -Sum).Sum
|
||||||
|
if ($null -eq $size) {
|
||||||
|
$size = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$size = $file.Length
|
||||||
|
}
|
||||||
|
|
||||||
|
$size
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Extract-Zip($src, $dest) {
|
||||||
|
$archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8)
|
||||||
|
foreach ($entry in $archive.Entries) {
|
||||||
|
$archive_name = $entry.FullName
|
||||||
|
|
||||||
|
# FullName may be appended with / or \, determine if it is padded and remove it
|
||||||
|
$padding_length = $archive_name.Length % 4
|
||||||
|
if ($padding_length -eq 0) {
|
||||||
|
$is_dir = $false
|
||||||
|
$base64_name = $archive_name
|
||||||
|
} elseif ($padding_length -eq 1) {
|
||||||
|
$is_dir = $true
|
||||||
|
if ($archive_name.EndsWith("/") -or $archive_name.EndsWith("`\")) {
|
||||||
|
$base64_name = $archive_name.Substring(0, $archive_name.Length - 1)
|
||||||
|
} else {
|
||||||
|
throw "invalid base64 archive name '$archive_name'"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "invalid base64 length '$archive_name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# to handle unicode character, win_copy action plugin has encoded the filename
|
||||||
|
$decoded_archive_name = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_name))
|
||||||
|
# re-add the / to the entry full name if it was a directory
|
||||||
|
if ($is_dir) {
|
||||||
|
$decoded_archive_name = "$decoded_archive_name/"
|
||||||
|
}
|
||||||
|
$entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_name)
|
||||||
|
$entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $entry_dir)) {
|
||||||
|
New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_dir -eq $false) {
|
||||||
|
if (-not $check_mode) {
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$archive.Dispose() # release the handle of the zip file
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Extract-ZipLegacy($src, $dest) {
|
||||||
|
if (-not (Test-Path -LiteralPath $dest)) {
|
||||||
|
New-Item -Path $dest -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||||
|
}
|
||||||
|
$shell = New-Object -ComObject Shell.Application
|
||||||
|
$zip = $shell.NameSpace($src)
|
||||||
|
$dest_path = $shell.NameSpace($dest)
|
||||||
|
|
||||||
|
foreach ($entry in $zip.Items()) {
|
||||||
|
$is_dir = $entry.IsFolder
|
||||||
|
$encoded_archive_entry = $entry.Name
|
||||||
|
# to handle unicode character, win_copy action plugin has encoded the filename
|
||||||
|
$decoded_archive_entry = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded_archive_entry))
|
||||||
|
if ($is_dir) {
|
||||||
|
$decoded_archive_entry = "$decoded_archive_entry/"
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_entry)
|
||||||
|
$entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $entry_dir)) {
|
||||||
|
New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_dir -eq $false -and (-not $check_mode)) {
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866.aspx
|
||||||
|
# From Folder.CopyHere documentation, 1044 means:
|
||||||
|
# - 1024: do not display a user interface if an error occurs
|
||||||
|
# - 16: respond with "yes to all" for any dialog box that is displayed
|
||||||
|
# - 4: do not display a progress dialog box
|
||||||
|
$dest_path.CopyHere($entry, 1044)
|
||||||
|
|
||||||
|
# once file is extraced, we need to rename it with non base64 name
|
||||||
|
$combined_encoded_path = [System.IO.Path]::Combine($dest, $encoded_archive_entry)
|
||||||
|
Move-Item -LiteralPath $combined_encoded_path -Destination $entry_target_path -Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($copy_mode -eq "query") {
|
||||||
|
# we only return a list of files/directories that need to be copied over
|
||||||
|
# the source of the local file will be the key used
|
||||||
|
$changed_files = @()
|
||||||
|
$changed_directories = @()
|
||||||
|
$changed_symlinks = @()
|
||||||
|
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$filename = $file.dest
|
||||||
|
$local_checksum = $file.checksum
|
||||||
|
|
||||||
|
$filepath = Join-Path -Path $dest -ChildPath $filename
|
||||||
|
if (Test-Path -LiteralPath $filepath -PathType Leaf) {
|
||||||
|
if ($force) {
|
||||||
|
$checksum = Get-FileChecksum -path $filepath
|
||||||
|
if ($checksum -ne $local_checksum) {
|
||||||
|
$changed_files += $file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (Test-Path -LiteralPath $filepath -PathType Container) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy file to dest '$filepath': object at path is already a directory"
|
||||||
|
} else {
|
||||||
|
$changed_files += $file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($directory in $directories) {
|
||||||
|
$dirname = $directory.dest
|
||||||
|
|
||||||
|
$dirpath = Join-Path -Path $dest -ChildPath $dirname
|
||||||
|
$parent_dir = [System.IO.Path]::GetDirectoryName($dirpath)
|
||||||
|
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at parent directory path is already a file"
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $dirpath -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at path is already a file"
|
||||||
|
} elseif (-not (Test-Path -LiteralPath $dirpath -PathType Container)) {
|
||||||
|
$changed_directories += $directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: Handle symlinks
|
||||||
|
|
||||||
|
$result.files = $changed_files
|
||||||
|
$result.directories = $changed_directories
|
||||||
|
$result.symlinks = $changed_symlinks
|
||||||
|
} elseif ($copy_mode -eq "explode") {
|
||||||
|
# a single zip file containing the files and directories needs to be
|
||||||
|
# expanded this will always result in a change as the calculation is done
|
||||||
|
# on the win_copy action plugin and is only run if a change needs to occur
|
||||||
|
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||||
|
Fail-Json -obj $result -message "Cannot expand src zip file: '$src' as it does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect if the PS zip assemblies are available or whether to use Shell
|
||||||
|
$use_legacy = $false
|
||||||
|
try {
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
|
||||||
|
Add-Type -AssemblyName System.IO.Compression | Out-Null
|
||||||
|
} catch {
|
||||||
|
$use_legacy = $true
|
||||||
|
}
|
||||||
|
if ($use_legacy) {
|
||||||
|
Extract-ZipLegacy -src $src -dest $dest
|
||||||
|
} else {
|
||||||
|
Extract-Zip -src $src -dest $dest
|
||||||
|
}
|
||||||
|
|
||||||
|
$result.changed = $true
|
||||||
|
} elseif ($copy_mode -eq "remote") {
|
||||||
|
# all copy actions are happening on the remote side (windows host), need
|
||||||
|
# too copy source and dest using PS code
|
||||||
|
$result.src = $src
|
||||||
|
$result.dest = $dest
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $src)) {
|
||||||
|
Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $src -PathType Container) {
|
||||||
|
# we are copying a directory or the contents of a directory
|
||||||
|
$result.operation = 'folder_copy'
|
||||||
|
if ($src.EndsWith("/") -or $src.EndsWith("`\")) {
|
||||||
|
# copying the folder's contents to dest
|
||||||
|
$diff = ""
|
||||||
|
$child_files = Get-ChildItem -LiteralPath $src -Force
|
||||||
|
foreach ($child_file in $child_files) {
|
||||||
|
$dest_child_path = Join-Path -Path $dest -ChildPath $child_file.Name
|
||||||
|
if ($child_file.PSIsContainer) {
|
||||||
|
$diff += Copy-Folder -source $child_file.FullName -dest $dest_child_path
|
||||||
|
} else {
|
||||||
|
$diff += (Copy-File -source $child_file.FullName -dest $dest_child_path).diff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# copying the folder and it's contents to dest
|
||||||
|
$dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
|
||||||
|
$result.dest = $dest
|
||||||
|
$diff = Copy-Folder -source $src -dest $dest
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# we are just copying a single file to dest
|
||||||
|
$result.operation = 'file_copy'
|
||||||
|
|
||||||
|
$source_basename = (Get-Item -LiteralPath $src -Force).Name
|
||||||
|
$result.original_basename = $source_basename
|
||||||
|
|
||||||
|
if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) {
|
||||||
|
$dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
|
||||||
|
$result.dest = $dest
|
||||||
|
} else {
|
||||||
|
# check if the parent dir exists, this is only done if src is a
|
||||||
|
# file and dest if the path to a file (doesn't end with \ or /)
|
||||||
|
$parent_dir = Split-Path -LiteralPath $dest
|
||||||
|
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||||
|
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||||
|
Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$copy_result = Copy-File -source $src -dest $dest
|
||||||
|
$diff = $copy_result.diff
|
||||||
|
$result.checksum = $copy_result.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
# the file might not exist if running in check mode
|
||||||
|
if (-not $check_mode -or (Test-Path -LiteralPath $dest -PathType Leaf)) {
|
||||||
|
$result.size = Get-FileSize -path $dest
|
||||||
|
} else {
|
||||||
|
$result.size = $null
|
||||||
|
}
|
||||||
|
if ($diff_mode) {
|
||||||
|
$result.diff.prepared = $diff
|
||||||
|
}
|
||||||
|
} elseif ($copy_mode -eq "single") {
|
||||||
|
# a single file is located in src and we need to copy to dest, this will
|
||||||
|
# always result in a change as the calculation is done on the Ansible side
|
||||||
|
# before this is run. This should also never run in check mode
|
||||||
|
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||||
|
Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
# the dest parameter is a directory, we need to append original_basename
|
||||||
|
if ($dest.EndsWith("/") -or $dest.EndsWith("`\") -or (Test-Path -LiteralPath $dest -PathType Container)) {
|
||||||
|
$remote_dest = Join-Path -Path $dest -ChildPath $original_basename
|
||||||
|
$parent_dir = Split-Path -LiteralPath $remote_dest
|
||||||
|
|
||||||
|
# when dest ends with /, we need to create the destination directories
|
||||||
|
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||||
|
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||||
|
New-Item -Path $parent_dir -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$remote_dest = $dest
|
||||||
|
$parent_dir = Split-Path -LiteralPath $remote_dest
|
||||||
|
|
||||||
|
# check if the dest parent dirs exist, need to fail if they don't
|
||||||
|
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||||
|
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||||
|
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||||
|
Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backup) {
|
||||||
|
$result.backup_file = Backup-File -path $remote_dest -WhatIf:$check_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $src -Destination $remote_dest -Force | Out-Null
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json -obj $result
|
@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['stableinterface'],
|
||||||
|
'supported_by': 'core'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_copy
|
||||||
|
version_added: '1.9.2'
|
||||||
|
short_description: Copies files to remote locations on windows hosts
|
||||||
|
description:
|
||||||
|
- The C(win_copy) module copies a file on the local box to remote windows locations.
|
||||||
|
- For non-Windows targets, use the M(copy) module instead.
|
||||||
|
options:
|
||||||
|
content:
|
||||||
|
description:
|
||||||
|
- When used instead of C(src), sets the contents of a file directly to the
|
||||||
|
specified value.
|
||||||
|
- This is for simple values, for anything complex or with formatting please
|
||||||
|
switch to the M(template) module.
|
||||||
|
type: str
|
||||||
|
version_added: '2.3'
|
||||||
|
decrypt:
|
||||||
|
description:
|
||||||
|
- This option controls the autodecryption of source files using vault.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
version_added: '2.5'
|
||||||
|
dest:
|
||||||
|
description:
|
||||||
|
- Remote absolute path where the file should be copied to.
|
||||||
|
- If C(src) is a directory, this must be a directory too.
|
||||||
|
- Use \ for path separators or \\ when in "double quotes".
|
||||||
|
- If C(dest) ends with \ then source or the contents of source will be
|
||||||
|
copied to the directory without renaming.
|
||||||
|
- If C(dest) is a nonexistent path, it will only be created if C(dest) ends
|
||||||
|
with "/" or "\", or C(src) is a directory.
|
||||||
|
- If C(src) and C(dest) are files and if the parent directory of C(dest)
|
||||||
|
doesn't exist, then the task will fail.
|
||||||
|
type: path
|
||||||
|
required: yes
|
||||||
|
backup:
|
||||||
|
description:
|
||||||
|
- Determine whether a backup should be created.
|
||||||
|
- When set to C(yes), create a backup file including the timestamp information
|
||||||
|
so you can get the original file back if you somehow clobbered it incorrectly.
|
||||||
|
- No backup is taken when C(remote_src=False) and multiple files are being
|
||||||
|
copied.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
version_added: '2.8'
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- If set to C(yes), the file will only be transferred if the content
|
||||||
|
is different than destination.
|
||||||
|
- If set to C(no), the file will only be transferred if the
|
||||||
|
destination does not exist.
|
||||||
|
- If set to C(no), no checksuming of the content is performed which can
|
||||||
|
help improve performance on larger files.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
version_added: '2.3'
|
||||||
|
local_follow:
|
||||||
|
description:
|
||||||
|
- This flag indicates that filesystem links in the source tree, if they
|
||||||
|
exist, should be followed.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
version_added: '2.4'
|
||||||
|
remote_src:
|
||||||
|
description:
|
||||||
|
- If C(no), it will search for src at originating/master machine.
|
||||||
|
- If C(yes), it will go to the remote/target machine for the src.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
version_added: '2.3'
|
||||||
|
src:
|
||||||
|
description:
|
||||||
|
- Local path to a file to copy to the remote server; can be absolute or
|
||||||
|
relative.
|
||||||
|
- If path is a directory, it is copied (including the source folder name)
|
||||||
|
recursively to C(dest).
|
||||||
|
- If path is a directory and ends with "/", only the inside contents of
|
||||||
|
that directory are copied to the destination. Otherwise, if it does not
|
||||||
|
end with "/", the directory itself with all contents is copied.
|
||||||
|
- If path is a file and dest ends with "\", the file is copied to the
|
||||||
|
folder with the same filename.
|
||||||
|
- Required unless using C(content).
|
||||||
|
type: path
|
||||||
|
notes:
|
||||||
|
- Currently win_copy does not support copying symbolic links from both local to
|
||||||
|
remote and remote to remote.
|
||||||
|
- It is recommended that backslashes C(\) are used instead of C(/) when dealing
|
||||||
|
with remote paths.
|
||||||
|
- Because win_copy runs over WinRM, it is not a very efficient transfer
|
||||||
|
mechanism. If sending large files consider hosting them on a web service and
|
||||||
|
using M(win_get_url) instead.
|
||||||
|
seealso:
|
||||||
|
- module: assemble
|
||||||
|
- module: copy
|
||||||
|
- module: win_get_url
|
||||||
|
- module: win_robocopy
|
||||||
|
author:
|
||||||
|
- Jon Hawkesworth (@jhawkesworth)
|
||||||
|
- Jordan Borean (@jborean93)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Copy a single file
|
||||||
|
win_copy:
|
||||||
|
src: /srv/myfiles/foo.conf
|
||||||
|
dest: C:\Temp\renamed-foo.conf
|
||||||
|
|
||||||
|
- name: Copy a single file, but keep a backup
|
||||||
|
win_copy:
|
||||||
|
src: /srv/myfiles/foo.conf
|
||||||
|
dest: C:\Temp\renamed-foo.conf
|
||||||
|
backup: yes
|
||||||
|
|
||||||
|
- name: Copy a single file keeping the filename
|
||||||
|
win_copy:
|
||||||
|
src: /src/myfiles/foo.conf
|
||||||
|
dest: C:\Temp\
|
||||||
|
|
||||||
|
- name: Copy folder to C:\Temp (results in C:\Temp\temp_files)
|
||||||
|
win_copy:
|
||||||
|
src: files/temp_files
|
||||||
|
dest: C:\Temp
|
||||||
|
|
||||||
|
- name: Copy folder contents recursively
|
||||||
|
win_copy:
|
||||||
|
src: files/temp_files/
|
||||||
|
dest: C:\Temp
|
||||||
|
|
||||||
|
- name: Copy a single file where the source is on the remote host
|
||||||
|
win_copy:
|
||||||
|
src: C:\Temp\foo.txt
|
||||||
|
dest: C:\ansible\foo.txt
|
||||||
|
remote_src: yes
|
||||||
|
|
||||||
|
- name: Copy a folder recursively where the source is on the remote host
|
||||||
|
win_copy:
|
||||||
|
src: C:\Temp
|
||||||
|
dest: C:\ansible
|
||||||
|
remote_src: yes
|
||||||
|
|
||||||
|
- name: Set the contents of a file
|
||||||
|
win_copy:
|
||||||
|
content: abc123
|
||||||
|
dest: C:\Temp\foo.txt
|
||||||
|
|
||||||
|
- name: Copy a single file as another user
|
||||||
|
win_copy:
|
||||||
|
src: NuGet.config
|
||||||
|
dest: '%AppData%\NuGet\NuGet.config'
|
||||||
|
vars:
|
||||||
|
ansible_become_user: user
|
||||||
|
ansible_become_password: pass
|
||||||
|
# The tmp dir must be set when using win_copy as another user
|
||||||
|
# This ensures the become user will have permissions for the operation
|
||||||
|
# Make sure to specify a folder both the ansible_user and the become_user have access to (i.e not %TEMP% which is user specific and requires Admin)
|
||||||
|
ansible_remote_tmp: 'c:\tmp'
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
backup_file:
|
||||||
|
description: Name of the backup file that was created.
|
||||||
|
returned: if backup=yes
|
||||||
|
type: str
|
||||||
|
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
|
||||||
|
dest:
|
||||||
|
description: Destination file/path.
|
||||||
|
returned: changed
|
||||||
|
type: str
|
||||||
|
sample: C:\Temp\
|
||||||
|
src:
|
||||||
|
description: Source file used for the copy on the target machine.
|
||||||
|
returned: changed
|
||||||
|
type: str
|
||||||
|
sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
|
||||||
|
checksum:
|
||||||
|
description: SHA1 checksum of the file after running copy.
|
||||||
|
returned: success, src is a file
|
||||||
|
type: str
|
||||||
|
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||||
|
size:
|
||||||
|
description: Size of the target, after execution.
|
||||||
|
returned: changed, src is a file
|
||||||
|
type: int
|
||||||
|
sample: 1220
|
||||||
|
operation:
|
||||||
|
description: Whether a single file copy took place or a folder copy.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: file_copy
|
||||||
|
original_basename:
|
||||||
|
description: Basename of the copied file.
|
||||||
|
returned: changed, src is a file
|
||||||
|
type: str
|
||||||
|
sample: foo.txt
|
||||||
|
'''
|
@ -0,0 +1,129 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -OSVersion 6.3
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{
|
||||||
|
drive_letter = @{ type = "str"; required = $true }
|
||||||
|
state = @{ type = "str"; choices = "absent", "present"; default = "present"; }
|
||||||
|
settings = @{
|
||||||
|
type = "dict"
|
||||||
|
required = $false
|
||||||
|
options = @{
|
||||||
|
minimum_file_size = @{ type = "int"; default = 32768 }
|
||||||
|
minimum_file_age_days = @{ type = "int"; default = 2 }
|
||||||
|
no_compress = @{ type = "bool"; required = $false; default = $false }
|
||||||
|
optimize_in_use_files = @{ type = "bool"; required = $false; default = $false }
|
||||||
|
verify = @{ type = "bool"; required = $false; default = $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
|
||||||
|
$drive_letter = $module.Params.drive_letter
|
||||||
|
$state = $module.Params.state
|
||||||
|
$settings = $module.Params.settings
|
||||||
|
|
||||||
|
$module.Result.changed = $false
|
||||||
|
$module.Result.reboot_required = $false
|
||||||
|
$module.Result.msg = ""
|
||||||
|
|
||||||
|
function Set-DataDeduplication($volume, $state, $settings, $dedup_job) {
|
||||||
|
|
||||||
|
$current_state = 'absent'
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
|
||||||
|
} catch {
|
||||||
|
$dedup_info = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dedup_info.Enabled) {
|
||||||
|
$current_state = 'present'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $state -ne $current_state ) {
|
||||||
|
if( -not $module.CheckMode) {
|
||||||
|
if($state -eq 'present') {
|
||||||
|
# Enable-DedupVolume -Volume <String>
|
||||||
|
Enable-DedupVolume -Volume "$($volume.DriveLetter):"
|
||||||
|
} elseif ($state -eq 'absent') {
|
||||||
|
Disable-DedupVolume -Volume "$($volume.DriveLetter):"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$module.Result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq 'present') {
|
||||||
|
if ($null -ne $settings) {
|
||||||
|
Set-DataDedupJobSettings -volume $volume -settings $settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-DataDedupJobSettings ($volume, $settings) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
|
||||||
|
} catch {
|
||||||
|
$dedup_info = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach ($key in $settings.keys) {
|
||||||
|
|
||||||
|
# See Microsoft documentation:
|
||||||
|
# https://docs.microsoft.com/en-us/powershell/module/deduplication/set-dedupvolume?view=win10-ps
|
||||||
|
|
||||||
|
$update_key = $key
|
||||||
|
$update_value = $settings.$($key)
|
||||||
|
# Transform Ansible style options to Powershell params
|
||||||
|
$update_key = $update_key -replace('_', '')
|
||||||
|
|
||||||
|
if ($update_key -eq "MinimumFileSize" -and $update_value -lt 32768) {
|
||||||
|
$update_value = 32768
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_value = ($dedup_info | Select-Object -ExpandProperty $update_key)
|
||||||
|
|
||||||
|
if ($update_value -ne $current_value) {
|
||||||
|
$command_param = @{
|
||||||
|
$($update_key) = $update_value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set-DedupVolume -Volume <String>`
|
||||||
|
# -NoCompress <bool> `
|
||||||
|
# -MinimumFileAgeDays <UInt32> `
|
||||||
|
# -MinimumFileSize <UInt32> (minimum 32768)
|
||||||
|
if( -not $module.CheckMode ) {
|
||||||
|
Set-DedupVolume -Volume "$($volume.DriveLetter):" @command_param
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.Result.changed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install required feature
|
||||||
|
$feature_name = "FS-Data-Deduplication"
|
||||||
|
if( -not $module.CheckMode) {
|
||||||
|
$feature = Install-WindowsFeature -Name $feature_name
|
||||||
|
|
||||||
|
if ($feature.RestartNeeded -eq 'Yes') {
|
||||||
|
$module.Result.reboot_required = $true
|
||||||
|
$module.FailJson("$feature_name was installed but requires Windows to be rebooted to work.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$volume = Get-Volume -DriveLetter $drive_letter
|
||||||
|
|
||||||
|
Set-DataDeduplication -volume $volume -state $state -settings $settings -dedup_job $dedup_job
|
||||||
|
|
||||||
|
$module.ExitJson()
|
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_data_deduplication
|
||||||
|
version_added: "2.10"
|
||||||
|
short_description: Module to enable Data Deduplication on a volume.
|
||||||
|
description:
|
||||||
|
- This module can be used to enable Data Deduplication on a Windows volume.
|
||||||
|
- The module will install the FS-Data-Deduplication feature (a reboot will be necessary).
|
||||||
|
options:
|
||||||
|
drive_letter:
|
||||||
|
description:
|
||||||
|
- Windows drive letter on which to enable data deduplication.
|
||||||
|
required: yes
|
||||||
|
type: str
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Wether to enable or disable data deduplication on the selected volume.
|
||||||
|
default: present
|
||||||
|
type: str
|
||||||
|
choices: [ present, absent ]
|
||||||
|
settings:
|
||||||
|
description:
|
||||||
|
- Dictionary of settings to pass to the Set-DedupVolume powershell command.
|
||||||
|
type: dict
|
||||||
|
suboptions:
|
||||||
|
minimum_file_size:
|
||||||
|
description:
|
||||||
|
- Minimum file size you want to target for deduplication.
|
||||||
|
- It will default to 32768 if not defined or if the value is less than 32768.
|
||||||
|
type: int
|
||||||
|
default: 32768
|
||||||
|
minimum_file_age_days:
|
||||||
|
description:
|
||||||
|
- Minimum file age you want to target for deduplication.
|
||||||
|
type: int
|
||||||
|
default: 2
|
||||||
|
no_compress:
|
||||||
|
description:
|
||||||
|
- Wether you want to enabled filesystem compression or not.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
optimize_in_use_files:
|
||||||
|
description:
|
||||||
|
- Indicates that the server attempts to optimize currently open files.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
verify:
|
||||||
|
description:
|
||||||
|
- Indicates whether the deduplication engine performs a byte-for-byte verification for each duplicate chunk
|
||||||
|
that optimization creates, rather than relying on a cryptographically strong hash.
|
||||||
|
- This option is not recommend.
|
||||||
|
- Setting this parameter to True can degrade optimization performance.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
author:
|
||||||
|
- rnsc (@rnsc)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Enable Data Deduplication on D
|
||||||
|
win_data_deduplication:
|
||||||
|
drive_letter: 'D'
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Enable Data Deduplication on D
|
||||||
|
win_data_deduplication:
|
||||||
|
drive_letter: 'D'
|
||||||
|
state: present
|
||||||
|
settings:
|
||||||
|
no_compress: true
|
||||||
|
minimum_file_age_days: 1
|
||||||
|
minimum_file_size: 0
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
#
|
||||||
|
'''
|
@ -0,0 +1,398 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#Requires -Version 5
|
||||||
|
|
||||||
|
Function ConvertTo-ArgSpecType {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts the DSC parameter type to the arg spec type required for Ansible.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][String]$CimType
|
||||||
|
)
|
||||||
|
|
||||||
|
$arg_type = switch($CimType) {
|
||||||
|
Boolean { "bool" }
|
||||||
|
Char16 { [Func[[Object], [Char]]]{ [System.Char]::Parse($args[0].ToString()) } }
|
||||||
|
DateTime { [Func[[Object], [DateTime]]]{ [System.DateTime]($args[0].ToString()) } }
|
||||||
|
Instance { "dict" }
|
||||||
|
Real32 { "float" }
|
||||||
|
Real64 { [Func[[Object], [Double]]]{ [System.Double]::Parse($args[0].ToString()) } }
|
||||||
|
Reference { "dict" }
|
||||||
|
SInt16 { [Func[[Object], [Int16]]]{ [System.Int16]::Parse($args[0].ToString()) } }
|
||||||
|
SInt32 { "int" }
|
||||||
|
SInt64 { [Func[[Object], [Int64]]]{ [System.Int64]::Parse($args[0].ToString()) } }
|
||||||
|
SInt8 { [Func[[Object], [SByte]]]{ [System.SByte]::Parse($args[0].ToString()) } }
|
||||||
|
String { "str" }
|
||||||
|
UInt16 { [Func[[Object], [UInt16]]]{ [System.UInt16]::Parse($args[0].ToString()) } }
|
||||||
|
UInt32 { [Func[[Object], [UInt32]]]{ [System.UInt32]::Parse($args[0].ToString()) } }
|
||||||
|
UInt64 { [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0].ToString()) } }
|
||||||
|
UInt8 { [Func[[Object], [Byte]]]{ [System.Byte]::Parse($args[0].ToString()) } }
|
||||||
|
Unknown { "raw" }
|
||||||
|
default { "raw" }
|
||||||
|
}
|
||||||
|
return $arg_type
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-DscCimClassProperties {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Get's a list of CimProperties of a CIM Class. It filters out any magic or
|
||||||
|
read only properties that we don't need to know about.
|
||||||
|
#>
|
||||||
|
param([Parameter(Mandatory=$true)][String]$ClassName)
|
||||||
|
|
||||||
|
$resource = Get-CimClass -ClassName $ClassName -Namespace root\Microsoft\Windows\DesiredStateConfiguration
|
||||||
|
|
||||||
|
# Filter out any magic properties that are used internally on an OMI_BaseResource
|
||||||
|
# https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/DscSupport/CimDSCParser.cs#L1203
|
||||||
|
$magic_properties = @("ResourceId", "SourceInfo", "ModuleName", "ModuleVersion", "ConfigurationName")
|
||||||
|
$properties = $resource.CimClassProperties | Where-Object {
|
||||||
|
|
||||||
|
($resource.CimSuperClassName -ne "OMI_BaseResource" -or $_.Name -notin $magic_properties) -and
|
||||||
|
-not $_.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ,$properties
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Add-PropertyOption {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Adds the spec for the property type to the existing module specification.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Hashtable]$Spec,
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[Microsoft.Management.Infrastructure.CimPropertyDeclaration]$Property
|
||||||
|
)
|
||||||
|
|
||||||
|
$option = @{
|
||||||
|
required = $false
|
||||||
|
}
|
||||||
|
$property_name = $Property.Name
|
||||||
|
$property_type = $Property.CimType.ToString()
|
||||||
|
|
||||||
|
if ($Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Key) -or
|
||||||
|
$Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Required)) {
|
||||||
|
$option.required = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $Property.Qualifiers['Values']) {
|
||||||
|
$option.choices = [System.Collections.Generic.List`1[Object]]$Property.Qualifiers['Values'].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($property_name -eq "Name") {
|
||||||
|
# For backwards compatibility we support specifying the Name DSC property as item_name
|
||||||
|
$option.aliases = @("item_name")
|
||||||
|
} elseif ($property_name -ceq "key") {
|
||||||
|
# There seems to be a bug in the CIM property parsing when the property name is 'Key'. The CIM instance will
|
||||||
|
# think the name is 'key' when the MOF actually defines it as 'Key'. We set the proper casing so the module arg
|
||||||
|
# validator won't fire a case sensitive warning
|
||||||
|
$property_name = "Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Property.ReferenceClassName -eq "MSFT_Credential") {
|
||||||
|
# Special handling for the MSFT_Credential type (PSCredential), we handle this with having 2 options that
|
||||||
|
# have the suffix _username and _password.
|
||||||
|
$option_spec_pass = @{
|
||||||
|
type = "str"
|
||||||
|
required = $option.required
|
||||||
|
no_log = $true
|
||||||
|
}
|
||||||
|
$Spec.options."$($property_name)_password" = $option_spec_pass
|
||||||
|
$Spec.required_together.Add(@("$($property_name)_username", "$($property_name)_password")) > $null
|
||||||
|
|
||||||
|
$property_name = "$($property_name)_username"
|
||||||
|
$option.type = "str"
|
||||||
|
} elseif ($Property.ReferenceClassName -eq "MSFT_KeyValuePair") {
|
||||||
|
$option.type = "dict"
|
||||||
|
} elseif ($property_type.EndsWith("Array")) {
|
||||||
|
$option.type = "list"
|
||||||
|
$option.elements = ConvertTo-ArgSpecType -CimType $property_type.Substring(0, $property_type.Length - 5)
|
||||||
|
} else {
|
||||||
|
$option.type = ConvertTo-ArgSpecType -CimType $property_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($option.type -eq "dict" -or ($option.type -eq "list" -and $option.elements -eq "dict")) -and
|
||||||
|
$Property.ReferenceClassName -ne "MSFT_KeyValuePair") {
|
||||||
|
# Get the sub spec if the type is a Instance (CimInstance/dict)
|
||||||
|
$sub_option_spec = Get-OptionSpec -ClassName $Property.ReferenceClassName
|
||||||
|
$option += $sub_option_spec
|
||||||
|
}
|
||||||
|
|
||||||
|
$Spec.options.$property_name = $option
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-OptionSpec {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates the specifiec used in AnsibleModule for a CIM MOF resource name.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
This won't be able to retrieve the default values for an option as that is not defined in the MOF for a resource.
|
||||||
|
Default values are still preserved in the DSC engine if we don't pass in the property at all, we just can't report
|
||||||
|
on what they are automatically.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][String]$ClassName
|
||||||
|
)
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{}
|
||||||
|
required_together = [System.Collections.ArrayList]@()
|
||||||
|
}
|
||||||
|
$properties = Get-DscCimClassProperties -ClassName $ClassName
|
||||||
|
foreach ($property in $properties) {
|
||||||
|
Add-PropertyOption -Spec $spec -Property $property
|
||||||
|
}
|
||||||
|
|
||||||
|
return $spec
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-CimInstance {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a dict to a CimInstance of the specified Class. Also provides a
|
||||||
|
better error message if this fails that contains the option name that failed.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][String]$Name,
|
||||||
|
[Parameter(Mandatory=$true)][String]$ClassName,
|
||||||
|
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Value,
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||||
|
[Switch]$Recurse
|
||||||
|
)
|
||||||
|
|
||||||
|
$properties = @{}
|
||||||
|
foreach ($value_info in $Value.GetEnumerator()) {
|
||||||
|
# Need to remove all null values from existing dict so the conversion works
|
||||||
|
if ($null -eq $value_info.Value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$properties.($value_info.Key) = $value_info.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Recurse) {
|
||||||
|
# We want to validate and convert and values to what's required by DSC
|
||||||
|
$properties = ConvertTo-DscProperty -ClassName $ClassName -Params $properties -Module $Module
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (New-CimInstance -ClassName $ClassName -Property $properties -ClientOnly)
|
||||||
|
} catch {
|
||||||
|
# New-CimInstance raises a poor error message, make sure we mention what option it is for
|
||||||
|
$Module.FailJson("Failed to cast dict value for option '$Name' to a CimInstance: $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-DscProperty {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts the input module parameters that have been validated and casted
|
||||||
|
into the types expected by the DSC engine. This is mostly done to deal with
|
||||||
|
types like PSCredential and Dictionaries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][String]$ClassName,
|
||||||
|
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Params,
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module
|
||||||
|
)
|
||||||
|
$properties = Get-DscCimClassProperties -ClassName $ClassName
|
||||||
|
|
||||||
|
$dsc_properties = @{}
|
||||||
|
foreach ($property in $properties) {
|
||||||
|
$property_name = $property.Name
|
||||||
|
$property_type = $property.CimType.ToString()
|
||||||
|
|
||||||
|
if ($property.ReferenceClassName -eq "MSFT_Credential") {
|
||||||
|
$username = $Params."$($property_name)_username"
|
||||||
|
$password = $Params."$($property_name)_password"
|
||||||
|
|
||||||
|
# No user set == No option set in playbook, skip this property
|
||||||
|
if ($null -eq $username) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force
|
||||||
|
$value = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $sec_password
|
||||||
|
} else {
|
||||||
|
$value = $Params.$property_name
|
||||||
|
|
||||||
|
# The actual value wasn't set, skip adding this property
|
||||||
|
if ($null -eq $value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($property.ReferenceClassName -eq "MSFT_KeyValuePair") {
|
||||||
|
$key_value_pairs = [System.Collections.Generic.List`1[CimInstance]]@()
|
||||||
|
foreach ($value_info in $value.GetEnumerator()) {
|
||||||
|
$kvp = @{Key = $value_info.Key; Value = $value_info.Value.ToString()}
|
||||||
|
$cim_instance = ConvertTo-CimInstance -Name $property_name -ClassName MSFT_KeyValuePair `
|
||||||
|
-Value $kvp -Module $Module
|
||||||
|
$key_value_pairs.Add($cim_instance) > $null
|
||||||
|
}
|
||||||
|
$value = $key_value_pairs.ToArray()
|
||||||
|
} elseif ($null -ne $property.ReferenceClassName) {
|
||||||
|
# Convert the dict to a CimInstance (or list of CimInstances)
|
||||||
|
$convert_args = @{
|
||||||
|
ClassName = $property.ReferenceClassName
|
||||||
|
Module = $Module
|
||||||
|
Name = $property_name
|
||||||
|
Recurse = $true
|
||||||
|
}
|
||||||
|
if ($property_type.EndsWith("Array")) {
|
||||||
|
$value = [System.Collections.Generic.List`1[CimInstance]]@()
|
||||||
|
foreach ($raw in $Params.$property_name.GetEnumerator()) {
|
||||||
|
$cim_instance = ConvertTo-CimInstance -Value $raw @convert_args
|
||||||
|
$value.Add($cim_instance) > $null
|
||||||
|
}
|
||||||
|
$value = $value.ToArray() # Need to make sure we are dealing with an Array not a List
|
||||||
|
} else {
|
||||||
|
$value = ConvertTo-CimInstance -Value $value @convert_args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$dsc_properties.$property_name = $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dsc_properties
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Invoke-DscMethod {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Invokes the DSC Resource Method specified in another PS pipeline. This is
|
||||||
|
done so we can retrieve the Verbose stream and return it back to the user
|
||||||
|
for futher debugging.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||||
|
[Parameter(Mandatory=$true)][String]$Method,
|
||||||
|
[Parameter(Mandatory=$true)][Hashtable]$Arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invoke the DSC resource in a separate runspace so we can capture the Verbose output
|
||||||
|
$ps = [PowerShell]::Create()
|
||||||
|
$ps.AddCommand("Invoke-DscResource").AddParameter("Method", $Method) > $null
|
||||||
|
$ps.AddParameters($Arguments) > $null
|
||||||
|
|
||||||
|
$result = $ps.Invoke()
|
||||||
|
|
||||||
|
# Pass the warnings through to the AnsibleModule return result
|
||||||
|
foreach ($warning in $ps.Streams.Warning) {
|
||||||
|
$Module.Warn($warning.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
# If running at a high enough verbosity, add the verbose output to the AnsibleModule return result
|
||||||
|
if ($Module.Verbosity -ge 3) {
|
||||||
|
$verbose_logs = [System.Collections.Generic.List`1[String]]@()
|
||||||
|
foreach ($verbosity in $ps.Streams.Verbose) {
|
||||||
|
$verbose_logs.Add($verbosity.Message) > $null
|
||||||
|
}
|
||||||
|
$Module.Result."verbose_$($Method.ToLower())" = $verbose_logs
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ps.HadErrors) {
|
||||||
|
# Cannot pass in the ErrorRecord as it's a RemotingErrorRecord and doesn't contain the ScriptStackTrace
|
||||||
|
# or other info that would be useful
|
||||||
|
$Module.FailJson("Failed to invoke DSC $Method method: $($ps.Streams.Error[0].Exception.Message)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
# win_dsc is unique in that is builds the arg spec based on DSC Resource input. To get this info
|
||||||
|
# we need to read the resource_name and module_version value which is done outside of Ansible.Basic
|
||||||
|
if ($args.Length -gt 0) {
|
||||||
|
$params = Get-Content -Path $args[0] | ConvertFrom-Json
|
||||||
|
} else {
|
||||||
|
$params = $complex_args
|
||||||
|
}
|
||||||
|
if (-not $params.ContainsKey("resource_name")) {
|
||||||
|
$res = @{
|
||||||
|
msg = "missing required argument: resource_name"
|
||||||
|
failed = $true
|
||||||
|
}
|
||||||
|
Write-Output -InputObject (ConvertTo-Json -Compress -InputObject $res)
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$resource_name = $params.resource_name
|
||||||
|
|
||||||
|
if ($params.ContainsKey("module_version")) {
|
||||||
|
$module_version = $params.module_version
|
||||||
|
} else {
|
||||||
|
$module_version = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
$module_versions = (Get-DscResource -Name $resource_name -ErrorAction SilentlyContinue | Sort-Object -Property Version)
|
||||||
|
$resource = $null
|
||||||
|
if ($module_version -eq "latest" -and $null -ne $module_versions) {
|
||||||
|
$resource = $module_versions[-1]
|
||||||
|
} elseif ($module_version -ne "latest") {
|
||||||
|
$resource = $module_versions | Where-Object { $_.Version -eq $module_version }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $resource) {
|
||||||
|
if ($module_version -eq "latest") {
|
||||||
|
$msg = "Resource '$resource_name' not found."
|
||||||
|
} else {
|
||||||
|
$msg = "Resource '$resource_name' with version '$module_version' not found."
|
||||||
|
$msg += " Versions installed: '$($module_versions.Version -join "', '")'."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output -InputObject (ConvertTo-Json -Compress -InputObject @{ failed = $true; msg = $msg })
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build the base args for the DSC Invocation based on the resource selected
|
||||||
|
$dsc_args = @{
|
||||||
|
Name = $resource.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Binary resources are not working very well with that approach - need to guesstimate module name/version
|
||||||
|
$module_version = $null
|
||||||
|
if ($resource.Module) {
|
||||||
|
$dsc_args.ModuleName = @{
|
||||||
|
ModuleName = $resource.Module.Name
|
||||||
|
ModuleVersion = $resource.Module.Version
|
||||||
|
}
|
||||||
|
$module_version = $resource.Module.Version.ToString()
|
||||||
|
} else {
|
||||||
|
$dsc_args.ModuleName = "PSDesiredStateConfiguration"
|
||||||
|
}
|
||||||
|
|
||||||
|
# To ensure the class registered with CIM is the one based on our version, we want to run the Get method so the DSC
|
||||||
|
# engine updates the metadata propery. We don't care about any errors here
|
||||||
|
try {
|
||||||
|
Invoke-DscResource -Method Get -Property @{Fake="Fake"} @dsc_args > $null
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Dynamically build the option spec based on the resource_name specified and create the module object
|
||||||
|
$spec = Get-OptionSpec -ClassName $resource.ResourceType
|
||||||
|
$spec.supports_check_mode = $true
|
||||||
|
$spec.options.module_version = @{ type = "str"; default = "latest" }
|
||||||
|
$spec.options.resource_name = @{ type = "str"; required = $true }
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
$module.Result.reboot_required = $false
|
||||||
|
$module.Result.module_version = $module_version
|
||||||
|
|
||||||
|
# Build the DSC invocation arguments and invoke the resource
|
||||||
|
$dsc_args.Property = ConvertTo-DscProperty -ClassName $resource.ResourceType -Module $module -Params $Module.Params
|
||||||
|
$dsc_args.Verbose = $true
|
||||||
|
|
||||||
|
$test_result = Invoke-DscMethod -Module $module -Method Test -Arguments $dsc_args
|
||||||
|
if ($test_result.InDesiredState -ne $true) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
|
$result = Invoke-DscMethod -Module $module -Method Set -Arguments $dsc_args
|
||||||
|
$module.Result.reboot_required = $result.RebootRequired
|
||||||
|
}
|
||||||
|
$module.Result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.ExitJson()
|
@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_dsc
|
||||||
|
version_added: "2.4"
|
||||||
|
short_description: Invokes a PowerShell DSC configuration
|
||||||
|
description:
|
||||||
|
- Configures a resource using PowerShell DSC.
|
||||||
|
- Requires PowerShell version 5.0 or newer.
|
||||||
|
- Most of the options for this module are dynamic and will vary depending on
|
||||||
|
the DSC Resource specified in I(resource_name).
|
||||||
|
- See :doc:`/user_guide/windows_dsc` for more information on how to use this module.
|
||||||
|
options:
|
||||||
|
resource_name:
|
||||||
|
description:
|
||||||
|
- The name of the DSC Resource to use.
|
||||||
|
- Must be accessible to PowerShell using any of the default paths.
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
module_version:
|
||||||
|
description:
|
||||||
|
- Can be used to configure the exact version of the DSC resource to be
|
||||||
|
invoked.
|
||||||
|
- Useful if the target node has multiple versions installed of the module
|
||||||
|
containing the DSC resource.
|
||||||
|
- If not specified, the module will follow standard PowerShell convention
|
||||||
|
and use the highest version available.
|
||||||
|
type: str
|
||||||
|
default: latest
|
||||||
|
free_form:
|
||||||
|
description:
|
||||||
|
- The M(win_dsc) module takes in multiple free form options based on the
|
||||||
|
DSC resource being invoked by I(resource_name).
|
||||||
|
- There is no option actually named C(free_form) so see the examples.
|
||||||
|
- This module will try and convert the option to the correct type required
|
||||||
|
by the DSC resource and throw a warning if it fails.
|
||||||
|
- If the type of the DSC resource option is a C(CimInstance) or
|
||||||
|
C(CimInstance[]), this means the value should be a dictionary or list
|
||||||
|
of dictionaries based on the values required by that option.
|
||||||
|
- If the type of the DSC resource option is a C(PSCredential) then there
|
||||||
|
needs to be 2 options set in the Ansible task definition suffixed with
|
||||||
|
C(_username) and C(_password).
|
||||||
|
- If the type of the DSC resource option is an array, then a list should be
|
||||||
|
provided but a comma separated string also work. Use a list where
|
||||||
|
possible as no escaping is required and it works with more complex types
|
||||||
|
list C(CimInstance[]).
|
||||||
|
- If the type of the DSC resource option is a C(DateTime), you should use
|
||||||
|
a string in the form of an ISO 8901 string to ensure the exact date is
|
||||||
|
used.
|
||||||
|
- Since Ansible 2.8, Ansible will now validate the input fields against the
|
||||||
|
DSC resource definition automatically. Older versions will silently
|
||||||
|
ignore invalid fields.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
notes:
|
||||||
|
- By default there are a few builtin resources that come with PowerShell 5.0,
|
||||||
|
see U(https://docs.microsoft.com/en-us/powershell/scripting/dsc/resources/resources) for
|
||||||
|
more information on these resources.
|
||||||
|
- Custom DSC resources can be installed with M(win_psmodule) using the I(name)
|
||||||
|
option.
|
||||||
|
- The DSC engine run's each task as the SYSTEM account, any resources that need
|
||||||
|
to be accessed with a different account need to have C(PsDscRunAsCredential)
|
||||||
|
set.
|
||||||
|
- To see the valid options for a DSC resource, run the module with C(-vvv) to
|
||||||
|
show the possible module invocation. Default values are not shown in this
|
||||||
|
output but are applied within the DSC engine.
|
||||||
|
author:
|
||||||
|
- Trond Hindenes (@trondhindenes)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Extract zip file
|
||||||
|
win_dsc:
|
||||||
|
resource_name: Archive
|
||||||
|
Ensure: Present
|
||||||
|
Path: C:\Temp\zipfile.zip
|
||||||
|
Destination: C:\Temp\Temp2
|
||||||
|
|
||||||
|
- name: Install a Windows feature with the WindowsFeature resource
|
||||||
|
win_dsc:
|
||||||
|
resource_name: WindowsFeature
|
||||||
|
Name: telnet-client
|
||||||
|
|
||||||
|
- name: Edit HKCU reg key under specific user
|
||||||
|
win_dsc:
|
||||||
|
resource_name: Registry
|
||||||
|
Ensure: Present
|
||||||
|
Key: HKEY_CURRENT_USER\ExampleKey
|
||||||
|
ValueName: TestValue
|
||||||
|
ValueData: TestData
|
||||||
|
PsDscRunAsCredential_username: '{{ansible_user}}'
|
||||||
|
PsDscRunAsCredential_password: '{{ansible_password}}'
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Create file with multiple attributes
|
||||||
|
win_dsc:
|
||||||
|
resource_name: File
|
||||||
|
DestinationPath: C:\ansible\dsc
|
||||||
|
Attributes: # can also be a comma separated string, e.g. 'Hidden, System'
|
||||||
|
- Hidden
|
||||||
|
- System
|
||||||
|
Ensure: Present
|
||||||
|
Type: Directory
|
||||||
|
|
||||||
|
- name: Call DSC resource with DateTime option
|
||||||
|
win_dsc:
|
||||||
|
resource_name: DateTimeResource
|
||||||
|
DateTimeOption: '2019-02-22T13:57:31.2311892+00:00'
|
||||||
|
|
||||||
|
# more complex example using custom DSC resource and dict values
|
||||||
|
- name: Setup the xWebAdministration module
|
||||||
|
win_psmodule:
|
||||||
|
name: xWebAdministration
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create IIS Website with Binding and Authentication options
|
||||||
|
win_dsc:
|
||||||
|
resource_name: xWebsite
|
||||||
|
Ensure: Present
|
||||||
|
Name: DSC Website
|
||||||
|
State: Started
|
||||||
|
PhysicalPath: C:\inetpub\wwwroot
|
||||||
|
BindingInfo: # Example of a CimInstance[] DSC parameter (list of dicts)
|
||||||
|
- Protocol: https
|
||||||
|
Port: 1234
|
||||||
|
CertificateStoreName: MY
|
||||||
|
CertificateThumbprint: C676A89018C4D5902353545343634F35E6B3A659
|
||||||
|
HostName: DSCTest
|
||||||
|
IPAddress: '*'
|
||||||
|
SSLFlags: '1'
|
||||||
|
- Protocol: http
|
||||||
|
Port: 4321
|
||||||
|
IPAddress: '*'
|
||||||
|
AuthenticationInfo: # Example of a CimInstance DSC parameter (dict)
|
||||||
|
Anonymous: no
|
||||||
|
Basic: true
|
||||||
|
Digest: false
|
||||||
|
Windows: yes
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
module_version:
|
||||||
|
description: The version of the dsc resource/module used.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: "1.0.1"
|
||||||
|
reboot_required:
|
||||||
|
description: Flag returned from the DSC engine indicating whether or not
|
||||||
|
the machine requires a reboot for the invoked changes to take effect.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
verbose_test:
|
||||||
|
description: The verbose output as a list from executing the DSC test
|
||||||
|
method.
|
||||||
|
returned: Ansible verbosity is -vvv or greater
|
||||||
|
type: list
|
||||||
|
sample: [
|
||||||
|
"Perform operation 'Invoke CimMethod' with the following parameters, ",
|
||||||
|
"[SERVER]: LCM: [Start Test ] [[File]DirectResourceAccess]",
|
||||||
|
"Operation 'Invoke CimMethod' complete."
|
||||||
|
]
|
||||||
|
verbose_set:
|
||||||
|
description: The verbose output as a list from executing the DSC Set
|
||||||
|
method.
|
||||||
|
returned: Ansible verbosity is -vvv or greater and a change occurred
|
||||||
|
type: list
|
||||||
|
sample: [
|
||||||
|
"Perform operation 'Invoke CimMethod' with the following parameters, ",
|
||||||
|
"[SERVER]: LCM: [Start Set ] [[File]DirectResourceAccess]",
|
||||||
|
"Operation 'Invoke CimMethod' complete."
|
||||||
|
]
|
||||||
|
'''
|
@ -0,0 +1,111 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
|
Import-Module -Name ServerManager
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true
|
||||||
|
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||||
|
|
||||||
|
$name = Get-AnsibleParam -obj $params -name "name" -type "list" -failifempty $true
|
||||||
|
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
|
||||||
|
|
||||||
|
$include_sub_features = Get-AnsibleParam -obj $params -name "include_sub_features" -type "bool" -default $false
|
||||||
|
$include_management_tools = Get-AnsibleParam -obj $params -name "include_management_tools" -type "bool" -default $false
|
||||||
|
$source = Get-AnsibleParam -obj $params -name "source" -type "str"
|
||||||
|
|
||||||
|
$install_cmdlet = $false
|
||||||
|
if (Get-Command -Name Install-WindowsFeature -ErrorAction SilentlyContinue) {
|
||||||
|
Set-Alias -Name Install-AnsibleWindowsFeature -Value Install-WindowsFeature
|
||||||
|
Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Uninstall-WindowsFeature
|
||||||
|
$install_cmdlet = $true
|
||||||
|
} elseif (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue) {
|
||||||
|
Set-Alias -Name Install-AnsibleWindowsFeature -Value Add-WindowsFeature
|
||||||
|
Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Remove-WindowsFeature
|
||||||
|
} else {
|
||||||
|
Fail-Json -obj $result -message "This version of Windows does not support the cmdlets Install-WindowsFeature or Add-WindowsFeature"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "present") {
|
||||||
|
$install_args = @{
|
||||||
|
Name = $name
|
||||||
|
IncludeAllSubFeature = $include_sub_features
|
||||||
|
Restart = $false
|
||||||
|
WhatIf = $check_mode
|
||||||
|
ErrorAction = "Stop"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($install_cmdlet) {
|
||||||
|
$install_args.IncludeManagementTools = $include_management_tools
|
||||||
|
$install_args.Confirm = $false
|
||||||
|
if ($source) {
|
||||||
|
if (-not (Test-Path -Path $source)) {
|
||||||
|
Fail-Json -obj $result -message "Failed to find source path $source for feature install"
|
||||||
|
}
|
||||||
|
$install_args.Source = $source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$action_results = Install-AnsibleWindowsFeature @install_args
|
||||||
|
} catch {
|
||||||
|
Fail-Json -obj $result -message "Failed to install Windows Feature: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$uninstall_args = @{
|
||||||
|
Name = $name
|
||||||
|
Restart = $false
|
||||||
|
WhatIf = $check_mode
|
||||||
|
ErrorAction = "Stop"
|
||||||
|
}
|
||||||
|
if ($install_cmdlet) {
|
||||||
|
$uninstall_args.IncludeManagementTools = $include_management_tools
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$action_results = Uninstall-AnsibleWindowsFeature @uninstall_args
|
||||||
|
} catch {
|
||||||
|
Fail-Json -obj $result -message "Failed to uninstall Windows Feature: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Loop through results and create a hash containing details about
|
||||||
|
# each role/feature that is installed/removed
|
||||||
|
# $action_results.FeatureResult is not empty if anything was changed
|
||||||
|
$feature_results = @()
|
||||||
|
foreach ($action_result in $action_results.FeatureResult) {
|
||||||
|
$message = @()
|
||||||
|
foreach ($msg in $action_result.Message) {
|
||||||
|
$message += @{
|
||||||
|
message_type = $msg.MessageType.ToString()
|
||||||
|
error_code = $msg.ErrorCode
|
||||||
|
text = $msg.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$feature_results += @{
|
||||||
|
id = $action_result.Id
|
||||||
|
display_name = $action_result.DisplayName
|
||||||
|
message = $message
|
||||||
|
reboot_required = ConvertTo-Bool -obj $action_result.RestartNeeded
|
||||||
|
skip_reason = $action_result.SkipReason.ToString()
|
||||||
|
success = ConvertTo-Bool -obj $action_result.Success
|
||||||
|
restart_needed = ConvertTo-Bool -obj $action_result.RestartNeeded
|
||||||
|
}
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
$result.feature_result = $feature_results
|
||||||
|
$result.success = ConvertTo-Bool -obj $action_results.Success
|
||||||
|
$result.exitcode = $action_results.ExitCode.ToString()
|
||||||
|
$result.reboot_required = ConvertTo-Bool -obj $action_results.RestartNeeded
|
||||||
|
# controls whether Ansible will fail or not
|
||||||
|
$result.failed = (-not $action_results.Success)
|
||||||
|
|
||||||
|
Exit-Json -obj $result
|
@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
|
||||||
|
# Copyright: (c) 2014, Trond Hindenes <trond@hindenes.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_feature
|
||||||
|
version_added: "1.7"
|
||||||
|
short_description: Installs and uninstalls Windows Features on Windows Server
|
||||||
|
description:
|
||||||
|
- Installs or uninstalls Windows Roles or Features on Windows Server.
|
||||||
|
- This module uses the Add/Remove-WindowsFeature Cmdlets on Windows 2008 R2
|
||||||
|
and Install/Uninstall-WindowsFeature Cmdlets on Windows 2012, which are not available on client os machines.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Names of roles or features to install as a single feature or a comma-separated list of features.
|
||||||
|
- To list all available features use the PowerShell command C(Get-WindowsFeature).
|
||||||
|
type: list
|
||||||
|
required: yes
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the features or roles on the system.
|
||||||
|
type: str
|
||||||
|
choices: [ absent, present ]
|
||||||
|
default: present
|
||||||
|
include_sub_features:
|
||||||
|
description:
|
||||||
|
- Adds all subfeatures of the specified feature.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
include_management_tools:
|
||||||
|
description:
|
||||||
|
- Adds the corresponding management tools to the specified feature.
|
||||||
|
- Not supported in Windows 2008 R2 and will be ignored.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
source:
|
||||||
|
description:
|
||||||
|
- Specify a source to install the feature from.
|
||||||
|
- Not supported in Windows 2008 R2 and will be ignored.
|
||||||
|
- Can either be C({driveletter}:\sources\sxs) or C(\\{IP}\share\sources\sxs).
|
||||||
|
type: str
|
||||||
|
version_added: "2.1"
|
||||||
|
seealso:
|
||||||
|
- module: win_chocolatey
|
||||||
|
- module: win_package
|
||||||
|
author:
|
||||||
|
- Paul Durivage (@angstwad)
|
||||||
|
- Trond Hindenes (@trondhindenes)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Install IIS (Web-Server only)
|
||||||
|
win_feature:
|
||||||
|
name: Web-Server
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install IIS (Web-Server and Web-Common-Http)
|
||||||
|
win_feature:
|
||||||
|
name:
|
||||||
|
- Web-Server
|
||||||
|
- Web-Common-Http
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install NET-Framework-Core from file
|
||||||
|
win_feature:
|
||||||
|
name: NET-Framework-Core
|
||||||
|
source: C:\Temp\iso\sources\sxs
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install IIS Web-Server with sub features and management tools
|
||||||
|
win_feature:
|
||||||
|
name: Web-Server
|
||||||
|
state: present
|
||||||
|
include_sub_features: yes
|
||||||
|
include_management_tools: yes
|
||||||
|
register: win_feature
|
||||||
|
|
||||||
|
- name: Reboot if installing Web-Server feature requires it
|
||||||
|
win_reboot:
|
||||||
|
when: win_feature.reboot_required
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
exitcode:
|
||||||
|
description: The stringified exit code from the feature installation/removal command.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: Success
|
||||||
|
feature_result:
|
||||||
|
description: List of features that were installed or removed.
|
||||||
|
returned: success
|
||||||
|
type: complex
|
||||||
|
sample:
|
||||||
|
contains:
|
||||||
|
display_name:
|
||||||
|
description: Feature display name.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: "Telnet Client"
|
||||||
|
id:
|
||||||
|
description: A list of KB article IDs that apply to the update.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 44
|
||||||
|
message:
|
||||||
|
description: Any messages returned from the feature subsystem that occurred during installation or removal of this feature.
|
||||||
|
returned: always
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: []
|
||||||
|
reboot_required:
|
||||||
|
description: True when the target server requires a reboot as a result of installing or removing this feature.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
restart_needed:
|
||||||
|
description: DEPRECATED in Ansible 2.4 (refer to C(reboot_required) instead). True when the target server requires a reboot as a
|
||||||
|
result of installing or removing this feature.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
skip_reason:
|
||||||
|
description: The reason a feature installation or removal was skipped.
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: NotSkipped
|
||||||
|
success:
|
||||||
|
description: If the feature installation or removal was successful.
|
||||||
|
returned: always
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
reboot_required:
|
||||||
|
description: True when the target server requires a reboot to complete updates (no further updates can be installed until after a reboot).
|
||||||
|
returned: success
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
'''
|
@ -0,0 +1,152 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2017, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true
|
||||||
|
|
||||||
|
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
|
||||||
|
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
|
||||||
|
|
||||||
|
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","name"
|
||||||
|
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent","directory","file","touch"
|
||||||
|
|
||||||
|
# used in template/copy when dest is the path to a dir and source is a file
|
||||||
|
$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
|
||||||
|
if ((Test-Path -LiteralPath $path -PathType Container) -and ($null -ne $original_basename)) {
|
||||||
|
$path = Join-Path -Path $path -ChildPath $original_basename
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Used to delete symlinks as powershell cannot delete broken symlinks
|
||||||
|
$symlink_util = @"
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ansible.Command {
|
||||||
|
public class SymLinkHelper {
|
||||||
|
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||||
|
public static extern bool DeleteFileW(string lpFileName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||||
|
public static extern bool RemoveDirectoryW(string lpPathName);
|
||||||
|
|
||||||
|
public static void DeleteDirectory(string path) {
|
||||||
|
if (!RemoveDirectoryW(path))
|
||||||
|
throw new Exception(String.Format("RemoveDirectoryW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteFile(string path) {
|
||||||
|
if (!DeleteFileW(path))
|
||||||
|
throw new Exception(String.Format("DeleteFileW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
$original_tmp = $env:TMP
|
||||||
|
$env:TMP = $_remote_tmp
|
||||||
|
Add-Type -TypeDefinition $symlink_util
|
||||||
|
$env:TMP = $original_tmp
|
||||||
|
|
||||||
|
# Used to delete directories and files with logic on handling symbolic links
|
||||||
|
function Remove-File($file, $checkmode) {
|
||||||
|
try {
|
||||||
|
if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||||
|
# Bug with powershell, if you try and delete a symbolic link that is pointing
|
||||||
|
# to an invalid path it will fail, using Win32 API to do this instead
|
||||||
|
if ($file.PSIsContainer) {
|
||||||
|
if (-not $checkmode) {
|
||||||
|
[Ansible.Command.SymLinkHelper]::DeleteDirectory($file.FullName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (-not $checkmode) {
|
||||||
|
[Ansible.Command.SymlinkHelper]::DeleteFile($file.FullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($file.PSIsContainer) {
|
||||||
|
Remove-Directory -directory $file -checkmode $checkmode
|
||||||
|
} else {
|
||||||
|
Remove-Item -LiteralPath $file.FullName -Force -WhatIf:$checkmode
|
||||||
|
}
|
||||||
|
} catch [Exception] {
|
||||||
|
Fail-Json $result "Failed to delete $($file.FullName): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-Directory($directory, $checkmode) {
|
||||||
|
foreach ($file in Get-ChildItem -LiteralPath $directory.FullName) {
|
||||||
|
Remove-File -file $file -checkmode $checkmode
|
||||||
|
}
|
||||||
|
Remove-Item -LiteralPath $directory.FullName -Force -Recurse -WhatIf:$checkmode
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($state -eq "touch") {
|
||||||
|
if (Test-Path -LiteralPath $path) {
|
||||||
|
if (-not $check_mode) {
|
||||||
|
(Get-ChildItem -LiteralPath $path).LastWriteTime = Get-Date
|
||||||
|
}
|
||||||
|
$result.changed = $true
|
||||||
|
} else {
|
||||||
|
Write-Output $null | Out-File -LiteralPath $path -Encoding ASCII -WhatIf:$check_mode
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $path) {
|
||||||
|
$fileinfo = Get-Item -LiteralPath $path -Force
|
||||||
|
if ($state -eq "absent") {
|
||||||
|
Remove-File -file $fileinfo -checkmode $check_mode
|
||||||
|
$result.changed = $true
|
||||||
|
} else {
|
||||||
|
if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
|
||||||
|
Fail-Json $result "path $path is not a directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "file" -and $fileinfo.PsIsContainer) {
|
||||||
|
Fail-Json $result "path $path is not a file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
# If state is not supplied, test the $path to see if it looks like
|
||||||
|
# a file or a folder and set state to file or folder
|
||||||
|
if ($null -eq $state) {
|
||||||
|
$basename = Split-Path -Path $path -Leaf
|
||||||
|
if ($basename.length -gt 0) {
|
||||||
|
$state = "file"
|
||||||
|
} else {
|
||||||
|
$state = "directory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "directory") {
|
||||||
|
try {
|
||||||
|
New-Item -Path $path -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||||
|
} catch {
|
||||||
|
if ($_.CategoryInfo.Category -eq "ResourceExists") {
|
||||||
|
$fileinfo = Get-Item -LiteralPath $_.CategoryInfo.TargetName
|
||||||
|
if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
|
||||||
|
Fail-Json $result "path $path is not a directory"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Fail-Json $result $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result.changed = $true
|
||||||
|
} elseif ($state -eq "file") {
|
||||||
|
Fail-Json $result "path $path will not be created"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit-Json $result
|
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['stableinterface'],
|
||||||
|
'supported_by': 'core'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_file
|
||||||
|
version_added: "1.9.2"
|
||||||
|
short_description: Creates, touches or removes files or directories
|
||||||
|
description:
|
||||||
|
- Creates (empty) files, updates file modification stamps of existing files,
|
||||||
|
and can create or remove directories.
|
||||||
|
- Unlike M(file), does not modify ownership, permissions or manipulate links.
|
||||||
|
- For non-Windows targets, use the M(file) module instead.
|
||||||
|
options:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- Path to the file being managed.
|
||||||
|
required: yes
|
||||||
|
type: path
|
||||||
|
aliases: [ dest, name ]
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- If C(directory), all immediate subdirectories will be created if they
|
||||||
|
do not exist.
|
||||||
|
- If C(file), the file will NOT be created if it does not exist, see the M(copy)
|
||||||
|
or M(template) module if you want that behavior.
|
||||||
|
- If C(absent), directories will be recursively deleted, and files will be removed.
|
||||||
|
- If C(touch), an empty file will be created if the C(path) does not
|
||||||
|
exist, while an existing file or directory will receive updated file access and
|
||||||
|
modification times (similar to the way C(touch) works from the command line).
|
||||||
|
type: str
|
||||||
|
choices: [ absent, directory, file, touch ]
|
||||||
|
seealso:
|
||||||
|
- module: file
|
||||||
|
- module: win_acl
|
||||||
|
- module: win_acl_inheritance
|
||||||
|
- module: win_owner
|
||||||
|
- module: win_stat
|
||||||
|
author:
|
||||||
|
- Jon Hawkesworth (@jhawkesworth)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Touch a file (creates if not present, updates modification time if present)
|
||||||
|
win_file:
|
||||||
|
path: C:\Temp\foo.conf
|
||||||
|
state: touch
|
||||||
|
|
||||||
|
- name: Remove a file, if present
|
||||||
|
win_file:
|
||||||
|
path: C:\Temp\foo.conf
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Create directory structure
|
||||||
|
win_file:
|
||||||
|
path: C:\Temp\folder\subfolder
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: Remove directory structure
|
||||||
|
win_file:
|
||||||
|
path: C:\Temp
|
||||||
|
state: absent
|
||||||
|
'''
|
@ -0,0 +1,416 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2016, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#Requires -Module Ansible.ModuleUtils.LinkUtil
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{
|
||||||
|
paths = @{ type = "list"; elements = "str"; required = $true }
|
||||||
|
age = @{ type = "str" }
|
||||||
|
age_stamp = @{ type = "str"; default = "mtime"; choices = "mtime", "ctime", "atime" }
|
||||||
|
file_type = @{ type = "str"; default = "file"; choices = "file", "directory" }
|
||||||
|
follow = @{ type = "bool"; default = $false }
|
||||||
|
hidden = @{ type = "bool"; default = $false }
|
||||||
|
patterns = @{ type = "list"; elements = "str"; aliases = "regex", "regexp" }
|
||||||
|
recurse = @{ type = "bool"; default = $false }
|
||||||
|
size = @{ type = "str" }
|
||||||
|
use_regex = @{ type = "bool"; default = $false }
|
||||||
|
get_checksum = @{ type = "bool"; default = $true }
|
||||||
|
checksum_algorithm = @{ type = "str"; default = "sha1"; choices = "md5", "sha1", "sha256", "sha384", "sha512" }
|
||||||
|
}
|
||||||
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
|
||||||
|
$paths = $module.Params.paths
|
||||||
|
$age = $module.Params.age
|
||||||
|
$age_stamp = $module.Params.age_stamp
|
||||||
|
$file_type = $module.Params.file_type
|
||||||
|
$follow = $module.Params.follow
|
||||||
|
$hidden = $module.Params.hidden
|
||||||
|
$patterns = $module.Params.patterns
|
||||||
|
$recurse = $module.Params.recurse
|
||||||
|
$size = $module.Params.size
|
||||||
|
$use_regex = $module.Params.use_regex
|
||||||
|
$get_checksum = $module.Params.get_checksum
|
||||||
|
$checksum_algorithm = $module.Params.checksum_algorithm
|
||||||
|
|
||||||
|
$module.Result.examined = 0
|
||||||
|
$module.Result.files = @()
|
||||||
|
$module.Result.matched = 0
|
||||||
|
|
||||||
|
Load-LinkUtils
|
||||||
|
|
||||||
|
Function Assert-Age {
|
||||||
|
Param (
|
||||||
|
[System.IO.FileSystemInfo]$File,
|
||||||
|
[System.Int64]$Age,
|
||||||
|
[System.String]$AgeStamp
|
||||||
|
)
|
||||||
|
|
||||||
|
$actual_age = switch ($AgeStamp) {
|
||||||
|
mtime { $File.LastWriteTime.Ticks }
|
||||||
|
ctime { $File.CreationTime.Ticks }
|
||||||
|
atime { $File.LastAccessTime.Ticks }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Age -ge 0) {
|
||||||
|
return $Age -ge $actual_age
|
||||||
|
} else {
|
||||||
|
return ($Age * -1) -le $actual_age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Assert-FileType {
|
||||||
|
Param (
|
||||||
|
[System.IO.FileSystemInfo]$File,
|
||||||
|
[System.String]$FileType
|
||||||
|
)
|
||||||
|
|
||||||
|
$is_dir = $File.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||||||
|
return ($FileType -eq 'directory' -and $is_dir) -or ($FileType -eq 'file' -and -not $is_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Assert-FileHidden {
|
||||||
|
Param (
|
||||||
|
[System.IO.FileSystemInfo]$File,
|
||||||
|
[Switch]$IsHidden
|
||||||
|
)
|
||||||
|
|
||||||
|
$file_is_hidden = $File.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||||||
|
return $IsHidden.IsPresent -eq $file_is_hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Function Assert-FileNamePattern {
|
||||||
|
Param (
|
||||||
|
[System.IO.FileSystemInfo]$File,
|
||||||
|
[System.String[]]$Patterns,
|
||||||
|
[Switch]$UseRegex
|
||||||
|
)
|
||||||
|
|
||||||
|
$valid_match = $false
|
||||||
|
foreach ($pattern in $Patterns) {
|
||||||
|
if ($UseRegex) {
|
||||||
|
if ($File.Name -match $pattern) {
|
||||||
|
$valid_match = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($File.Name -like $pattern) {
|
||||||
|
$valid_match = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $valid_match
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Assert-FileSize {
|
||||||
|
Param (
|
||||||
|
[System.IO.FileSystemInfo]$File,
|
||||||
|
[System.Int64]$Size
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Size -ge 0) {
|
||||||
|
return $File.Length -ge $Size
|
||||||
|
} else {
|
||||||
|
return $File.Length -le ($Size * -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-FileChecksum {
|
||||||
|
Param (
|
||||||
|
[System.String]$Path,
|
||||||
|
[System.String]$Algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
$sp = switch ($algorithm) {
|
||||||
|
'md5' { New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||||||
|
'sha1' { New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||||||
|
'sha256' { New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||||||
|
'sha384' { New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||||||
|
'sha512' { New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||||||
|
}
|
||||||
|
|
||||||
|
$fp = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||||
|
try {
|
||||||
|
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
|
||||||
|
} finally {
|
||||||
|
$fp.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hash
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Search-Path {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param (
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[System.String]
|
||||||
|
$Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[System.Collections.Generic.HashSet`1[System.String]]
|
||||||
|
$CheckedPaths,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[Object]
|
||||||
|
$Module,
|
||||||
|
|
||||||
|
[System.Int64]
|
||||||
|
$Age,
|
||||||
|
|
||||||
|
[System.String]
|
||||||
|
$AgeStamp,
|
||||||
|
|
||||||
|
[System.String]
|
||||||
|
$FileType,
|
||||||
|
|
||||||
|
[Switch]
|
||||||
|
$Follow,
|
||||||
|
|
||||||
|
[Switch]
|
||||||
|
$GetChecksum,
|
||||||
|
|
||||||
|
[Switch]
|
||||||
|
$IsHidden,
|
||||||
|
|
||||||
|
[System.String[]]
|
||||||
|
$Patterns,
|
||||||
|
|
||||||
|
[Switch]
|
||||||
|
$Recurse,
|
||||||
|
|
||||||
|
[System.Int64]
|
||||||
|
$Size,
|
||||||
|
|
||||||
|
[Switch]
|
||||||
|
$UseRegex
|
||||||
|
)
|
||||||
|
|
||||||
|
$dir_obj = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path
|
||||||
|
if ([Int32]$dir_obj.Attributes -eq -1) {
|
||||||
|
$Module.Warn("Argument path '$Path' does not exist, skipping")
|
||||||
|
return
|
||||||
|
} elseif (-not $dir_obj.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||||||
|
$Module.Warn("Argument path '$Path' is a file not a directory, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir_files = @()
|
||||||
|
try {
|
||||||
|
$dir_files = $dir_obj.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::TopDirectoryOnly)
|
||||||
|
} catch [System.IO.DirectoryNotFoundException] { # Broken ReparsePoint/Symlink, cannot enumerate
|
||||||
|
} catch [System.UnauthorizedAccessException] {} # No ListDirectory permissions, Get-ChildItem ignored this
|
||||||
|
|
||||||
|
foreach ($dir_child in $dir_files) {
|
||||||
|
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -and $Recurse) {
|
||||||
|
if ($Follow -or -not $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
|
||||||
|
$PSBoundParameters.Remove('Path') > $null
|
||||||
|
Search-Path -Path $dir_child.FullName @PSBoundParameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check to see if we've already encountered this path and skip if we have.
|
||||||
|
if (-not $CheckedPaths.Add($dir_child.FullName.ToLowerInvariant())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$Module.Result.examined++
|
||||||
|
|
||||||
|
if ($PSBoundParameters.ContainsKey('Age')) {
|
||||||
|
$age_match = Assert-Age -File $dir_child -Age $Age -AgeStamp $AgeStamp
|
||||||
|
} else {
|
||||||
|
$age_match = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_type_match = Assert-FileType -File $dir_child -FileType $FileType
|
||||||
|
$hidden_match = Assert-FileHidden -File $dir_child -IsHidden:$IsHidden
|
||||||
|
|
||||||
|
if ($PSBoundParameters.ContainsKey('Patterns')) {
|
||||||
|
$pattern_match = Assert-FileNamePattern -File $dir_child -Patterns $Patterns -UseRegex:$UseRegex.IsPresent
|
||||||
|
} else {
|
||||||
|
$pattern_match = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSBoundParameters.ContainsKey('Size')) {
|
||||||
|
$size_match = Assert-FileSize -File $dir_child -Size $Size
|
||||||
|
} else {
|
||||||
|
$size_match = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($age_match -and $file_type_match -and $hidden_match -and $pattern_match -and $size_match)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# It passed all our filters so add it
|
||||||
|
$module.Result.matched++
|
||||||
|
|
||||||
|
# TODO: Make this generic so it can be shared with win_find and win_stat.
|
||||||
|
$epoch = New-Object -Type System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
|
||||||
|
$file_info = @{
|
||||||
|
attributes = $dir_child.Attributes.ToString()
|
||||||
|
checksum = $null
|
||||||
|
creationtime = (New-TimeSpan -Start $epoch -End $dir_child.CreationTime).TotalSeconds
|
||||||
|
exists = $true
|
||||||
|
extension = $null
|
||||||
|
filename = $dir_child.Name
|
||||||
|
isarchive = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Archive)
|
||||||
|
isdir = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||||||
|
ishidden = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||||||
|
isreadonly = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReadOnly)
|
||||||
|
isreg = $false
|
||||||
|
isshared = $false
|
||||||
|
lastaccesstime = (New-TimeSpan -Start $epoch -End $dir_child.LastAccessTime).TotalSeconds
|
||||||
|
lastwritetime = (New-TimeSpan -Start $epoch -End $dir_child.LastWriteTime).TotalSeconds
|
||||||
|
owner = $null
|
||||||
|
path = $dir_child.FullName
|
||||||
|
sharename = $null
|
||||||
|
size = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$file_info.owner = $dir_child.GetAccessControl().Owner
|
||||||
|
} catch {} # May not have rights to get the Owner, historical behaviour is to ignore.
|
||||||
|
|
||||||
|
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||||||
|
$share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($dir_child.FullName -replace '\\', '\\')'"
|
||||||
|
if ($null -ne $share_info) {
|
||||||
|
$file_info.isshared = $true
|
||||||
|
$file_info.sharename = $share_info.Name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$file_info.extension = $dir_child.Extension
|
||||||
|
$file_info.isreg = $true
|
||||||
|
$file_info.size = $dir_child.Length
|
||||||
|
|
||||||
|
if ($GetChecksum) {
|
||||||
|
try {
|
||||||
|
$file_info.checksum = Get-FileChecksum -Path $dir_child.FullName -Algorithm $checksum_algorithm
|
||||||
|
} catch {} # Just keep the checksum as $null in the case of a failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Append the link information if the path is a link
|
||||||
|
$link_info = @{
|
||||||
|
isjunction = $false
|
||||||
|
islnk = $false
|
||||||
|
nlink = 1
|
||||||
|
lnk_source = $null
|
||||||
|
lnk_target = $null
|
||||||
|
hlnk_targets = @()
|
||||||
|
}
|
||||||
|
$link_stat = Get-Link -link_path $dir_child.FullName
|
||||||
|
if ($null -ne $link_stat) {
|
||||||
|
switch ($link_stat.Type) {
|
||||||
|
"SymbolicLink" {
|
||||||
|
$link_info.islnk = $true
|
||||||
|
$link_info.isreg = $false
|
||||||
|
$link_info.lnk_source = $link_stat.AbsolutePath
|
||||||
|
$link_info.lnk_target = $link_stat.TargetPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"JunctionPoint" {
|
||||||
|
$link_info.isjunction = $true
|
||||||
|
$link_info.isreg = $false
|
||||||
|
$link_info.lnk_source = $link_stat.AbsolutePath
|
||||||
|
$link_info.lnk_target = $link_stat.TargetPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
"HardLink" {
|
||||||
|
$link_info.nlink = $link_stat.HardTargets.Count
|
||||||
|
|
||||||
|
# remove current path from the targets
|
||||||
|
$hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $dir_child.FullName }
|
||||||
|
$link_info.hlnk_targets = @($hlnk_targets)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($kv in $link_info.GetEnumerator()) {
|
||||||
|
$file_info.$($kv.Key) = $kv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output the file_info object
|
||||||
|
$file_info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$search_params = @{
|
||||||
|
CheckedPaths = [System.Collections.Generic.HashSet`1[System.String]]@()
|
||||||
|
GetChecksum = $get_checksum
|
||||||
|
Module = $module
|
||||||
|
FileType = $file_type
|
||||||
|
Follow = $follow
|
||||||
|
IsHidden = $hidden
|
||||||
|
Recurse = $recurse
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $age) {
|
||||||
|
$seconds_per_unit = @{'s'=1; 'm'=60; 'h'=3600; 'd'=86400; 'w'=604800}
|
||||||
|
$seconds_pattern = '^(-?\d+)(s|m|h|d|w)?$'
|
||||||
|
$match = $age -match $seconds_pattern
|
||||||
|
if ($Match) {
|
||||||
|
$specified_seconds = [Int64]$Matches[1]
|
||||||
|
if ($null -eq $Matches[2]) {
|
||||||
|
$chosen_unit = 's'
|
||||||
|
} else {
|
||||||
|
$chosen_unit = $Matches[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_seconds = $specified_seconds * ($seconds_per_unit.$chosen_unit)
|
||||||
|
|
||||||
|
if ($total_seconds -ge 0) {
|
||||||
|
$search_params.Age = (Get-Date).AddSeconds($total_seconds * -1).Ticks
|
||||||
|
} else {
|
||||||
|
# Make sure we add the positive value of seconds to current time then make it negative for later comparisons.
|
||||||
|
$age = (Get-Date).AddSeconds($total_seconds).Ticks
|
||||||
|
$search_params.Age = $age * -1
|
||||||
|
}
|
||||||
|
$search_params.AgeStamp = $age_stamp
|
||||||
|
} else {
|
||||||
|
$module.FailJson("Invalid age pattern specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $patterns) {
|
||||||
|
$search_params.Patterns = $patterns
|
||||||
|
$search_params.UseRegex = $use_regex
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $size) {
|
||||||
|
$bytes_per_unit = @{'b'=1; 'k'=1KB; 'm'=1MB; 'g'=1GB;'t'=1TB}
|
||||||
|
$size_pattern = '^(-?\d+)(b|k|m|g|t)?$'
|
||||||
|
$match = $size -match $size_pattern
|
||||||
|
if ($Match) {
|
||||||
|
$specified_size = [Int64]$Matches[1]
|
||||||
|
if ($null -eq $Matches[2]) {
|
||||||
|
$chosen_byte = 'b'
|
||||||
|
} else {
|
||||||
|
$chosen_byte = $Matches[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
$search_params.Size = $specified_size * ($bytes_per_unit.$chosen_byte)
|
||||||
|
} else {
|
||||||
|
$module.FailJson("Invalid size pattern specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$matched_files = foreach ($path in $paths) {
|
||||||
|
# Ensure we pass in an absolute path. We use the ExecutionContext as this is based on the PSProvider path not the
|
||||||
|
# process location which can be different.
|
||||||
|
$abs_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
|
||||||
|
Search-Path -Path $abs_path @search_params
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make sure we sort the files in alphabetical order.
|
||||||
|
$module.Result.files = @() + ($matched_files | Sort-Object -Property {$_.path})
|
||||||
|
|
||||||
|
$module.ExitJson()
|
||||||
|
|
@ -0,0 +1,345 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2016, Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_find
|
||||||
|
version_added: "2.3"
|
||||||
|
short_description: Return a list of files based on specific criteria
|
||||||
|
description:
|
||||||
|
- Return a list of files based on specified criteria.
|
||||||
|
- Multiple criteria are AND'd together.
|
||||||
|
- For non-Windows targets, use the M(find) module instead.
|
||||||
|
options:
|
||||||
|
age:
|
||||||
|
description:
|
||||||
|
- Select files or folders whose age is equal to or greater than
|
||||||
|
the specified time.
|
||||||
|
- Use a negative age to find files equal to or less than
|
||||||
|
the specified time.
|
||||||
|
- You can choose seconds, minutes, hours, days or weeks
|
||||||
|
by specifying the first letter of an of
|
||||||
|
those words (e.g., "2s", "10d", 1w").
|
||||||
|
type: str
|
||||||
|
age_stamp:
|
||||||
|
description:
|
||||||
|
- Choose the file property against which we compare C(age).
|
||||||
|
- The default attribute we compare with is the last modification time.
|
||||||
|
type: str
|
||||||
|
choices: [ atime, ctime, mtime ]
|
||||||
|
default: mtime
|
||||||
|
checksum_algorithm:
|
||||||
|
description:
|
||||||
|
- Algorithm to determine the checksum of a file.
|
||||||
|
- Will throw an error if the host is unable to use specified algorithm.
|
||||||
|
type: str
|
||||||
|
choices: [ md5, sha1, sha256, sha384, sha512 ]
|
||||||
|
default: sha1
|
||||||
|
file_type:
|
||||||
|
description: Type of file to search for.
|
||||||
|
type: str
|
||||||
|
choices: [ directory, file ]
|
||||||
|
default: file
|
||||||
|
follow:
|
||||||
|
description:
|
||||||
|
- Set this to C(yes) to follow symlinks in the path.
|
||||||
|
- This needs to be used in conjunction with C(recurse).
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
get_checksum:
|
||||||
|
description:
|
||||||
|
- Whether to return a checksum of the file in the return info (default sha1),
|
||||||
|
use C(checksum_algorithm) to change from the default.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
hidden:
|
||||||
|
description: Set this to include hidden files or folders.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
paths:
|
||||||
|
description:
|
||||||
|
- List of paths of directories to search for files or folders in.
|
||||||
|
- This can be supplied as a single path or a list of paths.
|
||||||
|
type: list
|
||||||
|
required: yes
|
||||||
|
patterns:
|
||||||
|
description:
|
||||||
|
- One or more (powershell or regex) patterns to compare filenames with.
|
||||||
|
- The type of pattern matching is controlled by C(use_regex) option.
|
||||||
|
- The patterns restrict the list of files or folders to be returned based on the filenames.
|
||||||
|
- For a file to be matched it only has to match with one pattern in a list provided.
|
||||||
|
type: list
|
||||||
|
aliases: [ "regex", "regexp" ]
|
||||||
|
recurse:
|
||||||
|
description:
|
||||||
|
- Will recursively descend into the directory looking for files or folders.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Select files or folders whose size is equal to or greater than the specified size.
|
||||||
|
- Use a negative value to find files equal to or less than the specified size.
|
||||||
|
- You can specify the size with a suffix of the byte type i.e. kilo = k, mega = m...
|
||||||
|
- Size is not evaluated for symbolic links.
|
||||||
|
type: str
|
||||||
|
use_regex:
|
||||||
|
description:
|
||||||
|
- Will set patterns to run as a regex check if set to C(yes).
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
author:
|
||||||
|
- Jordan Borean (@jborean93)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Find files in path
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
|
||||||
|
- name: Find hidden files in path
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
hidden: yes
|
||||||
|
|
||||||
|
- name: Find files in multiple paths
|
||||||
|
win_find:
|
||||||
|
paths:
|
||||||
|
- C:\Temp
|
||||||
|
- D:\Temp
|
||||||
|
|
||||||
|
- name: Find files in directory while searching recursively
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
recurse: yes
|
||||||
|
|
||||||
|
- name: Find files in directory while following symlinks
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
recurse: yes
|
||||||
|
follow: yes
|
||||||
|
|
||||||
|
- name: Find files with .log and .out extension using powershell wildcards
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
patterns: [ '*.log', '*.out' ]
|
||||||
|
|
||||||
|
- name: Find files in path based on regex pattern
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
patterns: out_\d{8}-\d{6}.log
|
||||||
|
|
||||||
|
- name: Find files older than 1 day
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
age: 86400
|
||||||
|
|
||||||
|
- name: Find files older than 1 day based on create time
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
age: 86400
|
||||||
|
age_stamp: ctime
|
||||||
|
|
||||||
|
- name: Find files older than 1 day with unit syntax
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
age: 1d
|
||||||
|
|
||||||
|
- name: Find files newer than 1 hour
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
age: -3600
|
||||||
|
|
||||||
|
- name: Find files newer than 1 hour with unit syntax
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
age: -1h
|
||||||
|
|
||||||
|
- name: Find files larger than 1MB
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
size: 1048576
|
||||||
|
|
||||||
|
- name: Find files larger than 1GB with unit syntax
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
size: 1g
|
||||||
|
|
||||||
|
- name: Find files smaller than 1MB
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
size: -1048576
|
||||||
|
|
||||||
|
- name: Find files smaller than 1GB with unit syntax
|
||||||
|
win_find:
|
||||||
|
paths: D:\Temp
|
||||||
|
size: -1g
|
||||||
|
|
||||||
|
- name: Find folders/symlinks in multiple paths
|
||||||
|
win_find:
|
||||||
|
paths:
|
||||||
|
- C:\Temp
|
||||||
|
- D:\Temp
|
||||||
|
file_type: directory
|
||||||
|
|
||||||
|
- name: Find files and return SHA256 checksum of files found
|
||||||
|
win_find:
|
||||||
|
paths: C:\Temp
|
||||||
|
get_checksum: yes
|
||||||
|
checksum_algorithm: sha256
|
||||||
|
|
||||||
|
- name: Find files and do not return the checksum
|
||||||
|
win_find:
|
||||||
|
paths: C:\Temp
|
||||||
|
get_checksum: no
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
examined:
|
||||||
|
description: The number of files/folders that was checked.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 10
|
||||||
|
matched:
|
||||||
|
description: The number of files/folders that match the criteria.
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 2
|
||||||
|
files:
|
||||||
|
description: Information on the files/folders that match the criteria returned as a list of dictionary elements
|
||||||
|
for each file matched. The entries are sorted by the path value alphabetically.
|
||||||
|
returned: success
|
||||||
|
type: complex
|
||||||
|
contains:
|
||||||
|
attributes:
|
||||||
|
description: attributes of the file at path in raw form.
|
||||||
|
returned: success, path exists
|
||||||
|
type: str
|
||||||
|
sample: "Archive, Hidden"
|
||||||
|
checksum:
|
||||||
|
description: The checksum of a file based on checksum_algorithm specified.
|
||||||
|
returned: success, path exists, path is a file, get_checksum == True
|
||||||
|
type: str
|
||||||
|
sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
|
||||||
|
creationtime:
|
||||||
|
description: The create time of the file represented in seconds since epoch.
|
||||||
|
returned: success, path exists
|
||||||
|
type: float
|
||||||
|
sample: 1477984205.15
|
||||||
|
exists:
|
||||||
|
description: Whether the file exists, will always be true for M(win_find).
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
extension:
|
||||||
|
description: The extension of the file at path.
|
||||||
|
returned: success, path exists, path is a file
|
||||||
|
type: str
|
||||||
|
sample: ".ps1"
|
||||||
|
filename:
|
||||||
|
description: The name of the file.
|
||||||
|
returned: success, path exists
|
||||||
|
type: str
|
||||||
|
sample: temp
|
||||||
|
hlnk_targets:
|
||||||
|
description: List of other files pointing to the same file (hard links), excludes the current file.
|
||||||
|
returned: success, path exists
|
||||||
|
type: list
|
||||||
|
sample:
|
||||||
|
- C:\temp\file.txt
|
||||||
|
- C:\Windows\update.log
|
||||||
|
isarchive:
|
||||||
|
description: If the path is ready for archiving or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
isdir:
|
||||||
|
description: If the path is a directory or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
ishidden:
|
||||||
|
description: If the path is hidden or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
isjunction:
|
||||||
|
description: If the path is a junction point.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
islnk:
|
||||||
|
description: If the path is a symbolic link.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
isreadonly:
|
||||||
|
description: If the path is read only or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
isreg:
|
||||||
|
description: If the path is a regular file or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
isshared:
|
||||||
|
description: If the path is shared or not.
|
||||||
|
returned: success, path exists
|
||||||
|
type: bool
|
||||||
|
sample: true
|
||||||
|
lastaccesstime:
|
||||||
|
description: The last access time of the file represented in seconds since epoch.
|
||||||
|
returned: success, path exists
|
||||||
|
type: float
|
||||||
|
sample: 1477984205.15
|
||||||
|
lastwritetime:
|
||||||
|
description: The last modification time of the file represented in seconds since epoch.
|
||||||
|
returned: success, path exists
|
||||||
|
type: float
|
||||||
|
sample: 1477984205.15
|
||||||
|
lnk_source:
|
||||||
|
description: The target of the symlink normalized for the remote filesystem.
|
||||||
|
returned: success, path exists, path is a symbolic link or junction point
|
||||||
|
type: str
|
||||||
|
sample: C:\temp
|
||||||
|
lnk_target:
|
||||||
|
description: The target of the symlink. Note that relative paths remain relative, will return null if not a link.
|
||||||
|
returned: success, path exists, path is a symbolic link or junction point
|
||||||
|
type: str
|
||||||
|
sample: temp
|
||||||
|
nlink:
|
||||||
|
description: Number of links to the file (hard links)
|
||||||
|
returned: success, path exists
|
||||||
|
type: int
|
||||||
|
sample: 1
|
||||||
|
owner:
|
||||||
|
description: The owner of the file.
|
||||||
|
returned: success, path exists
|
||||||
|
type: str
|
||||||
|
sample: BUILTIN\Administrators
|
||||||
|
path:
|
||||||
|
description: The full absolute path to the file.
|
||||||
|
returned: success, path exists
|
||||||
|
type: str
|
||||||
|
sample: BUILTIN\Administrators
|
||||||
|
sharename:
|
||||||
|
description: The name of share if folder is shared.
|
||||||
|
returned: success, path exists, path is a directory and isshared == True
|
||||||
|
type: str
|
||||||
|
sample: file-share
|
||||||
|
size:
|
||||||
|
description: The size in bytes of the file.
|
||||||
|
returned: success, path exists, path is a file
|
||||||
|
type: int
|
||||||
|
sample: 1024
|
||||||
|
'''
|
@ -0,0 +1,200 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -OSVersion 6.2
|
||||||
|
|
||||||
|
Set-StrictMode -Version 2
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{
|
||||||
|
drive_letter = @{ type = "str" }
|
||||||
|
path = @{ type = "str" }
|
||||||
|
label = @{ type = "str" }
|
||||||
|
new_label = @{ type = "str" }
|
||||||
|
file_system = @{ type = "str"; choices = "ntfs", "refs", "exfat", "fat32", "fat" }
|
||||||
|
allocation_unit_size = @{ type = "int" }
|
||||||
|
large_frs = @{ type = "bool" }
|
||||||
|
full = @{ type = "bool"; default = $false }
|
||||||
|
compress = @{ type = "bool" }
|
||||||
|
integrity_streams = @{ type = "bool" }
|
||||||
|
force = @{ type = "bool"; default = $false }
|
||||||
|
}
|
||||||
|
mutually_exclusive = @(
|
||||||
|
,@('drive_letter', 'path', 'label')
|
||||||
|
)
|
||||||
|
required_one_of = @(
|
||||||
|
,@('drive_letter', 'path', 'label')
|
||||||
|
)
|
||||||
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
|
||||||
|
$drive_letter = $module.Params.drive_letter
|
||||||
|
$path = $module.Params.path
|
||||||
|
$label = $module.Params.label
|
||||||
|
$new_label = $module.Params.new_label
|
||||||
|
$file_system = $module.Params.file_system
|
||||||
|
$allocation_unit_size = $module.Params.allocation_unit_size
|
||||||
|
$large_frs = $module.Params.large_frs
|
||||||
|
$full_format = $module.Params.full
|
||||||
|
$compress_volume = $module.Params.compress
|
||||||
|
$integrity_streams = $module.Params.integrity_streams
|
||||||
|
$force_format = $module.Params.force
|
||||||
|
|
||||||
|
# Some pre-checks
|
||||||
|
if ($null -ne $drive_letter -and $drive_letter -notmatch "^[a-zA-Z]$") {
|
||||||
|
$module.FailJson("The parameter drive_letter should be a single character A-Z")
|
||||||
|
}
|
||||||
|
if ($integrity_streams -eq $true -and $file_system -ne "refs") {
|
||||||
|
$module.FailJson("Integrity streams can be enabled only on ReFS volumes. You specified: $($file_system)")
|
||||||
|
}
|
||||||
|
if ($compress_volume -eq $true) {
|
||||||
|
if ($file_system -eq "ntfs") {
|
||||||
|
if ($null -ne $allocation_unit_size -and $allocation_unit_size -gt 4096) {
|
||||||
|
$module.FailJson("NTFS compression is not supported for allocation unit sizes above 4096")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$module.FailJson("Compression can be enabled only on NTFS volumes. You specified: $($file_system)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AnsibleVolume {
|
||||||
|
param(
|
||||||
|
$DriveLetter,
|
||||||
|
$Path,
|
||||||
|
$Label
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -ne $DriveLetter) {
|
||||||
|
try {
|
||||||
|
$volume = Get-Volume -DriveLetter $DriveLetter
|
||||||
|
} catch {
|
||||||
|
$module.FailJson("There was an error retrieving the volume using drive_letter $($DriveLetter): $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($null -ne $Path) {
|
||||||
|
try {
|
||||||
|
$volume = Get-Volume -Path $Path
|
||||||
|
} catch {
|
||||||
|
$module.FailJson("There was an error retrieving the volume using path $($Path): $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($null -ne $Label) {
|
||||||
|
try {
|
||||||
|
$volume = Get-Volume -FileSystemLabel $Label
|
||||||
|
} catch {
|
||||||
|
$module.FailJson("There was an error retrieving the volume using label $($Label): $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$module.FailJson("Unable to locate volume: drive_letter, path and label were not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return $volume
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-AnsibleVolume {
|
||||||
|
param(
|
||||||
|
$Path,
|
||||||
|
$Label,
|
||||||
|
$FileSystem,
|
||||||
|
$Full,
|
||||||
|
$UseLargeFRS,
|
||||||
|
$Compress,
|
||||||
|
$SetIntegrityStreams,
|
||||||
|
$AllocationUnitSize
|
||||||
|
)
|
||||||
|
$parameters = @{
|
||||||
|
Path = $Path
|
||||||
|
Full = $Full
|
||||||
|
}
|
||||||
|
if ($null -ne $UseLargeFRS) {
|
||||||
|
$parameters.Add("UseLargeFRS", $UseLargeFRS)
|
||||||
|
}
|
||||||
|
if ($null -ne $SetIntegrityStreams) {
|
||||||
|
$parameters.Add("SetIntegrityStreams", $SetIntegrityStreams)
|
||||||
|
}
|
||||||
|
if ($null -ne $Compress){
|
||||||
|
$parameters.Add("Compress", $Compress)
|
||||||
|
}
|
||||||
|
if ($null -ne $Label) {
|
||||||
|
$parameters.Add("NewFileSystemLabel", $Label)
|
||||||
|
}
|
||||||
|
if ($null -ne $FileSystem) {
|
||||||
|
$parameters.Add("FileSystem", $FileSystem)
|
||||||
|
}
|
||||||
|
if ($null -ne $AllocationUnitSize) {
|
||||||
|
$parameters.Add("AllocationUnitSize", $AllocationUnitSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
Format-Volume @parameters -Confirm:$false | Out-Null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansible_volume = Get-AnsibleVolume -DriveLetter $drive_letter -Path $path -Label $label
|
||||||
|
$ansible_file_system = $ansible_volume.FileSystem
|
||||||
|
$ansible_volume_size = $ansible_volume.Size
|
||||||
|
$ansible_volume_alu = (Get-CimInstance -ClassName Win32_Volume -Filter "DeviceId = '$($ansible_volume.path.replace('\','\\'))'" -Property BlockSize).BlockSize
|
||||||
|
|
||||||
|
$ansible_partition = Get-Partition -Volume $ansible_volume
|
||||||
|
|
||||||
|
if (-not $force_format -and $null -ne $allocation_unit_size -and $ansible_volume_alu -ne 0 -and $null -ne $ansible_volume_alu -and $allocation_unit_size -ne $ansible_volume_alu) {
|
||||||
|
$module.FailJson("Force format must be specified since target allocation unit size: $($allocation_unit_size) is different from the current allocation unit size of the volume: $($ansible_volume_alu)")
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($access_path in $ansible_partition.AccessPaths) {
|
||||||
|
if ($access_path -ne $Path) {
|
||||||
|
if ($null -ne $file_system -and
|
||||||
|
-not [string]::IsNullOrEmpty($ansible_file_system) -and
|
||||||
|
$file_system -ne $ansible_file_system)
|
||||||
|
{
|
||||||
|
if (-not $force_format)
|
||||||
|
{
|
||||||
|
$no_files_in_volume = (Get-ChildItem -LiteralPath $access_path -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0
|
||||||
|
if($no_files_in_volume)
|
||||||
|
{
|
||||||
|
$module.FailJson("Force format must be specified since target file system: $($file_system) is different from the current file system of the volume: $($ansible_file_system.ToLower())")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$module.FailJson("Force format must be specified to format non-pristine volumes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$pristine = -not $force_format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($force_format) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
|
Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume -AllocationUnitSize $allocation_unit_size
|
||||||
|
}
|
||||||
|
$module.Result.changed = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($pristine) {
|
||||||
|
if ($null -eq $new_label) {
|
||||||
|
$new_label = $ansible_volume.FileSystemLabel
|
||||||
|
}
|
||||||
|
# Conditions for formatting
|
||||||
|
if ($ansible_volume_size -eq 0 -or
|
||||||
|
$ansible_volume.FileSystemLabel -ne $new_label) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
|
Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume -AllocationUnitSize $allocation_unit_size
|
||||||
|
}
|
||||||
|
$module.Result.changed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.ExitJson()
|
@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {
|
||||||
|
'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'
|
||||||
|
}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
module: win_format
|
||||||
|
version_added: '2.8'
|
||||||
|
short_description: Formats an existing volume or a new volume on an existing partition on Windows
|
||||||
|
description:
|
||||||
|
- The M(win_format) module formats an existing volume or a new volume on an existing partition on Windows
|
||||||
|
options:
|
||||||
|
drive_letter:
|
||||||
|
description:
|
||||||
|
- Used to specify the drive letter of the volume to be formatted.
|
||||||
|
type: str
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- Used to specify the path to the volume to be formatted.
|
||||||
|
type: str
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Used to specify the label of the volume to be formatted.
|
||||||
|
type: str
|
||||||
|
new_label:
|
||||||
|
description:
|
||||||
|
- Used to specify the new file system label of the formatted volume.
|
||||||
|
type: str
|
||||||
|
file_system:
|
||||||
|
description:
|
||||||
|
- Used to specify the file system to be used when formatting the target volume.
|
||||||
|
type: str
|
||||||
|
choices: [ ntfs, refs, exfat, fat32, fat ]
|
||||||
|
allocation_unit_size:
|
||||||
|
description:
|
||||||
|
- Specifies the cluster size to use when formatting the volume.
|
||||||
|
- If no cluster size is specified when you format a partition, defaults are selected based on
|
||||||
|
the size of the partition.
|
||||||
|
- This value must be a multiple of the physical sector size of the disk.
|
||||||
|
type: int
|
||||||
|
large_frs:
|
||||||
|
description:
|
||||||
|
- Specifies that large File Record System (FRS) should be used.
|
||||||
|
type: bool
|
||||||
|
compress:
|
||||||
|
description:
|
||||||
|
- Enable compression on the resulting NTFS volume.
|
||||||
|
- NTFS compression is not supported where I(allocation_unit_size) is more than 4096.
|
||||||
|
type: bool
|
||||||
|
integrity_streams:
|
||||||
|
description:
|
||||||
|
- Enable integrity streams on the resulting ReFS volume.
|
||||||
|
type: bool
|
||||||
|
full:
|
||||||
|
description:
|
||||||
|
- A full format writes to every sector of the disk, takes much longer to perform than the
|
||||||
|
default (quick) format, and is not recommended on storage that is thinly provisioned.
|
||||||
|
- Specify C(true) for full format.
|
||||||
|
type: bool
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- Specify if formatting should be forced for volumes that are not created from new partitions
|
||||||
|
or if the source and target file system are different.
|
||||||
|
type: bool
|
||||||
|
notes:
|
||||||
|
- Microsoft Windows Server 2012 or Microsoft Windows 8 or newer is required to use this module. To check if your system is compatible, see
|
||||||
|
U(https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version).
|
||||||
|
- One of three parameters (I(drive_letter), I(path) and I(label)) are mandatory to identify the target
|
||||||
|
volume but more than one cannot be specified at the same time.
|
||||||
|
- This module is idempotent if I(force) is not specified and file system labels remain preserved.
|
||||||
|
- For more information, see U(https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/format-msft-volume)
|
||||||
|
seealso:
|
||||||
|
- module: win_disk_facts
|
||||||
|
- module: win_partition
|
||||||
|
author:
|
||||||
|
- Varun Chopra (@chopraaa) <v@chopraaa.com>
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Create a partition with drive letter D and size 5 GiB
|
||||||
|
win_partition:
|
||||||
|
drive_letter: D
|
||||||
|
partition_size: 5 GiB
|
||||||
|
disk_number: 1
|
||||||
|
|
||||||
|
- name: Full format the newly created partition as NTFS and label it
|
||||||
|
win_format:
|
||||||
|
drive_letter: D
|
||||||
|
file_system: NTFS
|
||||||
|
new_label: Formatted
|
||||||
|
full: True
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
#
|
||||||
|
'''
|
@ -0,0 +1,277 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2015, Paul Durivage <paul.durivage@rackspace.com>
|
||||||
|
# Copyright: (c) 2015, Tal Auslander <tal@cloudshare.com>
|
||||||
|
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
|
||||||
|
# Copyright: (c) 2019, Viktor Utkin <viktor_utkin@epam.com>
|
||||||
|
# Copyright: (c) 2019, Uladzimir Klybik <uladzimir_klybik@epam.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||||
|
#Requires -Module Ansible.ModuleUtils.WebRequest
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{
|
||||||
|
url = @{ type="str"; required=$true }
|
||||||
|
dest = @{ type='path'; required=$true }
|
||||||
|
force = @{ type='bool'; default=$true }
|
||||||
|
checksum = @{ type='str' }
|
||||||
|
checksum_algorithm = @{ type='str'; default='sha1'; choices = @("md5", "sha1", "sha256", "sha384", "sha512") }
|
||||||
|
checksum_url = @{ type='str' }
|
||||||
|
|
||||||
|
# Defined for the alias backwards compatibility, remove once aliases are removed
|
||||||
|
url_username = @{
|
||||||
|
aliases = @("user", "username")
|
||||||
|
deprecated_aliases = @(
|
||||||
|
@{ name = "user"; version = "2.14" },
|
||||||
|
@{ name = "username"; version = "2.14" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
url_password = @{
|
||||||
|
aliases = @("password")
|
||||||
|
deprecated_aliases = @(
|
||||||
|
@{ name = "password"; version = "2.14" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutually_exclusive = @(
|
||||||
|
,@('checksum', 'checksum_url')
|
||||||
|
)
|
||||||
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
$spec = Merge-WebRequestSpec -ModuleSpec $spec
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
|
||||||
|
$url = $module.Params.url
|
||||||
|
$dest = $module.Params.dest
|
||||||
|
$force = $module.Params.force
|
||||||
|
$checksum = $module.Params.checksum
|
||||||
|
$checksum_algorithm = $module.Params.checksum_algorithm
|
||||||
|
$checksum_url = $module.Params.checksum_url
|
||||||
|
|
||||||
|
$module.Result.elapsed = 0
|
||||||
|
$module.Result.url = $url
|
||||||
|
|
||||||
|
Function Get-ChecksumFromUri {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||||
|
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||||
|
[Uri]$SourceUri
|
||||||
|
)
|
||||||
|
|
||||||
|
$script = {
|
||||||
|
param($Response, $Stream)
|
||||||
|
|
||||||
|
$read_stream = New-Object -TypeName System.IO.StreamReader -ArgumentList $Stream
|
||||||
|
$web_checksum = $read_stream.ReadToEnd()
|
||||||
|
$basename = (Split-Path -Path $SourceUri.LocalPath -Leaf)
|
||||||
|
$basename = [regex]::Escape($basename)
|
||||||
|
$web_checksum_str = $web_checksum -split '\r?\n' | Select-String -Pattern $("\s+\.?\/?\\?" + $basename + "\s*$")
|
||||||
|
if (-not $web_checksum_str) {
|
||||||
|
$Module.FailJson("Checksum record not found for file name '$basename' in file from url: '$Uri'")
|
||||||
|
}
|
||||||
|
|
||||||
|
$web_checksum_str_splitted = $web_checksum_str[0].ToString().split(" ", 2)
|
||||||
|
$hash_from_file = $web_checksum_str_splitted[0].Trim()
|
||||||
|
# Remove any non-alphanumeric characters
|
||||||
|
$hash_from_file = $hash_from_file -replace '\W+', ''
|
||||||
|
|
||||||
|
Write-Output -InputObject $hash_from_file
|
||||||
|
}
|
||||||
|
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
|
||||||
|
} catch {
|
||||||
|
$Module.FailJson("Error when getting the remote checksum from '$Uri'. $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Compare-ModifiedFile {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares the remote URI resource against the local Dest resource. Will
|
||||||
|
return true if the LastWriteTime/LastModificationDate of the remote is
|
||||||
|
newer than the local resource date.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||||
|
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||||
|
[Parameter(Mandatory=$true)][String]$Dest
|
||||||
|
)
|
||||||
|
|
||||||
|
$dest_last_mod = (Get-AnsibleItem -Path $Dest).LastWriteTimeUtc
|
||||||
|
|
||||||
|
# If the URI is a file we don't need to go through the whole WebRequest
|
||||||
|
if ($Uri.IsFile) {
|
||||||
|
$src_last_mod = (Get-AnsibleItem -Path $Uri.AbsolutePath).LastWriteTimeUtc
|
||||||
|
} else {
|
||||||
|
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||||
|
$web_request.Method = switch ($web_request.GetType().Name) {
|
||||||
|
FtpWebRequest { [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp }
|
||||||
|
HttpWebRequest { [System.Net.WebRequestMethods+Http]::Head }
|
||||||
|
}
|
||||||
|
$script = { param($Response, $Stream); $Response.LastModified }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$src_last_mod = Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
|
||||||
|
} catch {
|
||||||
|
$Module.FailJson("Error when requesting 'Last-Modified' date from '$Uri'. $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return $true if the Uri LastModification date is newer than the Dest LastModification date
|
||||||
|
((Get-Date -Date $src_last_mod).ToUniversalTime() -gt $dest_last_mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-Checksum {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][String]$Path,
|
||||||
|
[String]$Algorithm = "sha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($Algorithm) {
|
||||||
|
'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||||||
|
'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||||||
|
'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||||||
|
'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||||||
|
'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||||||
|
}
|
||||||
|
|
||||||
|
$fs = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read,
|
||||||
|
[System.IO.FileShare]::ReadWrite)
|
||||||
|
try {
|
||||||
|
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fs)).Replace("-", "").ToLower()
|
||||||
|
} finally {
|
||||||
|
$fs.Dispose()
|
||||||
|
}
|
||||||
|
return $hash
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Invoke-DownloadFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||||
|
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||||
|
[Parameter(Mandatory=$true)][String]$Dest,
|
||||||
|
[String]$Checksum,
|
||||||
|
[String]$ChecksumAlgorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message.
|
||||||
|
$dest_parent = Split-Path -LiteralPath $Dest
|
||||||
|
if (-not (Test-Path -LiteralPath $dest_parent -PathType Container)) {
|
||||||
|
$module.FailJson("The path '$dest_parent' does not exist for destination '$Dest', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
|
||||||
|
}
|
||||||
|
|
||||||
|
$download_script = {
|
||||||
|
param($Response, $Stream)
|
||||||
|
|
||||||
|
# Download the file to a temporary directory so we can compare it
|
||||||
|
$tmp_dest = Join-Path -Path $Module.Tmpdir -ChildPath ([System.IO.Path]::GetRandomFileName())
|
||||||
|
$fs = [System.IO.File]::Create($tmp_dest)
|
||||||
|
try {
|
||||||
|
$Stream.CopyTo($fs)
|
||||||
|
$fs.Flush()
|
||||||
|
} finally {
|
||||||
|
$fs.Dispose()
|
||||||
|
}
|
||||||
|
$tmp_checksum = Get-Checksum -Path $tmp_dest -Algorithm $ChecksumAlgorithm
|
||||||
|
$Module.Result.checksum_src = $tmp_checksum
|
||||||
|
|
||||||
|
# If the checksum has been set, verify the checksum of the remote against the input checksum.
|
||||||
|
if ($Checksum -and $Checksum -ne $tmp_checksum) {
|
||||||
|
$Module.FailJson(("The checksum for {0} did not match '{1}', it was '{2}'" -f $Uri, $Checksum, $tmp_checksum))
|
||||||
|
}
|
||||||
|
|
||||||
|
$download = $true
|
||||||
|
if (Test-Path -LiteralPath $Dest) {
|
||||||
|
# Validate the remote checksum against the existing downloaded file
|
||||||
|
$dest_checksum = Get-Checksum -Path $Dest -Algorithm $ChecksumAlgorithm
|
||||||
|
|
||||||
|
# If we don't need to download anything, save the dest checksum so we don't waste time calculating it
|
||||||
|
# again at the end of the script
|
||||||
|
if ($dest_checksum -eq $tmp_checksum) {
|
||||||
|
$download = $false
|
||||||
|
$Module.Result.checksum_dest = $dest_checksum
|
||||||
|
$Module.Result.size = (Get-AnsibleItem -Path $Dest).Length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($download) {
|
||||||
|
Copy-Item -LiteralPath $tmp_dest -Destination $Dest -Force -WhatIf:$Module.CheckMode > $null
|
||||||
|
$Module.Result.changed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-WithWebRequest -Module $Module -Request $web_request -Script $download_script
|
||||||
|
} catch {
|
||||||
|
$Module.FailJson("Error downloading '$Uri' to '$Dest': $($_.Exception.Message)", $_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use last part of url for dest file name if a directory is supplied for $dest
|
||||||
|
if (Test-Path -LiteralPath $dest -PathType Container) {
|
||||||
|
$uri = [System.Uri]$url
|
||||||
|
$basename = Split-Path -Path $uri.LocalPath -Leaf
|
||||||
|
if ($uri.LocalPath -and $uri.LocalPath -ne '/' -and $basename) {
|
||||||
|
$url_basename = Split-Path -Path $uri.LocalPath -Leaf
|
||||||
|
$dest = Join-Path -Path $dest -ChildPath $url_basename
|
||||||
|
} else {
|
||||||
|
$dest = Join-Path -Path $dest -ChildPath $uri.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure we have a string instead of a PS object to avoid serialization issues
|
||||||
|
$dest = $dest.ToString()
|
||||||
|
} elseif (([System.IO.Path]::GetFileName($dest)) -eq '') {
|
||||||
|
# We have a trailing path separator
|
||||||
|
$module.FailJson("The destination path '$dest' does not exist, or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.Result.dest = $dest
|
||||||
|
|
||||||
|
if ($checksum) {
|
||||||
|
$checksum = $checksum.Trim().ToLower()
|
||||||
|
}
|
||||||
|
if ($checksum_algorithm) {
|
||||||
|
$checksum_algorithm = $checksum_algorithm.Trim().ToLower()
|
||||||
|
}
|
||||||
|
if ($checksum_url) {
|
||||||
|
$checksum_url = $checksum_url.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for case $checksum variable contain url. If yes, get file data from url and replace original value in $checksum
|
||||||
|
if ($checksum_url) {
|
||||||
|
$checksum_uri = [System.Uri]$checksum_url
|
||||||
|
if ($checksum_uri.Scheme -notin @("file", "ftp", "http", "https")) {
|
||||||
|
$module.FailJson("Unsupported 'checksum_url' value for '$dest': '$checksum_url'")
|
||||||
|
}
|
||||||
|
|
||||||
|
$checksum = Get-ChecksumFromUri -Module $Module -Uri $checksum_uri -SourceUri $url
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($force -or -not (Test-Path -LiteralPath $dest)) {
|
||||||
|
# force=yes or dest does not exist, download the file
|
||||||
|
# Note: Invoke-DownloadFile will compare the checksums internally if dest exists
|
||||||
|
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
|
||||||
|
-ChecksumAlgorithm $checksum_algorithm
|
||||||
|
} else {
|
||||||
|
# force=no, we want to check the last modified dates and only download if they don't match
|
||||||
|
$is_modified = Compare-ModifiedFile -Module $module -Uri $url -Dest $dest
|
||||||
|
if ($is_modified) {
|
||||||
|
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
|
||||||
|
-ChecksumAlgorithm $checksum_algorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((-not $module.Result.ContainsKey("checksum_dest")) -and (Test-Path -LiteralPath $dest)) {
|
||||||
|
# Calculate the dest file checksum if it hasn't already been done
|
||||||
|
$module.Result.checksum_dest = Get-Checksum -Path $dest -Algorithm $checksum_algorithm
|
||||||
|
$module.Result.size = (Get-AnsibleItem -Path $dest).Length
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.ExitJson()
|
||||||
|
|
@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>, and others
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# This is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['stableinterface'],
|
||||||
|
'supported_by': 'core'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_get_url
|
||||||
|
version_added: "1.7"
|
||||||
|
short_description: Downloads file from HTTP, HTTPS, or FTP to node
|
||||||
|
description:
|
||||||
|
- Downloads files from HTTP, HTTPS, or FTP to the remote server.
|
||||||
|
- The remote server I(must) have direct access to the remote resource.
|
||||||
|
- For non-Windows targets, use the M(get_url) module instead.
|
||||||
|
options:
|
||||||
|
url:
|
||||||
|
description:
|
||||||
|
- The full URL of a file to download.
|
||||||
|
type: str
|
||||||
|
required: yes
|
||||||
|
dest:
|
||||||
|
description:
|
||||||
|
- The location to save the file at the URL.
|
||||||
|
- Be sure to include a filename and extension as appropriate.
|
||||||
|
type: path
|
||||||
|
required: yes
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- If C(yes), will download the file every time and replace the file if the contents change. If C(no), will only
|
||||||
|
download the file if it does not exist or the remote file has been
|
||||||
|
modified more recently than the local file.
|
||||||
|
- This works by sending an http HEAD request to retrieve last modified
|
||||||
|
time of the requested resource, so for this to work, the remote web
|
||||||
|
server must support HEAD requests.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
version_added: "2.0"
|
||||||
|
checksum:
|
||||||
|
description:
|
||||||
|
- If a I(checksum) is passed to this parameter, the digest of the
|
||||||
|
destination file will be calculated after it is downloaded to ensure
|
||||||
|
its integrity and verify that the transfer completed successfully.
|
||||||
|
- This option cannot be set with I(checksum_url).
|
||||||
|
type: str
|
||||||
|
version_added: "2.8"
|
||||||
|
checksum_algorithm:
|
||||||
|
description:
|
||||||
|
- Specifies the hashing algorithm used when calculating the checksum of
|
||||||
|
the remote and destination file.
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
- md5
|
||||||
|
- sha1
|
||||||
|
- sha256
|
||||||
|
- sha384
|
||||||
|
- sha512
|
||||||
|
default: sha1
|
||||||
|
version_added: "2.8"
|
||||||
|
checksum_url:
|
||||||
|
description:
|
||||||
|
- Specifies a URL that contains the checksum values for the resource at
|
||||||
|
I(url).
|
||||||
|
- Like C(checksum), this is used to verify the integrity of the remote
|
||||||
|
transfer.
|
||||||
|
- This option cannot be set with I(checksum).
|
||||||
|
type: str
|
||||||
|
version_added: "2.8"
|
||||||
|
url_username:
|
||||||
|
description:
|
||||||
|
- The username to use for authentication.
|
||||||
|
- The aliases I(user) and I(username) are deprecated and will be removed in
|
||||||
|
Ansible 2.14.
|
||||||
|
aliases:
|
||||||
|
- user
|
||||||
|
- username
|
||||||
|
url_password:
|
||||||
|
description:
|
||||||
|
- The password for I(url_username).
|
||||||
|
- The alias I(password) is deprecated and will be removed in Ansible 2.14.
|
||||||
|
aliases:
|
||||||
|
- password
|
||||||
|
proxy_url:
|
||||||
|
version_added: "2.0"
|
||||||
|
proxy_username:
|
||||||
|
version_added: "2.0"
|
||||||
|
proxy_password:
|
||||||
|
version_added: "2.0"
|
||||||
|
headers:
|
||||||
|
version_added: "2.4"
|
||||||
|
use_proxy:
|
||||||
|
version_added: "2.4"
|
||||||
|
follow_redirects:
|
||||||
|
version_added: "2.9"
|
||||||
|
maximum_redirection:
|
||||||
|
version_added: "2.9"
|
||||||
|
client_cert:
|
||||||
|
version_added: "2.9"
|
||||||
|
client_cert_password:
|
||||||
|
version_added: "2.9"
|
||||||
|
method:
|
||||||
|
description:
|
||||||
|
- This option is not for use with C(win_get_url) and should be ignored.
|
||||||
|
version_added: "2.9"
|
||||||
|
notes:
|
||||||
|
- If your URL includes an escaped slash character (%2F) this module will convert it to a real slash.
|
||||||
|
This is a result of the behaviour of the System.Uri class as described in
|
||||||
|
L(the documentation,https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/network/schemesettings-element-uri-settings#remarks).
|
||||||
|
- Since Ansible 2.8, the module will skip reporting a change if the remote
|
||||||
|
checksum is the same as the local local even when C(force=yes). This is to
|
||||||
|
better align with M(get_url).
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- url_windows
|
||||||
|
seealso:
|
||||||
|
- module: get_url
|
||||||
|
- module: uri
|
||||||
|
- module: win_uri
|
||||||
|
author:
|
||||||
|
- Paul Durivage (@angstwad)
|
||||||
|
- Takeshi Kuramochi (@tksarah)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Download earthrise.jpg to specified path
|
||||||
|
win_get_url:
|
||||||
|
url: http://www.example.com/earthrise.jpg
|
||||||
|
dest: C:\Users\RandomUser\earthrise.jpg
|
||||||
|
|
||||||
|
- name: Download earthrise.jpg to specified path only if modified
|
||||||
|
win_get_url:
|
||||||
|
url: http://www.example.com/earthrise.jpg
|
||||||
|
dest: C:\Users\RandomUser\earthrise.jpg
|
||||||
|
force: no
|
||||||
|
|
||||||
|
- name: Download earthrise.jpg to specified path through a proxy server.
|
||||||
|
win_get_url:
|
||||||
|
url: http://www.example.com/earthrise.jpg
|
||||||
|
dest: C:\Users\RandomUser\earthrise.jpg
|
||||||
|
proxy_url: http://10.0.0.1:8080
|
||||||
|
proxy_username: username
|
||||||
|
proxy_password: password
|
||||||
|
|
||||||
|
- name: Download file from FTP with authentication
|
||||||
|
win_get_url:
|
||||||
|
url: ftp://server/file.txt
|
||||||
|
dest: '%TEMP%\ftp-file.txt'
|
||||||
|
url_username: ftp-user
|
||||||
|
url_password: ftp-password
|
||||||
|
|
||||||
|
- name: Download src with sha256 checksum url
|
||||||
|
win_get_url:
|
||||||
|
url: http://www.example.com/earthrise.jpg
|
||||||
|
dest: C:\temp\earthrise.jpg
|
||||||
|
checksum_url: http://www.example.com/sha256sum.txt
|
||||||
|
checksum_algorithm: sha256
|
||||||
|
force: True
|
||||||
|
|
||||||
|
- name: Download src with sha256 checksum url
|
||||||
|
win_get_url:
|
||||||
|
url: http://www.example.com/earthrise.jpg
|
||||||
|
dest: C:\temp\earthrise.jpg
|
||||||
|
checksum: a97e6837f60cec6da4491bab387296bbcd72bdba
|
||||||
|
checksum_algorithm: sha1
|
||||||
|
force: True
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
dest:
|
||||||
|
description: destination file/path
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: C:\Users\RandomUser\earthrise.jpg
|
||||||
|
checksum_dest:
|
||||||
|
description: <algorithm> checksum of the file after the download
|
||||||
|
returned: success and dest has been downloaded
|
||||||
|
type: str
|
||||||
|
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||||
|
checksum_src:
|
||||||
|
description: <algorithm> checksum of the remote resource
|
||||||
|
returned: force=yes or dest did not exist
|
||||||
|
type: str
|
||||||
|
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||||
|
elapsed:
|
||||||
|
description: The elapsed seconds between the start of poll and the end of the module.
|
||||||
|
returned: always
|
||||||
|
type: float
|
||||||
|
sample: 2.1406487
|
||||||
|
size:
|
||||||
|
description: size of the dest file
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 1220
|
||||||
|
url:
|
||||||
|
description: requested url
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: http://www.example.com/earthrise.jpg
|
||||||
|
msg:
|
||||||
|
description: Error message, or HTTP status message from web-server
|
||||||
|
returned: always
|
||||||
|
type: str
|
||||||
|
sample: OK
|
||||||
|
status_code:
|
||||||
|
description: HTTP status code
|
||||||
|
returned: always
|
||||||
|
type: int
|
||||||
|
sample: 200
|
||||||
|
'''
|
@ -0,0 +1,257 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# Copyright: (c) 2018, Micah Hunsberger (@mhunsber)
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
|
||||||
|
Set-StrictMode -Version 2
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$spec = @{
|
||||||
|
options = @{
|
||||||
|
state = @{ type = "str"; choices = "absent", "present"; default = "present" }
|
||||||
|
aliases = @{ type = "list"; elements = "str" }
|
||||||
|
canonical_name = @{ type = "str" }
|
||||||
|
ip_address = @{ type = "str" }
|
||||||
|
action = @{ type = "str"; choices = "add", "remove", "set"; default = "set" }
|
||||||
|
}
|
||||||
|
required_if = @(,@( "state", "present", @("canonical_name", "ip_address")))
|
||||||
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
|
||||||
|
$state = $module.Params.state
|
||||||
|
$aliases = $module.Params.aliases
|
||||||
|
$canonical_name = $module.Params.canonical_name
|
||||||
|
$ip_address = $module.Params.ip_address
|
||||||
|
$action = $module.Params.action
|
||||||
|
|
||||||
|
$tmp = [ipaddress]::None
|
||||||
|
if($ip_address -and -not [ipaddress]::TryParse($ip_address, [ref]$tmp)){
|
||||||
|
$module.FailJson("win_hosts: Argument ip_address needs to be a valid ip address, but was $ip_address")
|
||||||
|
}
|
||||||
|
$ip_address_type = $tmp.AddressFamily
|
||||||
|
|
||||||
|
$hosts_file = Get-Item -LiteralPath "$env:SystemRoot\System32\drivers\etc\hosts"
|
||||||
|
|
||||||
|
Function Get-CommentIndex($line) {
|
||||||
|
$c_index = $line.IndexOf('#')
|
||||||
|
if($c_index -lt 0) {
|
||||||
|
$c_index = $line.Length
|
||||||
|
}
|
||||||
|
return $c_index
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-HostEntryParts($line) {
|
||||||
|
$success = $true
|
||||||
|
$c_index = Get-CommentIndex -line $line
|
||||||
|
$pure_line = $line.Substring(0,$c_index).Trim()
|
||||||
|
$bits = $pure_line -split "\s+"
|
||||||
|
if($bits.Length -lt 2){
|
||||||
|
return @{
|
||||||
|
success = $false
|
||||||
|
ip_address = ""
|
||||||
|
ip_type = ""
|
||||||
|
canonical_name = ""
|
||||||
|
aliases = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ip_obj = [ipaddress]::None
|
||||||
|
if(-not [ipaddress]::TryParse($bits[0], [ref]$ip_obj) ){
|
||||||
|
$success = $false
|
||||||
|
}
|
||||||
|
$cname = $bits[1]
|
||||||
|
$als = New-Object string[] ($bits.Length - 2)
|
||||||
|
[array]::Copy($bits, 2, $als, 0, $als.Length)
|
||||||
|
return @{
|
||||||
|
success = $success
|
||||||
|
ip_address = $ip_obj.IPAddressToString
|
||||||
|
ip_type = $ip_obj.AddressFamily
|
||||||
|
canonical_name = $cname
|
||||||
|
aliases = $als
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Find-HostName($line, $name) {
|
||||||
|
$c_idx = Get-CommentIndex -line $line
|
||||||
|
$re = New-Object regex ("\s+$($name.Replace('.',"\."))(\s|$)", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
||||||
|
$match = $re.Match($line, 0, $c_idx)
|
||||||
|
return $match
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Remove-HostEntry($list, $idx) {
|
||||||
|
$module.Result.changed = $true
|
||||||
|
$list.RemoveAt($idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Add-HostEntry($list, $cname, $aliases, $ip) {
|
||||||
|
$module.Result.changed = $true
|
||||||
|
$line = "$ip $cname $($aliases -join ' ')"
|
||||||
|
$list.Add($line) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Remove-HostnamesFromEntry($list, $idx, $aliases) {
|
||||||
|
$line = $list[$idx]
|
||||||
|
$line_removed = $false
|
||||||
|
|
||||||
|
foreach($name in $aliases){
|
||||||
|
$match = Find-HostName -line $line -name $name
|
||||||
|
if($match.Success){
|
||||||
|
$line = $line.Remove($match.Index + 1, $match.Length -1)
|
||||||
|
# was this the last alias? (check for space characters after trimming)
|
||||||
|
if($line.Substring(0,(Get-CommentIndex -line $line)).Trim() -inotmatch "\s") {
|
||||||
|
$list.RemoveAt($idx)
|
||||||
|
$line_removed = $true
|
||||||
|
# we're done
|
||||||
|
return @{
|
||||||
|
line_removed = $line_removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($line -ne $list[$idx]){
|
||||||
|
$module.Result.changed = $true
|
||||||
|
$list[$idx] = $line
|
||||||
|
}
|
||||||
|
return @{
|
||||||
|
line_removed = $line_removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Add-AliasesToEntry($list, $idx, $aliases) {
|
||||||
|
$line = $list[$idx]
|
||||||
|
foreach($name in $aliases){
|
||||||
|
$match = Find-HostName -line $line -name $name
|
||||||
|
if(-not $match.Success) {
|
||||||
|
# just add the alias before the comment
|
||||||
|
$line = $line.Insert((Get-CommentIndex -line $line), " $name ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($line -ne $list[$idx]){
|
||||||
|
$module.Result.changed = $true
|
||||||
|
$list[$idx] = $line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hosts_lines = New-Object System.Collections.ArrayList
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $hosts_file.FullName | ForEach-Object { $hosts_lines.Add($_) } | Out-Null
|
||||||
|
$module.Diff.before = ($hosts_lines -join "`n") + "`n"
|
||||||
|
|
||||||
|
if ($state -eq 'absent') {
|
||||||
|
# go through and remove canonical_name and ip
|
||||||
|
for($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
|
||||||
|
$entry = $hosts_lines[$idx]
|
||||||
|
# skip comment lines
|
||||||
|
if(-not $entry.Trim().StartsWith('#')) {
|
||||||
|
$entry_parts = Get-HostEntryParts -line $entry
|
||||||
|
if($entry_parts.success) {
|
||||||
|
if(-not $ip_address -or $entry_parts.ip_address -eq $ip_address) {
|
||||||
|
if(-not $canonical_name -or $entry_parts.canonical_name -eq $canonical_name) {
|
||||||
|
if(Remove-HostEntry -list $hosts_lines -idx $idx){
|
||||||
|
# keep index correct if we removed the line
|
||||||
|
$idx = $idx - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($state -eq 'present') {
|
||||||
|
$entry_idx = -1
|
||||||
|
$aliases_to_keep = @()
|
||||||
|
# go through lines, find the entry and determine what to remove based on action
|
||||||
|
for($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
|
||||||
|
$entry = $hosts_lines[$idx]
|
||||||
|
# skip comment lines
|
||||||
|
if(-not $entry.Trim().StartsWith('#')) {
|
||||||
|
$entry_parts = Get-HostEntryParts -line $entry
|
||||||
|
if($entry_parts.success) {
|
||||||
|
$aliases_to_remove = @()
|
||||||
|
if($entry_parts.ip_address -eq $ip_address) {
|
||||||
|
if($entry_parts.canonical_name -eq $canonical_name) {
|
||||||
|
$entry_idx = $idx
|
||||||
|
|
||||||
|
if($action -eq 'set') {
|
||||||
|
$aliases_to_remove = $entry_parts.aliases | Where-Object { $aliases -notcontains $_ }
|
||||||
|
} elseif($action -eq 'remove') {
|
||||||
|
$aliases_to_remove = $aliases
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# this is the right ip_address, but not the cname we were looking for.
|
||||||
|
# we need to make sure none of aliases or canonical_name exist for this entry
|
||||||
|
# since the given canonical_name should be an A/AAAA record,
|
||||||
|
# and aliases should be cname records for the canonical_name.
|
||||||
|
$aliases_to_remove = $aliases + $canonical_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# this is not the ip_address we are looking for
|
||||||
|
if ($ip_address_type -eq $entry_parts.ip_type) {
|
||||||
|
if ($entry_parts.canonical_name -eq $canonical_name) {
|
||||||
|
Remove-HostEntry -list $hosts_lines -idx $idx
|
||||||
|
$idx = $idx - 1
|
||||||
|
if ($action -ne "set") {
|
||||||
|
# keep old aliases intact
|
||||||
|
$aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
|
||||||
|
}
|
||||||
|
} elseif ($action -eq "remove") {
|
||||||
|
$aliases_to_remove = $canonical_name
|
||||||
|
} elseif ($aliases -contains $entry_parts.canonical_name) {
|
||||||
|
Remove-HostEntry -list $hosts_lines -idx $idx
|
||||||
|
$idx = $idx - 1
|
||||||
|
if ($action -eq "add") {
|
||||||
|
# keep old aliases intact
|
||||||
|
$aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$aliases_to_remove = $aliases + $canonical_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# TODO: Better ipv6 support. There is odd behavior for when an alias can be used for both ipv6 and ipv4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($aliases_to_remove) {
|
||||||
|
if((Remove-HostnamesFromEntry -list $hosts_lines -idx $idx -aliases $aliases_to_remove).line_removed) {
|
||||||
|
$idx = $idx - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($entry_idx -ge 0) {
|
||||||
|
$aliases_to_add = @()
|
||||||
|
$entry_parts = Get-HostEntryParts -line $hosts_lines[$entry_idx]
|
||||||
|
if($action -eq 'remove') {
|
||||||
|
$aliases_to_add = $aliases_to_keep | Where-Object { $entry_parts.aliases -notcontains $_ }
|
||||||
|
} else {
|
||||||
|
$aliases_to_add = ($aliases + $aliases_to_keep) | Where-Object { $entry_parts.aliases -notcontains $_ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if($aliases_to_add) {
|
||||||
|
Add-AliasesToEntry -list $hosts_lines -idx $entry_idx -aliases $aliases_to_add
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# add the entry at the end
|
||||||
|
if($action -eq 'remove') {
|
||||||
|
if($aliases_to_keep) {
|
||||||
|
Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases $aliases_to_keep
|
||||||
|
} else {
|
||||||
|
Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases ($aliases + $aliases_to_keep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.Diff.after = ($hosts_lines -join "`n") + "`n"
|
||||||
|
if( $module.Result.changed -and -not $module.CheckMode ) {
|
||||||
|
Set-Content -LiteralPath $hosts_file.FullName -Value $hosts_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.ExitJson()
|
@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2018, Micah Hunsberger (@mhunsber)
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_hosts
|
||||||
|
version_added: '2.8'
|
||||||
|
short_description: Manages hosts file entries on Windows.
|
||||||
|
description:
|
||||||
|
- Manages hosts file entries on Windows.
|
||||||
|
- Maps IPv4 or IPv6 addresses to canonical names.
|
||||||
|
- Adds, removes, or sets cname records for ip and hostname pairs.
|
||||||
|
- Modifies %windir%\\system32\\drivers\\etc\\hosts.
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the entry should be present or absent.
|
||||||
|
- If only I(canonical_name) is provided when C(state=absent), then
|
||||||
|
all hosts entries with the canonical name of I(canonical_name)
|
||||||
|
will be removed.
|
||||||
|
- If only I(ip_address) is provided when C(state=absent), then all
|
||||||
|
hosts entries with the ip address of I(ip_address) will be removed.
|
||||||
|
- If I(ip_address) and I(canonical_name) are both omitted when
|
||||||
|
C(state=absent), then all hosts entries will be removed.
|
||||||
|
choices:
|
||||||
|
- absent
|
||||||
|
- present
|
||||||
|
default: present
|
||||||
|
type: str
|
||||||
|
canonical_name:
|
||||||
|
description:
|
||||||
|
- A canonical name for the host entry.
|
||||||
|
- required for C(state=present).
|
||||||
|
type: str
|
||||||
|
ip_address:
|
||||||
|
description:
|
||||||
|
- The ip address for the host entry.
|
||||||
|
- Can be either IPv4 (A record) or IPv6 (AAAA record).
|
||||||
|
- Required for C(state=present).
|
||||||
|
type: str
|
||||||
|
aliases:
|
||||||
|
description:
|
||||||
|
- A list of additional names (cname records) for the host entry.
|
||||||
|
- Only applicable when C(state=present).
|
||||||
|
type: list
|
||||||
|
action:
|
||||||
|
choices:
|
||||||
|
- add
|
||||||
|
- remove
|
||||||
|
- set
|
||||||
|
description:
|
||||||
|
- Controls the behavior of I(aliases).
|
||||||
|
- Only applicable when C(state=present).
|
||||||
|
- If C(add), each alias in I(aliases) will be added to the host entry.
|
||||||
|
- If C(set), each alias in I(aliases) will be added to the host entry,
|
||||||
|
and other aliases will be removed from the entry.
|
||||||
|
default: set
|
||||||
|
type: str
|
||||||
|
author:
|
||||||
|
- Micah Hunsberger (@mhunsber)
|
||||||
|
notes:
|
||||||
|
- Each canonical name can only be mapped to one IPv4 and one IPv6 address.
|
||||||
|
If I(canonical_name) is provided with C(state=present) and is found
|
||||||
|
to be mapped to another IP address that is the same type as, but unique
|
||||||
|
from I(ip_address), then I(canonical_name) and all I(aliases) will
|
||||||
|
be removed from the entry and added to an entry with the provided IP address.
|
||||||
|
- Each alias can only be mapped to one canonical name. If I(aliases) is provided
|
||||||
|
with C(state=present) and an alias is found to be mapped to another canonical
|
||||||
|
name, then the alias will be removed from the entry and either added to or removed
|
||||||
|
from (depending on I(action)) an entry with the provided canonical name.
|
||||||
|
seealso:
|
||||||
|
- module: win_template
|
||||||
|
- module: win_file
|
||||||
|
- module: win_copy
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Add 127.0.0.1 as an A record for localhost
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
canonical_name: localhost
|
||||||
|
ip_address: 127.0.0.1
|
||||||
|
|
||||||
|
- name: Add ::1 as an AAAA record for localhost
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
canonical_name: localhost
|
||||||
|
ip_address: '::1'
|
||||||
|
|
||||||
|
- name: Remove 'bar' and 'zed' from the list of aliases for foo (192.168.1.100)
|
||||||
|
win_hosts:
|
||||||
|
state: present
|
||||||
|
canoncial_name: foo
|
||||||
|
ip_address: 192.168.1.100
|
||||||
|
action: remove
|
||||||
|
aliases:
|
||||||
|
- bar
|
||||||
|
- zed
|
||||||
|
|
||||||
|
- name: Remove hosts entries with canonical name 'bar'
|
||||||
|
win_hosts:
|
||||||
|
state: absent
|
||||||
|
canonical_name: bar
|
||||||
|
|
||||||
|
- name: Remove 10.2.0.1 from the list of hosts
|
||||||
|
win_hosts:
|
||||||
|
state: absent
|
||||||
|
ip_address: 10.2.0.1
|
||||||
|
|
||||||
|
- name: Ensure all name resolution is handled by DNS
|
||||||
|
win_hosts:
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
'''
|
@ -0,0 +1,450 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Backup
|
||||||
|
|
||||||
|
function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) {
|
||||||
|
Try {
|
||||||
|
$temppath = [System.IO.Path]::GetTempFileName();
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json @{} "Cannot create temporary file! ($($_.Exception.Message))";
|
||||||
|
}
|
||||||
|
$joined = $outlines -join $linesep;
|
||||||
|
[System.IO.File]::WriteAllText($temppath, $joined, $encodingobj);
|
||||||
|
|
||||||
|
If ($validate) {
|
||||||
|
|
||||||
|
If (-not ($validate -like "*%s*")) {
|
||||||
|
Fail-Json @{} "validate must contain %s: $validate";
|
||||||
|
}
|
||||||
|
|
||||||
|
$validate = $validate.Replace("%s", $temppath);
|
||||||
|
|
||||||
|
$parts = [System.Collections.ArrayList] $validate.Split(" ");
|
||||||
|
$cmdname = $parts[0];
|
||||||
|
|
||||||
|
$cmdargs = $validate.Substring($cmdname.Length + 1);
|
||||||
|
|
||||||
|
$process = [Diagnostics.Process]::Start($cmdname, $cmdargs);
|
||||||
|
$process.WaitForExit();
|
||||||
|
|
||||||
|
If ($process.ExitCode -ne 0) {
|
||||||
|
[string] $output = $process.StandardOutput.ReadToEnd();
|
||||||
|
[string] $error = $process.StandardError.ReadToEnd();
|
||||||
|
Remove-Item $temppath -force;
|
||||||
|
Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Commit changes to the path
|
||||||
|
$cleanpath = $path.Replace("/", "\");
|
||||||
|
Try {
|
||||||
|
Copy-Item -Path $temppath -Destination $cleanpath -Force -WhatIf:$check_mode;
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))";
|
||||||
|
}
|
||||||
|
|
||||||
|
Try {
|
||||||
|
Remove-Item -Path $temppath -Force -WhatIf:$check_mode;
|
||||||
|
}
|
||||||
|
Catch {
|
||||||
|
Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $joined;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Implement the functionality for state == 'present'
|
||||||
|
function Present($path, $regex, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
|
||||||
|
|
||||||
|
# Note that we have to clean up the path because ansible wants to treat / and \ as
|
||||||
|
# interchangeable in windows pathnames, but .NET framework internals do not support that.
|
||||||
|
$cleanpath = $path.Replace("/", "\");
|
||||||
|
|
||||||
|
# Check if path exists. If it does not exist, either create it if create == "yes"
|
||||||
|
# was specified or fail with a reasonable error message.
|
||||||
|
If (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
If (-not $create) {
|
||||||
|
Fail-Json @{} "Path $path does not exist !";
|
||||||
|
}
|
||||||
|
# Create new empty file, using the specified encoding to write correct BOM
|
||||||
|
[System.IO.File]::WriteAllLines($cleanpath, "", $encodingobj);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize result information
|
||||||
|
$result = @{
|
||||||
|
backup = "";
|
||||||
|
changed = $false;
|
||||||
|
msg = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the dest file lines using the indicated encoding into a mutable ArrayList.
|
||||||
|
$before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj)
|
||||||
|
If ($null -eq $before) {
|
||||||
|
$lines = New-Object System.Collections.ArrayList;
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$lines = [System.Collections.ArrayList] $before;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diff_support) {
|
||||||
|
$result.diff = @{
|
||||||
|
before = $before -join $linesep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compile the regex specified, if provided
|
||||||
|
$mre = $null;
|
||||||
|
If ($regex) {
|
||||||
|
$mre = New-Object Regex $regex, 'Compiled';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compile the regex for insertafter or insertbefore, if provided
|
||||||
|
$insre = $null;
|
||||||
|
If ($insertafter -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") {
|
||||||
|
$insre = New-Object Regex $insertafter, 'Compiled';
|
||||||
|
}
|
||||||
|
ElseIf ($insertbefore -and $insertbefore -ne "BOF") {
|
||||||
|
$insre = New-Object Regex $insertbefore, 'Compiled';
|
||||||
|
}
|
||||||
|
|
||||||
|
# index[0] is the line num where regex has been found
|
||||||
|
# index[1] is the line num where insertafter/insertbefore has been found
|
||||||
|
$index = -1, -1;
|
||||||
|
$lineno = 0;
|
||||||
|
|
||||||
|
# The latest match object and matched line
|
||||||
|
$matched_line = "";
|
||||||
|
|
||||||
|
# Iterate through the lines in the file looking for matches
|
||||||
|
Foreach ($cur_line in $lines) {
|
||||||
|
If ($regex) {
|
||||||
|
$m = $mre.Match($cur_line);
|
||||||
|
$match_found = $m.Success;
|
||||||
|
If ($match_found) {
|
||||||
|
$matched_line = $cur_line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$match_found = $line -ceq $cur_line;
|
||||||
|
}
|
||||||
|
If ($match_found) {
|
||||||
|
$index[0] = $lineno;
|
||||||
|
}
|
||||||
|
ElseIf ($insre -and $insre.Match($cur_line).Success) {
|
||||||
|
If ($insertafter) {
|
||||||
|
$index[1] = $lineno + 1;
|
||||||
|
}
|
||||||
|
If ($insertbefore) {
|
||||||
|
$index[1] = $lineno;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$lineno = $lineno + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($index[0] -ne -1) {
|
||||||
|
If ($backrefs) {
|
||||||
|
$new_line = [regex]::Replace($matched_line, $regex, $line);
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$new_line = $line;
|
||||||
|
}
|
||||||
|
If ($lines[$index[0]] -cne $new_line) {
|
||||||
|
$lines[$index[0]] = $new_line;
|
||||||
|
$result.changed = $true;
|
||||||
|
$result.msg = "line replaced";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElseIf ($backrefs) {
|
||||||
|
# No matches - no-op
|
||||||
|
}
|
||||||
|
ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") {
|
||||||
|
$lines.Insert(0, $line);
|
||||||
|
$result.changed = $true;
|
||||||
|
$result.msg = "line added";
|
||||||
|
}
|
||||||
|
ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) {
|
||||||
|
$lines.Add($line) > $null;
|
||||||
|
$result.changed = $true;
|
||||||
|
$result.msg = "line added";
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$lines.Insert($index[1], $line);
|
||||||
|
$result.changed = $true;
|
||||||
|
$result.msg = "line added";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write changes to the path if changes were made
|
||||||
|
If ($result.changed) {
|
||||||
|
|
||||||
|
# Write backup file if backup == "yes"
|
||||||
|
If ($backup) {
|
||||||
|
$result.backup_file = Backup-File -path $path -WhatIf:$check_mode
|
||||||
|
# Ensure backward compatibility (deprecate in future)
|
||||||
|
$result.backup = $result.backup_file
|
||||||
|
}
|
||||||
|
|
||||||
|
$writelines_params = @{
|
||||||
|
outlines = $lines
|
||||||
|
path = $path
|
||||||
|
linesep = $linesep
|
||||||
|
encodingobj = $encodingobj
|
||||||
|
validate = $validate
|
||||||
|
check_mode = $check_mode
|
||||||
|
}
|
||||||
|
$after = WriteLines @writelines_params;
|
||||||
|
|
||||||
|
if ($diff_support) {
|
||||||
|
$result.diff.after = $after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result.encoding = $encodingobj.WebName;
|
||||||
|
|
||||||
|
Exit-Json $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Implement the functionality for state == 'absent'
|
||||||
|
function Absent($path, $regex, $line, $backup, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
|
||||||
|
|
||||||
|
# Check if path exists. If it does not exist, fail with a reasonable error message.
|
||||||
|
If (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
Fail-Json @{} "Path $path does not exist !";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize result information
|
||||||
|
$result = @{
|
||||||
|
backup = "";
|
||||||
|
changed = $false;
|
||||||
|
msg = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the dest file lines using the indicated encoding into a mutable ArrayList. Note
|
||||||
|
# that we have to clean up the path because ansible wants to treat / and \ as
|
||||||
|
# interchangeable in windows pathnames, but .NET framework internals do not support that.
|
||||||
|
$cleanpath = $path.Replace("/", "\");
|
||||||
|
$before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj);
|
||||||
|
If ($null -eq $before) {
|
||||||
|
$lines = New-Object System.Collections.ArrayList;
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$lines = [System.Collections.ArrayList] $before;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diff_support) {
|
||||||
|
$result.diff = @{
|
||||||
|
before = $before -join $linesep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compile the regex specified, if provided
|
||||||
|
$cre = $null;
|
||||||
|
If ($regex) {
|
||||||
|
$cre = New-Object Regex $regex, 'Compiled';
|
||||||
|
}
|
||||||
|
|
||||||
|
$found = New-Object System.Collections.ArrayList;
|
||||||
|
$left = New-Object System.Collections.ArrayList;
|
||||||
|
|
||||||
|
Foreach ($cur_line in $lines) {
|
||||||
|
If ($regex) {
|
||||||
|
$m = $cre.Match($cur_line);
|
||||||
|
$match_found = $m.Success;
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$match_found = $line -ceq $cur_line;
|
||||||
|
}
|
||||||
|
If ($match_found) {
|
||||||
|
$found.Add($cur_line) > $null;
|
||||||
|
$result.changed = $true;
|
||||||
|
}
|
||||||
|
Else {
|
||||||
|
$left.Add($cur_line) > $null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write changes to the path if changes were made
|
||||||
|
If ($result.changed) {
|
||||||
|
|
||||||
|
# Write backup file if backup == "yes"
|
||||||
|
If ($backup) {
|
||||||
|
$result.backup_file = Backup-File -path $path -WhatIf:$check_mode
|
||||||
|
# Ensure backward compatibility (deprecate in future)
|
||||||
|
$result.backup = $result.backup_file
|
||||||
|
}
|
||||||
|
|
||||||
|
$writelines_params = @{
|
||||||
|
outlines = $left
|
||||||
|
path = $path
|
||||||
|
linesep = $linesep
|
||||||
|
encodingobj = $encodingobj
|
||||||
|
validate = $validate
|
||||||
|
check_mode = $check_mode
|
||||||
|
}
|
||||||
|
$after = WriteLines @writelines_params;
|
||||||
|
|
||||||
|
if ($diff_support) {
|
||||||
|
$result.diff.after = $after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result.encoding = $encodingobj.WebName;
|
||||||
|
$result.found = $found.Count;
|
||||||
|
$result.msg = "$($found.Count) line(s) removed";
|
||||||
|
|
||||||
|
Exit-Json $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Parse the parameters file dropped by the Ansible machinery
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true;
|
||||||
|
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false;
|
||||||
|
$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false;
|
||||||
|
|
||||||
|
# Initialize defaults for input parameters.
|
||||||
|
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","destfile","name";
|
||||||
|
$regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "regexp";
|
||||||
|
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent";
|
||||||
|
$line = Get-AnsibleParam -obj $params -name "line" -type "str";
|
||||||
|
$backrefs = Get-AnsibleParam -obj $params -name "backrefs" -type "bool" -default $false;
|
||||||
|
$insertafter = Get-AnsibleParam -obj $params -name "insertafter" -type "str";
|
||||||
|
$insertbefore = Get-AnsibleParam -obj $params -name "insertbefore" -type "str";
|
||||||
|
$create = Get-AnsibleParam -obj $params -name "create" -type "bool" -default $false;
|
||||||
|
$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false;
|
||||||
|
$validate = Get-AnsibleParam -obj $params -name "validate" -type "str";
|
||||||
|
$encoding = Get-AnsibleParam -obj $params -name "encoding" -type "str" -default "auto";
|
||||||
|
$newline = Get-AnsibleParam -obj $params -name "newline" -type "str" -default "windows" -validateset "unix","windows";
|
||||||
|
|
||||||
|
# Fail if the path is not a file
|
||||||
|
If (Test-Path -LiteralPath $path -PathType "container") {
|
||||||
|
Fail-Json @{} "Path $path is a directory";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default to windows line separator - probably most common
|
||||||
|
$linesep = "`r`n"
|
||||||
|
If ($newline -eq "unix") {
|
||||||
|
$linesep = "`n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Figure out the proper encoding to use for reading / writing the target file.
|
||||||
|
|
||||||
|
# The default encoding is UTF-8 without BOM
|
||||||
|
$encodingobj = [System.Text.UTF8Encoding] $false;
|
||||||
|
|
||||||
|
# If an explicit encoding is specified, use that instead
|
||||||
|
If ($encoding -ne "auto") {
|
||||||
|
$encodingobj = [System.Text.Encoding]::GetEncoding($encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Otherwise see if we can determine the current encoding of the target file.
|
||||||
|
# If the file doesn't exist yet (create == 'yes') we use the default or
|
||||||
|
# explicitly specified encoding set above.
|
||||||
|
ElseIf (Test-Path -LiteralPath $path) {
|
||||||
|
|
||||||
|
# Get a sorted list of encodings with preambles, longest first
|
||||||
|
$max_preamble_len = 0;
|
||||||
|
$sortedlist = New-Object System.Collections.SortedList;
|
||||||
|
Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) {
|
||||||
|
$encoding = $encodinginfo.GetEncoding();
|
||||||
|
$plen = $encoding.GetPreamble().Length;
|
||||||
|
If ($plen -gt $max_preamble_len) {
|
||||||
|
$max_preamble_len = $plen;
|
||||||
|
}
|
||||||
|
If ($plen -gt 0) {
|
||||||
|
$sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding) > $null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the first N bytes from the file, where N is the max preamble length we saw
|
||||||
|
[Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -LiteralPath $path;
|
||||||
|
|
||||||
|
# Iterate through the sorted encodings, looking for a full match.
|
||||||
|
$found = $false;
|
||||||
|
Foreach ($encoding in $sortedlist.GetValueList()) {
|
||||||
|
$preamble = $encoding.GetPreamble();
|
||||||
|
If ($preamble -and $bom) {
|
||||||
|
Foreach ($i in 0..($preamble.Length - 1)) {
|
||||||
|
If ($i -ge $bom.Length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
If ($preamble[$i] -ne $bom[$i]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ElseIf ($i + 1 -eq $preamble.Length) {
|
||||||
|
$encodingobj = $encoding;
|
||||||
|
$found = $true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
If ($found) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Main dispatch - based on the value of 'state', perform argument validation and
|
||||||
|
# call the appropriate handler function.
|
||||||
|
If ($state -eq "present") {
|
||||||
|
|
||||||
|
If ($backrefs -and -not $regex) {
|
||||||
|
Fail-Json @{} "regexp= is required with backrefs=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
If (-not $line) {
|
||||||
|
Fail-Json @{} "line= is required with state=present";
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($insertbefore -and $insertafter) {
|
||||||
|
Add-Warning $result "Both insertbefore and insertafter parameters found, ignoring `"insertafter=$insertafter`""
|
||||||
|
}
|
||||||
|
|
||||||
|
If (-not $insertbefore -and -not $insertafter) {
|
||||||
|
$insertafter = "EOF";
|
||||||
|
}
|
||||||
|
|
||||||
|
$present_params = @{
|
||||||
|
path = $path
|
||||||
|
regex = $regex
|
||||||
|
line = $line
|
||||||
|
insertafter = $insertafter
|
||||||
|
insertbefore = $insertbefore
|
||||||
|
create = $create
|
||||||
|
backup = $backup
|
||||||
|
backrefs = $backrefs
|
||||||
|
validate = $validate
|
||||||
|
encodingobj = $encodingobj
|
||||||
|
linesep = $linesep
|
||||||
|
check_mode = $check_mode
|
||||||
|
diff_support = $diff_support
|
||||||
|
}
|
||||||
|
Present @present_params;
|
||||||
|
|
||||||
|
}
|
||||||
|
ElseIf ($state -eq "absent") {
|
||||||
|
|
||||||
|
If (-not $regex -and -not $line) {
|
||||||
|
Fail-Json @{} "one of line= or regexp= is required with state=absent";
|
||||||
|
}
|
||||||
|
|
||||||
|
$absent_params = @{
|
||||||
|
path = $path
|
||||||
|
regex = $regex
|
||||||
|
line = $line
|
||||||
|
backup = $backup
|
||||||
|
validate = $validate
|
||||||
|
encodingobj = $encodingobj
|
||||||
|
linesep = $linesep
|
||||||
|
check_mode = $check_mode
|
||||||
|
diff_support = $diff_support
|
||||||
|
}
|
||||||
|
Absent @absent_params;
|
||||||
|
}
|
@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_lineinfile
|
||||||
|
short_description: Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression
|
||||||
|
description:
|
||||||
|
- This module will search a file for a line, and ensure that it is present or absent.
|
||||||
|
- This is primarily useful when you want to change a single line in a file only.
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The path of the file to modify.
|
||||||
|
- Note that the Windows path delimiter C(\) must be escaped as C(\\) when the line is double quoted.
|
||||||
|
- Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
|
||||||
|
type: path
|
||||||
|
required: yes
|
||||||
|
aliases: [ dest, destfile, name ]
|
||||||
|
backup:
|
||||||
|
description:
|
||||||
|
- Determine whether a backup should be created.
|
||||||
|
- When set to C(yes), create a backup file including the timestamp information
|
||||||
|
so you can get the original file back if you somehow clobbered it incorrectly.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
regex:
|
||||||
|
description:
|
||||||
|
- The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found
|
||||||
|
will be replaced. For C(state=absent), the pattern of the line to remove. Uses .NET compatible regular expressions;
|
||||||
|
see U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx).
|
||||||
|
aliases: [ "regexp" ]
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the line should be there or not.
|
||||||
|
type: str
|
||||||
|
choices: [ absent, present ]
|
||||||
|
default: present
|
||||||
|
line:
|
||||||
|
description:
|
||||||
|
- Required for C(state=present). The line to insert/replace into the file. If C(backrefs) is set, may contain backreferences that will get
|
||||||
|
expanded with the C(regexp) capture groups if the regexp matches.
|
||||||
|
- Be aware that the line is processed first on the controller and thus is dependent on yaml quoting rules. Any double quoted line
|
||||||
|
will have control characters, such as '\r\n', expanded. To print such characters literally, use single or no quotes.
|
||||||
|
type: str
|
||||||
|
backrefs:
|
||||||
|
description:
|
||||||
|
- Used with C(state=present). If set, line can contain backreferences (both positional and named) that will get populated if the C(regexp)
|
||||||
|
matches. This flag changes the operation of the module slightly; C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp)
|
||||||
|
doesn't match anywhere in the file, the file will be left unchanged.
|
||||||
|
- If the C(regexp) does match, the last matching line will be replaced by the expanded line parameter.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
insertafter:
|
||||||
|
description:
|
||||||
|
- Used with C(state=present). If specified, the line will be inserted after the last match of specified regular expression. A special value is
|
||||||
|
available; C(EOF) for inserting the line at the end of the file.
|
||||||
|
- If specified regular expression has no matches, EOF will be used instead. May not be used with C(backrefs).
|
||||||
|
type: str
|
||||||
|
choices: [ EOF, '*regex*' ]
|
||||||
|
default: EOF
|
||||||
|
insertbefore:
|
||||||
|
description:
|
||||||
|
- Used with C(state=present). If specified, the line will be inserted before the last match of specified regular expression. A value is available;
|
||||||
|
C(BOF) for inserting the line at the beginning of the file.
|
||||||
|
- If specified regular expression has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs).
|
||||||
|
type: str
|
||||||
|
choices: [ BOF, '*regex*' ]
|
||||||
|
create:
|
||||||
|
description:
|
||||||
|
- Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing.
|
||||||
|
type: bool
|
||||||
|
default: no
|
||||||
|
validate:
|
||||||
|
description:
|
||||||
|
- Validation to run before copying into place. Use %s in the command to indicate the current file to validate.
|
||||||
|
- The command is passed securely so shell features like expansion and pipes won't work.
|
||||||
|
type: str
|
||||||
|
encoding:
|
||||||
|
description:
|
||||||
|
- Specifies the encoding of the source text file to operate on (and thus what the output encoding will be). The default of C(auto) will cause
|
||||||
|
the module to auto-detect the encoding of the source file and ensure that the modified file is written with the same encoding.
|
||||||
|
- An explicit encoding can be passed as a string that is a valid value to pass to the .NET framework System.Text.Encoding.GetEncoding() method -
|
||||||
|
see U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx).
|
||||||
|
- This is mostly useful with C(create=yes) if you want to create a new file with a specific encoding. If C(create=yes) is specified without a
|
||||||
|
specific encoding, the default encoding (UTF-8, no BOM) will be used.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
newline:
|
||||||
|
description:
|
||||||
|
- Specifies the line separator style to use for the modified file. This defaults to the windows line separator (C(\r\n)). Note that the indicated
|
||||||
|
line separator will be used for file output regardless of the original line separator that appears in the input file.
|
||||||
|
type: str
|
||||||
|
choices: [ unix, windows ]
|
||||||
|
default: windows
|
||||||
|
notes:
|
||||||
|
- As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
|
||||||
|
seealso:
|
||||||
|
- module: assemble
|
||||||
|
- module: lineinfile
|
||||||
|
author:
|
||||||
|
- Brian Lloyd (@brianlloyd)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
# Before Ansible 2.3, option 'dest', 'destfile' or 'name' was used instead of 'path'
|
||||||
|
- name: Insert path without converting \r\n
|
||||||
|
win_lineinfile:
|
||||||
|
path: c:\file.txt
|
||||||
|
line: c:\return\new
|
||||||
|
|
||||||
|
- win_lineinfile:
|
||||||
|
path: C:\Temp\example.conf
|
||||||
|
regex: '^name='
|
||||||
|
line: 'name=JohnDoe'
|
||||||
|
|
||||||
|
- win_lineinfile:
|
||||||
|
path: C:\Temp\example.conf
|
||||||
|
regex: '^name='
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- win_lineinfile:
|
||||||
|
path: C:\Temp\example.conf
|
||||||
|
regex: '^127\.0\.0\.1'
|
||||||
|
line: '127.0.0.1 localhost'
|
||||||
|
|
||||||
|
- win_lineinfile:
|
||||||
|
path: C:\Temp\httpd.conf
|
||||||
|
regex: '^Listen '
|
||||||
|
insertafter: '^#Listen '
|
||||||
|
line: Listen 8080
|
||||||
|
|
||||||
|
- win_lineinfile:
|
||||||
|
path: C:\Temp\services
|
||||||
|
regex: '^# port for http'
|
||||||
|
insertbefore: '^www.*80/tcp'
|
||||||
|
line: '# port for http by default'
|
||||||
|
|
||||||
|
- name: Create file if it doesn't exist with a specific encoding
|
||||||
|
win_lineinfile:
|
||||||
|
path: C:\Temp\utf16.txt
|
||||||
|
create: yes
|
||||||
|
encoding: utf-16
|
||||||
|
line: This is a utf-16 encoded file
|
||||||
|
|
||||||
|
- name: Add a line to a file and ensure the resulting file uses unix line separators
|
||||||
|
win_lineinfile:
|
||||||
|
path: C:\Temp\testfile.txt
|
||||||
|
line: Line added to file
|
||||||
|
newline: unix
|
||||||
|
|
||||||
|
- name: Update a line using backrefs
|
||||||
|
win_lineinfile:
|
||||||
|
path: C:\Temp\example.conf
|
||||||
|
backrefs: yes
|
||||||
|
regex: '(^name=)'
|
||||||
|
line: '$1JohnDoe'
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
backup:
|
||||||
|
description:
|
||||||
|
- Name of the backup file that was created.
|
||||||
|
- This is now deprecated, use C(backup_file) instead.
|
||||||
|
returned: if backup=yes
|
||||||
|
type: str
|
||||||
|
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
|
||||||
|
backup_file:
|
||||||
|
description: Name of the backup file that was created.
|
||||||
|
returned: if backup=yes
|
||||||
|
type: str
|
||||||
|
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
|
||||||
|
'''
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue