diff --git a/shippable.yml b/shippable.yml
index 1ed7999ed98..d24b2bd9a35 100644
--- a/shippable.yml
+++ b/shippable.yml
@@ -63,6 +63,11 @@ matrix:
- env: T=windows/2016/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=vyos/1.1.8/2.7/1
diff --git a/test/integration/targets/incidental_win_copy/aliases b/test/integration/targets/incidental_win_copy/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_copy/defaults/main.yml b/test/integration/targets/incidental_win_copy/defaults/main.yml
new file mode 100644
index 00000000000..5d8a1d23513
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/defaults/main.yml
@@ -0,0 +1 @@
+test_win_copy_path: C:\ansible\win_copy .ÅÑŚÌβŁÈ [$!@^&test(;)]
diff --git a/test/integration/targets/incidental_win_copy/files-different/vault/folder/nested-vault-file b/test/integration/targets/incidental_win_copy/files-different/vault/folder/nested-vault-file
new file mode 100644
index 00000000000..d8d1549874c
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files-different/vault/folder/nested-vault-file
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+65653164323866373138353632323531393664393563633665373635623763353561386431373366
+3232353263363034313136663062623336663463373966320a333763323032646463386432626161
+36386330356637666362396661653935653064623038333031653335626164376465353235303636
+3335616231663838620a303632343938326538656233393562303162343261383465623261646664
+33613932343461626339333832363930303962633364303736376634396364643861
diff --git a/test/integration/targets/incidental_win_copy/files-different/vault/readme.txt b/test/integration/targets/incidental_win_copy/files-different/vault/readme.txt
new file mode 100644
index 00000000000..dae883b5eed
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files-different/vault/readme.txt
@@ -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
diff --git a/test/integration/targets/incidental_win_copy/files-different/vault/vault-file b/test/integration/targets/incidental_win_copy/files-different/vault/vault-file
new file mode 100644
index 00000000000..2fff7619a75
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files-different/vault/vault-file
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+30353665333635633433356261616636356130386330363962386533303566313463383734373532
+3933643234323638623939613462346361313431363939370a303532656338353035346661353965
+34656231633238396361393131623834316262306533663838336362366137306562646561383766
+6363373965633337640a373666336461613337346131353564383134326139616561393664663563
+3431
diff --git a/test/integration/targets/incidental_win_copy/files/empty.txt b/test/integration/targets/incidental_win_copy/files/empty.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/test/integration/targets/incidental_win_copy/files/foo.txt b/test/integration/targets/incidental_win_copy/files/foo.txt
new file mode 100644
index 00000000000..7c6ded14ecf
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files/foo.txt
@@ -0,0 +1 @@
+foo.txt
diff --git a/test/integration/targets/incidental_win_copy/files/subdir/bar.txt b/test/integration/targets/incidental_win_copy/files/subdir/bar.txt
new file mode 100644
index 00000000000..76018072e09
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files/subdir/bar.txt
@@ -0,0 +1 @@
+baz
diff --git a/test/integration/targets/incidental_win_copy/files/subdir/subdir2/baz.txt b/test/integration/targets/incidental_win_copy/files/subdir/subdir2/baz.txt
new file mode 100644
index 00000000000..76018072e09
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files/subdir/subdir2/baz.txt
@@ -0,0 +1 @@
+baz
diff --git a/test/integration/targets/incidental_win_copy/files/subdir/subdir2/subdir3/subdir4/qux.txt b/test/integration/targets/incidental_win_copy/files/subdir/subdir2/subdir3/subdir4/qux.txt
new file mode 100644
index 00000000000..78df5b06bd3
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/files/subdir/subdir2/subdir3/subdir4/qux.txt
@@ -0,0 +1 @@
+qux
\ No newline at end of file
diff --git a/test/integration/targets/incidental_win_copy/tasks/main.yml b/test/integration/targets/incidental_win_copy/tasks/main.yml
new file mode 100644
index 00000000000..b2ee103fd06
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/tasks/main.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_copy/tasks/remote_tests.yml b/test/integration/targets/incidental_win_copy/tasks/remote_tests.yml
new file mode 100644
index 00000000000..5abb50200ba
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/tasks/remote_tests.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_copy/tasks/tests.yml b/test/integration/targets/incidental_win_copy/tasks/tests.yml
new file mode 100644
index 00000000000..d15e71f65c0
--- /dev/null
+++ b/test/integration/targets/incidental_win_copy/tasks/tests.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_data_deduplication/aliases b/test/integration/targets/incidental_win_data_deduplication/aliases
new file mode 100644
index 00000000000..c7657537a7e
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/aliases
@@ -0,0 +1,5 @@
+shippable/windows/incidental
+windows
+skip/windows/2008
+skip/windows/2008-R2
+skip/windows/2012
diff --git a/test/integration/targets/incidental_win_data_deduplication/meta/main.yml b/test/integration/targets/incidental_win_data_deduplication/meta/main.yml
new file mode 100644
index 00000000000..9f37e96cd90
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/incidental_win_data_deduplication/tasks/main.yml b/test/integration/targets/incidental_win_data_deduplication/tasks/main.yml
new file mode 100644
index 00000000000..ae6be90ecb9
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include: pre_test.yml
diff --git a/test/integration/targets/incidental_win_data_deduplication/tasks/pre_test.yml b/test/integration/targets/incidental_win_data_deduplication/tasks/pre_test.yml
new file mode 100644
index 00000000000..f72955e46bf
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/tasks/pre_test.yml
@@ -0,0 +1,40 @@
+---
+- set_fact:
+ AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx'
+
+- name: Install FS-Data-Deduplication
+ win_feature:
+ name: FS-Data-Deduplication
+ include_sub_features: true
+ state: present
+ register: data_dedup_feat_reg
+
+- name: Reboot windows after the feature has been installed
+ win_reboot:
+ reboot_timeout: 3600
+ when:
+ - data_dedup_feat_reg.success
+ - data_dedup_feat_reg.reboot_required
+
+- name: Copy VHDX scripts
+ win_template:
+ src: "{{ item.src }}"
+ dest: '{{ remote_tmp_dir }}\{{ item.dest }}'
+ loop:
+ - { src: partition_creation_script.j2, dest: partition_creation_script.txt }
+ - { src: partition_deletion_script.j2, dest: partition_deletion_script.txt }
+
+- name: Create partition
+ win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_creation_script.txt
+
+- name: Format T with NTFS
+ win_format:
+ drive_letter: T
+ file_system: ntfs
+
+- name: Run tests
+ block:
+ - include: tests.yml
+ always:
+ - name: Detach disk
+ win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_deletion_script.txt
diff --git a/test/integration/targets/incidental_win_data_deduplication/tasks/tests.yml b/test/integration/targets/incidental_win_data_deduplication/tasks/tests.yml
new file mode 100644
index 00000000000..64a42927130
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/tasks/tests.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_data_deduplication/templates/partition_creation_script.j2 b/test/integration/targets/incidental_win_data_deduplication/templates/partition_creation_script.j2
new file mode 100644
index 00000000000..8e47fda95ba
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/templates/partition_creation_script.j2
@@ -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"
diff --git a/test/integration/targets/incidental_win_data_deduplication/templates/partition_deletion_script.j2 b/test/integration/targets/incidental_win_data_deduplication/templates/partition_deletion_script.j2
new file mode 100644
index 00000000000..c2be9cd1446
--- /dev/null
+++ b/test/integration/targets/incidental_win_data_deduplication/templates/partition_deletion_script.j2
@@ -0,0 +1,3 @@
+select vdisk file="{{ AnsibleVhdx }}"
+
+detach vdisk
diff --git a/test/integration/targets/incidental_win_dsc/aliases b/test/integration/targets/incidental_win_dsc/aliases
new file mode 100644
index 00000000000..9114c742276
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/aliases
@@ -0,0 +1,6 @@
+shippable/windows/incidental
+windows
+skip/windows/2008
+skip/windows/2008-R2
+skip/windows/2012
+skip/windows/2012-R2
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.psm1 b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.psm1
new file mode 100644
index 00000000000..dbf1ecf3ee7
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.psm1
@@ -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
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.schema.mof b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.schema.mof
new file mode 100644
index 00000000000..288b8877224
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xSetReboot/ANSIBLE_xSetReboot.schema.mof
@@ -0,0 +1,7 @@
+[ClassVersion("1.0.0"), FriendlyName("xSetReboot")]
+class ANSIBLE_xSetReboot : OMI_BaseResource
+{
+ [Key] String KeyParam;
+ [Write] Boolean Value;
+};
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1 b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1
new file mode 100644
index 00000000000..79f64969623
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1
@@ -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
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof
new file mode 100644
index 00000000000..c61b2b1e6a9
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof
@@ -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[];
+};
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/xTestDsc.psd1 b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/xTestDsc.psd1
new file mode 100644
index 00000000000..3d61611d70a
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.0/xTestDsc.psd1
@@ -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 = '*'
+}
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1 b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1
new file mode 100644
index 00000000000..d75256e1d90
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.psm1
@@ -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
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof
new file mode 100644
index 00000000000..9301664b3cd
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/DSCResources/ANSIBLE_xTestResource/ANSIBLE_xTestResource.schema.mof
@@ -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[];
+};
+
diff --git a/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1 b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1
new file mode 100644
index 00000000000..0c43b85238d
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/files/xTestDsc/1.0.1/xTestDsc.psd1
@@ -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 = '*'
+}
+
diff --git a/test/integration/targets/incidental_win_dsc/meta/main.yml b/test/integration/targets/incidental_win_dsc/meta/main.yml
new file mode 100644
index 00000000000..9f37e96cd90
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/incidental_win_dsc/tasks/main.yml b/test/integration/targets/incidental_win_dsc/tasks/main.yml
new file mode 100644
index 00000000000..f37295ab711
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/tasks/main.yml
@@ -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 }}'
diff --git a/test/integration/targets/incidental_win_dsc/tasks/tests.yml b/test/integration/targets/incidental_win_dsc/tasks/tests.yml
new file mode 100644
index 00000000000..d2a6802fdfa
--- /dev/null
+++ b/test/integration/targets/incidental_win_dsc/tasks/tests.yml
@@ -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'
diff --git a/test/integration/targets/incidental_win_hosts/aliases b/test/integration/targets/incidental_win_hosts/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_hosts/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_hosts/defaults/main.yml b/test/integration/targets/incidental_win_hosts/defaults/main.yml
new file mode 100644
index 00000000000..c6270216d68
--- /dev/null
+++ b/test/integration/targets/incidental_win_hosts/defaults/main.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_hosts/meta/main.yml b/test/integration/targets/incidental_win_hosts/meta/main.yml
new file mode 100644
index 00000000000..9f37e96cd90
--- /dev/null
+++ b/test/integration/targets/incidental_win_hosts/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/incidental_win_hosts/tasks/main.yml b/test/integration/targets/incidental_win_hosts/tasks/main.yml
new file mode 100644
index 00000000000..0997375f9fd
--- /dev/null
+++ b/test/integration/targets/incidental_win_hosts/tasks/main.yml
@@ -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
diff --git a/test/integration/targets/incidental_win_hosts/tasks/tests.yml b/test/integration/targets/incidental_win_hosts/tasks/tests.yml
new file mode 100644
index 00000000000..a29e01a708b
--- /dev/null
+++ b/test/integration/targets/incidental_win_hosts/tasks/tests.yml
@@ -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"
diff --git a/test/integration/targets/incidental_win_lineinfile/aliases b/test/integration/targets/incidental_win_lineinfile/aliases
new file mode 100644
index 00000000000..194cbc3f2e0
--- /dev/null
+++ b/test/integration/targets/incidental_win_lineinfile/aliases
@@ -0,0 +1,3 @@
+shippable/windows/incidental
+windows
+skip/windows/2016 # Host takes a while to run and module isn't OS dependent
diff --git a/test/integration/targets/incidental_win_lineinfile/files/test.txt b/test/integration/targets/incidental_win_lineinfile/files/test.txt
new file mode 100644
index 00000000000..8187db9f029
--- /dev/null
+++ b/test/integration/targets/incidental_win_lineinfile/files/test.txt
@@ -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
diff --git a/test/integration/targets/incidental_win_lineinfile/files/test_linebreak.txt b/test/integration/targets/incidental_win_lineinfile/files/test_linebreak.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/test/integration/targets/incidental_win_lineinfile/files/test_quoting.txt b/test/integration/targets/incidental_win_lineinfile/files/test_quoting.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/test/integration/targets/incidental_win_lineinfile/files/testempty.txt b/test/integration/targets/incidental_win_lineinfile/files/testempty.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/test/integration/targets/incidental_win_lineinfile/files/testnoeof.txt b/test/integration/targets/incidental_win_lineinfile/files/testnoeof.txt
new file mode 100644
index 00000000000..152780b9ff0
--- /dev/null
+++ b/test/integration/targets/incidental_win_lineinfile/files/testnoeof.txt
@@ -0,0 +1,2 @@
+This is line 1
+This is line 2
\ No newline at end of file
diff --git a/test/integration/targets/incidental_win_lineinfile/meta/main.yml b/test/integration/targets/incidental_win_lineinfile/meta/main.yml
new file mode 100644
index 00000000000..e0ff46db127
--- /dev/null
+++ b/test/integration/targets/incidental_win_lineinfile/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - incidental_win_prepare_tests
diff --git a/test/integration/targets/incidental_win_lineinfile/tasks/main.yml b/test/integration/targets/incidental_win_lineinfile/tasks/main.yml
new file mode 100644
index 00000000000..e5f047bec30
--- /dev/null
+++ b/test/integration/targets/incidental_win_lineinfile/tasks/main.yml
@@ -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 .
+
+
+- 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'
diff --git a/test/integration/targets/incidental_win_ping/aliases b/test/integration/targets/incidental_win_ping/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_ping/library/win_ping_set_attr.ps1 b/test/integration/targets/incidental_win_ping/library/win_ping_set_attr.ps1
new file mode 100644
index 00000000000..f17049643b4
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/library/win_ping_set_attr.ps1
@@ -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 .
+
+# 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;
diff --git a/test/integration/targets/incidental_win_ping/library/win_ping_strict_mode_error.ps1 b/test/integration/targets/incidental_win_ping/library/win_ping_strict_mode_error.ps1
new file mode 100644
index 00000000000..508174afcc6
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/library/win_ping_strict_mode_error.ps1
@@ -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 .
+
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args $true;
+
+$params.thisPropertyDoesNotExist
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1 b/test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1
new file mode 100644
index 00000000000..d4c9f07ad55
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/library/win_ping_syntax_error.ps1
@@ -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 .
+
+# 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;
diff --git a/test/integration/targets/incidental_win_ping/library/win_ping_throw.ps1 b/test/integration/targets/incidental_win_ping/library/win_ping_throw.ps1
new file mode 100644
index 00000000000..7306f4d2808
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/library/win_ping_throw.ps1
@@ -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 .
+
+# POWERSHELL_COMMON
+
+throw
+
+$params = Parse-Args $args $true;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/test/integration/targets/incidental_win_ping/library/win_ping_throw_string.ps1 b/test/integration/targets/incidental_win_ping/library/win_ping_throw_string.ps1
new file mode 100644
index 00000000000..09e3b7cb458
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/library/win_ping_throw_string.ps1
@@ -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 .
+
+# 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;
diff --git a/test/integration/targets/incidental_win_ping/tasks/main.yml b/test/integration/targets/incidental_win_ping/tasks/main.yml
new file mode 100644
index 00000000000..a7e6ba7fc4e
--- /dev/null
+++ b/test/integration/targets/incidental_win_ping/tasks/main.yml
@@ -0,0 +1,67 @@
+# test code for the win_ping module
+# (c) 2014, Chris Church
+
+# 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 .
+
+- 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'
diff --git a/test/integration/targets/incidental_win_prepare_tests/aliases b/test/integration/targets/incidental_win_prepare_tests/aliases
new file mode 100644
index 00000000000..136c05e0d02
--- /dev/null
+++ b/test/integration/targets/incidental_win_prepare_tests/aliases
@@ -0,0 +1 @@
+hidden
diff --git a/test/integration/targets/incidental_win_prepare_tests/meta/main.yml b/test/integration/targets/incidental_win_prepare_tests/meta/main.yml
new file mode 100644
index 00000000000..cf5427b6084
--- /dev/null
+++ b/test/integration/targets/incidental_win_prepare_tests/meta/main.yml
@@ -0,0 +1,3 @@
+---
+
+allow_duplicates: yes
diff --git a/test/integration/targets/incidental_win_prepare_tests/tasks/main.yml b/test/integration/targets/incidental_win_prepare_tests/tasks/main.yml
new file mode 100644
index 00000000000..e87b614b22c
--- /dev/null
+++ b/test/integration/targets/incidental_win_prepare_tests/tasks/main.yml
@@ -0,0 +1,29 @@
+# test code for the windows versions of copy, file and template module
+# originally
+# (c) 2014, Michael DeHaan
+
+# 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 .
+
+
+- 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
diff --git a/test/integration/targets/incidental_win_psexec/aliases b/test/integration/targets/incidental_win_psexec/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_psexec/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_psexec/meta/main.yml b/test/integration/targets/incidental_win_psexec/meta/main.yml
new file mode 100644
index 00000000000..9f37e96cd90
--- /dev/null
+++ b/test/integration/targets/incidental_win_psexec/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+- setup_remote_tmp_dir
diff --git a/test/integration/targets/incidental_win_psexec/tasks/main.yml b/test/integration/targets/incidental_win_psexec/tasks/main.yml
new file mode 100644
index 00000000000..27783f9e62a
--- /dev/null
+++ b/test/integration/targets/incidental_win_psexec/tasks/main.yml
@@ -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\""
diff --git a/test/integration/targets/incidental_win_reboot/aliases b/test/integration/targets/incidental_win_reboot/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_reboot/tasks/main.yml b/test/integration/targets/incidental_win_reboot/tasks/main.yml
new file mode 100644
index 00000000000..7757e08fcdd
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/tasks/main.yml
@@ -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'"
diff --git a/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 b/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1
new file mode 100644
index 00000000000..e4a99a721de
--- /dev/null
+++ b/test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1
@@ -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
+}
diff --git a/test/integration/targets/incidental_win_security_policy/aliases b/test/integration/targets/incidental_win_security_policy/aliases
new file mode 100644
index 00000000000..a5fc90dcf48
--- /dev/null
+++ b/test/integration/targets/incidental_win_security_policy/aliases
@@ -0,0 +1,2 @@
+shippable/windows/incidental
+windows
diff --git a/test/integration/targets/incidental_win_security_policy/library/test_win_security_policy.ps1 b/test/integration/targets/incidental_win_security_policy/library/test_win_security_policy.ps1
new file mode 100644
index 00000000000..5c83c1b5d0d
--- /dev/null
+++ b/test/integration/targets/incidental_win_security_policy/library/test_win_security_policy.ps1
@@ -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
diff --git a/test/integration/targets/incidental_win_security_policy/tasks/main.yml b/test/integration/targets/incidental_win_security_policy/tasks/main.yml
new file mode 100644
index 00000000000..28fdb5ea094
--- /dev/null
+++ b/test/integration/targets/incidental_win_security_policy/tasks/main.yml
@@ -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}}"
diff --git a/test/integration/targets/incidental_win_security_policy/tasks/tests.yml b/test/integration/targets/incidental_win_security_policy/tasks/tests.yml
new file mode 100644
index 00000000000..724b6010a34
--- /dev/null
+++ b/test/integration/targets/incidental_win_security_policy/tasks/tests.yml
@@ -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 == ''
diff --git a/test/lib/ansible_test/_internal/sanity/integration_aliases.py b/test/lib/ansible_test/_internal/sanity/integration_aliases.py
index ee9adb11f86..1238d120041 100644
--- a/test/lib/ansible_test/_internal/sanity/integration_aliases.py
+++ b/test/lib/ansible_test/_internal/sanity/integration_aliases.py
@@ -243,6 +243,7 @@ class IntegrationAliasesTest(SanityVersionNeutral):
messages += self.check_ci_group(
targets=windows_targets,
find=self.format_shippable_group_alias('windows'),
+ find_incidental=['shippable/windows/incidental/'],
)
return messages
diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt
index ebef06c3afc..3b78d23f86e 100644
--- a/test/sanity/ignore.txt
+++ b/test/sanity/ignore.txt
@@ -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/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_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 metaclass-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_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/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/contrib/inventory/test_vmware_inventory.py future-import-boilerplate
test/units/contrib/inventory/test_vmware_inventory.py metaclass-boilerplate
diff --git a/test/support/windows-integration/plugins/action/win_copy.py b/test/support/windows-integration/plugins/action/win_copy.py
new file mode 100644
index 00000000000..adb918be290
--- /dev/null
+++ b/test/support/windows-integration/plugins/action/win_copy.py
@@ -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
diff --git a/test/support/windows-integration/plugins/action/win_reboot.py b/test/support/windows-integration/plugins/action/win_reboot.py
new file mode 100644
index 00000000000..c408f4f30ec
--- /dev/null
+++ b/test/support/windows-integration/plugins/action/win_reboot.py
@@ -0,0 +1,96 @@
+# Copyright: (c) 2018, Matt Davis
+# 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
diff --git a/test/support/windows-integration/plugins/action/win_template.py b/test/support/windows-integration/plugins/action/win_template.py
new file mode 100644
index 00000000000..20494b93e77
--- /dev/null
+++ b/test/support/windows-integration/plugins/action/win_template.py
@@ -0,0 +1,29 @@
+# (c) 2012-2014, Michael DeHaan
+#
+# 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 .
+
+# 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'
diff --git a/test/support/windows-integration/plugins/become/runas.py b/test/support/windows-integration/plugins/become/runas.py
new file mode 100644
index 00000000000..c8ae881c3c2
--- /dev/null
+++ b/test/support/windows-integration/plugins/become/runas.py
@@ -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
diff --git a/test/support/windows-integration/plugins/module_utils/Ansible.Service.cs b/test/support/windows-integration/plugins/module_utils/Ansible.Service.cs
new file mode 100644
index 00000000000..be0f3db3f3b
--- /dev/null
+++ b/test/support/windows-integration/plugins/module_utils/Ansible.Service.cs
@@ -0,0 +1,1341 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+using Ansible.Privilege;
+
+namespace Ansible.Service
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct ENUM_SERVICE_STATUSW
+ {
+ public string lpServiceName;
+ public string lpDisplayName;
+ public SERVICE_STATUS ServiceStatus;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct QUERY_SERVICE_CONFIGW
+ {
+ public ServiceType dwServiceType;
+ public ServiceStartType dwStartType;
+ public ErrorControl dwErrorControl;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpBinaryPathName;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpLoadOrderGroup;
+ public Int32 dwTagId;
+ public IntPtr lpDependencies; // Can't rely on marshaling as dependencies are delimited by \0.
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpServiceStartName;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpDisplayName;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SC_ACTION
+ {
+ public FailureAction Type;
+ public UInt32 Delay;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_DELAYED_AUTO_START_INFO
+ {
+ public bool fDelayedAutostart;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct SERVICE_DESCRIPTIONW
+ {
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpDescription;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_FAILURE_ACTIONS_FLAG
+ {
+ public bool fFailureActionsOnNonCrashFailures;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct SERVICE_FAILURE_ACTIONSW
+ {
+ public UInt32 dwResetPeriod;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpRebootMsg;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpCommand;
+ public UInt32 cActions;
+ public IntPtr lpsaActions;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_LAUNCH_PROTECTED_INFO
+ {
+ public LaunchProtection dwLaunchProtected;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_PREFERRED_NODE_INFO
+ {
+ public UInt16 usPreferredNode;
+ public bool fDelete;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_PRESHUTDOWN_INFO
+ {
+ public UInt32 dwPreshutdownTimeout;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct SERVICE_REQUIRED_PRIVILEGES_INFOW
+ {
+ // Can't rely on marshaling as privileges are delimited by \0.
+ public IntPtr pmszRequiredPrivileges;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_SID_INFO
+ {
+ public ServiceSidInfo dwServiceSidType;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_STATUS
+ {
+ public ServiceType dwServiceType;
+ public ServiceStatus dwCurrentState;
+ public ControlsAccepted dwControlsAccepted;
+ public UInt32 dwWin32ExitCode;
+ public UInt32 dwServiceSpecificExitCode;
+ public UInt32 dwCheckPoint;
+ public UInt32 dwWaitHint;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_STATUS_PROCESS
+ {
+ public ServiceType dwServiceType;
+ public ServiceStatus dwCurrentState;
+ public ControlsAccepted dwControlsAccepted;
+ public UInt32 dwWin32ExitCode;
+ public UInt32 dwServiceSpecificExitCode;
+ public UInt32 dwCheckPoint;
+ public UInt32 dwWaitHint;
+ public UInt32 dwProcessId;
+ public ServiceFlags dwServiceFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_TRIGGER
+ {
+ public TriggerType dwTriggerType;
+ public TriggerAction dwAction;
+ public IntPtr pTriggerSubtype;
+ public UInt32 cDataItems;
+ public IntPtr pDataItems;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_TRIGGER_SPECIFIC_DATA_ITEM
+ {
+ public TriggerDataType dwDataType;
+ public UInt32 cbData;
+ public IntPtr pData;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SERVICE_TRIGGER_INFO
+ {
+ public UInt32 cTriggers;
+ public IntPtr pTriggers;
+ public IntPtr pReserved;
+ }
+
+ public enum ConfigInfoLevel : uint
+ {
+ SERVICE_CONFIG_DESCRIPTION = 0x00000001,
+ SERVICE_CONFIG_FAILURE_ACTIONS = 0x00000002,
+ SERVICE_CONFIG_DELAYED_AUTO_START_INFO = 0x00000003,
+ SERVICE_CONFIG_FAILURE_ACTIONS_FLAG = 0x00000004,
+ SERVICE_CONFIG_SERVICE_SID_INFO = 0x00000005,
+ SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO = 0x00000006,
+ SERVICE_CONFIG_PRESHUTDOWN_INFO = 0x00000007,
+ SERVICE_CONFIG_TRIGGER_INFO = 0x00000008,
+ SERVICE_CONFIG_PREFERRED_NODE = 0x00000009,
+ SERVICE_CONFIG_LAUNCH_PROTECTED = 0x0000000c,
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern bool ChangeServiceConfigW(
+ SafeHandle hService,
+ ServiceType dwServiceType,
+ ServiceStartType dwStartType,
+ ErrorControl dwErrorControl,
+ string lpBinaryPathName,
+ string lpLoadOrderGroup,
+ IntPtr lpdwTagId,
+ string lpDependencies,
+ string lpServiceStartName,
+ string lpPassword,
+ string lpDisplayName);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern bool ChangeServiceConfig2W(
+ SafeHandle hService,
+ NativeHelpers.ConfigInfoLevel dwInfoLevel,
+ IntPtr lpInfo);
+
+ [DllImport("Advapi32.dll", SetLastError = true)]
+ public static extern bool CloseServiceHandle(
+ IntPtr hSCObject);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern SafeServiceHandle CreateServiceW(
+ SafeHandle hSCManager,
+ string lpServiceName,
+ string lpDisplayName,
+ ServiceRights dwDesiredAccess,
+ ServiceType dwServiceType,
+ ServiceStartType dwStartType,
+ ErrorControl dwErrorControl,
+ string lpBinaryPathName,
+ string lpLoadOrderGroup,
+ IntPtr lpdwTagId,
+ string lpDependencies,
+ string lpServiceStartName,
+ string lpPassword);
+
+ [DllImport("Advapi32.dll", SetLastError = true)]
+ public static extern bool DeleteService(
+ SafeHandle hService);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern bool EnumDependentServicesW(
+ SafeHandle hService,
+ UInt32 dwServiceState,
+ SafeMemoryBuffer lpServices,
+ UInt32 cbBufSize,
+ out UInt32 pcbBytesNeeded,
+ out UInt32 lpServicesReturned);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern SafeServiceHandle OpenSCManagerW(
+ string lpMachineName,
+ string lpDatabaseNmae,
+ SCMRights dwDesiredAccess);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern SafeServiceHandle OpenServiceW(
+ SafeHandle hSCManager,
+ string lpServiceName,
+ ServiceRights dwDesiredAccess);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern bool QueryServiceConfigW(
+ SafeHandle hService,
+ IntPtr lpServiceConfig,
+ UInt32 cbBufSize,
+ out UInt32 pcbBytesNeeded);
+
+ [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern bool QueryServiceConfig2W(
+ SafeHandle hservice,
+ NativeHelpers.ConfigInfoLevel dwInfoLevel,
+ IntPtr lpBuffer,
+ UInt32 cbBufSize,
+ out UInt32 pcbBytesNeeded);
+
+ [DllImport("Advapi32.dll", SetLastError = true)]
+ public static extern bool QueryServiceStatusEx(
+ SafeHandle hService,
+ UInt32 InfoLevel,
+ IntPtr lpBuffer,
+ UInt32 cbBufSize,
+ out UInt32 pcbBytesNeeded);
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public UInt32 BufferLength { get; internal set; }
+
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ BufferLength = (UInt32)cb;
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ internal class SafeServiceHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeServiceHandle() : base(true) { }
+ public SafeServiceHandle(IntPtr handle) : base(true) { this.handle = handle; }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.CloseServiceHandle(handle);
+ }
+ }
+
+ [Flags]
+ public enum ControlsAccepted : uint
+ {
+ None = 0x00000000,
+ Stop = 0x00000001,
+ PauseContinue = 0x00000002,
+ Shutdown = 0x00000004,
+ ParamChange = 0x00000008,
+ NetbindChange = 0x00000010,
+ HardwareProfileChange = 0x00000020,
+ PowerEvent = 0x00000040,
+ SessionChange = 0x00000080,
+ PreShutdown = 0x00000100,
+ }
+
+ public enum ErrorControl : uint
+ {
+ Ignore = 0x00000000,
+ Normal = 0x00000001,
+ Severe = 0x00000002,
+ Critical = 0x00000003,
+ }
+
+ public enum FailureAction : uint
+ {
+ None = 0x00000000,
+ Restart = 0x00000001,
+ Reboot = 0x00000002,
+ RunCommand = 0x00000003,
+ }
+
+ public enum LaunchProtection : uint
+ {
+ None = 0,
+ Windows = 1,
+ WindowsLight = 2,
+ AntimalwareLight = 3,
+ }
+
+ [Flags]
+ public enum SCMRights : uint
+ {
+ Connect = 0x00000001,
+ CreateService = 0x00000002,
+ EnumerateService = 0x00000004,
+ Lock = 0x00000008,
+ QueryLockStatus = 0x00000010,
+ ModifyBootConfig = 0x00000020,
+ AllAccess = 0x000F003F,
+ }
+
+ [Flags]
+ public enum ServiceFlags : uint
+ {
+ None = 0x0000000,
+ RunsInSystemProcess = 0x00000001,
+ }
+
+ [Flags]
+ public enum ServiceRights : uint
+ {
+ QueryConfig = 0x00000001,
+ ChangeConfig = 0x00000002,
+ QueryStatus = 0x00000004,
+ EnumerateDependents = 0x00000008,
+ Start = 0x00000010,
+ Stop = 0x00000020,
+ PauseContinue = 0x00000040,
+ Interrogate = 0x00000080,
+ UserDefinedControl = 0x00000100,
+ Delete = 0x00010000,
+ ReadControl = 0x00020000,
+ WriteDac = 0x00040000,
+ WriteOwner = 0x00080000,
+ AllAccess = 0x000F01FF,
+ AccessSystemSecurity = 0x01000000,
+ }
+
+ public enum ServiceStartType : uint
+ {
+ BootStart = 0x00000000,
+ SystemStart = 0x00000001,
+ AutoStart = 0x00000002,
+ DemandStart = 0x00000003,
+ Disabled = 0x00000004,
+
+ // Not part of ChangeServiceConfig enumeration but built by the Srvice class for the StartType property.
+ AutoStartDelayed = 0x1000000
+ }
+
+ [Flags]
+ public enum ServiceType : uint
+ {
+ KernelDriver = 0x00000001,
+ FileSystemDriver = 0x00000002,
+ Adapter = 0x00000004,
+ RecognizerDriver = 0x00000008,
+ Driver = KernelDriver | FileSystemDriver | RecognizerDriver,
+ Win32OwnProcess = 0x00000010,
+ Win32ShareProcess = 0x00000020,
+ Win32 = Win32OwnProcess | Win32ShareProcess,
+ UserProcess = 0x00000040,
+ UserOwnprocess = Win32OwnProcess | UserProcess,
+ UserShareProcess = Win32ShareProcess | UserProcess,
+ UserServiceInstance = 0x00000080,
+ InteractiveProcess = 0x00000100,
+ PkgService = 0x00000200,
+ }
+
+ public enum ServiceSidInfo : uint
+ {
+ None,
+ Unrestricted,
+ Restricted = 3,
+ }
+
+ public enum ServiceStatus : uint
+ {
+ Stopped = 0x00000001,
+ StartPending = 0x00000002,
+ StopPending = 0x00000003,
+ Running = 0x00000004,
+ ContinuePending = 0x00000005,
+ PausePending = 0x00000006,
+ Paused = 0x00000007,
+ }
+
+ public enum TriggerAction : uint
+ {
+ ServiceStart = 0x00000001,
+ ServiceStop = 0x000000002,
+ }
+
+ public enum TriggerDataType : uint
+ {
+ Binary = 00000001,
+ String = 0x00000002,
+ Level = 0x00000003,
+ KeywordAny = 0x00000004,
+ KeywordAll = 0x00000005,
+ }
+
+ public enum TriggerType : uint
+ {
+ DeviceInterfaceArrival = 0x00000001,
+ IpAddressAvailability = 0x00000002,
+ DomainJoin = 0x00000003,
+ FirewallPortEvent = 0x00000004,
+ GroupPolicy = 0x00000005,
+ NetworkEndpoint = 0x00000006,
+ Custom = 0x00000014,
+ }
+
+ public class ServiceManagerException : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+
+ public ServiceManagerException(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public ServiceManagerException(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
+ }
+
+ public override string Message { get { return _msg; } }
+ public static explicit operator ServiceManagerException(string message)
+ {
+ return new ServiceManagerException(message);
+ }
+ }
+
+ public class Action
+ {
+ public FailureAction Type;
+ public UInt32 Delay;
+ }
+
+ public class FailureActions
+ {
+ public UInt32? ResetPeriod = null; // Get is always populated, can be null on set to preserve existing.
+ public string RebootMsg = null;
+ public string Command = null;
+ public List Actions = null;
+
+ public FailureActions() { }
+
+ internal FailureActions(NativeHelpers.SERVICE_FAILURE_ACTIONSW actions)
+ {
+ ResetPeriod = actions.dwResetPeriod;
+ RebootMsg = actions.lpRebootMsg;
+ Command = actions.lpCommand;
+ Actions = new List();
+
+ int actionLength = Marshal.SizeOf(typeof(NativeHelpers.SC_ACTION));
+ for (int i = 0; i < actions.cActions; i++)
+ {
+ IntPtr actionPtr = IntPtr.Add(actions.lpsaActions, i * actionLength);
+
+ NativeHelpers.SC_ACTION rawAction = (NativeHelpers.SC_ACTION)Marshal.PtrToStructure(
+ actionPtr, typeof(NativeHelpers.SC_ACTION));
+
+ Actions.Add(new Action()
+ {
+ Type = rawAction.Type,
+ Delay = rawAction.Delay,
+ });
+ }
+ }
+ }
+
+ public class TriggerItem
+ {
+ public TriggerDataType Type;
+ public object Data; // Can be string, List, byte, byte[], or Int64 depending on Type.
+
+ public TriggerItem() { }
+
+ internal TriggerItem(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM dataItem)
+ {
+ Type = dataItem.dwDataType;
+
+ byte[] itemBytes = new byte[dataItem.cbData];
+ Marshal.Copy(dataItem.pData, itemBytes, 0, itemBytes.Length);
+
+ switch (dataItem.dwDataType)
+ {
+ case TriggerDataType.String:
+ string value = Encoding.Unicode.GetString(itemBytes, 0, itemBytes.Length);
+
+ if (value.EndsWith("\0\0"))
+ {
+ // Multistring with a delimiter of \0 and terminated with \0\0.
+ Data = new List(value.Split(new char[1] { '\0' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ else
+ // Just a single string with null character at the end, strip it off.
+ Data = value.Substring(0, value.Length - 1);
+ break;
+ case TriggerDataType.Level:
+ Data = itemBytes[0];
+ break;
+ case TriggerDataType.KeywordAll:
+ case TriggerDataType.KeywordAny:
+ Data = BitConverter.ToUInt64(itemBytes, 0);
+ break;
+ default:
+ Data = itemBytes;
+ break;
+ }
+ }
+ }
+
+ public class Trigger
+ {
+ // https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger
+ public const string NAMED_PIPE_EVENT_GUID = "1f81d131-3fac-4537-9e0c-7e7b0c2f4b55";
+ public const string RPC_INTERFACE_EVENT_GUID = "bc90d167-9470-4139-a9ba-be0bbbf5b74d";
+ public const string DOMAIN_JOIN_GUID = "1ce20aba-9851-4421-9430-1ddeb766e809";
+ public const string DOMAIN_LEAVE_GUID = "ddaf516e-58c2-4866-9574-c3b615d42ea1";
+ public const string FIREWALL_PORT_OPEN_GUID = "b7569e07-8421-4ee0-ad10-86915afdad09";
+ public const string FIREWALL_PORT_CLOSE_GUID = "a144ed38-8e12-4de4-9d96-e64740b1a524";
+ public const string MACHINE_POLICY_PRESENT_GUID = "659fcae6-5bdb-4da9-b1ff-ca2a178d46e0";
+ public const string NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID = "4f27f2de-14e2-430b-a549-7cd48cbc8245";
+ public const string NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID = "cc4ba62a-162e-4648-847a-b6bdf993e335";
+ public const string USER_POLICY_PRESENT_GUID = "54fb46c8-f089-464c-b1fd-59d1b62c3b50";
+
+ public TriggerType Type;
+ public TriggerAction Action;
+ public Guid SubType;
+ public List DataItems = new List();
+
+ public Trigger() { }
+
+ internal Trigger(NativeHelpers.SERVICE_TRIGGER trigger)
+ {
+ Type = trigger.dwTriggerType;
+ Action = trigger.dwAction;
+ SubType = (Guid)Marshal.PtrToStructure(trigger.pTriggerSubtype, typeof(Guid));
+
+ int dataItemLength = Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM));
+ for (int i = 0; i < trigger.cDataItems; i++)
+ {
+ IntPtr dataPtr = IntPtr.Add(trigger.pDataItems, i * dataItemLength);
+
+ var dataItem = (NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)Marshal.PtrToStructure(
+ dataPtr, typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM));
+
+ DataItems.Add(new TriggerItem(dataItem));
+ }
+ }
+ }
+
+ public class Service : IDisposable
+ {
+ private const UInt32 SERVICE_NO_CHANGE = 0xFFFFFFFF;
+
+ private SafeServiceHandle _scmHandle;
+ private SafeServiceHandle _serviceHandle;
+ private SafeMemoryBuffer _rawServiceConfig;
+ private NativeHelpers.SERVICE_STATUS_PROCESS _statusProcess;
+
+ private NativeHelpers.QUERY_SERVICE_CONFIGW _ServiceConfig
+ {
+ get
+ {
+ return (NativeHelpers.QUERY_SERVICE_CONFIGW)Marshal.PtrToStructure(
+ _rawServiceConfig.DangerousGetHandle(), typeof(NativeHelpers.QUERY_SERVICE_CONFIGW));
+ }
+ }
+
+ // ServiceConfig
+ public string ServiceName { get; private set; }
+
+ public ServiceType ServiceType
+ {
+ get { return _ServiceConfig.dwServiceType; }
+ set { ChangeServiceConfig(serviceType: value); }
+ }
+
+ public ServiceStartType StartType
+ {
+ get
+ {
+ ServiceStartType startType = _ServiceConfig.dwStartType;
+ if (startType == ServiceStartType.AutoStart)
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DELAYED_AUTO_START_INFO);
+
+ if (value.fDelayedAutostart)
+ startType = ServiceStartType.AutoStartDelayed;
+ }
+
+ return startType;
+ }
+ set
+ {
+ ServiceStartType newStartType = value;
+ bool delayedStart = false;
+ if (value == ServiceStartType.AutoStartDelayed)
+ {
+ newStartType = ServiceStartType.AutoStart;
+ delayedStart = true;
+ }
+
+ ChangeServiceConfig(startType: newStartType);
+
+ var info = new NativeHelpers.SERVICE_DELAYED_AUTO_START_INFO()
+ {
+ fDelayedAutostart = delayedStart,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DELAYED_AUTO_START_INFO, info);
+ }
+ }
+
+ public ErrorControl ErrorControl
+ {
+ get { return _ServiceConfig.dwErrorControl; }
+ set { ChangeServiceConfig(errorControl: value); }
+ }
+
+ public string Path
+ {
+ get { return _ServiceConfig.lpBinaryPathName; }
+ set { ChangeServiceConfig(binaryPath: value); }
+ }
+
+ public string LoadOrderGroup
+ {
+ get { return _ServiceConfig.lpLoadOrderGroup; }
+ set { ChangeServiceConfig(loadOrderGroup: value); }
+ }
+
+ public List DependentOn
+ {
+ get
+ {
+ StringBuilder deps = new StringBuilder();
+ IntPtr depPtr = _ServiceConfig.lpDependencies;
+
+ bool wasNull = false;
+ while (true)
+ {
+ // Get the current char at the pointer and add it to the StringBuilder.
+ byte[] charBytes = new byte[sizeof(char)];
+ Marshal.Copy(depPtr, charBytes, 0, charBytes.Length);
+ depPtr = IntPtr.Add(depPtr, charBytes.Length);
+ char currentChar = BitConverter.ToChar(charBytes, 0);
+ deps.Append(currentChar);
+
+ // If the previous and current char is \0 exit the loop.
+ if (currentChar == '\0' && wasNull)
+ break;
+ wasNull = currentChar == '\0';
+ }
+
+ return new List(deps.ToString().Split(new char[1] { '\0' },
+ StringSplitOptions.RemoveEmptyEntries));
+ }
+ set { ChangeServiceConfig(dependencies: value); }
+ }
+
+ public IdentityReference Account
+ {
+ get
+ {
+ if (_ServiceConfig.lpServiceStartName == null)
+ // User services don't have the start name specified and will be null.
+ return null;
+ else if (_ServiceConfig.lpServiceStartName == "LocalSystem")
+ // Special string used for the SYSTEM account, this is the same even for different localisations.
+ return (NTAccount)new SecurityIdentifier("S-1-5-18").Translate(typeof(NTAccount));
+ else
+ return new NTAccount(_ServiceConfig.lpServiceStartName);
+ }
+ set
+ {
+ string startName = null;
+ string pass = null;
+
+ if (value != null)
+ {
+ // Create a SID and convert back from a SID to get the Netlogon form regardless of the input
+ // specified.
+ SecurityIdentifier accountSid = (SecurityIdentifier)value.Translate(typeof(SecurityIdentifier));
+ NTAccount accountName = (NTAccount)accountSid.Translate(typeof(NTAccount));
+ string[] accountSplit = accountName.Value.Split(new char[1] { '\\' }, 2);
+
+ // SYSTEM, Local Service, Network Service
+ List serviceAccounts = new List { "S-1-5-18", "S-1-5-19", "S-1-5-20" };
+
+ // Well known service accounts and MSAs should have no password set. Explicitly blank out the
+ // existing password to ensure older passwords are no longer stored by Windows.
+ if (serviceAccounts.Contains(accountSid.Value) || accountSplit[1].EndsWith("$"))
+ pass = "";
+
+ // The SYSTEM account uses this special string to specify that account otherwise use the original
+ // NTAccount value in case it is in a custom format (not Netlogon) for a reason.
+ if (accountSid.Value == serviceAccounts[0])
+ startName = "LocalSystem";
+ else
+ startName = value.Translate(typeof(NTAccount)).Value;
+ }
+
+ ChangeServiceConfig(startName: startName, password: pass);
+ }
+ }
+
+ public string Password { set { ChangeServiceConfig(password: value); } }
+
+ public string DisplayName
+ {
+ get { return _ServiceConfig.lpDisplayName; }
+ set { ChangeServiceConfig(displayName: value); }
+ }
+
+ // ServiceConfig2
+
+ public string Description
+ {
+ get
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DESCRIPTION);
+
+ return value.lpDescription;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_DESCRIPTIONW()
+ {
+ lpDescription = value,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DESCRIPTION, info);
+ }
+ }
+
+ public FailureActions FailureActions
+ {
+ get
+ {
+ using (SafeMemoryBuffer b = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS))
+ {
+ NativeHelpers.SERVICE_FAILURE_ACTIONSW value = (NativeHelpers.SERVICE_FAILURE_ACTIONSW)
+ Marshal.PtrToStructure(b.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_FAILURE_ACTIONSW));
+
+ return new FailureActions(value);
+ }
+ }
+ set
+ {
+ // dwResetPeriod and lpsaActions must be set together, we need to read the existing config if someone
+ // wants to update 1 or the other but both aren't explicitly defined.
+ UInt32? resetPeriod = value.ResetPeriod;
+ List actions = value.Actions;
+ if ((resetPeriod != null && actions == null) || (resetPeriod == null && actions != null))
+ {
+ FailureActions existingValue = this.FailureActions;
+
+ if (resetPeriod != null && existingValue.Actions.Count == 0)
+ throw new ArgumentException(
+ "Cannot set FailureAction ResetPeriod without explicit Actions and no existing Actions");
+ else if (resetPeriod == null)
+ resetPeriod = (UInt32)existingValue.ResetPeriod;
+
+ if (actions == null)
+ actions = existingValue.Actions;
+ }
+
+ var info = new NativeHelpers.SERVICE_FAILURE_ACTIONSW()
+ {
+ dwResetPeriod = resetPeriod == null ? 0 : (UInt32)resetPeriod,
+ lpRebootMsg = value.RebootMsg,
+ lpCommand = value.Command,
+ cActions = actions == null ? 0 : (UInt32)actions.Count,
+ lpsaActions = IntPtr.Zero,
+ };
+
+ // null means to keep the existing actions whereas an empty list deletes the actions.
+ if (actions == null)
+ {
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, info);
+ return;
+ }
+
+ int actionLength = Marshal.SizeOf(typeof(NativeHelpers.SC_ACTION));
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(actionLength * actions.Count))
+ {
+ info.lpsaActions = buffer.DangerousGetHandle();
+ HashSet privileges = new HashSet();
+
+ for (int i = 0; i < actions.Count; i++)
+ {
+ IntPtr actionPtr = IntPtr.Add(info.lpsaActions, i * actionLength);
+ NativeHelpers.SC_ACTION action = new NativeHelpers.SC_ACTION()
+ {
+ Delay = actions[i].Delay,
+ Type = actions[i].Type,
+ };
+ Marshal.StructureToPtr(action, actionPtr, false);
+
+ // Need to make sure the SeShutdownPrivilege is enabled when adding a reboot failure action.
+ if (action.Type == FailureAction.Reboot)
+ privileges.Add("SeShutdownPrivilege");
+ }
+
+ using (new PrivilegeEnabler(true, privileges.ToList().ToArray()))
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, info);
+ }
+ }
+ }
+
+ public bool FailureActionsOnNonCrashFailures
+ {
+ get
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG);
+
+ return value.fFailureActionsOnNonCrashFailures;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_FAILURE_ACTIONS_FLAG()
+ {
+ fFailureActionsOnNonCrashFailures = value,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, info);
+ }
+ }
+
+ public ServiceSidInfo ServiceSidInfo
+ {
+ get
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_SERVICE_SID_INFO);
+
+ return value.dwServiceSidType;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_SID_INFO()
+ {
+ dwServiceSidType = value,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_SERVICE_SID_INFO, info);
+ }
+ }
+
+ public List RequiredPrivileges
+ {
+ get
+ {
+ using (SafeMemoryBuffer buffer = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO))
+ {
+ var value = (NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW)Marshal.PtrToStructure(
+ buffer.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW));
+
+ int structLength = Marshal.SizeOf(value);
+ int stringLength = ((int)buffer.BufferLength - structLength) / sizeof(char);
+
+ if (stringLength > 0)
+ {
+ string privilegesString = Marshal.PtrToStringUni(value.pmszRequiredPrivileges, stringLength);
+ return new List(privilegesString.Split(new char[1] { '\0' },
+ StringSplitOptions.RemoveEmptyEntries));
+ }
+ else
+ return new List();
+ }
+ }
+ set
+ {
+ string privilegeString = String.Join("\0", value ?? new List()) + "\0\0";
+
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(Marshal.StringToHGlobalUni(privilegeString)))
+ {
+ var info = new NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW()
+ {
+ pmszRequiredPrivileges = buffer.DangerousGetHandle(),
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO, info);
+ }
+ }
+ }
+
+ public UInt32 PreShutdownTimeout
+ {
+ get
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PRESHUTDOWN_INFO);
+
+ return value.dwPreshutdownTimeout;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_PRESHUTDOWN_INFO()
+ {
+ dwPreshutdownTimeout = value,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PRESHUTDOWN_INFO, info);
+ }
+ }
+
+ public List Triggers
+ {
+ get
+ {
+ List triggers = new List();
+
+ using (SafeMemoryBuffer b = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO))
+ {
+ var value = (NativeHelpers.SERVICE_TRIGGER_INFO)Marshal.PtrToStructure(
+ b.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_TRIGGER_INFO));
+
+ int triggerLength = Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER));
+ for (int i = 0; i < value.cTriggers; i++)
+ {
+ IntPtr triggerPtr = IntPtr.Add(value.pTriggers, i * triggerLength);
+ var trigger = (NativeHelpers.SERVICE_TRIGGER)Marshal.PtrToStructure(triggerPtr,
+ typeof(NativeHelpers.SERVICE_TRIGGER));
+
+ triggers.Add(new Trigger(trigger));
+ }
+ }
+
+ return triggers;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_TRIGGER_INFO()
+ {
+ cTriggers = value == null ? 0 : (UInt32)value.Count,
+ pTriggers = IntPtr.Zero,
+ pReserved = IntPtr.Zero,
+ };
+
+ if (info.cTriggers == 0)
+ {
+ try
+ {
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO, info);
+ }
+ catch (ServiceManagerException e)
+ {
+ // Can fail with ERROR_INVALID_PARAMETER if no triggers were already set on the service, just
+ // continue as the service is what we want it to be.
+ if (e.NativeErrorCode != 87)
+ throw;
+ }
+ return;
+ }
+
+ // Due to the dynamic nature of the trigger structure(s) we need to manually calculate the size of the
+ // data items on each trigger if present. This also serializes the raw data items to bytes here.
+ int structDataLength = 0;
+ int dataLength = 0;
+ Queue dataItems = new Queue();
+ foreach (Trigger trigger in value)
+ {
+ if (trigger.DataItems == null || trigger.DataItems.Count == 0)
+ continue;
+
+ foreach (TriggerItem dataItem in trigger.DataItems)
+ {
+ structDataLength += Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM));
+
+ byte[] dataItemBytes;
+ Type dataItemType = dataItem.Data.GetType();
+ if (dataItemType == typeof(byte))
+ dataItemBytes = new byte[1] { (byte)dataItem.Data };
+ else if (dataItemType == typeof(byte[]))
+ dataItemBytes = (byte[])dataItem.Data;
+ else if (dataItemType == typeof(UInt64))
+ dataItemBytes = BitConverter.GetBytes((UInt64)dataItem.Data);
+ else if (dataItemType == typeof(string))
+ dataItemBytes = Encoding.Unicode.GetBytes((string)dataItem.Data + "\0");
+ else if (dataItemType == typeof(List))
+ dataItemBytes = Encoding.Unicode.GetBytes(
+ String.Join("\0", (List)dataItem.Data) + "\0");
+ else
+ throw new ArgumentException(String.Format("Trigger data type '{0}' not a value type",
+ dataItemType.Name));
+
+ dataLength += dataItemBytes.Length;
+ dataItems.Enqueue(dataItemBytes);
+ }
+ }
+
+ using (SafeMemoryBuffer triggerBuffer = new SafeMemoryBuffer(
+ value.Count * Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER))))
+ using (SafeMemoryBuffer triggerGuidBuffer = new SafeMemoryBuffer(
+ value.Count * Marshal.SizeOf(typeof(Guid))))
+ using (SafeMemoryBuffer dataItemBuffer = new SafeMemoryBuffer(structDataLength))
+ using (SafeMemoryBuffer dataBuffer = new SafeMemoryBuffer(dataLength))
+ {
+ info.pTriggers = triggerBuffer.DangerousGetHandle();
+
+ IntPtr triggerPtr = triggerBuffer.DangerousGetHandle();
+ IntPtr guidPtr = triggerGuidBuffer.DangerousGetHandle();
+ IntPtr dataItemPtr = dataItemBuffer.DangerousGetHandle();
+ IntPtr dataPtr = dataBuffer.DangerousGetHandle();
+
+ foreach (Trigger trigger in value)
+ {
+ int dataCount = trigger.DataItems == null ? 0 : trigger.DataItems.Count;
+ var rawTrigger = new NativeHelpers.SERVICE_TRIGGER()
+ {
+ dwTriggerType = trigger.Type,
+ dwAction = trigger.Action,
+ pTriggerSubtype = guidPtr,
+ cDataItems = (UInt32)dataCount,
+ pDataItems = dataCount == 0 ? IntPtr.Zero : dataItemPtr,
+ };
+ guidPtr = StructureToPtr(trigger.SubType, guidPtr);
+
+ for (int i = 0; i < rawTrigger.cDataItems; i++)
+ {
+ byte[] dataItemBytes = dataItems.Dequeue();
+ var rawTriggerData = new NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM()
+ {
+ dwDataType = trigger.DataItems[i].Type,
+ cbData = (UInt32)dataItemBytes.Length,
+ pData = dataPtr,
+ };
+ Marshal.Copy(dataItemBytes, 0, dataPtr, dataItemBytes.Length);
+ dataPtr = IntPtr.Add(dataPtr, dataItemBytes.Length);
+
+ dataItemPtr = StructureToPtr(rawTriggerData, dataItemPtr);
+ }
+
+ triggerPtr = StructureToPtr(rawTrigger, triggerPtr);
+ }
+
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO, info);
+ }
+ }
+ }
+
+ public UInt16? PreferredNode
+ {
+ get
+ {
+ try
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PREFERRED_NODE);
+
+ return value.usPreferredNode;
+ }
+ catch (ServiceManagerException e)
+ {
+ // If host has no NUMA support this will fail with ERROR_INVALID_PARAMETER
+ if (e.NativeErrorCode == 0x00000057) // ERROR_INVALID_PARAMETER
+ return null;
+
+ throw;
+ }
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_PREFERRED_NODE_INFO();
+ if (value == null)
+ info.fDelete = true;
+ else
+ info.usPreferredNode = (UInt16)value;
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PREFERRED_NODE, info);
+ }
+ }
+
+ public LaunchProtection LaunchProtection
+ {
+ get
+ {
+ var value = QueryServiceConfig2(
+ NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_LAUNCH_PROTECTED);
+
+ return value.dwLaunchProtected;
+ }
+ set
+ {
+ var info = new NativeHelpers.SERVICE_LAUNCH_PROTECTED_INFO()
+ {
+ dwLaunchProtected = value,
+ };
+ ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_LAUNCH_PROTECTED, info);
+ }
+ }
+
+ // ServiceStatus
+ public ServiceStatus State { get { return _statusProcess.dwCurrentState; } }
+
+ public ControlsAccepted ControlsAccepted { get { return _statusProcess.dwControlsAccepted; } }
+
+ public UInt32 Win32ExitCode { get { return _statusProcess.dwWin32ExitCode; } }
+
+ public UInt32 ServiceExitCode { get { return _statusProcess.dwServiceSpecificExitCode; } }
+
+ public UInt32 Checkpoint { get { return _statusProcess.dwCheckPoint; } }
+
+ public UInt32 WaitHint { get { return _statusProcess.dwWaitHint; } }
+
+ public UInt32 ProcessId { get { return _statusProcess.dwProcessId; } }
+
+ public ServiceFlags ServiceFlags { get { return _statusProcess.dwServiceFlags; } }
+
+ public Service(string name) : this(name, ServiceRights.AllAccess) { }
+
+ public Service(string name, ServiceRights access) : this(name, access, SCMRights.Connect) { }
+
+ public Service(string name, ServiceRights access, SCMRights scmAccess)
+ {
+ ServiceName = name;
+ _scmHandle = OpenSCManager(scmAccess);
+ _serviceHandle = NativeMethods.OpenServiceW(_scmHandle, name, access);
+ if (_serviceHandle.IsInvalid)
+ throw new ServiceManagerException(String.Format("Failed to open service '{0}'", name));
+
+ Refresh();
+ }
+
+ private Service(SafeServiceHandle scmHandle, SafeServiceHandle serviceHandle, string name)
+ {
+ ServiceName = name;
+ _scmHandle = scmHandle;
+ _serviceHandle = serviceHandle;
+
+ Refresh();
+ }
+
+ // EnumDependentServices
+ public List DependedBy
+ {
+ get
+ {
+ UInt32 bytesNeeded = 0;
+ UInt32 numServices = 0;
+ NativeMethods.EnumDependentServicesW(_serviceHandle, 3, new SafeMemoryBuffer(IntPtr.Zero), 0,
+ out bytesNeeded, out numServices);
+
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded))
+ {
+ if (!NativeMethods.EnumDependentServicesW(_serviceHandle, 3, buffer, bytesNeeded, out bytesNeeded,
+ out numServices))
+ {
+ throw new ServiceManagerException("Failed to enumerated dependent services");
+ }
+
+ List dependents = new List();
+ Type enumType = typeof(NativeHelpers.ENUM_SERVICE_STATUSW);
+ for (int i = 0; i < numServices; i++)
+ {
+ var service = (NativeHelpers.ENUM_SERVICE_STATUSW)Marshal.PtrToStructure(
+ IntPtr.Add(buffer.DangerousGetHandle(), i * Marshal.SizeOf(enumType)), enumType);
+
+ dependents.Add(service.lpServiceName);
+ }
+
+ return dependents;
+ }
+ }
+ }
+
+ public static Service Create(string name, string binaryPath, string displayName = null,
+ ServiceType serviceType = ServiceType.Win32OwnProcess,
+ ServiceStartType startType = ServiceStartType.DemandStart, ErrorControl errorControl = ErrorControl.Normal,
+ string loadOrderGroup = null, List dependencies = null, string startName = null,
+ string password = null)
+ {
+ SafeServiceHandle scmHandle = OpenSCManager(SCMRights.CreateService | SCMRights.Connect);
+
+ if (displayName == null)
+ displayName = name;
+
+ string depString = null;
+ if (dependencies != null && dependencies.Count > 0)
+ depString = String.Join("\0", dependencies) + "\0\0";
+
+ SafeServiceHandle serviceHandle = NativeMethods.CreateServiceW(scmHandle, name, displayName,
+ ServiceRights.AllAccess, serviceType, startType, errorControl, binaryPath,
+ loadOrderGroup, IntPtr.Zero, depString, startName, password);
+
+ if (serviceHandle.IsInvalid)
+ throw new ServiceManagerException(String.Format("Failed to create new service '{0}'", name));
+
+ return new Service(scmHandle, serviceHandle, name);
+ }
+
+ public void Delete()
+ {
+ if (!NativeMethods.DeleteService(_serviceHandle))
+ throw new ServiceManagerException("Failed to delete service");
+ Dispose();
+ }
+
+ public void Dispose()
+ {
+ if (_serviceHandle != null)
+ _serviceHandle.Dispose();
+
+ if (_scmHandle != null)
+ _scmHandle.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ public void Refresh()
+ {
+ UInt32 bytesNeeded;
+ NativeMethods.QueryServiceConfigW(_serviceHandle, IntPtr.Zero, 0, out bytesNeeded);
+
+ _rawServiceConfig = new SafeMemoryBuffer((int)bytesNeeded);
+ if (!NativeMethods.QueryServiceConfigW(_serviceHandle, _rawServiceConfig.DangerousGetHandle(), bytesNeeded,
+ out bytesNeeded))
+ {
+ throw new ServiceManagerException("Failed to query service config");
+ }
+
+ NativeMethods.QueryServiceStatusEx(_serviceHandle, 0, IntPtr.Zero, 0, out bytesNeeded);
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded))
+ {
+ if (!NativeMethods.QueryServiceStatusEx(_serviceHandle, 0, buffer.DangerousGetHandle(), bytesNeeded,
+ out bytesNeeded))
+ {
+ throw new ServiceManagerException("Failed to query service status");
+ }
+
+ _statusProcess = (NativeHelpers.SERVICE_STATUS_PROCESS)Marshal.PtrToStructure(
+ buffer.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_STATUS_PROCESS));
+ }
+ }
+
+ private void ChangeServiceConfig(ServiceType serviceType = (ServiceType)SERVICE_NO_CHANGE,
+ ServiceStartType startType = (ServiceStartType)SERVICE_NO_CHANGE,
+ ErrorControl errorControl = (ErrorControl)SERVICE_NO_CHANGE, string binaryPath = null,
+ string loadOrderGroup = null, List dependencies = null, string startName = null,
+ string password = null, string displayName = null)
+ {
+ string depString = null;
+ if (dependencies != null && dependencies.Count > 0)
+ depString = String.Join("\0", dependencies) + "\0\0";
+
+ if (!NativeMethods.ChangeServiceConfigW(_serviceHandle, serviceType, startType, errorControl, binaryPath,
+ loadOrderGroup, IntPtr.Zero, depString, startName, password, displayName))
+ {
+ throw new ServiceManagerException("Failed to change service config");
+ }
+
+ Refresh();
+ }
+
+ private void ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel, object info)
+ {
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(Marshal.SizeOf(info)))
+ {
+ Marshal.StructureToPtr(info, buffer.DangerousGetHandle(), false);
+
+ if (!NativeMethods.ChangeServiceConfig2W(_serviceHandle, infoLevel, buffer.DangerousGetHandle()))
+ throw new ServiceManagerException("Failed to change service config");
+ }
+ }
+
+ private static SafeServiceHandle OpenSCManager(SCMRights desiredAccess)
+ {
+ SafeServiceHandle handle = NativeMethods.OpenSCManagerW(null, null, desiredAccess);
+ if (handle.IsInvalid)
+ throw new ServiceManagerException("Failed to open SCManager");
+
+ return handle;
+ }
+
+ private T QueryServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel)
+ {
+ using (SafeMemoryBuffer buffer = QueryServiceConfig2(infoLevel))
+ return (T)Marshal.PtrToStructure(buffer.DangerousGetHandle(), typeof(T));
+ }
+
+ private SafeMemoryBuffer QueryServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel)
+ {
+ UInt32 bytesNeeded = 0;
+ NativeMethods.QueryServiceConfig2W(_serviceHandle, infoLevel, IntPtr.Zero, 0, out bytesNeeded);
+
+ SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded);
+ if (!NativeMethods.QueryServiceConfig2W(_serviceHandle, infoLevel, buffer.DangerousGetHandle(), bytesNeeded,
+ out bytesNeeded))
+ {
+ throw new ServiceManagerException(String.Format("QueryServiceConfig2W({0}) failed",
+ infoLevel.ToString()));
+ }
+
+ return buffer;
+ }
+
+ private static IntPtr StructureToPtr(object structure, IntPtr ptr)
+ {
+ Marshal.StructureToPtr(structure, ptr, false);
+ return IntPtr.Add(ptr, Marshal.SizeOf(structure));
+ }
+
+ ~Service() { Dispose(); }
+ }
+}
diff --git a/test/support/windows-integration/plugins/modules/async_status.ps1 b/test/support/windows-integration/plugins/modules/async_status.ps1
new file mode 100644
index 00000000000..1ce3ff40f3a
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/async_status.ps1
@@ -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
diff --git a/test/support/windows-integration/plugins/modules/setup.ps1 b/test/support/windows-integration/plugins/modules/setup.ps1
new file mode 100644
index 00000000000..50647239860
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/setup.ps1
@@ -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
diff --git a/test/support/windows-integration/plugins/modules/slurp.ps1 b/test/support/windows-integration/plugins/modules/slurp.ps1
new file mode 100644
index 00000000000..eb506c7c7f8
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/slurp.ps1
@@ -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";
+}
diff --git a/test/support/windows-integration/plugins/modules/win_acl.ps1 b/test/support/windows-integration/plugins/modules/win_acl.ps1
new file mode 100644
index 00000000000..e3c3813038d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_acl.ps1
@@ -0,0 +1,225 @@
+#!powershell
+
+# Copyright: (c) 2015, Phil Schwartz
+# Copyright: (c) 2015, Trond Hindenes
+# Copyright: (c) 2015, Hans-Joachim Kliemeck
+# 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
diff --git a/test/support/windows-integration/plugins/modules/win_acl.py b/test/support/windows-integration/plugins/modules/win_acl.py
new file mode 100644
index 00000000000..14fbd82f3ac
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_acl.py
@@ -0,0 +1,132 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Phil Schwartz
+# Copyright: (c) 2015, Trond Hindenes
+# Copyright: (c) 2015, Hans-Joachim Kliemeck
+# 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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_command.ps1 b/test/support/windows-integration/plugins/modules/win_command.ps1
new file mode 100644
index 00000000000..e2a30650d29
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_command.ps1
@@ -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
diff --git a/test/support/windows-integration/plugins/modules/win_command.py b/test/support/windows-integration/plugins/modules/win_command.py
new file mode 100644
index 00000000000..508419b28b1
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_command.py
@@ -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 ...']
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_copy.ps1 b/test/support/windows-integration/plugins/modules/win_copy.ps1
new file mode 100644
index 00000000000..6a26ee722d0
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_copy.ps1
@@ -0,0 +1,403 @@
+#!powershell
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth)
+# 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
diff --git a/test/support/windows-integration/plugins/modules/win_copy.py b/test/support/windows-integration/plugins/modules/win_copy.py
new file mode 100644
index 00000000000..a55f4c65b7e
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_copy.py
@@ -0,0 +1,207 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth)
+# 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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1 b/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1
new file mode 100644
index 00000000000..593ee763815
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1
@@ -0,0 +1,129 @@
+#!powershell
+
+# Copyright: 2019, rnsc(@rnsc)
+# 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
+ 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 `
+ # -NoCompress `
+ # -MinimumFileAgeDays `
+ # -MinimumFileSize (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()
diff --git a/test/support/windows-integration/plugins/modules/win_data_deduplication.py b/test/support/windows-integration/plugins/modules/win_data_deduplication.py
new file mode 100644
index 00000000000..d320b9f7c2b
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_data_deduplication.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2019, rnsc(@rnsc)
+# 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'''
+#
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_dsc.ps1 b/test/support/windows-integration/plugins/modules/win_dsc.ps1
new file mode 100644
index 00000000000..690f391a7b4
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_dsc.ps1
@@ -0,0 +1,398 @@
+#!powershell
+
+# Copyright: (c) 2015, Trond Hindenes , 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()
diff --git a/test/support/windows-integration/plugins/modules/win_dsc.py b/test/support/windows-integration/plugins/modules/win_dsc.py
new file mode 100644
index 00000000000..200d025eb85
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_dsc.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Trond Hindenes , 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."
+ ]
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_feature.ps1 b/test/support/windows-integration/plugins/modules/win_feature.ps1
new file mode 100644
index 00000000000..9a7e1c30814
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_feature.ps1
@@ -0,0 +1,111 @@
+#!powershell
+
+# Copyright: (c) 2014, Paul Durivage
+# 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
diff --git a/test/support/windows-integration/plugins/modules/win_feature.py b/test/support/windows-integration/plugins/modules/win_feature.py
new file mode 100644
index 00000000000..62e310b282b
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_feature.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Paul Durivage
+# Copyright: (c) 2014, Trond Hindenes
+# 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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_file.ps1 b/test/support/windows-integration/plugins/modules/win_file.ps1
new file mode 100644
index 00000000000..54427549254
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_file.ps1
@@ -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
diff --git a/test/support/windows-integration/plugins/modules/win_file.py b/test/support/windows-integration/plugins/modules/win_file.py
new file mode 100644
index 00000000000..28149579cda
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_file.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth)
+# 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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_find.ps1 b/test/support/windows-integration/plugins/modules/win_find.ps1
new file mode 100644
index 00000000000..bc57c5ff5d2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_find.ps1
@@ -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()
+
diff --git a/test/support/windows-integration/plugins/modules/win_find.py b/test/support/windows-integration/plugins/modules/win_find.py
new file mode 100644
index 00000000000..f506f956f29
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_find.py
@@ -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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_format.ps1 b/test/support/windows-integration/plugins/modules/win_format.ps1
new file mode 100644
index 00000000000..b5fd3ae0380
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_format.ps1
@@ -0,0 +1,200 @@
+#!powershell
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa)
+# 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()
diff --git a/test/support/windows-integration/plugins/modules/win_format.py b/test/support/windows-integration/plugins/modules/win_format.py
new file mode 100644
index 00000000000..f8f18ed7b77
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_format.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa)
+# 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)
+'''
+
+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'''
+#
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_get_url.ps1 b/test/support/windows-integration/plugins/modules/win_get_url.ps1
new file mode 100644
index 00000000000..a4c6e13d326
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_get_url.ps1
@@ -0,0 +1,277 @@
+#!powershell
+
+# Copyright: (c) 2015, Paul Durivage
+# Copyright: (c) 2015, Tal Auslander
+# Copyright: (c) 2017, Dag Wieers
+# Copyright: (c) 2019, Viktor Utkin
+# Copyright: (c) 2019, Uladzimir Klybik
+# 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()
+
diff --git a/test/support/windows-integration/plugins/modules/win_get_url.py b/test/support/windows-integration/plugins/modules/win_get_url.py
new file mode 100644
index 00000000000..ef5b5f970b4
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_get_url.py
@@ -0,0 +1,215 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Paul Durivage , 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: checksum of the file after the download
+ returned: success and dest has been downloaded
+ type: str
+ sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
+checksum_src:
+ description: 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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_hosts.ps1 b/test/support/windows-integration/plugins/modules/win_hosts.ps1
new file mode 100644
index 00000000000..9e617c66643
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_hosts.ps1
@@ -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()
diff --git a/test/support/windows-integration/plugins/modules/win_hosts.py b/test/support/windows-integration/plugins/modules/win_hosts.py
new file mode 100644
index 00000000000..9fd2d1d10d2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_hosts.py
@@ -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'''
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_lineinfile.ps1 b/test/support/windows-integration/plugins/modules/win_lineinfile.ps1
new file mode 100644
index 00000000000..38dd8b8bc09
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_lineinfile.ps1
@@ -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;
+}
diff --git a/test/support/windows-integration/plugins/modules/win_lineinfile.py b/test/support/windows-integration/plugins/modules/win_lineinfile.py
new file mode 100644
index 00000000000..f4fb7f5afa7
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_lineinfile.py
@@ -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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_path.ps1 b/test/support/windows-integration/plugins/modules/win_path.ps1
new file mode 100644
index 00000000000..04eb41a3748
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_path.ps1
@@ -0,0 +1,145 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = "Stop"
+
+$system_path = "System\CurrentControlSet\Control\Session Manager\Environment"
+$user_path = "Environment"
+
+# list/arraylist methods don't allow IEqualityComparer override for case/backslash/quote-insensitivity, roll our own search
+Function Get-IndexOfPathElement ($list, [string]$value) {
+ $idx = 0
+ $value = $value.Trim('"').Trim('\')
+ ForEach($el in $list) {
+ If ([string]$el.Trim('"').Trim('\') -ieq $value) {
+ return $idx
+ }
+
+ $idx++
+ }
+
+ return -1
+}
+
+# alters list in place, returns true if at least one element was added
+Function Add-Elements ($existing_elements, $elements_to_add) {
+ $last_idx = -1
+ $changed = $false
+
+ ForEach($el in $elements_to_add) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+
+ # add missing elements at the end
+ If ($idx -eq -1) {
+ $last_idx = $existing_elements.Add($el)
+ $changed = $true
+ }
+ ElseIf ($idx -lt $last_idx) {
+ $existing_elements.RemoveAt($idx) | Out-Null
+ $existing_elements.Add($el) | Out-Null
+ $last_idx = $existing_elements.Count - 1
+ $changed = $true
+ }
+ Else {
+ $last_idx = $idx
+ }
+ }
+
+ return $changed
+}
+
+# alters list in place, returns true if at least one element was removed
+Function Remove-Elements ($existing_elements, $elements_to_remove) {
+ $count = $existing_elements.Count
+
+ ForEach($el in $elements_to_remove) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+ $result.removed_idx = $idx
+ If ($idx -gt -1) {
+ $existing_elements.RemoveAt($idx)
+ }
+ }
+
+ return $count -ne $existing_elements.Count
+}
+
+# PS registry provider doesn't allow access to unexpanded REG_EXPAND_SZ; fall back to .NET
+Function Get-RawPathVar ($scope) {
+ If ($scope -eq "user") {
+ $env_key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($user_path)
+ }
+ ElseIf ($scope -eq "machine") {
+ $env_key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($system_path)
+ }
+
+ return $env_key.GetValue($var_name, "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+}
+
+Function Set-RawPathVar($path_value, $scope) {
+ If ($scope -eq "user") {
+ $var_path = "HKCU:\" + $user_path
+ }
+ ElseIf ($scope -eq "machine") {
+ $var_path = "HKLM:\" + $system_path
+ }
+
+ Set-ItemProperty $var_path -Name $var_name -Value $path_value -Type ExpandString | Out-Null
+
+ return $path_value
+}
+
+$parsed_args = Parse-Args $args -supports_check_mode $true
+
+$result = @{changed=$false}
+
+$var_name = Get-AnsibleParam $parsed_args "name" -Default "PATH"
+$elements = Get-AnsibleParam $parsed_args "elements" -FailIfEmpty $result
+$state = Get-AnsibleParam $parsed_args "state" -Default "present" -ValidateSet "present","absent"
+$scope = Get-AnsibleParam $parsed_args "scope" -Default "machine" -ValidateSet "machine","user"
+
+$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -Default $false
+
+If ($elements -is [string]) {
+ $elements = @($elements)
+}
+
+If ($elements -isnot [Array]) {
+ Fail-Json $result "elements must be a string or list of path strings"
+}
+
+$current_value = Get-RawPathVar $scope
+$result.path_value = $current_value
+
+# TODO: test case-canonicalization on wacky unicode values (eg turkish i)
+# TODO: detect and warn/fail on unparseable path? (eg, unbalanced quotes, invalid path chars)
+# TODO: detect and warn/fail if system path and Powershell isn't on it?
+
+$existing_elements = New-Object System.Collections.ArrayList
+
+# split on semicolons, accounting for quoted values with embedded semicolons (which may or may not be wrapped in whitespace)
+$pathsplit_re = [regex] '((?\s*"[^"]+"\s*)|(?[^;]+))(;$|$|;)'
+
+ForEach ($m in $pathsplit_re.Matches($current_value)) {
+ $existing_elements.Add($m.Groups['q'].Value) | Out-Null
+}
+
+If ($state -eq "absent") {
+ $result.changed = Remove-Elements $existing_elements $elements
+}
+ElseIf ($state -eq "present") {
+ $result.changed = Add-Elements $existing_elements $elements
+}
+
+# calculate the new path value from the existing elements
+$path_value = [String]::Join(";", $existing_elements.ToArray())
+$result.path_value = $path_value
+
+If ($result.changed -and -not $check_mode) {
+ Set-RawPathVar $path_value $scope | Out-Null
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_path.py b/test/support/windows-integration/plugins/modules/win_path.py
new file mode 100644
index 00000000000..6404504fa2d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_path.py
@@ -0,0 +1,79 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Red Hat | Ansible
+# 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': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_path
+version_added: "2.3"
+short_description: Manage Windows path environment variables
+description:
+ - Allows element-based ordering, addition, and removal of Windows path environment variables.
+options:
+ name:
+ description:
+ - Target path environment variable name.
+ type: str
+ default: PATH
+ elements:
+ description:
+ - A single path element, or a list of path elements (ie, directories) to add or remove.
+ - When multiple elements are included in the list (and C(state) is C(present)), the elements are guaranteed to appear in the same relative order
+ in the resultant path value.
+ - Variable expansions (eg, C(%VARNAME%)) are allowed, and are stored unexpanded in the target path element.
+ - Any existing path elements not mentioned in C(elements) are always preserved in their current order.
+ - New path elements are appended to the path, and existing path elements may be moved closer to the end to satisfy the requested ordering.
+ - Paths are compared in a case-insensitive fashion, and trailing backslashes are ignored for comparison purposes. However, note that trailing
+ backslashes in YAML require quotes.
+ type: list
+ required: yes
+ state:
+ description:
+ - Whether the path elements specified in C(elements) should be present or absent.
+ type: str
+ choices: [ absent, present ]
+ scope:
+ description:
+ - The level at which the environment variable specified by C(name) should be managed (either for the current user or global machine scope).
+ type: str
+ choices: [ machine, user ]
+ default: machine
+notes:
+ - This module is for modifying individual elements of path-like
+ environment variables. For general-purpose management of other
+ environment vars, use the M(win_environment) module.
+ - This module does not broadcast change events.
+ This means that the minority of windows applications which can have
+ their environment changed without restarting will not be notified and
+ therefore will need restarting to pick up new environment settings.
+ - User level environment variables will require an interactive user to
+ log out and in again before they become available.
+seealso:
+- module: win_environment
+author:
+- Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+- name: Ensure that system32 and Powershell are present on the global system path, and in the specified order
+ win_path:
+ elements:
+ - '%SystemRoot%\system32'
+ - '%SystemRoot%\system32\WindowsPowerShell\v1.0'
+
+- name: Ensure that C:\Program Files\MyJavaThing is not on the current user's CLASSPATH
+ win_path:
+ name: CLASSPATH
+ elements: C:\Program Files\MyJavaThing
+ scope: user
+ state: absent
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_ping.ps1 b/test/support/windows-integration/plugins/modules/win_ping.ps1
new file mode 100644
index 00000000000..c848b9121ea
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_ping.ps1
@@ -0,0 +1,21 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "pong" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_ping.py b/test/support/windows-integration/plugins/modules/win_ping.py
new file mode 100644
index 00000000000..6d35f379940
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_ping.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Michael DeHaan , 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_ping
+version_added: "1.7"
+short_description: A windows version of the classic ping module
+description:
+ - Checks management connectivity of a windows host.
+ - This is NOT ICMP ping, this is just a trivial test module.
+ - For non-Windows targets, use the M(ping) module instead.
+ - For Network targets, use the M(net_ping) module instead.
+options:
+ data:
+ description:
+ - Alternate data to return instead of 'pong'.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+- module: ping
+author:
+- Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+# Test connectivity to a windows host
+# ansible winserver -m win_ping
+
+- name: Example from an Ansible Playbook
+ win_ping:
+
+- name: Induce an exception to see what happens
+ win_ping:
+ data: crash
+'''
+
+RETURN = r'''
+ping:
+ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_psexec.ps1 b/test/support/windows-integration/plugins/modules/win_psexec.ps1
new file mode 100644
index 00000000000..04a512706e1
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_psexec.ps1
@@ -0,0 +1,152 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers)
+# 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.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+# See also: https://technet.microsoft.com/en-us/sysinternals/pxexec.aspx
+
+$spec = @{
+ options = @{
+ command = @{ type='str'; required=$true }
+ executable = @{ type='path'; default='psexec.exe' }
+ hostnames = @{ type='list' }
+ username = @{ type='str' }
+ password = @{ type='str'; no_log=$true }
+ chdir = @{ type='path' }
+ wait = @{ type='bool'; default=$true }
+ nobanner = @{ type='bool'; default=$false }
+ noprofile = @{ type='bool'; default=$false }
+ elevated = @{ type='bool'; default=$false }
+ limited = @{ type='bool'; default=$false }
+ system = @{ type='bool'; default=$false }
+ interactive = @{ type='bool'; default=$false }
+ session = @{ type='int' }
+ priority = @{ type='str'; choices=@( 'background', 'low', 'belownormal', 'abovenormal', 'high', 'realtime' ) }
+ timeout = @{ type='int' }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$command = $module.Params.command
+$executable = $module.Params.executable
+$hostnames = $module.Params.hostnames
+$username = $module.Params.username
+$password = $module.Params.password
+$chdir = $module.Params.chdir
+$wait = $module.Params.wait
+$nobanner = $module.Params.nobanner
+$noprofile = $module.Params.noprofile
+$elevated = $module.Params.elevated
+$limited = $module.Params.limited
+$system = $module.Params.system
+$interactive = $module.Params.interactive
+$session = $module.Params.session
+$priority = $module.Params.Priority
+$timeout = $module.Params.timeout
+
+$module.Result.changed = $true
+
+If (-Not (Get-Command $executable -ErrorAction SilentlyContinue)) {
+ $module.FailJson("Executable '$executable' was not found.")
+}
+
+$arguments = [System.Collections.Generic.List`1[String]]@($executable)
+
+If ($nobanner -eq $true) {
+ $arguments.Add("-nobanner")
+}
+
+# Support running on local system if no hostname is specified
+If ($hostnames) {
+ $hostname_argument = ($hostnames | sort -Unique) -join ','
+ $arguments.Add("\\$hostname_argument")
+}
+
+# Username is optional
+If ($null -ne $username) {
+ $arguments.Add("-u")
+ $arguments.Add($username)
+}
+
+# Password is optional
+If ($null -ne $password) {
+ $arguments.Add("-p")
+ $arguments.Add($password)
+}
+
+If ($null -ne $chdir) {
+ $arguments.Add("-w")
+ $arguments.Add($chdir)
+}
+
+If ($wait -eq $false) {
+ $arguments.Add("-d")
+}
+
+If ($noprofile -eq $true) {
+ $arguments.Add("-e")
+}
+
+If ($elevated -eq $true) {
+ $arguments.Add("-h")
+}
+
+If ($system -eq $true) {
+ $arguments.Add("-s")
+}
+
+If ($interactive -eq $true) {
+ $arguments.Add("-i")
+ If ($null -ne $session) {
+ $arguments.Add($session)
+ }
+}
+
+If ($limited -eq $true) {
+ $arguments.Add("-l")
+}
+
+If ($null -ne $priority) {
+ $arguments.Add("-$priority")
+}
+
+If ($null -ne $timeout) {
+ $arguments.Add("-n")
+ $arguments.Add($timeout)
+}
+
+$arguments.Add("-accepteula")
+
+$argument_string = Argv-ToString -arguments $arguments
+
+# Add the command at the end of the argument string, we don't want to escape
+# that as psexec doesn't expect it to be one arg
+$argument_string += " $command"
+
+$start_datetime = [DateTime]::UtcNow
+$module.Result.psexec_command = $argument_string
+
+$command_result = Run-Command -command $argument_string
+
+$end_datetime = [DateTime]::UtcNow
+
+$module.Result.stdout = $command_result.stdout
+$module.Result.stderr = $command_result.stderr
+
+If ($wait -eq $true) {
+ $module.Result.rc = $command_result.rc
+} else {
+ $module.Result.rc = 0
+ $module.Result.pid = $command_result.rc
+}
+
+$module.Result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_psexec.py b/test/support/windows-integration/plugins/modules/win_psexec.py
new file mode 100644
index 00000000000..c3fc37e4a60
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_psexec.py
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2017, Dag Wieers (@dagwieers)
+# 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_psexec
+version_added: '2.3'
+short_description: Runs commands (remotely) as another (privileged) user
+description:
+- Run commands (remotely) through the PsExec service.
+- Run commands as another (domain) user (with elevated privileges).
+requirements:
+- Microsoft PsExec
+options:
+ command:
+ description:
+ - The command line to run through PsExec (limited to 260 characters).
+ type: str
+ required: yes
+ executable:
+ description:
+ - The location of the PsExec utility (in case it is not located in your PATH).
+ type: path
+ default: psexec.exe
+ hostnames:
+ description:
+ - The hostnames to run the command.
+ - If not provided, the command is run locally.
+ type: list
+ username:
+ description:
+ - The (remote) user to run the command as.
+ - If not provided, the current user is used.
+ type: str
+ password:
+ description:
+ - The password for the (remote) user to run the command as.
+ - This is mandatory in order authenticate yourself.
+ type: str
+ chdir:
+ description:
+ - Run the command from this (remote) directory.
+ type: path
+ nobanner:
+ description:
+ - Do not display the startup banner and copyright message.
+ - This only works for specific versions of the PsExec binary.
+ type: bool
+ default: no
+ version_added: '2.4'
+ noprofile:
+ description:
+ - Run the command without loading the account's profile.
+ type: bool
+ default: no
+ elevated:
+ description:
+ - Run the command with elevated privileges.
+ type: bool
+ default: no
+ interactive:
+ description:
+ - Run the program so that it interacts with the desktop on the remote system.
+ type: bool
+ default: no
+ session:
+ description:
+ - Specifies the session ID to use.
+ - This parameter works in conjunction with I(interactive).
+ - It has no effect when I(interactive) is set to C(no).
+ type: int
+ version_added: '2.7'
+ limited:
+ description:
+ - Run the command as limited user (strips the Administrators group and allows only privileges assigned to the Users group).
+ type: bool
+ default: no
+ system:
+ description:
+ - Run the remote command in the System account.
+ type: bool
+ default: no
+ priority:
+ description:
+ - Used to run the command at a different priority.
+ choices: [ abovenormal, background, belownormal, high, low, realtime ]
+ timeout:
+ description:
+ - The connection timeout in seconds
+ type: int
+ wait:
+ description:
+ - Wait for the application to terminate.
+ - Only use for non-interactive applications.
+ type: bool
+ default: yes
+notes:
+- More information related to Microsoft PsExec is available from
+ U(https://technet.microsoft.com/en-us/sysinternals/bb897553.aspx)
+seealso:
+- module: psexec
+- module: raw
+- module: win_command
+- module: win_shell
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Test the PsExec connection to the local system (target node) with your user
+ win_psexec:
+ command: whoami.exe
+
+- name: Run regedit.exe locally (on target node) as SYSTEM and interactively
+ win_psexec:
+ command: regedit.exe
+ interactive: yes
+ system: yes
+
+- name: Run the setup.exe installer on multiple servers using the Domain Administrator
+ win_psexec:
+ command: E:\setup.exe /i /IACCEPTEULA
+ hostnames:
+ - remote_server1
+ - remote_server2
+ username: DOMAIN\Administrator
+ password: some_password
+ priority: high
+
+- name: Run PsExec from custom location C:\Program Files\sysinternals\
+ win_psexec:
+ command: netsh advfirewall set allprofiles state off
+ executable: C:\Program Files\sysinternals\psexec.exe
+ hostnames: [ remote_server ]
+ password: some_password
+ priority: low
+'''
+
+RETURN = r'''
+cmd:
+ description: The complete command line used by the module, including PsExec call and additional options.
+ returned: always
+ type: str
+ sample: psexec.exe -nobanner \\remote_server -u "DOMAIN\Administrator" -p "some_password" -accepteula E:\setup.exe
+pid:
+ description: The PID of the async process created by PsExec.
+ returned: when C(wait=False)
+ type: int
+ sample: 1532
+rc:
+ description: The return code for the command.
+ returned: always
+ type: int
+ sample: 0
+stdout:
+ description: The standard output from the command.
+ returned: always
+ type: str
+ sample: Success.
+stderr:
+ description: The error output from the command.
+ returned: always
+ type: str
+ sample: Error 15 running E:\setup.exe
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_reboot.py b/test/support/windows-integration/plugins/modules/win_reboot.py
new file mode 100644
index 00000000000..1431804143d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_reboot.py
@@ -0,0 +1,131 @@
+#!/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': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_reboot
+short_description: Reboot a windows machine
+description:
+- Reboot a Windows machine, wait for it to go down, come back up, and respond to commands.
+- For non-Windows targets, use the M(reboot) module instead.
+version_added: '2.1'
+options:
+ pre_reboot_delay:
+ description:
+ - Seconds to wait before reboot. Passed as a parameter to the reboot command.
+ type: int
+ default: 2
+ aliases: [ pre_reboot_delay_sec ]
+ post_reboot_delay:
+ description:
+ - Seconds to wait after the reboot command was successful before attempting to validate the system rebooted successfully.
+ - This is useful if you want wait for something to settle despite your connection already working.
+ type: int
+ default: 0
+ version_added: '2.4'
+ aliases: [ post_reboot_delay_sec ]
+ shutdown_timeout:
+ description:
+ - Maximum seconds to wait for shutdown to occur.
+ - Increase this timeout for very slow hardware, large update applications, etc.
+ - This option has been removed since Ansible 2.5 as the win_reboot behavior has changed.
+ type: int
+ default: 600
+ aliases: [ shutdown_timeout_sec ]
+ reboot_timeout:
+ description:
+ - Maximum seconds to wait for machine to re-appear on the network and respond to a test command.
+ - This timeout is evaluated separately for both reboot verification and test command success so maximum clock time is actually twice this value.
+ type: int
+ default: 600
+ aliases: [ reboot_timeout_sec ]
+ connect_timeout:
+ description:
+ - Maximum seconds to wait for a single successful TCP connection to the WinRM endpoint before trying again.
+ type: int
+ default: 5
+ aliases: [ connect_timeout_sec ]
+ test_command:
+ description:
+ - Command to expect success for to determine the machine is ready for management.
+ type: str
+ default: whoami
+ msg:
+ description:
+ - Message to display to users.
+ type: str
+ default: Reboot initiated by Ansible
+ boot_time_command:
+ description:
+ - Command to run that returns a unique string indicating the last time the system was booted.
+ - Setting this to a command that has different output each time it is run will cause the task to fail.
+ type: str
+ default: '(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime'
+ version_added: '2.10'
+notes:
+- If a shutdown was already scheduled on the system, C(win_reboot) will abort the scheduled shutdown and enforce its own shutdown.
+- Beware that when C(win_reboot) returns, the Windows system may not have settled yet and some base services could be in limbo.
+ This can result in unexpected behavior. Check the examples for ways to mitigate this.
+- The connection user must have the C(SeRemoteShutdownPrivilege) privilege enabled, see
+ U(https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/force-shutdown-from-a-remote-system)
+ for more information.
+seealso:
+- module: reboot
+author:
+- Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+- name: Reboot the machine with all defaults
+ win_reboot:
+
+- name: Reboot a slow machine that might have lots of updates to apply
+ win_reboot:
+ reboot_timeout: 3600
+
+# Install a Windows feature and reboot if necessary
+- name: Install IIS Web-Server
+ win_feature:
+ name: Web-Server
+ register: iis_install
+
+- name: Reboot when Web-Server feature requires it
+ win_reboot:
+ when: iis_install.reboot_required
+
+# One way to ensure the system is reliable, is to set WinRM to a delayed startup
+- name: Ensure WinRM starts when the system has settled and is ready to work reliably
+ win_service:
+ name: WinRM
+ start_mode: delayed
+
+
+# Additionally, you can add a delay before running the next task
+- name: Reboot a machine that takes time to settle after being booted
+ win_reboot:
+ post_reboot_delay: 120
+
+# Or you can make win_reboot validate exactly what you need to work before running the next task
+- name: Validate that the netlogon service has started, before running the next task
+ win_reboot:
+ test_command: 'exit (Get-Service -Name Netlogon).Status -ne "Running"'
+'''
+
+RETURN = r'''
+rebooted:
+ description: True if the machine was rebooted.
+ returned: always
+ type: bool
+ sample: true
+elapsed:
+ description: The number of seconds that elapsed waiting for the system to be rebooted.
+ returned: always
+ type: float
+ sample: 23.2
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_regedit.ps1 b/test/support/windows-integration/plugins/modules/win_regedit.ps1
new file mode 100644
index 00000000000..c56b48335dd
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_regedit.ps1
@@ -0,0 +1,495 @@
+#!powershell
+
+# Copyright: (c) 2015, Adam Keech
+# Copyright: (c) 2015, Josh Ludwig
+# Copyright: (c) 2017, Jordan Borean
+# 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
+
+$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
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true -aliases "key"
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -aliases "entry","value"
+$data = Get-AnsibleParam -obj $params -name "data"
+$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "string" -validateset "none","binary","dword","expandstring","multistring","string","qword" -aliases "datatype"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
+$delete_key = Get-AnsibleParam -obj $params -name "delete_key" -type "bool" -default $true
+$hive = Get-AnsibleParam -obj $params -name "hive" -type "path"
+
+$result = @{
+ changed = $false
+ data_changed = $false
+ data_type_changed = $false
+}
+
+if ($diff_mode) {
+ $result.diff = @{
+ before = ""
+ after = ""
+ }
+}
+
+$registry_util = @'
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Ansible.WinRegedit
+{
+ internal class NativeMethods
+ {
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
+ public static extern int RegLoadKeyW(
+ UInt32 hKey,
+ string lpSubKey,
+ string lpFile);
+
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
+ public static extern int RegUnLoadKeyW(
+ UInt32 hKey,
+ string lpSubKey);
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ public class Hive : IDisposable
+ {
+ private const UInt32 SCOPE = 0x80000002; // HKLM
+ private string hiveKey;
+ private bool loaded = false;
+
+ public Hive(string hiveKey, string hivePath)
+ {
+ this.hiveKey = hiveKey;
+ int ret = NativeMethods.RegLoadKeyW(SCOPE, hiveKey, hivePath);
+ if (ret != 0)
+ throw new Win32Exception(ret, String.Format("Failed to load registry hive at {0}", hivePath));
+ loaded = true;
+ }
+
+ public static void UnloadHive(string hiveKey)
+ {
+ int ret = NativeMethods.RegUnLoadKeyW(SCOPE, hiveKey);
+ if (ret != 0)
+ throw new Win32Exception(ret, String.Format("Failed to unload registry hive at {0}", hiveKey));
+ }
+
+ public void Dispose()
+ {
+ if (loaded)
+ {
+ // Make sure the garbage collector disposes all unused handles and waits until it is complete
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ UnloadHive(hiveKey);
+ loaded = false;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~Hive() { this.Dispose(); }
+ }
+}
+'@
+
+# fire a warning if the property name isn't specified, the (Default) key ($null) can only be a string
+if ($null -eq $name -and $type -ne "string") {
+ Add-Warning -obj $result -message "the data type when name is not specified can only be 'string', the type has automatically been converted"
+ $type = "string"
+}
+
+# Check that the registry path is in PSDrive format: HKCC, HKCR, HKCU, HKLM, HKU
+if ($path -notmatch "^HK(CC|CR|CU|LM|U):\\") {
+ Fail-Json $result "path: $path is not a valid powershell path, see module documentation for examples."
+}
+
+# Add a warning if the path does not contains a \ and is not the leaf path
+$registry_path = (Split-Path -Path $path -NoQualifier).Substring(1) # removes the hive: and leading \
+$registry_leaf = Split-Path -Path $path -Leaf
+if ($registry_path -ne $registry_leaf -and -not $registry_path.Contains('\')) {
+ $msg = "path is not using '\' as a separator, support for '/' as a separator will be removed in a future Ansible version"
+ Add-DeprecationWarning -obj $result -message $msg -version 2.12
+ $registry_path = $registry_path.Replace('/', '\')
+}
+
+# Simplified version of Convert-HexStringToByteArray from
+# https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert
+# Expects a hex in the format you get when you run reg.exe export,
+# and converts to a byte array so powershell can modify binary registry entries
+# import format is like 'hex:be,ef,be,ef,be,ef,be,ef,be,ef'
+Function Convert-RegExportHexStringToByteArray($string) {
+ # Remove 'hex:' from the front of the string if present
+ $string = $string.ToLower() -replace '^hex\:',''
+
+ # Remove whitespace and any other non-hex crud.
+ $string = $string -replace '[^a-f0-9\\,x\-\:]',''
+
+ # Turn commas into colons
+ $string = $string -replace ',',':'
+
+ # Maybe there's nothing left over to convert...
+ if ($string.Length -eq 0) {
+ return ,@()
+ }
+
+ # Split string with or without colon delimiters.
+ if ($string.Length -eq 1) {
+ return ,@([System.Convert]::ToByte($string,16))
+ } elseif (($string.Length % 2 -eq 0) -and ($string.IndexOf(":") -eq -1)) {
+ return ,@($string -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}})
+ } elseif ($string.IndexOf(":") -ne -1) {
+ return ,@($string -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)})
+ } else {
+ return ,@()
+ }
+}
+
+Function Compare-RegistryProperties($existing, $new) {
+ # Outputs $true if the property values don't match
+ if ($existing -is [Array]) {
+ (Compare-Object -ReferenceObject $existing -DifferenceObject $new -SyncWindow 0).Length -ne 0
+ } else {
+ $existing -cne $new
+ }
+}
+
+Function Get-DiffValue {
+ param(
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryValueKind]$Type,
+ [Parameter(Mandatory=$true)][Object]$Value
+ )
+
+ $diff = @{ type = $Type.ToString(); value = $Value }
+
+ $enum = [Microsoft.Win32.RegistryValueKind]
+ if ($Type -in @($enum::Binary, $enum::None)) {
+ $diff.value = [System.Collections.Generic.List`1[String]]@()
+ foreach ($dec_value in $Value) {
+ $diff.value.Add("0x{0:x2}" -f $dec_value)
+ }
+ } elseif ($Type -eq $enum::DWord) {
+ $diff.value = "0x{0:x8}" -f $Value
+ } elseif ($Type -eq $enum::QWord) {
+ $diff.value = "0x{0:x16}" -f $Value
+ }
+
+ return $diff
+}
+
+Function Set-StateAbsent {
+ param(
+ # Used for diffs and exception messages to match up against Ansible input
+ [Parameter(Mandatory=$true)][String]$PrintPath,
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
+ [Parameter(Mandatory=$true)][String]$Path,
+ [String]$Name,
+ [Switch]$DeleteKey
+ )
+
+ $key = $Hive.OpenSubKey($Path, $true)
+ if ($null -eq $key) {
+ # Key does not exist, no need to delete anything
+ return
+ }
+
+ try {
+ if ($DeleteKey -and -not $Name) {
+ # delete_key=yes is set and name is null/empty, so delete the entire key
+ $key.Dispose()
+ $key = $null
+ if (-not $check_mode) {
+ try {
+ $Hive.DeleteSubKeyTree($Path, $false)
+ } catch {
+ Fail-Json -obj $result -message "failed to delete registry key at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before = @{$PrintPath = @{}}
+ $result.diff.after = @{}
+ }
+ } else {
+ # delete_key=no or name is not null/empty, delete the property not the full key
+ $property = $key.GetValue($Name)
+ if ($null -eq $property) {
+ # property does not exist
+ return
+ }
+ $property_type = $key.GetValueKind($Name) # used for the diff
+
+ if (-not $check_mode) {
+ try {
+ $key.DeleteValue($Name)
+ } catch {
+ Fail-Json -obj $result -message "failed to delete registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+
+ $result.changed = $true
+ if ($diff_mode) {
+ $diff_value = Get-DiffValue -Type $property_type -Value $property
+ $result.diff.before = @{ $PrintPath = @{ $Name = $diff_value } }
+ $result.diff.after = @{ $PrintPath = @{} }
+ }
+ }
+ } finally {
+ if ($key) {
+ $key.Dispose()
+ }
+ }
+}
+
+Function Set-StatePresent {
+ param(
+ [Parameter(Mandatory=$true)][String]$PrintPath,
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
+ [Parameter(Mandatory=$true)][String]$Path,
+ [String]$Name,
+ [Object]$Data,
+ [Microsoft.Win32.RegistryValueKind]$Type
+ )
+
+ $key = $Hive.OpenSubKey($Path, $true)
+ try {
+ if ($null -eq $key) {
+ # the key does not exist, create it so the next steps work
+ if (-not $check_mode) {
+ try {
+ $key = $Hive.CreateSubKey($Path)
+ } catch {
+ Fail-Json -obj $result -message "failed to create registry key at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before = @{}
+ $result.diff.after = @{$PrintPath = @{}}
+ }
+ } elseif ($diff_mode) {
+ # Make sure the diff is in an expected state for the key
+ $result.diff.before = @{$PrintPath = @{}}
+ $result.diff.after = @{$PrintPath = @{}}
+ }
+
+ if ($null -eq $key -or $null -eq $Data) {
+ # Check mode and key was created above, we cannot do any more work, or $Data is $null which happens when
+ # we create a new key but haven't explicitly set the data
+ return
+ }
+
+ $property = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+ if ($null -ne $property) {
+ # property exists, need to compare the values and type
+ $existing_type = $key.GetValueKind($name)
+ $change_value = $false
+
+ if ($Type -ne $existing_type) {
+ $change_value = $true
+ $result.data_type_changed = $true
+ $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
+ if ($data_mismatch) {
+ $result.data_changed = $true
+ }
+ } else {
+ $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
+ if ($data_mismatch) {
+ $change_value = $true
+ $result.data_changed = $true
+ }
+ }
+
+ if ($change_value) {
+ if (-not $check_mode) {
+ try {
+ $key.SetValue($Name, $Data, $Type)
+ } catch {
+ Fail-Json -obj $result -message "failed to change registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before.$PrintPath.$Name = Get-DiffValue -Type $existing_type -Value $property
+ $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
+ }
+ } elseif ($diff_mode) {
+ $diff_value = Get-DiffValue -Type $existing_type -Value $property
+ $result.diff.before.$PrintPath.$Name = $diff_value
+ $result.diff.after.$PrintPath.$Name = $diff_value
+ }
+ } else {
+ # property doesn't exist just create a new one
+ if (-not $check_mode) {
+ try {
+ $key.SetValue($Name, $Data, $Type)
+ } catch {
+ Fail-Json -obj $result -message "failed to create registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
+ }
+ }
+ } finally {
+ if ($key) {
+ $key.Dispose()
+ }
+ }
+}
+
+# convert property names "" to $null as "" refers to (Default)
+if ($name -eq "") {
+ $name = $null
+}
+
+# convert the data to the required format
+if ($type -in @("binary", "none")) {
+ if ($null -eq $data) {
+ $data = ""
+ }
+
+ # convert the data from string to byte array if in hex: format
+ if ($data -is [String]) {
+ $data = [byte[]](Convert-RegExportHexStringToByteArray -string $data)
+ } elseif ($data -is [Int]) {
+ if ($data -gt 255) {
+ Fail-Json $result "cannot convert binary data '$data' to byte array, please specify this value as a yaml byte array or a comma separated hex value string"
+ }
+ $data = [byte[]]@([byte]$data)
+ } elseif ($data -is [Array]) {
+ $data = [byte[]]$data
+ }
+} elseif ($type -in @("dword", "qword")) {
+ # dword's and dword's don't allow null values, set to 0
+ if ($null -eq $data) {
+ $data = 0
+ }
+
+ if ($data -is [String]) {
+ # if the data is a string we need to convert it to an unsigned int64
+ # it needs to be unsigned as Ansible passes in an unsigned value while
+ # powershell uses a signed data type. The value will then be converted
+ # below
+ $data = [UInt64]$data
+ }
+
+ if ($type -eq "dword") {
+ if ($data -gt [UInt32]::MaxValue) {
+ Fail-Json $result "data cannot be larger than 0xffffffff when type is dword"
+ } elseif ($data -gt [Int32]::MaxValue) {
+ # when dealing with larger int32 (> 2147483647 or 0x7FFFFFFF) powershell
+ # automatically converts it to a signed int64. We need to convert this to
+ # signed int32 by parsing the hex string value.
+ $data = "0x$("{0:x}" -f $data)"
+ }
+ $data = [Int32]$data
+ } else {
+ if ($data -gt [UInt64]::MaxValue) {
+ Fail-Json $result "data cannot be larger than 0xffffffffffffffff when type is qword"
+ } elseif ($data -gt [Int64]::MaxValue) {
+ $data = "0x$("{0:x}" -f $data)"
+ }
+ $data = [Int64]$data
+ }
+} elseif ($type -in @("string", "expandstring") -and $name) {
+ # a null string or expandstring must be empty quotes
+ # Only do this if $name has been defined (not the default key)
+ if ($null -eq $data) {
+ $data = ""
+ }
+} elseif ($type -eq "multistring") {
+ # convert the data for a multistring to a String[] array
+ if ($null -eq $data) {
+ $data = [String[]]@()
+ } elseif ($data -isnot [Array]) {
+ $new_data = New-Object -TypeName String[] -ArgumentList 1
+ $new_data[0] = $data.ToString([CultureInfo]::InvariantCulture)
+ $data = $new_data
+ } else {
+ $new_data = New-Object -TypeName String[] -ArgumentList $data.Count
+ foreach ($entry in $data) {
+ $new_data[$data.IndexOf($entry)] = $entry.ToString([CultureInfo]::InvariantCulture)
+ }
+ $data = $new_data
+ }
+}
+
+# convert the type string to the .NET class
+$type = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $type, $true)
+
+$registry_hive = switch(Split-Path -Path $path -Qualifier) {
+ "HKCR:" { [Microsoft.Win32.Registry]::ClassesRoot }
+ "HKCC:" { [Microsoft.Win32.Registry]::CurrentConfig }
+ "HKCU:" { [Microsoft.Win32.Registry]::CurrentUser }
+ "HKLM:" { [Microsoft.Win32.Registry]::LocalMachine }
+ "HKU:" { [Microsoft.Win32.Registry]::Users }
+}
+$loaded_hive = $null
+try {
+ if ($hive) {
+ if (-not (Test-Path -LiteralPath $hive)) {
+ Fail-Json -obj $result -message "hive at path '$hive' is not valid or accessible, cannot load hive"
+ }
+
+ $original_tmp = $env:TMP
+ $env:TMP = $_remote_tmp
+ Add-Type -TypeDefinition $registry_util
+ $env:TMP = $original_tmp
+
+ try {
+ Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
+ Set-AnsiblePrivilege -Name SeRestorePrivilege -Value $true
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to enable SeBackupPrivilege and SeRestorePrivilege for the current process: $($_.Exception.Message)"
+ }
+
+ if (Test-Path -Path HKLM:\ANSIBLE) {
+ Add-Warning -obj $result -message "hive already loaded at HKLM:\ANSIBLE, had to unload hive for win_regedit to continue"
+ try {
+ [Ansible.WinRegedit.Hive]::UnloadHive("ANSIBLE")
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to unload registry hive HKLM:\ANSIBLE from $($hive): $($_.Exception.Message)"
+ }
+ }
+
+ try {
+ $loaded_hive = New-Object -TypeName Ansible.WinRegedit.Hive -ArgumentList "ANSIBLE", $hive
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to load registry hive from '$hive' to HKLM:\ANSIBLE: $($_.Exception.Message)"
+ }
+ }
+
+ if ($state -eq "present") {
+ Set-StatePresent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -Data $data -Type $type
+ } else {
+ Set-StateAbsent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -DeleteKey:$delete_key
+ }
+} finally {
+ $registry_hive.Dispose()
+ if ($loaded_hive) {
+ $loaded_hive.Dispose()
+ }
+}
+
+Exit-Json $result
+
diff --git a/test/support/windows-integration/plugins/modules/win_regedit.py b/test/support/windows-integration/plugins/modules/win_regedit.py
new file mode 100644
index 00000000000..2c0fff71245
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_regedit.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Adam Keech
+# Copyright: (c) 2015, Josh Ludwig
+# 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': 'core'}
+
+
+DOCUMENTATION = r'''
+---
+module: win_regedit
+version_added: '2.0'
+short_description: Add, change, or remove registry keys and values
+description:
+- Add, modify or remove registry keys and values.
+- More information about the windows registry from Wikipedia
+ U(https://en.wikipedia.org/wiki/Windows_Registry).
+options:
+ path:
+ description:
+ - Name of the registry path.
+ - 'Should be in one of the following registry hives: HKCC, HKCR, HKCU,
+ HKLM, HKU.'
+ type: str
+ required: yes
+ aliases: [ key ]
+ name:
+ description:
+ - Name of the registry entry in the above C(path) parameters.
+ - If not provided, or empty then the '(Default)' property for the key will
+ be used.
+ type: str
+ aliases: [ entry, value ]
+ data:
+ description:
+ - Value of the registry entry C(name) in C(path).
+ - If not specified then the value for the property will be null for the
+ corresponding C(type).
+ - Binary and None data should be expressed in a yaml byte array or as comma
+ separated hex values.
+ - An easy way to generate this is to run C(regedit.exe) and use the
+ I(export) option to save the registry values to a file.
+ - In the exported file, binary value will look like C(hex:be,ef,be,ef), the
+ C(hex:) prefix is optional.
+ - DWORD and QWORD values should either be represented as a decimal number
+ or a hex value.
+ - Multistring values should be passed in as a list.
+ - See the examples for more details on how to format this data.
+ type: str
+ type:
+ description:
+ - The registry value data type.
+ type: str
+ choices: [ binary, dword, expandstring, multistring, string, qword ]
+ default: string
+ aliases: [ datatype ]
+ state:
+ description:
+ - The state of the registry entry.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ delete_key:
+ description:
+ - When C(state) is 'absent' then this will delete the entire key.
+ - If C(no) then it will only clear out the '(Default)' property for
+ that key.
+ type: bool
+ default: yes
+ version_added: '2.4'
+ hive:
+ description:
+ - A path to a hive key like C:\Users\Default\NTUSER.DAT to load in the
+ registry.
+ - This hive is loaded under the HKLM:\ANSIBLE key which can then be used
+ in I(name) like any other path.
+ - This can be used to load the default user profile registry hive or any
+ other hive saved as a file.
+ - Using this function requires the user to have the C(SeRestorePrivilege)
+ and C(SeBackupPrivilege) privileges enabled.
+ type: path
+ version_added: '2.5'
+notes:
+- Check-mode C(-C/--check) and diff output C(-D/--diff) are supported, so that you can test every change against the active configuration before
+ applying changes.
+- Beware that some registry hives (C(HKEY_USERS) in particular) do not allow to create new registry paths in the root folder.
+- Since ansible 2.4, when checking if a string registry value has changed, a case-sensitive test is used. Previously the test was case-insensitive.
+seealso:
+- module: win_reg_stat
+- module: win_regmerge
+author:
+- Adam Keech (@smadam813)
+- Josh Ludwig (@joshludwig)
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create registry path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+
+- name: Add or update registry path MyCompany, with entry 'hello', and containing 'world'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: world
+
+- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 1337 as the decimal value
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: 1337
+ type: dword
+
+- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 0xff2500ae as the hex value
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: 0xff2500ae
+ type: dword
+
+- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in hex-string format
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: hex:be,ef,be,ef,be,ef,be,ef,be,ef
+ type: binary
+
+- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in yaml format
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: [0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef]
+ type: binary
+
+- name: Add or update registry path MyCompany, with expand string entry 'hello'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: '%appdata%\local'
+ type: expandstring
+
+- name: Add or update registry path MyCompany, with multi string entry 'hello'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: ['hello', 'world']
+ type: multistring
+
+- name: Disable keyboard layout hotkey for all users (changes existing)
+ win_regedit:
+ path: HKU:\.DEFAULT\Keyboard Layout\Toggle
+ name: Layout Hotkey
+ data: 3
+ type: dword
+
+- name: Disable language hotkey for current users (adds new)
+ win_regedit:
+ path: HKCU:\Keyboard Layout\Toggle
+ name: Language Hotkey
+ data: 3
+ type: dword
+
+- name: Remove registry path MyCompany (including all entries it contains)
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ state: absent
+ delete_key: yes
+
+- name: Clear the existing (Default) entry at path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ state: absent
+ delete_key: no
+
+- name: Remove entry 'hello' from registry path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ state: absent
+
+- name: Change default mouse trailing settings for new users
+ win_regedit:
+ path: HKLM:\ANSIBLE\Control Panel\Mouse
+ name: MouseTrails
+ data: 10
+ type: str
+ state: present
+ hive: C:\Users\Default\NTUSER.dat
+'''
+
+RETURN = r'''
+data_changed:
+ description: Whether this invocation changed the data in the registry value.
+ returned: success
+ type: bool
+ sample: false
+data_type_changed:
+ description: Whether this invocation changed the datatype of the registry value.
+ returned: success
+ type: bool
+ sample: true
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_security_policy.ps1 b/test/support/windows-integration/plugins/modules/win_security_policy.ps1
new file mode 100644
index 00000000000..274204b6aaf
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_security_policy.ps1
@@ -0,0 +1,196 @@
+#!powershell
+
+# Copyright: (c) 2017, Jordan Borean
+# 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" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $Params -name "_ansible_diff" -type "bool" -default $false
+
+$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true
+$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true
+$value = Get-AnsibleParam -obj $params -name "value" -failifempty $true
+
+$result = @{
+ changed = $false
+ section = $section
+ key = $key
+ value = $value
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+Function Run-SecEdit($arguments) {
+ $stdout = $null
+ $stderr = $null
+ $log_path = [IO.Path]::GetTempFileName()
+ $arguments = $arguments + @("/log", $log_path)
+
+ try {
+ $stdout = &SecEdit.exe $arguments | Out-String
+ } catch {
+ $stderr = $_.Exception.Message
+ }
+ $log = Get-Content -Path $log_path
+ Remove-Item -Path $log_path -Force
+
+ $return = @{
+ log = ($log -join "`n").Trim()
+ stdout = $stdout
+ stderr = $stderr
+ rc = $LASTEXITCODE
+ }
+
+ return $return
+}
+
+Function Export-SecEdit() {
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ # while this will technically make a change to the system in check mode by
+ # creating a new file, we need these values to be able to do anything
+ # substantial in check mode
+ $export_result = Run-SecEdit -arguments @("/export", "/cfg", $secedit_ini_path, "/quiet")
+
+ # check the return code and if the file has been populated, otherwise error out
+ if (($export_result.rc -ne 0) -or ((Get-Item -Path $secedit_ini_path).Length -eq 0)) {
+ Remove-Item -Path $secedit_ini_path -Force
+ $result.rc = $export_result.rc
+ $result.stdout = $export_result.stdout
+ $result.stderr = $export_result.stderr
+ Fail-Json $result "Failed to export secedit.ini file to $($secedit_ini_path)"
+ }
+ $secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path
+
+ return $secedit_ini
+}
+
+Function Import-SecEdit($ini) {
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ $secedit_db_path = [IO.Path]::GetTempFileName()
+ Remove-Item -Path $secedit_db_path -Force # needs to be deleted for SecEdit.exe /import to work
+
+ $ini_contents = ConvertTo-Ini -ini $ini
+ Set-Content -Path $secedit_ini_path -Value $ini_contents
+ $result.changed = $true
+
+ $import_result = Run-SecEdit -arguments @("/configure", "/db", $secedit_db_path, "/cfg", $secedit_ini_path, "/quiet")
+ $result.import_log = $import_result.log
+ Remove-Item -Path $secedit_ini_path -Force
+ if ($import_result.rc -ne 0) {
+ $result.rc = $import_result.rc
+ $result.stdout = $import_result.stdout
+ $result.stderr = $import_result.stderr
+ Fail-Json $result "Failed to import secedit.ini file from $($secedit_ini_path)"
+ }
+}
+
+Function ConvertTo-Ini($ini) {
+ $content = @()
+ foreach ($key in $ini.GetEnumerator()) {
+ $section = $key.Name
+ $values = $key.Value
+
+ $content += "[$section]"
+ foreach ($value in $values.GetEnumerator()) {
+ $value_key = $value.Name
+ $value_value = $value.Value
+
+ if ($null -ne $value_value) {
+ $content += "$value_key = $value_value"
+ }
+ }
+ }
+
+ return $content -join "`r`n"
+}
+
+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
+ }
+ }
+
+ return $ini
+}
+
+if ($section -eq "Privilege Rights") {
+ Add-Warning -obj $result -message "Using this module to edit rights and privileges is error-prone, use the win_user_right module instead"
+}
+
+$will_change = $false
+$secedit_ini = Export-SecEdit
+if (-not ($secedit_ini.ContainsKey($section))) {
+ Fail-Json $result "The section '$section' does not exist in SecEdit.exe output ini"
+}
+
+if ($secedit_ini.$section.ContainsKey($key)) {
+ $current_value = $secedit_ini.$section.$key
+
+ if ($current_value -cne $value) {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
+-$key = $current_value
++$key = $value
+"@
+ }
+
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+ }
+} elseif ([string]$value -eq "") {
+ # Value is requested to be removed, and has already been removed, do nothing
+} else {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
++$key = $value
+"@
+ }
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+}
+
+if ($will_change -eq $true) {
+ $result.changed = $true
+ if (-not $check_mode) {
+ Import-SecEdit -ini $secedit_ini
+
+ # secedit doesn't error out on improper entries, re-export and verify
+ # the changes occurred
+ $verification_ini = Export-SecEdit
+ $new_section_values = $verification_ini.$section
+ if ($new_section_values.ContainsKey($key)) {
+ $new_value = $new_section_values.$key
+ if ($new_value -cne $value) {
+ Fail-Json $result "Failed to change the value for key '$key' in section '$section', the value is still $new_value"
+ }
+ } elseif ([string]$value -eq "") {
+ # Value was empty, so OK if no longer in the result
+ } else {
+ Fail-Json $result "The key '$key' in section '$section' is not a valid key, cannot set this value"
+ }
+ }
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_security_policy.py b/test/support/windows-integration/plugins/modules/win_security_policy.py
new file mode 100644
index 00000000000..d582a532317
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_security_policy.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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_security_policy
+version_added: '2.4'
+short_description: Change local security policy settings
+description:
+- Allows you to set the local security policies that are configured by
+ SecEdit.exe.
+options:
+ section:
+ description:
+ - The ini section the key exists in.
+ - If the section does not exist then the module will return an error.
+ - Example sections to use are 'Account Policies', 'Local Policies',
+ 'Event Log', 'Restricted Groups', 'System Services', 'Registry' and
+ 'File System'
+ - If wanting to edit the C(Privilege Rights) section, use the
+ M(win_user_right) module instead.
+ type: str
+ required: yes
+ key:
+ description:
+ - The ini key of the section or policy name to modify.
+ - The module will return an error if this key is invalid.
+ type: str
+ required: yes
+ value:
+ description:
+ - The value for the ini key or policy name.
+ - If the key takes in a boolean value then 0 = False and 1 = True.
+ type: str
+ required: yes
+notes:
+- This module uses the SecEdit.exe tool to configure the values, more details
+ of the areas and keys that can be configured can be found here
+ U(https://msdn.microsoft.com/en-us/library/bb742512.aspx).
+- If you are in a domain environment these policies may be set by a GPO policy,
+ this module can temporarily change these values but the GPO will override
+ it if the value differs.
+- You can also run C(SecEdit.exe /export /cfg C:\temp\output.ini) to view the
+ current policies set on your system.
+- When assigning user rights, use the M(win_user_right) module instead.
+seealso:
+- module: win_user_right
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Change the guest account name
+ win_security_policy:
+ section: System Access
+ key: NewGuestName
+ value: Guest Account
+
+- name: Set the maximum password age
+ win_security_policy:
+ section: System Access
+ key: MaximumPasswordAge
+ value: 15
+
+- name: Do not store passwords using reversible encryption
+ win_security_policy:
+ section: System Access
+ key: ClearTextPassword
+ value: 0
+
+- name: Enable system events
+ win_security_policy:
+ section: Event Audit
+ key: AuditSystemEvents
+ value: 1
+'''
+
+RETURN = r'''
+rc:
+ description: The return code after a failure when running SecEdit.exe.
+ returned: failure with secedit calls
+ type: int
+ sample: -1
+stdout:
+ description: The output of the STDOUT buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: check log for error details
+stderr:
+ description: The output of the STDERR buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: failed to import security policy
+import_log:
+ description: The log of the SecEdit.exe /configure job that configured the
+ local policies. This is used for debugging purposes on failures.
+ returned: secedit.exe /import run and change occurred
+ type: str
+ sample: Completed 6 percent (0/15) \tProcess Privilege Rights area.
+key:
+ description: The key in the section passed to the module to modify.
+ returned: success
+ type: str
+ sample: NewGuestName
+section:
+ description: The section passed to the module to modify.
+ returned: success
+ type: str
+ sample: System Access
+value:
+ description: The value passed to the module to modify to.
+ returned: success
+ type: str
+ sample: Guest Account
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_shell.ps1 b/test/support/windows-integration/plugins/modules/win_shell.ps1
new file mode 100644
index 00000000000..54aef8de120
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_shell.ps1
@@ -0,0 +1,138 @@
+#!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"
+
+# Cleanse CLIXML from stderr (sift out error stream data, discard others for now)
+Function Cleanse-Stderr($raw_stderr) {
+ Try {
+ # NB: this regex isn't perfect, but is decent at finding CLIXML amongst other stderr noise
+ If($raw_stderr -match "(?s)(?.*)#< CLIXML(?.*)(?)(?.*)") {
+ $clixml = [xml]$matches["clixml"]
+
+ $merged_stderr = "{0}{1}{2}{3}" -f @(
+ $matches["prenoise1"],
+ $matches["prenoise2"],
+ # filter out just the Error-tagged strings for now, and zap embedded CRLF chars
+ ($clixml.Objs.ChildNodes | Where-Object { $_.Name -eq 'S' } | Where-Object { $_.S -eq 'Error' } | ForEach-Object { $_.'#text'.Replace('_x000D__x000A_','') } | Out-String),
+ $matches["postnoise"]) | Out-String
+
+ return $merged_stderr.Trim()
+
+ # FUTURE: parse/return other streams
+ }
+ Else {
+ $raw_stderr
+ }
+ }
+ Catch {
+ "***EXCEPTION PARSING CLIXML: $_***" + $raw_stderr
+ }
+}
+
+$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"
+$executable = Get-AnsibleParam -obj $params -name "executable" -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"
+$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
+$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}
+}
+
+$exec_args = $null
+If(-not $executable -or $executable -eq "powershell") {
+ $exec_application = "powershell.exe"
+
+ # force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up
+ $raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line
+
+ # Base64 encode the command so we don't have to worry about the various levels of escaping
+ $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line))
+
+ if ($stdin) {
+ $exec_args = "-encodedcommand $encoded_command"
+ } else {
+ $exec_args = "-noninteractive -encodedcommand $encoded_command"
+ }
+
+ if ($no_profile) {
+ $exec_args = "-noprofile $exec_args"
+ }
+}
+Else {
+ # FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter?
+ $exec_application = $executable
+ if (-not ($exec_application.EndsWith(".exe"))) {
+ $exec_application = "$($exec_application).exe"
+ }
+ $exec_args = "/c $raw_command_line"
+}
+
+$command = "`"$exec_application`" $exec_args"
+$run_command_arg = @{
+ command = $command
+}
+if ($chdir) {
+ $run_command_arg['working_directory'] = $chdir
+}
+if ($stdin) {
+ $run_command_arg['stdin'] = $stdin
+}
+if ($output_encoding_override) {
+ $run_command_arg['output_encoding_override'] = $output_encoding_override
+}
+
+$start_datetime = [DateTime]::UtcNow
+try {
+ $command_result = Run-Command @run_command_arg
+} catch {
+ $result.changed = $false
+ try {
+ $result.rc = $_.Exception.NativeErrorCode
+ } catch {
+ $result.rc = 2
+ }
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+# TODO: decode CLIXML stderr output (and other streams?)
+$result.stdout = $command_result.stdout
+$result.stderr = Cleanse-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
diff --git a/test/support/windows-integration/plugins/modules/win_shell.py b/test/support/windows-integration/plugins/modules/win_shell.py
new file mode 100644
index 00000000000..ee2cd76240d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_shell.py
@@ -0,0 +1,167 @@
+#!/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_shell
+short_description: Execute shell commands on target hosts
+version_added: 2.2
+description:
+ - The C(win_shell) module takes the command name followed by a list of space-delimited arguments.
+ It is similar to the M(win_command) module, but runs
+ the command via a shell (defaults to PowerShell) on the target host.
+ - For non-Windows targets, use the M(shell) module instead.
+options:
+ free_form:
+ description:
+ - The C(win_shell) 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
+ executable:
+ description:
+ - Change the shell used to execute the command (eg, C(cmd)).
+ - The target shell must accept a C(/c) parameter followed by the raw command line to be executed.
+ type: path
+ stdin:
+ description:
+ - Set the stdin of the command directly to the specified value.
+ type: str
+ version_added: '2.5'
+ no_profile:
+ description:
+ - Do not load the user profile before running a command. This is only valid
+ when using PowerShell as the executable.
+ type: bool
+ default: no
+ version_added: '2.8'
+ 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 an executable securely and predictably, it may be
+ better to use the M(win_command) module instead. Best practices when writing
+ playbooks will follow the trend of using M(win_command) unless C(win_shell) is
+ explicitly required. When running ad-hoc commands, use your best judgement.
+ - WinRM will not return from a command execution until all child processes created have exited.
+ Thus, it is not possible to use C(win_shell) to spawn long-running child or background processes.
+ Consider creating a Windows service for managing background processes.
+seealso:
+- module: psexec
+- module: raw
+- module: script
+- module: shell
+- module: win_command
+- module: win_psexec
+author:
+ - Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+# Execute a command in the remote shell; stdout goes to the specified
+# file on the remote.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt
+
+# Change the working directory to somedir/ before executing the command.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt chdir=C:\somedir
+
+# You can also use the 'args' form to provide the options. This command
+# will change the working directory to somedir/ and will only run when
+# somedir/somelog.txt doesn't exist.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt
+ args:
+ chdir: C:\somedir
+ creates: C:\somelog.txt
+
+# Run a command under a non-Powershell interpreter (cmd in this case)
+- win_shell: echo %HOMEDIR%
+ args:
+ executable: cmd
+ register: homedir_out
+
+- name: Run multi-lined shell commands
+ win_shell: |
+ $value = Test-Path -Path C:\temp
+ if ($value) {
+ Remove-Item -Path C:\temp -Force
+ }
+ New-Item -Path C:\temp -ItemType Directory
+
+- name: Retrieve the input based on stdin
+ win_shell: '$string = [Console]::In.ReadToEnd(); Write-Output $string.Trim()'
+ args:
+ stdin: Input message
+'''
+
+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 ...']
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_stat.ps1 b/test/support/windows-integration/plugins/modules/win_stat.ps1
new file mode 100644
index 00000000000..071eb11ceb1
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_stat.ps1
@@ -0,0 +1,186 @@
+#!powershell
+
+# 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 -Module Ansible.ModuleUtils.FileUtil
+#Requires -Module Ansible.ModuleUtils.LinkUtil
+
+function ConvertTo-Timestamp($start_date, $end_date) {
+ if ($start_date -and $end_date) {
+ return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds
+ }
+}
+
+function Get-FileChecksum($path, $algorithm) {
+ 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 }
+ default { Fail-Json -obj $result -message "Unsupported hash algorithm supplied '$algorithm'" }
+ }
+
+ $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 Get-FileInfo {
+ param([String]$Path, [Switch]$Follow)
+
+ $info = Get-AnsibleItem -Path $Path -ErrorAction SilentlyContinue
+ $link_info = $null
+ if ($null -ne $info) {
+ try {
+ $link_info = Get-Link -link_path $info.FullName
+ } catch {
+ $module.Warn("Failed to check/get link info for file: $($_.Exception.Message)")
+ }
+
+ # If follow=true we want to follow the link all the way back to root object
+ if ($Follow -and $null -ne $link_info -and $link_info.Type -in @("SymbolicLink", "JunctionPoint")) {
+ $info, $link_info = Get-FileInfo -Path $link_info.AbsolutePath -Follow
+ }
+ }
+
+ return $info, $link_info
+}
+
+$spec = @{
+ options = @{
+ path = @{ type='path'; required=$true; aliases=@( 'dest', 'name' ) }
+ get_checksum = @{ type='bool'; default=$true }
+ checksum_algorithm = @{ type='str'; default='sha1'; choices=@( 'md5', 'sha1', 'sha256', 'sha384', 'sha512' ) }
+ follow = @{ type='bool'; default=$false }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$get_checksum = $module.Params.get_checksum
+$checksum_algorithm = $module.Params.checksum_algorithm
+$follow = $module.Params.follow
+
+$module.Result.stat = @{ exists=$false }
+
+Load-LinkUtils
+$info, $link_info = Get-FileInfo -Path $path -Follow:$follow
+If ($null -ne $info) {
+ $epoch_date = Get-Date -Date "01/01/1970"
+ $attributes = @()
+ foreach ($attribute in ($info.Attributes -split ',')) {
+ $attributes += $attribute.Trim()
+ }
+
+ # default values that are always set, specific values are set below this
+ # but are kept commented for easier readability
+ $stat = @{
+ exists = $true
+ attributes = $info.Attributes.ToString()
+ isarchive = ($attributes -contains "Archive")
+ isdir = $false
+ ishidden = ($attributes -contains "Hidden")
+ isjunction = $false
+ islnk = $false
+ isreadonly = ($attributes -contains "ReadOnly")
+ isreg = $false
+ isshared = $false
+ nlink = 1 # Number of links to the file (hard links), overriden below if islnk
+ # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative
+ # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem
+ hlnk_targets = @()
+ creationtime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.CreationTime)
+ lastaccesstime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastAccessTime)
+ lastwritetime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastWriteTime)
+ # size = a file and directory - calculated below
+ path = $info.FullName
+ filename = $info.Name
+ # extension = a file
+ # owner = set outsite this dict in case it fails
+ # sharename = a directory and isshared is True
+ # checksum = a file and get_checksum: True
+ }
+ try {
+ $stat.owner = $info.GetAccessControl().Owner
+ } catch {
+ # may not have rights, historical behaviour was to just set to $null
+ # due to ErrorActionPreference being set to "Continue"
+ $stat.owner = $null
+ }
+
+ # values that are set according to the type of file
+ if ($info.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
+ $stat.isdir = $true
+ $share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($stat.path -replace '\\', '\\')'"
+ if ($null -ne $share_info) {
+ $stat.isshared = $true
+ $stat.sharename = $share_info.Name
+ }
+
+ try {
+ $size = 0
+ foreach ($file in $info.EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)) {
+ $size += $file.Length
+ }
+ $stat.size = $size
+ } catch {
+ $stat.size = 0
+ }
+ } else {
+ $stat.extension = $info.Extension
+ $stat.isreg = $true
+ $stat.size = $info.Length
+
+ if ($get_checksum) {
+ try {
+ $stat.checksum = Get-FileChecksum -path $path -algorithm $checksum_algorithm
+ } catch {
+ $module.FailJson("Failed to get hash of file, set get_checksum to False to ignore this error: $($_.Exception.Message)", $_)
+ }
+ }
+ }
+
+ # Get symbolic link, junction point, hard link info
+ if ($null -ne $link_info) {
+ switch ($link_info.Type) {
+ "SymbolicLink" {
+ $stat.islnk = $true
+ $stat.isreg = $false
+ $stat.lnk_target = $link_info.TargetPath
+ $stat.lnk_source = $link_info.AbsolutePath
+ break
+ }
+ "JunctionPoint" {
+ $stat.isjunction = $true
+ $stat.isreg = $false
+ $stat.lnk_target = $link_info.TargetPath
+ $stat.lnk_source = $link_info.AbsolutePath
+ break
+ }
+ "HardLink" {
+ $stat.lnk_type = "hard"
+ $stat.nlink = $link_info.HardTargets.Count
+
+ # remove current path from the targets
+ $hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $stat.path }
+ $stat.hlnk_targets = @($hlnk_targets)
+ break
+ }
+ }
+ }
+
+ $module.Result.stat = $stat
+}
+
+$module.ExitJson()
+
diff --git a/test/support/windows-integration/plugins/modules/win_stat.py b/test/support/windows-integration/plugins/modules/win_stat.py
new file mode 100644
index 00000000000..0676b5b2350
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_stat.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, 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': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_stat
+version_added: "1.7"
+short_description: Get information about Windows files
+description:
+ - Returns information about a Windows file.
+ - For non-Windows targets, use the M(stat) module instead.
+options:
+ path:
+ description:
+ - The full path of the file/object to get the facts of; both forward and
+ back slashes are accepted.
+ type: path
+ required: yes
+ aliases: [ dest, name ]
+ get_checksum:
+ description:
+ - Whether to return a checksum of the file (default sha1)
+ type: bool
+ default: yes
+ version_added: "2.1"
+ checksum_algorithm:
+ description:
+ - Algorithm to determine checksum of file.
+ - Will throw an error if the host is unable to use specified algorithm.
+ type: str
+ default: sha1
+ choices: [ md5, sha1, sha256, sha384, sha512 ]
+ version_added: "2.3"
+ follow:
+ description:
+ - Whether to follow symlinks or junction points.
+ - In the case of C(path) pointing to another link, then that will
+ be followed until no more links are found.
+ type: bool
+ default: no
+ version_added: "2.8"
+seealso:
+- module: stat
+- module: win_acl
+- module: win_file
+- module: win_owner
+author:
+- Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+- name: Obtain information about a file
+ win_stat:
+ path: C:\foo.ini
+ register: file_info
+
+- name: Obtain information about a folder
+ win_stat:
+ path: C:\bar
+ register: folder_info
+
+- name: Get MD5 checksum of a file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ checksum_algorithm: md5
+ register: md5_checksum
+
+- debug:
+ var: md5_checksum.stat.checksum
+
+- name: Get SHA1 checksum of file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ register: sha1_checksum
+
+- debug:
+ var: sha1_checksum.stat.checksum
+
+- name: Get SHA256 checksum of file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ checksum_algorithm: sha256
+ register: sha256_checksum
+
+- debug:
+ var: sha256_checksum.stat.checksum
+'''
+
+RETURN = r'''
+changed:
+ description: Whether anything was changed
+ returned: always
+ type: bool
+ sample: true
+stat:
+ description: dictionary containing all the stat data
+ 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 exist, path is a file, get_checksum == True
+ checksum_algorithm specified is supported
+ 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: If the path exists or not.
+ returned: success
+ 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 (without path).
+ returned: success, path exists, path is a file
+ type: str
+ sample: foo.ini
+ 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 or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ islnk:
+ description: If the path is a symbolic link or not.
+ 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.
+ 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: Target of the symlink normalized for the remote filesystem.
+ returned: success, path exists and the path is a symbolic link or junction point
+ type: str
+ sample: C:\temp\link
+ lnk_target:
+ description: Target of the symlink. Note that relative paths remain relative.
+ returned: success, path exists and the path is a symbolic link or junction point
+ type: str
+ sample: ..\link
+ 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, file exists
+ type: str
+ sample: C:\foo.ini
+ sharename:
+ description: The name of share if folder is shared.
+ returned: success, path exists, file is a directory and isshared == True
+ type: str
+ sample: file-share
+ size:
+ description: The size in bytes of a file or folder.
+ returned: success, path exists, file is not a link
+ type: int
+ sample: 1024
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_tempfile.ps1 b/test/support/windows-integration/plugins/modules/win_tempfile.ps1
new file mode 100644
index 00000000000..9a1a7174397
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_tempfile.ps1
@@ -0,0 +1,72 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Function New-TempFile {
+ Param ([string]$path, [string]$prefix, [string]$suffix, [string]$type, [bool]$checkmode)
+ $temppath = $null
+ $curerror = $null
+ $attempt = 0
+
+ # Since we don't know if the file already exists, we try 5 times with a random name
+ do {
+ $attempt += 1
+ $randomname = [System.IO.Path]::GetRandomFileName()
+ $temppath = (Join-Path -Path $path -ChildPath "$prefix$randomname$suffix")
+ Try {
+ $file = New-Item -Path $temppath -ItemType $type -WhatIf:$checkmode
+ # Makes sure we get the full absolute path of the created temp file and not a relative or DOS 8.3 dir
+ if (-not $checkmode) {
+ $temppath = $file.FullName
+ } else {
+ # Just rely on GetFulLpath for check mode
+ $temppath = [System.IO.Path]::GetFullPath($temppath)
+ }
+ } Catch {
+ $temppath = $null
+ $curerror = $_
+ }
+ } until (($null -ne $temppath) -or ($attempt -ge 5))
+
+ # If it fails 5 times, something is wrong and we have to report the details
+ if ($null -eq $temppath) {
+ $module.FailJson("No random temporary file worked in $attempt attempts. Error: $($curerror.Exception.Message)", $curerror)
+ }
+
+ return $temppath.ToString()
+}
+
+$spec = @{
+ options = @{
+ path = @{ type='path'; default='%TEMP%'; aliases=@( 'dest' ) }
+ state = @{ type='str'; default='file'; choices=@( 'directory', 'file') }
+ prefix = @{ type='str'; default='ansible.' }
+ suffix = @{ type='str' }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$state = $module.Params.state
+$prefix = $module.Params.prefix
+$suffix = $module.Params.suffix
+
+# Expand environment variables on non-path types
+if ($null -ne $prefix) {
+ $prefix = [System.Environment]::ExpandEnvironmentVariables($prefix)
+}
+if ($null -ne $suffix) {
+ $suffix = [System.Environment]::ExpandEnvironmentVariables($suffix)
+}
+
+$module.Result.changed = $true
+$module.Result.state = $state
+
+$module.Result.path = New-TempFile -Path $path -Prefix $prefix -Suffix $suffix -Type $state -CheckMode $module.CheckMode
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_tempfile.py b/test/support/windows-integration/plugins/modules/win_tempfile.py
new file mode 100644
index 00000000000..58dd6501373
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_tempfile.py
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright: (c) 2017, Dag Wieers
+# 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_tempfile
+version_added: "2.3"
+short_description: Creates temporary files and directories
+description:
+ - Creates temporary files and directories.
+ - For non-Windows targets, please use the M(tempfile) module instead.
+options:
+ state:
+ description:
+ - Whether to create file or directory.
+ type: str
+ choices: [ directory, file ]
+ default: file
+ path:
+ description:
+ - Location where temporary file or directory should be created.
+ - If path is not specified default system temporary directory (%TEMP%) will be used.
+ type: path
+ default: '%TEMP%'
+ aliases: [ dest ]
+ prefix:
+ description:
+ - Prefix of file/directory name created by module.
+ type: str
+ default: ansible.
+ suffix:
+ description:
+ - Suffix of file/directory name created by module.
+ type: str
+ default: ''
+seealso:
+- module: tempfile
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r"""
+- name: Create temporary build directory
+ win_tempfile:
+ state: directory
+ suffix: build
+
+- name: Create temporary file
+ win_tempfile:
+ state: file
+ suffix: temp
+"""
+
+RETURN = r'''
+path:
+ description: The absolute path to the created file or directory.
+ returned: success
+ type: str
+ sample: C:\Users\Administrator\AppData\Local\Temp\ansible.bMlvdk
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_template.py b/test/support/windows-integration/plugins/modules/win_template.py
new file mode 100644
index 00000000000..bd8b2492fa6
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_template.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a virtual module that is entirely implemented server side
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_template
+version_added: "1.9.2"
+short_description: Template a file out to a remote server
+options:
+ 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
+ version_added: '2.8'
+ newline_sequence:
+ default: '\r\n'
+ force:
+ version_added: '2.4'
+notes:
+- Beware fetching files from windows machines when creating templates because certain tools, such as Powershell ISE,
+ and regedit's export facility add a Byte Order Mark as the first character of the file, which can cause tracebacks.
+- You can use the M(win_copy) module with the C(content:) option if you prefer the template inline, as part of the
+ playbook.
+- For Linux you can use M(template) which uses '\\n' as C(newline_sequence) by default.
+seealso:
+- module: win_copy
+- module: copy
+- module: template
+author:
+- Jon Hawkesworth (@jhawkesworth)
+extends_documentation_fragment:
+- template_common
+'''
+
+EXAMPLES = r'''
+- name: Create a file from a Jinja2 template
+ win_template:
+ src: /mytemplates/file.conf.j2
+ dest: C:\Temp\file.conf
+
+- name: Create a Unix-style file from a Jinja2 template
+ win_template:
+ src: unix/config.conf.j2
+ dest: C:\share\unix\config.conf
+ newline_sequence: '\n'
+ backup: yes
+'''
+
+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
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_user.ps1 b/test/support/windows-integration/plugins/modules/win_user.ps1
new file mode 100644
index 00000000000..54905cb2eb6
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user.ps1
@@ -0,0 +1,273 @@
+#!powershell
+
+# Copyright: (c) 2014, Paul Durivage
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+########
+$ADS_UF_PASSWD_CANT_CHANGE = 64
+$ADS_UF_DONT_EXPIRE_PASSWD = 65536
+
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+
+function Get-User($user) {
+ $adsi.Children | Where-Object {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user }
+ return
+}
+
+function Get-UserFlag($user, $flag) {
+ If ($user.UserFlags[0] -band $flag) {
+ $true
+ }
+ Else {
+ $false
+ }
+}
+
+function Set-UserFlag($user, $flag) {
+ $user.UserFlags = ($user.UserFlags[0] -BOR $flag)
+}
+
+function Clear-UserFlag($user, $flag) {
+ $user.UserFlags = ($user.UserFlags[0] -BXOR $flag)
+}
+
+function Get-Group($grp) {
+ $adsi.Children | Where-Object { $_.SchemaClassName -eq 'Group' -and $_.Name -eq $grp }
+ return
+}
+
+Function Test-LocalCredential {
+ param([String]$Username, [String]$Password)
+
+ try {
+ $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($Username, $null, $Password, "Network", "Default")
+ $handle.Dispose()
+ $valid_credentials = $true
+ } catch [Ansible.AccessToken.Win32Exception] {
+ # following errors indicate the creds are correct but the user was
+ # unable to log on for other reasons, which we don't care about
+ $success_codes = @(
+ 0x0000052F, # ERROR_ACCOUNT_RESTRICTION
+ 0x00000530, # ERROR_INVALID_LOGON_HOURS
+ 0x00000531, # ERROR_INVALID_WORKSTATION
+ 0x00000569 # ERROR_LOGON_TYPE_GRANTED
+ )
+
+ if ($_.Exception.NativeErrorCode -eq 0x0000052E) {
+ # ERROR_LOGON_FAILURE - the user or pass was incorrect
+ $valid_credentials = $false
+ } elseif ($_.Exception.NativeErrorCode -in $success_codes) {
+ $valid_credentials = $true
+ } else {
+ # an unknown failure, reraise exception
+ throw $_
+ }
+ }
+ return $valid_credentials
+}
+
+########
+
+$params = Parse-Args $args;
+
+$result = @{
+ changed = $false
+};
+
+$username = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$fullname = Get-AnsibleParam -obj $params -name "fullname" -type "str"
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$password = Get-AnsibleParam -obj $params -name "password" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","query"
+$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "str" -default "always" -validateset "always","on_create"
+$password_expired = Get-AnsibleParam -obj $params -name "password_expired" -type "bool"
+$password_never_expires = Get-AnsibleParam -obj $params -name "password_never_expires" -type "bool"
+$user_cannot_change_password = Get-AnsibleParam -obj $params -name "user_cannot_change_password" -type "bool"
+$account_disabled = Get-AnsibleParam -obj $params -name "account_disabled" -type "bool"
+$account_locked = Get-AnsibleParam -obj $params -name "account_locked" -type "bool"
+$groups = Get-AnsibleParam -obj $params -name "groups"
+$groups_action = Get-AnsibleParam -obj $params -name "groups_action" -type "str" -default "replace" -validateset "add","remove","replace"
+
+If ($null -ne $account_locked -and $account_locked) {
+ Fail-Json $result "account_locked must be set to 'no' if provided"
+}
+
+If ($null -ne $groups) {
+ If ($groups -is [System.String]) {
+ [string[]]$groups = $groups.Split(",")
+ }
+ ElseIf ($groups -isnot [System.Collections.IList]) {
+ Fail-Json $result "groups must be a string or array"
+ }
+ $groups = $groups | ForEach-Object { ([string]$_).Trim() } | Where-Object { $_ }
+ If ($null -eq $groups) {
+ $groups = @()
+ }
+}
+
+$user_obj = Get-User $username
+
+If ($state -eq 'present') {
+ # Add or update user
+ try {
+ If (-not $user_obj) {
+ $user_obj = $adsi.Create("User", $username)
+ If ($null -ne $password) {
+ $user_obj.SetPassword($password)
+ }
+ $user_obj.SetInfo()
+ $result.changed = $true
+ }
+ ElseIf (($null -ne $password) -and ($update_password -eq 'always')) {
+ # ValidateCredentials will fail if either of these are true- just force update...
+ If($user_obj.AccountDisabled -or $user_obj.PasswordExpired) {
+ $password_match = $false
+ }
+ Else {
+ try {
+ $password_match = Test-LocalCredential -Username $username -Password $password
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "Failed to validate the user's credentials: $($_.Exception.Message)"
+ }
+ }
+
+ If (-not $password_match) {
+ $user_obj.SetPassword($password)
+ $result.changed = $true
+ }
+ }
+ If (($null -ne $fullname) -and ($fullname -ne $user_obj.FullName[0])) {
+ $user_obj.FullName = $fullname
+ $result.changed = $true
+ }
+ If (($null -ne $description) -and ($description -ne $user_obj.Description[0])) {
+ $user_obj.Description = $description
+ $result.changed = $true
+ }
+ If (($null -ne $password_expired) -and ($password_expired -ne ($user_obj.PasswordExpired | ConvertTo-Bool))) {
+ $user_obj.PasswordExpired = If ($password_expired) { 1 } Else { 0 }
+ $result.changed = $true
+ }
+ If (($null -ne $password_never_expires) -and ($password_never_expires -ne (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD))) {
+ If ($password_never_expires) {
+ Set-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
+ }
+ Else {
+ Clear-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
+ }
+ $result.changed = $true
+ }
+ If (($null -ne $user_cannot_change_password) -and ($user_cannot_change_password -ne (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE))) {
+ If ($user_cannot_change_password) {
+ Set-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
+ }
+ Else {
+ Clear-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
+ }
+ $result.changed = $true
+ }
+ If (($null -ne $account_disabled) -and ($account_disabled -ne $user_obj.AccountDisabled)) {
+ $user_obj.AccountDisabled = $account_disabled
+ $result.changed = $true
+ }
+ If (($null -ne $account_locked) -and ($account_locked -ne $user_obj.IsAccountLocked)) {
+ $user_obj.IsAccountLocked = $account_locked
+ $result.changed = $true
+ }
+ If ($result.changed) {
+ $user_obj.SetInfo()
+ }
+ If ($null -ne $groups) {
+ [string[]]$current_groups = $user_obj.Groups() | ForEach-Object { $_.GetType().InvokeMember("Name", "GetProperty", $null, $_, $null) }
+ If (($groups_action -eq "remove") -or ($groups_action -eq "replace")) {
+ ForEach ($grp in $current_groups) {
+ If ((($groups_action -eq "remove") -and ($groups -contains $grp)) -or (($groups_action -eq "replace") -and ($groups -notcontains $grp))) {
+ $group_obj = Get-Group $grp
+ If ($group_obj) {
+ $group_obj.Remove($user_obj.Path)
+ $result.changed = $true
+ }
+ Else {
+ Fail-Json $result "group '$grp' not found"
+ }
+ }
+ }
+ }
+ If (($groups_action -eq "add") -or ($groups_action -eq "replace")) {
+ ForEach ($grp in $groups) {
+ If ($current_groups -notcontains $grp) {
+ $group_obj = Get-Group $grp
+ If ($group_obj) {
+ $group_obj.Add($user_obj.Path)
+ $result.changed = $true
+ }
+ Else {
+ Fail-Json $result "group '$grp' not found"
+ }
+ }
+ }
+ }
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+ElseIf ($state -eq 'absent') {
+ # Remove user
+ try {
+ If ($user_obj) {
+ $username = $user_obj.Name.Value
+ $adsi.delete("User", $user_obj.Name.Value)
+ $result.changed = $true
+ $result.msg = "User '$username' deleted successfully"
+ $user_obj = $null
+ } else {
+ $result.msg = "User '$username' was not found"
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+
+try {
+ If ($user_obj -and $user_obj -is [System.DirectoryServices.DirectoryEntry]) {
+ $user_obj.RefreshCache()
+ $result.name = $user_obj.Name[0]
+ $result.fullname = $user_obj.FullName[0]
+ $result.path = $user_obj.Path
+ $result.description = $user_obj.Description[0]
+ $result.password_expired = ($user_obj.PasswordExpired | ConvertTo-Bool)
+ $result.password_never_expires = (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD)
+ $result.user_cannot_change_password = (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE)
+ $result.account_disabled = $user_obj.AccountDisabled
+ $result.account_locked = $user_obj.IsAccountLocked
+ $result.sid = (New-Object System.Security.Principal.SecurityIdentifier($user_obj.ObjectSid.Value, 0)).Value
+ $user_groups = @()
+ ForEach ($grp in $user_obj.Groups()) {
+ $group_result = @{
+ name = $grp.GetType().InvokeMember("Name", "GetProperty", $null, $grp, $null)
+ path = $grp.GetType().InvokeMember("ADsPath", "GetProperty", $null, $grp, $null)
+ }
+ $user_groups += $group_result;
+ }
+ $result.groups = $user_groups
+ $result.state = "present"
+ }
+ Else {
+ $result.name = $username
+ if ($state -eq 'query') {
+ $result.msg = "User '$username' was not found"
+ }
+ $result.state = "absent"
+ }
+}
+catch {
+ Fail-Json $result $_.Exception.Message
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_user.py b/test/support/windows-integration/plugins/modules/win_user.py
new file mode 100644
index 00000000000..5fc0633d06b
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user.py
@@ -0,0 +1,194 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Matt Martz , 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_user
+version_added: "1.7"
+short_description: Manages local Windows user accounts
+description:
+ - Manages local Windows user accounts.
+ - For non-Windows targets, use the M(user) module instead.
+options:
+ name:
+ description:
+ - Name of the user to create, remove or modify.
+ type: str
+ required: yes
+ fullname:
+ description:
+ - Full name of the user.
+ type: str
+ version_added: "1.9"
+ description:
+ description:
+ - Description of the user.
+ type: str
+ version_added: "1.9"
+ password:
+ description:
+ - Optionally set the user's password to this (plain text) value.
+ type: str
+ update_password:
+ description:
+ - C(always) will update passwords if they differ. C(on_create) will
+ only set the password for newly created users.
+ type: str
+ choices: [ always, on_create ]
+ default: always
+ version_added: "1.9"
+ password_expired:
+ description:
+ - C(yes) will require the user to change their password at next login.
+ - C(no) will clear the expired password flag.
+ type: bool
+ version_added: "1.9"
+ password_never_expires:
+ description:
+ - C(yes) will set the password to never expire.
+ - C(no) will allow the password to expire.
+ type: bool
+ version_added: "1.9"
+ user_cannot_change_password:
+ description:
+ - C(yes) will prevent the user from changing their password.
+ - C(no) will allow the user to change their password.
+ type: bool
+ version_added: "1.9"
+ account_disabled:
+ description:
+ - C(yes) will disable the user account.
+ - C(no) will clear the disabled flag.
+ type: bool
+ version_added: "1.9"
+ account_locked:
+ description:
+ - C(no) will unlock the user account if locked.
+ choices: [ 'no' ]
+ version_added: "1.9"
+ groups:
+ description:
+ - Adds or removes the user from this comma-separated list of groups,
+ depending on the value of I(groups_action).
+ - When I(groups_action) is C(replace) and I(groups) is set to the empty
+ string ('groups='), the user is removed from all groups.
+ version_added: "1.9"
+ groups_action:
+ description:
+ - If C(add), the user is added to each group in I(groups) where not
+ already a member.
+ - If C(replace), the user is added as a member of each group in
+ I(groups) and removed from any other groups.
+ - If C(remove), the user is removed from each group in I(groups).
+ type: str
+ choices: [ add, replace, remove ]
+ default: replace
+ version_added: "1.9"
+ state:
+ description:
+ - When C(absent), removes the user account if it exists.
+ - When C(present), creates or updates the user account.
+ - When C(query) (new in 1.9), retrieves the user account details
+ without making any changes.
+ type: str
+ choices: [ absent, present, query ]
+ default: present
+seealso:
+- module: user
+- module: win_domain_membership
+- module: win_domain_user
+- module: win_group
+- module: win_group_membership
+- module: win_user_profile
+author:
+ - Paul Durivage (@angstwad)
+ - Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+- name: Ensure user bob is present
+ win_user:
+ name: bob
+ password: B0bP4ssw0rd
+ state: present
+ groups:
+ - Users
+
+- name: Ensure user bob is absent
+ win_user:
+ name: bob
+ state: absent
+'''
+
+RETURN = r'''
+account_disabled:
+ description: Whether the user is disabled.
+ returned: user exists
+ type: bool
+ sample: false
+account_locked:
+ description: Whether the user is locked.
+ returned: user exists
+ type: bool
+ sample: false
+description:
+ description: The description set for the user.
+ returned: user exists
+ type: str
+ sample: Username for test
+fullname:
+ description: The full name set for the user.
+ returned: user exists
+ type: str
+ sample: Test Username
+groups:
+ description: A list of groups and their ADSI path the user is a member of.
+ returned: user exists
+ type: list
+ sample: [
+ {
+ "name": "Administrators",
+ "path": "WinNT://WORKGROUP/USER-PC/Administrators"
+ }
+ ]
+name:
+ description: The name of the user
+ returned: always
+ type: str
+ sample: username
+password_expired:
+ description: Whether the password is expired.
+ returned: user exists
+ type: bool
+ sample: false
+password_never_expires:
+ description: Whether the password is set to never expire.
+ returned: user exists
+ type: bool
+ sample: true
+path:
+ description: The ADSI path for the user.
+ returned: user exists
+ type: str
+ sample: "WinNT://WORKGROUP/USER-PC/username"
+sid:
+ description: The SID for the user.
+ returned: user exists
+ type: str
+ sample: S-1-5-21-3322259488-2828151810-3939402796-1001
+user_cannot_change_password:
+ description: Whether the user can change their own password.
+ returned: user exists
+ type: bool
+ sample: false
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_user_right.ps1 b/test/support/windows-integration/plugins/modules/win_user_right.ps1
new file mode 100644
index 00000000000..3fac52a8a82
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user_right.ps1
@@ -0,0 +1,349 @@
+#!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.SID
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $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
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$users = Get-AnsibleParam -obj $params -name "users" -type "list" -failifempty $true
+$action = Get-AnsibleParam -obj $params -name "action" -type "str" -default "set" -validateset "add","remove","set"
+
+$result = @{
+ changed = $false
+ added = @()
+ removed = @()
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+$sec_helper_util = @"
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+
+namespace Ansible
+{
+ public class LsaRightHelper : IDisposable
+ {
+ // Code modified from https://gallery.technet.microsoft.com/scriptcenter/Grant-Revoke-Query-user-26e259b0
+
+ enum Access : int
+ {
+ POLICY_READ = 0x20006,
+ POLICY_ALL_ACCESS = 0x00F0FFF,
+ POLICY_EXECUTE = 0X20801,
+ POLICY_WRITE = 0X207F8
+ }
+
+ IntPtr lsaHandle;
+
+ const string LSA_DLL = "advapi32.dll";
+ const CharSet DEFAULT_CHAR_SET = CharSet.Unicode;
+
+ const uint STATUS_NO_MORE_ENTRIES = 0x8000001a;
+ const uint STATUS_NO_SUCH_PRIVILEGE = 0xc0000060;
+
+ internal sealed class Sid : IDisposable
+ {
+ public IntPtr pSid = IntPtr.Zero;
+ public SecurityIdentifier sid = null;
+
+ public Sid(string sidString)
+ {
+ try
+ {
+ sid = new SecurityIdentifier(sidString);
+ } catch
+ {
+ throw new ArgumentException(String.Format("SID string {0} could not be converted to SecurityIdentifier", sidString));
+ }
+
+ Byte[] buffer = new Byte[sid.BinaryLength];
+ sid.GetBinaryForm(buffer, 0);
+
+ pSid = Marshal.AllocHGlobal(sid.BinaryLength);
+ Marshal.Copy(buffer, 0, pSid, sid.BinaryLength);
+ }
+
+ public void Dispose()
+ {
+ if (pSid != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(pSid);
+ pSid = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~Sid() { Dispose(); }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct LSA_OBJECT_ATTRIBUTES
+ {
+ public int Length;
+ public IntPtr RootDirectory;
+ public IntPtr ObjectName;
+ public int Attributes;
+ public IntPtr SecurityDescriptor;
+ public IntPtr SecurityQualityOfService;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = DEFAULT_CHAR_SET)]
+ private struct LSA_UNICODE_STRING
+ {
+ public ushort Length;
+ public ushort MaximumLength;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct LSA_ENUMERATION_INFORMATION
+ {
+ public IntPtr Sid;
+ }
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaOpenPolicy(
+ LSA_UNICODE_STRING[] SystemName,
+ ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
+ int AccessMask,
+ out IntPtr PolicyHandle
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaAddAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr pSID,
+ LSA_UNICODE_STRING[] UserRights,
+ int CountOfRights
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaRemoveAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr pSID,
+ bool AllRights,
+ LSA_UNICODE_STRING[] UserRights,
+ int CountOfRights
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaEnumerateAccountsWithUserRight(
+ IntPtr PolicyHandle,
+ LSA_UNICODE_STRING[] UserRights,
+ out IntPtr EnumerationBuffer,
+ out ulong CountReturned
+ );
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaNtStatusToWinError(int NTSTATUS);
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaClose(IntPtr PolicyHandle);
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaFreeMemory(IntPtr Buffer);
+
+ public LsaRightHelper()
+ {
+ LSA_OBJECT_ATTRIBUTES lsaAttr;
+ lsaAttr.RootDirectory = IntPtr.Zero;
+ lsaAttr.ObjectName = IntPtr.Zero;
+ lsaAttr.Attributes = 0;
+ lsaAttr.SecurityDescriptor = IntPtr.Zero;
+ lsaAttr.SecurityQualityOfService = IntPtr.Zero;
+ lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
+
+ lsaHandle = IntPtr.Zero;
+
+ LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
+ system[0] = InitLsaString("");
+
+ uint ret = LsaOpenPolicy(system, ref lsaAttr, (int)Access.POLICY_ALL_ACCESS, out lsaHandle);
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public void AddPrivilege(string sidString, string privilege)
+ {
+ uint ret = 0;
+ using (Sid sid = new Sid(sidString))
+ {
+ LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
+ privileges[0] = InitLsaString(privilege);
+ ret = LsaAddAccountRights(lsaHandle, sid.pSid, privileges, 1);
+ }
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public void RemovePrivilege(string sidString, string privilege)
+ {
+ uint ret = 0;
+ using (Sid sid = new Sid(sidString))
+ {
+ LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
+ privileges[0] = InitLsaString(privilege);
+ ret = LsaRemoveAccountRights(lsaHandle, sid.pSid, false, privileges, 1);
+ }
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public string[] EnumerateAccountsWithUserRight(string privilege)
+ {
+ uint ret = 0;
+ ulong count = 0;
+ LSA_UNICODE_STRING[] rights = new LSA_UNICODE_STRING[1];
+ rights[0] = InitLsaString(privilege);
+ IntPtr buffer = IntPtr.Zero;
+
+ ret = LsaEnumerateAccountsWithUserRight(lsaHandle, rights, out buffer, out count);
+ switch (ret)
+ {
+ case 0:
+ string[] accounts = new string[count];
+ for (int i = 0; i < (int)count; i++)
+ {
+ LSA_ENUMERATION_INFORMATION LsaInfo = (LSA_ENUMERATION_INFORMATION)Marshal.PtrToStructure(
+ IntPtr.Add(buffer, i * Marshal.SizeOf(typeof(LSA_ENUMERATION_INFORMATION))),
+ typeof(LSA_ENUMERATION_INFORMATION));
+
+ accounts[i] = new SecurityIdentifier(LsaInfo.Sid).ToString();
+ }
+ LsaFreeMemory(buffer);
+ return accounts;
+
+ case STATUS_NO_MORE_ENTRIES:
+ return new string[0];
+
+ case STATUS_NO_SUCH_PRIVILEGE:
+ throw new ArgumentException(String.Format("Invalid privilege {0} not found in LSA database", privilege));
+
+ default:
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+ }
+
+ static LSA_UNICODE_STRING InitLsaString(string s)
+ {
+ // Unicode strings max. 32KB
+ if (s.Length > 0x7ffe)
+ throw new ArgumentException("String too long");
+
+ LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING();
+ lus.Buffer = s;
+ lus.Length = (ushort)(s.Length * sizeof(char));
+ lus.MaximumLength = (ushort)(lus.Length + sizeof(char));
+
+ return lus;
+ }
+
+ public void Dispose()
+ {
+ if (lsaHandle != IntPtr.Zero)
+ {
+ LsaClose(lsaHandle);
+ lsaHandle = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~LsaRightHelper() { Dispose(); }
+ }
+}
+"@
+
+$original_tmp = $env:TMP
+$env:TMP = $_remote_tmp
+Add-Type -TypeDefinition $sec_helper_util
+$env:TMP = $original_tmp
+
+Function Compare-UserList($existing_users, $new_users) {
+ $added_users = [String[]]@()
+ $removed_users = [String[]]@()
+ if ($action -eq "add") {
+ $added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
+ } elseif ($action -eq "remove") {
+ $removed_users = [Linq.Enumerable]::Intersect($new_users, $existing_users)
+ } else {
+ $added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
+ $removed_users = [Linq.Enumerable]::Except($existing_users, $new_users)
+ }
+
+ $change_result = @{
+ added = $added_users
+ removed = $removed_users
+ }
+
+ return $change_result
+}
+
+# C# class we can use to enumerate/add/remove rights
+$lsa_helper = New-Object -TypeName Ansible.LsaRightHelper
+
+$new_users = [System.Collections.ArrayList]@()
+foreach ($user in $users) {
+ $user_sid = Convert-ToSID -account_name $user
+ $new_users.Add($user_sid) > $null
+}
+$new_users = [String[]]$new_users.ToArray()
+try {
+ $existing_users = $lsa_helper.EnumerateAccountsWithUserRight($name)
+} catch [ArgumentException] {
+ Fail-Json -obj $result -message "the specified right $name is not a valid right"
+} catch {
+ Fail-Json -obj $result -message "failed to enumerate existing accounts with right: $($_.Exception.Message)"
+}
+
+$change_result = Compare-UserList -existing_users $existing_users -new_user $new_users
+if (($change_result.added.Length -gt 0) -or ($change_result.removed.Length -gt 0)) {
+ $result.changed = $true
+ $diff_text = "[$name]`n"
+
+ # used in diff mode calculation
+ $new_user_list = [System.Collections.ArrayList]$existing_users
+ foreach ($user in $change_result.removed) {
+ if (-not $check_mode) {
+ $lsa_helper.RemovePrivilege($user, $name)
+ }
+ $user_name = Convert-FromSID -sid $user
+ $result.removed += $user_name
+ $diff_text += "-$user_name`n"
+ $new_user_list.Remove($user) > $null
+ }
+ foreach ($user in $change_result.added) {
+ if (-not $check_mode) {
+ $lsa_helper.AddPrivilege($user, $name)
+ }
+ $user_name = Convert-FromSID -sid $user
+ $result.added += $user_name
+ $diff_text += "+$user_name`n"
+ $new_user_list.Add($user) > $null
+ }
+
+ if ($diff_mode) {
+ if ($new_user_list.Count -eq 0) {
+ $diff_text = "-$diff_text"
+ } else {
+ if ($existing_users.Count -eq 0) {
+ $diff_text = "+$diff_text"
+ }
+ }
+ $result.diff.prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_user_right.py b/test/support/windows-integration/plugins/modules/win_user_right.py
new file mode 100644
index 00000000000..5588208333c
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user_right.py
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, 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_user_right
+version_added: '2.4'
+short_description: Manage Windows User Rights
+description:
+- Add, remove or set User Rights for a group or users or groups.
+- You can set user rights for both local and domain accounts.
+options:
+ name:
+ description:
+ - The name of the User Right as shown by the C(Constant Name) value from
+ U(https://technet.microsoft.com/en-us/library/dd349804.aspx).
+ - The module will return an error if the right is invalid.
+ type: str
+ required: yes
+ users:
+ description:
+ - A list of users or groups to add/remove on the User Right.
+ - These can be in the form DOMAIN\user-group, user-group@DOMAIN.COM for
+ domain users/groups.
+ - For local users/groups it can be in the form user-group, .\user-group,
+ SERVERNAME\user-group where SERVERNAME is the name of the remote server.
+ - You can also add special local accounts like SYSTEM and others.
+ - Can be set to an empty list with I(action=set) to remove all accounts
+ from the right.
+ type: list
+ required: yes
+ action:
+ description:
+ - C(add) will add the users/groups to the existing right.
+ - C(remove) will remove the users/groups from the existing right.
+ - C(set) will replace the users/groups of the existing right.
+ type: str
+ default: set
+ choices: [ add, remove, set ]
+notes:
+- If the server is domain joined this module can change a right but if a GPO
+ governs this right then the changes won't last.
+seealso:
+- module: win_group
+- module: win_group_membership
+- module: win_user
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+---
+- name: Replace the entries of Deny log on locally
+ win_user_right:
+ name: SeDenyInteractiveLogonRight
+ users:
+ - Guest
+ - Users
+ action: set
+
+- name: Add account to Log on as a service
+ win_user_right:
+ name: SeServiceLogonRight
+ users:
+ - .\Administrator
+ - '{{ansible_hostname}}\local-user'
+ action: add
+
+- name: Remove accounts who can create Symbolic links
+ win_user_right:
+ name: SeCreateSymbolicLinkPrivilege
+ users:
+ - SYSTEM
+ - Administrators
+ - DOMAIN\User
+ - group@DOMAIN.COM
+ action: remove
+
+- name: Remove all accounts who cannot log on remote interactively
+ win_user_right:
+ name: SeDenyRemoteInteractiveLogonRight
+ users: []
+'''
+
+RETURN = r'''
+added:
+ description: A list of accounts that were added to the right, this is empty
+ if no accounts were added.
+ returned: success
+ type: list
+ sample: ["NT AUTHORITY\\SYSTEM", "DOMAIN\\User"]
+removed:
+ description: A list of accounts that were removed from the right, this is
+ empty if no accounts were removed.
+ returned: success
+ type: list
+ sample: ["SERVERNAME\\Administrator", "BUILTIN\\Administrators"]
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_whoami.ps1 b/test/support/windows-integration/plugins/modules/win_whoami.ps1
new file mode 100644
index 00000000000..6c9965af7e7
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_whoami.ps1
@@ -0,0 +1,837 @@
+#!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.CamelConversion
+
+$ErrorActionPreference = "Stop"
+
+$params = Parse-Args $args -supports_check_mode $true
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$session_util = @'
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+
+namespace Ansible
+{
+ public class SessionInfo
+ {
+ // SECURITY_LOGON_SESSION_DATA
+ public UInt64 LogonId { get; internal set; }
+ public Sid Account { get; internal set; }
+ public string LoginDomain { get; internal set; }
+ public string AuthenticationPackage { get; internal set; }
+ public SECURITY_LOGON_TYPE LogonType { get; internal set; }
+ public string LoginTime { get; internal set; }
+ public string LogonServer { get; internal set; }
+ public string DnsDomainName { get; internal set; }
+ public string Upn { get; internal set; }
+ public ArrayList UserFlags { get; internal set; }
+
+ // TOKEN_STATISTICS
+ public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel { get; internal set; }
+ public TOKEN_TYPE TokenType { get; internal set; }
+
+ // TOKEN_GROUPS
+ public ArrayList Groups { get; internal set; }
+ public ArrayList Rights { get; internal set; }
+
+ // TOKEN_MANDATORY_LABEL
+ public Sid Label { get; internal set; }
+
+ // TOKEN_PRIVILEGES
+ public Hashtable Privileges { get; internal set; }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct LSA_UNICODE_STRING
+ {
+ public UInt16 Length;
+ public UInt16 MaximumLength;
+ public IntPtr buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID
+ {
+ public UInt32 LowPart;
+ public Int32 HighPart;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SECURITY_LOGON_SESSION_DATA
+ {
+ public UInt32 Size;
+ public LUID LogonId;
+ public LSA_UNICODE_STRING Username;
+ public LSA_UNICODE_STRING LoginDomain;
+ public LSA_UNICODE_STRING AuthenticationPackage;
+ public SECURITY_LOGON_TYPE LogonType;
+ public UInt32 Session;
+ public IntPtr Sid;
+ public UInt64 LoginTime;
+ public LSA_UNICODE_STRING LogonServer;
+ public LSA_UNICODE_STRING DnsDomainName;
+ public LSA_UNICODE_STRING Upn;
+ public UInt32 UserFlags;
+ public LSA_LAST_INTER_LOGON_INFO LastLogonInfo;
+ public LSA_UNICODE_STRING LogonScript;
+ public LSA_UNICODE_STRING ProfilePath;
+ public LSA_UNICODE_STRING HomeDirectory;
+ public LSA_UNICODE_STRING HomeDirectoryDrive;
+ public UInt64 LogoffTime;
+ public UInt64 KickOffTime;
+ public UInt64 PasswordLastSet;
+ public UInt64 PasswordCanChange;
+ public UInt64 PasswordMustChange;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LSA_LAST_INTER_LOGON_INFO
+ {
+ public UInt64 LastSuccessfulLogon;
+ public UInt64 LastFailedLogon;
+ public UInt32 FailedAttemptCountSinceLastSuccessfulLogon;
+ }
+
+ public enum TOKEN_TYPE
+ {
+ TokenPrimary = 1,
+ TokenImpersonation
+ }
+
+ public enum SECURITY_IMPERSONATION_LEVEL
+ {
+ SecurityAnonymous,
+ SecurityIdentification,
+ SecurityImpersonation,
+ SecurityDelegation
+ }
+
+ public enum SECURITY_LOGON_TYPE
+ {
+ System = 0, // Used only by the Sytem account
+ Interactive = 2,
+ Network,
+ Batch,
+ Service,
+ Proxy,
+ Unlock,
+ NetworkCleartext,
+ NewCredentials,
+ RemoteInteractive,
+ CachedInteractive,
+ CachedRemoteInteractive,
+ CachedUnlock
+ }
+
+ [Flags]
+ public enum TokenGroupAttributes : uint
+ {
+ SE_GROUP_ENABLED = 0x00000004,
+ SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002,
+ SE_GROUP_INTEGRITY = 0x00000020,
+ SE_GROUP_INTEGRITY_ENABLED = 0x00000040,
+ SE_GROUP_LOGON_ID = 0xC0000000,
+ SE_GROUP_MANDATORY = 0x00000001,
+ SE_GROUP_OWNER = 0x00000008,
+ SE_GROUP_RESOURCE = 0x20000000,
+ SE_GROUP_USE_FOR_DENY_ONLY = 0x00000010,
+ }
+
+ [Flags]
+ public enum UserFlags : uint
+ {
+ LOGON_OPTIMIZED = 0x4000,
+ LOGON_WINLOGON = 0x8000,
+ LOGON_PKINIT = 0x10000,
+ LOGON_NOT_OPTMIZED = 0x20000,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SID_AND_ATTRIBUTES
+ {
+ public IntPtr Sid;
+ public UInt32 Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID_AND_ATTRIBUTES
+ {
+ public LUID Luid;
+ public UInt32 Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_GROUPS
+ {
+ public UInt32 GroupCount;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
+ public SID_AND_ATTRIBUTES[] Groups;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_MANDATORY_LABEL
+ {
+ public SID_AND_ATTRIBUTES Label;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_STATISTICS
+ {
+ public LUID TokenId;
+ public LUID AuthenticationId;
+ public UInt64 ExpirationTime;
+ public TOKEN_TYPE TokenType;
+ public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
+ public UInt32 DynamicCharged;
+ public UInt32 DynamicAvailable;
+ public UInt32 GroupCount;
+ public UInt32 PrivilegeCount;
+ public LUID ModifiedId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_PRIVILEGES
+ {
+ public UInt32 PrivilegeCount;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
+ public LUID_AND_ATTRIBUTES[] Privileges;
+ }
+
+ public class AccessToken : IDisposable
+ {
+ public enum TOKEN_INFORMATION_CLASS
+ {
+ TokenUser = 1,
+ TokenGroups,
+ TokenPrivileges,
+ TokenOwner,
+ TokenPrimaryGroup,
+ TokenDefaultDacl,
+ TokenSource,
+ TokenType,
+ TokenImpersonationLevel,
+ TokenStatistics,
+ TokenRestrictedSids,
+ TokenSessionId,
+ TokenGroupsAndPrivileges,
+ TokenSessionReference,
+ TokenSandBoxInert,
+ TokenAuditPolicy,
+ TokenOrigin,
+ TokenElevationType,
+ TokenLinkedToken,
+ TokenElevation,
+ TokenHasRestrictions,
+ TokenAccessInformation,
+ TokenVirtualizationAllowed,
+ TokenVirtualizationEnabled,
+ TokenIntegrityLevel,
+ TokenUIAccess,
+ TokenMandatoryPolicy,
+ TokenLogonSid,
+ TokenIsAppContainer,
+ TokenCapabilities,
+ TokenAppContainerSid,
+ TokenAppContainerNumber,
+ TokenUserClaimAttributes,
+ TokenDeviceClaimAttributes,
+ TokenRestrictedUserClaimAttributes,
+ TokenRestrictedDeviceClaimAttributes,
+ TokenDeviceGroups,
+ TokenRestrictedDeviceGroups,
+ TokenSecurityAttributes,
+ TokenIsRestricted,
+ MaxTokenInfoClass
+ }
+
+ public IntPtr hToken = IntPtr.Zero;
+
+ [DllImport("kernel32.dll")]
+ private static extern IntPtr GetCurrentProcess();
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern bool OpenProcessToken(
+ IntPtr ProcessHandle,
+ TokenAccessLevels DesiredAccess,
+ out IntPtr TokenHandle);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern bool GetTokenInformation(
+ IntPtr TokenHandle,
+ TOKEN_INFORMATION_CLASS TokenInformationClass,
+ IntPtr TokenInformation,
+ UInt32 TokenInformationLength,
+ out UInt32 ReturnLength);
+
+ public AccessToken(TokenAccessLevels tokenAccessLevels)
+ {
+ IntPtr currentProcess = GetCurrentProcess();
+ if (!OpenProcessToken(currentProcess, tokenAccessLevels, out hToken))
+ throw new Win32Exception("OpenProcessToken() for current process failed");
+ }
+
+ public IntPtr GetTokenInformation(out T tokenInformation, TOKEN_INFORMATION_CLASS tokenClass)
+ {
+ UInt32 tokenLength = 0;
+ GetTokenInformation(hToken, tokenClass, IntPtr.Zero, 0, out tokenLength);
+
+ IntPtr infoPtr = Marshal.AllocHGlobal((int)tokenLength);
+
+ if (!GetTokenInformation(hToken, tokenClass, infoPtr, tokenLength, out tokenLength))
+ throw new Win32Exception(String.Format("GetTokenInformation() data for {0} failed", tokenClass.ToString()));
+
+ tokenInformation = (T)Marshal.PtrToStructure(infoPtr, typeof(T));
+ return infoPtr;
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ ~AccessToken() { Dispose(); }
+ }
+
+ public class LsaHandle : IDisposable
+ {
+ [Flags]
+ public enum DesiredAccess : uint
+ {
+ POLICY_VIEW_LOCAL_INFORMATION = 0x00000001,
+ POLICY_VIEW_AUDIT_INFORMATION = 0x00000002,
+ POLICY_GET_PRIVATE_INFORMATION = 0x00000004,
+ POLICY_TRUST_ADMIN = 0x00000008,
+ POLICY_CREATE_ACCOUNT = 0x00000010,
+ POLICY_CREATE_SECRET = 0x00000020,
+ POLICY_CREATE_PRIVILEGE = 0x00000040,
+ POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080,
+ POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100,
+ POLICY_AUDIT_LOG_ADMIN = 0x00000200,
+ POLICY_SERVER_ADMIN = 0x00000400,
+ POLICY_LOOKUP_NAMES = 0x00000800,
+ POLICY_NOTIFICATION = 0x00001000
+ }
+
+ public IntPtr handle = IntPtr.Zero;
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern uint LsaOpenPolicy(
+ LSA_UNICODE_STRING[] SystemName,
+ ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
+ DesiredAccess AccessMask,
+ out IntPtr PolicyHandle);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern uint LsaClose(
+ IntPtr ObjectHandle);
+
+ [DllImport("advapi32.dll", SetLastError = false)]
+ private static extern int LsaNtStatusToWinError(
+ uint Status);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LSA_OBJECT_ATTRIBUTES
+ {
+ public int Length;
+ public IntPtr RootDirectory;
+ public IntPtr ObjectName;
+ public int Attributes;
+ public IntPtr SecurityDescriptor;
+ public IntPtr SecurityQualityOfService;
+ }
+
+ public LsaHandle(DesiredAccess desiredAccess)
+ {
+ LSA_OBJECT_ATTRIBUTES lsaAttr;
+ lsaAttr.RootDirectory = IntPtr.Zero;
+ lsaAttr.ObjectName = IntPtr.Zero;
+ lsaAttr.Attributes = 0;
+ lsaAttr.SecurityDescriptor = IntPtr.Zero;
+ lsaAttr.SecurityQualityOfService = IntPtr.Zero;
+ lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
+ LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
+ system[0].buffer = IntPtr.Zero;
+
+ uint res = LsaOpenPolicy(system, ref lsaAttr, desiredAccess, out handle);
+ if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaOpenPolicy() failed");
+ }
+
+ public void Dispose()
+ {
+ if (handle != IntPtr.Zero)
+ {
+ LsaClose(handle);
+ handle = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~LsaHandle() { Dispose(); }
+ }
+
+ public class Sid
+ {
+ public string SidString { get; internal set; }
+ public string DomainName { get; internal set; }
+ public string AccountName { get; internal set; }
+ public SID_NAME_USE SidType { get; internal set; }
+
+ public enum SID_NAME_USE
+ {
+ SidTypeUser = 1,
+ SidTypeGroup,
+ SidTypeDomain,
+ SidTypeAlias,
+ SidTypeWellKnownGroup,
+ SidTypeDeletedAccount,
+ SidTypeInvalid,
+ SidTypeUnknown,
+ SidTypeComputer,
+ SidTypeLabel,
+ SidTypeLogon,
+ }
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool LookupAccountSid(
+ string lpSystemName,
+ [MarshalAs(UnmanagedType.LPArray)]
+ byte[] Sid,
+ StringBuilder lpName,
+ ref UInt32 cchName,
+ StringBuilder ReferencedDomainName,
+ ref UInt32 cchReferencedDomainName,
+ out SID_NAME_USE peUse);
+
+ public Sid(IntPtr sidPtr)
+ {
+ SecurityIdentifier sid;
+ try
+ {
+ sid = new SecurityIdentifier(sidPtr);
+ }
+ catch (Exception e)
+ {
+ throw new ArgumentException(String.Format("Failed to cast IntPtr to SecurityIdentifier: {0}", e));
+ }
+
+ SetSidInfo(sid);
+ }
+
+ public Sid(SecurityIdentifier sid)
+ {
+ SetSidInfo(sid);
+ }
+
+ public override string ToString()
+ {
+ return SidString;
+ }
+
+ private void SetSidInfo(SecurityIdentifier sid)
+ {
+ byte[] sidBytes = new byte[sid.BinaryLength];
+ sid.GetBinaryForm(sidBytes, 0);
+
+ StringBuilder lpName = new StringBuilder();
+ UInt32 cchName = 0;
+ StringBuilder referencedDomainName = new StringBuilder();
+ UInt32 cchReferencedDomainName = 0;
+ SID_NAME_USE peUse;
+ LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse);
+
+ lpName.EnsureCapacity((int)cchName);
+ referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);
+
+ SidString = sid.ToString();
+ if (!LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse))
+ {
+ int lastError = Marshal.GetLastWin32Error();
+
+ if (lastError != 1332 && lastError != 1789) // Fails to lookup Logon Sid
+ {
+ throw new Win32Exception(lastError, String.Format("LookupAccountSid() failed for SID: {0} {1}", sid.ToString(), lastError));
+ }
+ else if (SidString.StartsWith("S-1-5-5-"))
+ {
+ AccountName = String.Format("LogonSessionId_{0}", SidString.Substring(8));
+ DomainName = "NT AUTHORITY";
+ SidType = SID_NAME_USE.SidTypeLogon;
+ }
+ else
+ {
+ AccountName = null;
+ DomainName = null;
+ SidType = SID_NAME_USE.SidTypeUnknown;
+ }
+ }
+ else
+ {
+ AccountName = lpName.ToString();
+ DomainName = referencedDomainName.ToString();
+ SidType = peUse;
+ }
+ }
+ }
+
+ public class SessionUtil
+ {
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaFreeReturnBuffer(
+ IntPtr Buffer);
+
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaEnumerateLogonSessions(
+ out UInt64 LogonSessionCount,
+ out IntPtr LogonSessionList);
+
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaGetLogonSessionData(
+ IntPtr LogonId,
+ out IntPtr ppLogonSessionData);
+
+ [DllImport("advapi32.dll", SetLastError = false)]
+ private static extern int LsaNtStatusToWinError(
+ uint Status);
+
+ [DllImport("advapi32", SetLastError = true)]
+ private static extern uint LsaEnumerateAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr AccountSid,
+ out IntPtr UserRights,
+ out UInt64 CountOfRights);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool LookupPrivilegeName(
+ string lpSystemName,
+ ref LUID lpLuid,
+ StringBuilder lpName,
+ ref UInt32 cchName);
+
+ private const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001;
+ private const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
+ private const UInt32 STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
+ private const UInt32 STATUS_ACCESS_DENIED = 0xC0000022;
+
+ public static SessionInfo GetSessionInfo()
+ {
+ AccessToken accessToken = new AccessToken(TokenAccessLevels.Query);
+
+ // Get Privileges
+ Hashtable privilegeInfo = new Hashtable();
+ TOKEN_PRIVILEGES privileges;
+ IntPtr privilegesPtr = accessToken.GetTokenInformation(out privileges, AccessToken.TOKEN_INFORMATION_CLASS.TokenPrivileges);
+ LUID_AND_ATTRIBUTES[] luidAndAttributes = new LUID_AND_ATTRIBUTES[privileges.PrivilegeCount];
+ try
+ {
+ PtrToStructureArray(luidAndAttributes, privilegesPtr.ToInt64() + Marshal.SizeOf(privileges.PrivilegeCount));
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(privilegesPtr);
+ }
+ foreach (LUID_AND_ATTRIBUTES luidAndAttribute in luidAndAttributes)
+ {
+ LUID privLuid = luidAndAttribute.Luid;
+ UInt32 privNameLen = 0;
+ StringBuilder privName = new StringBuilder();
+ LookupPrivilegeName(null, ref privLuid, null, ref privNameLen);
+ privName.EnsureCapacity((int)(privNameLen + 1));
+ if (!LookupPrivilegeName(null, ref privLuid, privName, ref privNameLen))
+ throw new Win32Exception("LookupPrivilegeName() failed");
+
+ string state = "disabled";
+ if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED) == SE_PRIVILEGE_ENABLED)
+ state = "enabled";
+ if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT) == SE_PRIVILEGE_ENABLED_BY_DEFAULT)
+ state = "enabled-by-default";
+ privilegeInfo.Add(privName.ToString(), state);
+ }
+
+ // Get Current Process LogonSID, User Rights and Groups
+ ArrayList userRights = new ArrayList();
+ ArrayList userGroups = new ArrayList();
+ TOKEN_GROUPS groups;
+ IntPtr groupsPtr = accessToken.GetTokenInformation(out groups, AccessToken.TOKEN_INFORMATION_CLASS.TokenGroups);
+ SID_AND_ATTRIBUTES[] sidAndAttributes = new SID_AND_ATTRIBUTES[groups.GroupCount];
+ LsaHandle lsaHandle = null;
+ // We can only get rights if we are an admin
+ if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
+ lsaHandle = new LsaHandle(LsaHandle.DesiredAccess.POLICY_LOOKUP_NAMES);
+ try
+ {
+ PtrToStructureArray(sidAndAttributes, groupsPtr.ToInt64() + IntPtr.Size);
+ foreach (SID_AND_ATTRIBUTES sidAndAttribute in sidAndAttributes)
+ {
+ TokenGroupAttributes attributes = (TokenGroupAttributes)sidAndAttribute.Attributes;
+ if (attributes.HasFlag(TokenGroupAttributes.SE_GROUP_ENABLED) && lsaHandle != null)
+ {
+ ArrayList rights = GetAccountRights(lsaHandle.handle, sidAndAttribute.Sid);
+ foreach (string right in rights)
+ {
+ // Includes both Privileges and Account Rights, only add the ones with Logon in the name
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx
+ if (!userRights.Contains(right) && right.Contains("Logon"))
+ userRights.Add(right);
+ }
+ }
+ // Do not include the Logon SID in the groups category
+ if (!attributes.HasFlag(TokenGroupAttributes.SE_GROUP_LOGON_ID))
+ {
+ Hashtable groupInfo = new Hashtable();
+ Sid group = new Sid(sidAndAttribute.Sid);
+ ArrayList groupAttributes = new ArrayList();
+ foreach (TokenGroupAttributes attribute in Enum.GetValues(typeof(TokenGroupAttributes)))
+ {
+ if (attributes.HasFlag(attribute))
+ {
+ string attributeName = attribute.ToString().Substring(9);
+ attributeName = attributeName.Replace('_', ' ');
+ attributeName = attributeName.First().ToString().ToUpper() + attributeName.Substring(1).ToLower();
+ groupAttributes.Add(attributeName);
+ }
+ }
+ // Using snake_case here as I can't generically convert all dict keys in PS (see Privileges)
+ groupInfo.Add("sid", group.SidString);
+ groupInfo.Add("domain_name", group.DomainName);
+ groupInfo.Add("account_name", group.AccountName);
+ groupInfo.Add("type", group.SidType);
+ groupInfo.Add("attributes", groupAttributes);
+ userGroups.Add(groupInfo);
+ }
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(groupsPtr);
+ if (lsaHandle != null)
+ lsaHandle.Dispose();
+ }
+
+ // Get Integrity Level
+ Sid integritySid = null;
+ TOKEN_MANDATORY_LABEL mandatoryLabel;
+ IntPtr mandatoryLabelPtr = accessToken.GetTokenInformation(out mandatoryLabel, AccessToken.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel);
+ Marshal.FreeHGlobal(mandatoryLabelPtr);
+ integritySid = new Sid(mandatoryLabel.Label.Sid);
+
+ // Get Token Statistics
+ TOKEN_STATISTICS tokenStats;
+ IntPtr tokenStatsPtr = accessToken.GetTokenInformation(out tokenStats, AccessToken.TOKEN_INFORMATION_CLASS.TokenStatistics);
+ Marshal.FreeHGlobal(tokenStatsPtr);
+
+ SessionInfo sessionInfo = GetSessionDataForLogonSession(tokenStats.AuthenticationId);
+ sessionInfo.Groups = userGroups;
+ sessionInfo.Label = integritySid;
+ sessionInfo.ImpersonationLevel = tokenStats.ImpersonationLevel;
+ sessionInfo.TokenType = tokenStats.TokenType;
+ sessionInfo.Privileges = privilegeInfo;
+ sessionInfo.Rights = userRights;
+ return sessionInfo;
+ }
+
+ private static ArrayList GetAccountRights(IntPtr lsaHandle, IntPtr sid)
+ {
+ UInt32 res;
+ ArrayList rights = new ArrayList();
+ IntPtr userRightsPointer = IntPtr.Zero;
+ UInt64 countOfRights = 0;
+
+ res = LsaEnumerateAccountRights(lsaHandle, sid, out userRightsPointer, out countOfRights);
+ if (res != 0 && res != STATUS_OBJECT_NAME_NOT_FOUND)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateAccountRights() failed");
+ else if (res != STATUS_OBJECT_NAME_NOT_FOUND)
+ {
+ LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[countOfRights];
+ PtrToStructureArray(userRights, userRightsPointer.ToInt64());
+ rights = new ArrayList();
+ foreach (LSA_UNICODE_STRING right in userRights)
+ rights.Add(Marshal.PtrToStringUni(right.buffer));
+ }
+
+ return rights;
+ }
+
+ private static SessionInfo GetSessionDataForLogonSession(LUID logonSession)
+ {
+ uint res;
+ UInt64 count = 0;
+ IntPtr luidPtr = IntPtr.Zero;
+ SessionInfo sessionInfo = null;
+ UInt64 processDataId = ConvertLuidToUint(logonSession);
+
+ res = LsaEnumerateLogonSessions(out count, out luidPtr);
+ if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateLogonSessions() failed");
+ Int64 luidAddr = luidPtr.ToInt64();
+
+ try
+ {
+ for (UInt64 i = 0; i < count; i++)
+ {
+ IntPtr dataPointer = IntPtr.Zero;
+ res = LsaGetLogonSessionData(luidPtr, out dataPointer);
+ if (res == STATUS_ACCESS_DENIED) // Non admins won't be able to get info for session's that are not their own
+ {
+ luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
+ continue;
+ }
+ else if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), String.Format("LsaGetLogonSessionData() failed {0}", res));
+
+ SECURITY_LOGON_SESSION_DATA sessionData = (SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(dataPointer, typeof(SECURITY_LOGON_SESSION_DATA));
+ UInt64 sessionDataid = ConvertLuidToUint(sessionData.LogonId);
+
+ if (sessionDataid == processDataId)
+ {
+ ArrayList userFlags = new ArrayList();
+ UserFlags flags = (UserFlags)sessionData.UserFlags;
+ foreach (UserFlags flag in Enum.GetValues(typeof(UserFlags)))
+ {
+ if (flags.HasFlag(flag))
+ {
+ string flagName = flag.ToString().Substring(6);
+ flagName = flagName.Replace('_', ' ');
+ flagName = flagName.First().ToString().ToUpper() + flagName.Substring(1).ToLower();
+ userFlags.Add(flagName);
+ }
+ }
+
+ sessionInfo = new SessionInfo()
+ {
+ AuthenticationPackage = Marshal.PtrToStringUni(sessionData.AuthenticationPackage.buffer),
+ DnsDomainName = Marshal.PtrToStringUni(sessionData.DnsDomainName.buffer),
+ LoginDomain = Marshal.PtrToStringUni(sessionData.LoginDomain.buffer),
+ LoginTime = ConvertIntegerToDateString(sessionData.LoginTime),
+ LogonId = ConvertLuidToUint(sessionData.LogonId),
+ LogonServer = Marshal.PtrToStringUni(sessionData.LogonServer.buffer),
+ LogonType = sessionData.LogonType,
+ Upn = Marshal.PtrToStringUni(sessionData.Upn.buffer),
+ UserFlags = userFlags,
+ Account = new Sid(sessionData.Sid)
+ };
+ break;
+ }
+ luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
+ }
+ }
+ finally
+ {
+ LsaFreeReturnBuffer(new IntPtr(luidAddr));
+ }
+
+ if (sessionInfo == null)
+ throw new Exception(String.Format("Could not find the data for logon session {0}", processDataId));
+ return sessionInfo;
+ }
+
+ private static string ConvertIntegerToDateString(UInt64 time)
+ {
+ if (time == 0)
+ return null;
+ if (time > (UInt64)DateTime.MaxValue.ToFileTime())
+ return null;
+
+ DateTime dateTime = DateTime.FromFileTime((long)time);
+ return dateTime.ToString("o");
+ }
+
+ private static UInt64 ConvertLuidToUint(LUID luid)
+ {
+ UInt32 low = luid.LowPart;
+ UInt64 high = (UInt64)luid.HighPart;
+ high = high << 32;
+ UInt64 uintValue = (high | (UInt64)low);
+ return uintValue;
+ }
+
+ private static void PtrToStructureArray(T[] array, Int64 pointerAddress)
+ {
+ Int64 pointerOffset = pointerAddress;
+ for (int i = 0; i < array.Length; i++, pointerOffset += Marshal.SizeOf(typeof(T)))
+ array[i] = (T)Marshal.PtrToStructure(new IntPtr(pointerOffset), typeof(T));
+ }
+
+ public static IEnumerable GetValues()
+ {
+ return Enum.GetValues(typeof(T)).Cast();
+ }
+ }
+}
+'@
+
+$original_tmp = $env:TMP
+$env:TMP = $_remote_tmp
+Add-Type -TypeDefinition $session_util
+$env:TMP = $original_tmp
+
+$session_info = [Ansible.SessionUtil]::GetSessionInfo()
+
+Function Convert-Value($value) {
+ $new_value = $value
+ if ($value -is [System.Collections.ArrayList]) {
+ $new_value = [System.Collections.ArrayList]@()
+ foreach ($list_value in $value) {
+ $new_list_value = Convert-Value -value $list_value
+ [void]$new_value.Add($new_list_value)
+ }
+ } elseif ($value -is [Hashtable]) {
+ $new_value = @{}
+ foreach ($entry in $value.GetEnumerator()) {
+ $entry_value = Convert-Value -value $entry.Value
+ # manually convert Sid type entry to remove the SidType prefix
+ if ($entry.Name -eq "type") {
+ $entry_value = $entry_value.Replace("SidType", "")
+ }
+ $new_value[$entry.Name] = $entry_value
+ }
+ } elseif ($value -is [Ansible.Sid]) {
+ $new_value = @{
+ sid = $value.SidString
+ account_name = $value.AccountName
+ domain_name = $value.DomainName
+ type = $value.SidType.ToString().Replace("SidType", "")
+ }
+ } elseif ($value -is [Enum]) {
+ $new_value = $value.ToString()
+ }
+
+ return ,$new_value
+}
+
+$result = @{
+ changed = $false
+}
+
+$properties = [type][Ansible.SessionInfo]
+foreach ($property in $properties.DeclaredProperties) {
+ $property_name = $property.Name
+ $property_value = $session_info.$property_name
+ $snake_name = Convert-StringToSnakeCase -string $property_name
+
+ $result.$snake_name = Convert-Value -value $property_value
+}
+
+Exit-Json -obj $result
diff --git a/test/support/windows-integration/plugins/modules/win_whoami.py b/test/support/windows-integration/plugins/modules/win_whoami.py
new file mode 100644
index 00000000000..d647374b6c1
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_whoami.py
@@ -0,0 +1,203 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, 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_whoami
+version_added: "2.5"
+short_description: Get information about the current user and process
+description:
+- Designed to return the same information as the C(whoami /all) command.
+- Also includes information missing from C(whoami) such as logon metadata like
+ logon rights, id, type.
+notes:
+- If running this module with a non admin user, the logon rights will be an
+ empty list as Administrator rights are required to query LSA for the
+ information.
+seealso:
+- module: win_credential
+- module: win_group_membership
+- module: win_user_right
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Get whoami information
+ win_whoami:
+'''
+
+RETURN = r'''
+authentication_package:
+ description: The name of the authentication package used to authenticate the
+ user in the session.
+ returned: success
+ type: str
+ sample: Negotiate
+user_flags:
+ description: The user flags for the logon session, see UserFlags in
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380128).
+ returned: success
+ type: str
+ sample: Winlogon
+upn:
+ description: The user principal name of the current user.
+ returned: success
+ type: str
+ sample: Administrator@DOMAIN.COM
+logon_type:
+ description: The logon type that identifies the logon method, see
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380129.aspx).
+ returned: success
+ type: str
+ sample: Network
+privileges:
+ description: A dictionary of privileges and their state on the logon token.
+ returned: success
+ type: dict
+ sample: {
+ "SeChangeNotifyPrivileges": "enabled-by-default",
+ "SeRemoteShutdownPrivilege": "disabled",
+ "SeDebugPrivilege": "enabled"
+ }
+label:
+ description: The mandatory label set to the logon session.
+ returned: success
+ type: complex
+ contains:
+ domain_name:
+ description: The domain name of the label SID.
+ returned: success
+ type: str
+ sample: Mandatory Label
+ sid:
+ description: The SID in string form.
+ returned: success
+ type: str
+ sample: S-1-16-12288
+ account_name:
+ description: The account name of the label SID.
+ returned: success
+ type: str
+ sample: High Mandatory Level
+ type:
+ description: The type of SID.
+ returned: success
+ type: str
+ sample: Label
+impersonation_level:
+ description: The impersonation level of the token, only valid if
+ C(token_type) is C(TokenImpersonation), see
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa379572.aspx).
+ returned: success
+ type: str
+ sample: SecurityAnonymous
+login_time:
+ description: The logon time in ISO 8601 format
+ returned: success
+ type: str
+ sample: '2017-11-27T06:24:14.3321665+10:00'
+groups:
+ description: A list of groups and attributes that the user is a member of.
+ returned: success
+ type: list
+ sample: [
+ {
+ "account_name": "Domain Users",
+ "domain_name": "DOMAIN",
+ "attributes": [
+ "Mandatory",
+ "Enabled by default",
+ "Enabled"
+ ],
+ "sid": "S-1-5-21-1654078763-769949647-2968445802-513",
+ "type": "Group"
+ },
+ {
+ "account_name": "Administrators",
+ "domain_name": "BUILTIN",
+ "attributes": [
+ "Mandatory",
+ "Enabled by default",
+ "Enabled",
+ "Owner"
+ ],
+ "sid": "S-1-5-32-544",
+ "type": "Alias"
+ }
+ ]
+account:
+ description: The running account SID details.
+ returned: success
+ type: complex
+ contains:
+ domain_name:
+ description: The domain name of the account SID.
+ returned: success
+ type: str
+ sample: DOMAIN
+ sid:
+ description: The SID in string form.
+ returned: success
+ type: str
+ sample: S-1-5-21-1654078763-769949647-2968445802-500
+ account_name:
+ description: The account name of the account SID.
+ returned: success
+ type: str
+ sample: Administrator
+ type:
+ description: The type of SID.
+ returned: success
+ type: str
+ sample: User
+login_domain:
+ description: The name of the domain used to authenticate the owner of the
+ session.
+ returned: success
+ type: str
+ sample: DOMAIN
+rights:
+ description: A list of logon rights assigned to the logon.
+ returned: success and running user is a member of the local Administrators group
+ type: list
+ sample: [
+ "SeNetworkLogonRight",
+ "SeInteractiveLogonRight",
+ "SeBatchLogonRight",
+ "SeRemoteInteractiveLogonRight"
+ ]
+logon_server:
+ description: The name of the server used to authenticate the owner of the
+ logon session.
+ returned: success
+ type: str
+ sample: DC01
+logon_id:
+ description: The unique identifier of the logon session.
+ returned: success
+ type: int
+ sample: 20470143
+dns_domain_name:
+ description: The DNS name of the logon session, this is an empty string if
+ this is not set.
+ returned: success
+ type: str
+ sample: DOMAIN.COM
+token_type:
+ description: The token type to indicate whether it is a primary or
+ impersonation token.
+ returned: success
+ type: str
+ sample: TokenPrimary
+'''
diff --git a/test/utils/shippable/incidental/windows.sh b/test/utils/shippable/incidental/windows.sh
new file mode 100755
index 00000000000..f84d7c3ba31
--- /dev/null
+++ b/test/utils/shippable/incidental/windows.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+set -o pipefail -eux
+
+declare -a args
+IFS='/:' read -ra args <<< "$1"
+
+version="${args[1]}"
+
+target="shippable/windows/incidental/"
+
+stage="${S:-prod}"
+provider="${P:-default}"
+
+# python versions to test in order
+# python 2.7 runs full tests while other versions run minimal tests
+python_versions=(
+ 3.5
+ 3.6
+ 3.7
+ 3.8
+ 2.7
+)
+
+# version to test when only testing a single version
+single_version=2012-R2
+
+# shellcheck disable=SC2086
+ansible-test windows-integration --explain ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} > /tmp/explain.txt 2>&1 || { cat /tmp/explain.txt && false; }
+{ grep ' windows-integration: .* (targeted)$' /tmp/explain.txt || true; } > /tmp/windows.txt
+
+if [ -s /tmp/windows.txt ] || [ "${CHANGED:+$CHANGED}" == "" ]; then
+ echo "Detected changes requiring integration tests specific to Windows:"
+ cat /tmp/windows.txt
+
+ echo "Running Windows integration tests for multiple versions concurrently."
+
+ platforms=(
+ --windows "${version}"
+ )
+else
+ echo "No changes requiring integration tests specific to Windows were detected."
+ echo "Running Windows integration tests for a single version only: ${single_version}"
+
+ if [ "${version}" != "${single_version}" ]; then
+ echo "Skipping this job since it is for: ${version}"
+ exit 0
+ fi
+
+ platforms=(
+ --windows "${version}"
+ )
+fi
+
+for version in "${python_versions[@]}"; do
+ if [ "${version}" == "2.7" ]; then
+ # full tests for python 2.7
+ ci="${target}"
+ else
+ # minimal tests for other python versions
+ ci="incidental_win_ping"
+ fi
+
+ # terminate remote instances on the final python version tested
+ if [ "${version}" = "${python_versions[-1]}" ]; then
+ terminate="always"
+ else
+ terminate="never"
+ fi
+
+ # shellcheck disable=SC2086
+ ansible-test windows-integration --color -v --retry-on-error "${ci}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \
+ "${platforms[@]}" \
+ --docker default --python "${version}" \
+ --enable-test-support \
+ --remote-terminate "${terminate}" --remote-stage "${stage}" --remote-provider "${provider}"
+done