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
Matt Clay 5 years ago committed by GitHub
parent da30e6d2e1
commit f735fd672a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -63,6 +63,11 @@ matrix:
- env: T=windows/2016/7 - env: T=windows/2016/7
- env: T=windows/2019/7 - env: T=windows/2019/7
- env: T=i/windows/2012
- env: T=i/windows/2012-R2
- env: T=i/windows/2016
- env: T=i/windows/2019
- env: T=ios/csr1000v//1 - env: T=ios/csr1000v//1
- env: T=vyos/1.1.8/2.7/1 - env: T=vyos/1.1.8/2.7/1

@ -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,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,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,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 @@
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,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 == ''

@ -243,6 +243,7 @@ class IntegrationAliasesTest(SanityVersionNeutral):
messages += self.check_ci_group( messages += self.check_ci_group(
targets=windows_targets, targets=windows_targets,
find=self.format_shippable_group_alias('windows'), find=self.format_shippable_group_alias('windows'),
find_incidental=['shippable/windows/incidental/'],
) )
return messages return messages

@ -8106,6 +8106,13 @@ test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py futu
test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py metaclass-boilerplate test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py metaclass-boilerplate
test/integration/targets/incidental_script_inventory_vmware_inventory/vmware_inventory.py future-import-boilerplate test/integration/targets/incidental_script_inventory_vmware_inventory/vmware_inventory.py future-import-boilerplate
test/integration/targets/incidental_script_inventory_vmware_inventory/vmware_inventory.py metaclass-boilerplate test/integration/targets/incidental_script_inventory_vmware_inventory/vmware_inventory.py metaclass-boilerplate
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.psm1 pslint!skip
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1 pslint!skip
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/xTestDsc.psd1 pslint!skip
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1 pslint!skip
test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1 pslint!skip
test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1 pslint!skip
test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip
test/integration/targets/inventory_kubevirt/inventory_diff.py future-import-boilerplate test/integration/targets/inventory_kubevirt/inventory_diff.py future-import-boilerplate
test/integration/targets/inventory_kubevirt/inventory_diff.py metaclass-boilerplate test/integration/targets/inventory_kubevirt/inventory_diff.py metaclass-boilerplate
test/integration/targets/inventory_kubevirt/server.py future-import-boilerplate test/integration/targets/inventory_kubevirt/server.py future-import-boilerplate
@ -8285,6 +8292,16 @@ test/support/network-integration/collections/ansible_collections/vyos/vyos/plugi
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_logging.py metaclass-boilerplate test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_logging.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_static_route.py future-import-boilerplate test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_static_route.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_static_route.py metaclass-boilerplate test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_static_route.py metaclass-boilerplate
test/support/windows-integration/plugins/modules/async_status.ps1 pslint!skip
test/support/windows-integration/plugins/modules/setup.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_copy.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_dsc.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_feature.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_find.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_lineinfile.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_regedit.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_security_policy.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_shell.ps1 pslint!skip
test/units/config/manager/test_find_ini_config_file.py future-import-boilerplate test/units/config/manager/test_find_ini_config_file.py future-import-boilerplate
test/units/contrib/inventory/test_vmware_inventory.py future-import-boilerplate test/units/contrib/inventory/test_vmware_inventory.py future-import-boilerplate
test/units/contrib/inventory/test_vmware_inventory.py metaclass-boilerplate test/units/contrib/inventory/test_vmware_inventory.py metaclass-boilerplate

@ -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

@ -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…
Cancel
Save